Browse Source

[oozie] Add prepare statement in a workflow action

Add new field 'prepares' to Pig, MapReduce, Java actions
Update XML generation
Add tests
Fix 'Add Variable' js bug
Romain Rigaux 13 năm trước cách đây
mục cha
commit
219d5bdfb0

+ 3 - 0
apps/oozie/src/oozie/forms.py

@@ -61,6 +61,7 @@ class JavaForm(forms.ModelForm):
     exclude = NodeForm.Meta.ALWAYS_HIDE
     exclude = NodeForm.Meta.ALWAYS_HIDE
     widgets = {
     widgets = {
       'job_properties': forms.widgets.HiddenInput(),
       'job_properties': forms.widgets.HiddenInput(),
+      'prepares': forms.widgets.HiddenInput(),
       'files': forms.HiddenInput(),
       'files': forms.HiddenInput(),
       'archives': forms.HiddenInput(),
       'archives': forms.HiddenInput(),
       'jar_path': forms.TextInput(attrs={'class': 'pathChooser span5'}),
       'jar_path': forms.TextInput(attrs={'class': 'pathChooser span5'}),
@@ -75,6 +76,7 @@ class MapreduceForm(forms.ModelForm):
     exclude = NodeForm.Meta.ALWAYS_HIDE
     exclude = NodeForm.Meta.ALWAYS_HIDE
     widgets = {
     widgets = {
       'job_properties': forms.widgets.HiddenInput(),
       'job_properties': forms.widgets.HiddenInput(),
+      'prepares': forms.widgets.HiddenInput(),
       'files': forms.HiddenInput(),
       'files': forms.HiddenInput(),
       'archives': forms.HiddenInput(),
       'archives': forms.HiddenInput(),
       'jar_path': forms.TextInput(attrs={'class': 'pathChooser span5'}),
       'jar_path': forms.TextInput(attrs={'class': 'pathChooser span5'}),
@@ -101,6 +103,7 @@ class PigForm(forms.ModelForm):
     exclude = NodeForm.Meta.ALWAYS_HIDE
     exclude = NodeForm.Meta.ALWAYS_HIDE
     widgets = {
     widgets = {
       'job_properties': forms.widgets.HiddenInput(),
       'job_properties': forms.widgets.HiddenInput(),
+      'prepares': forms.widgets.HiddenInput(),
       'params': forms.widgets.HiddenInput(),
       'params': forms.widgets.HiddenInput(),
       'script_path': forms.TextInput(attrs={'class': 'pathChooser span5'}),
       'script_path': forms.TextInput(attrs={'class': 'pathChooser span5'}),
       'files': forms.widgets.HiddenInput(),
       'files': forms.widgets.HiddenInput(),

+ 16 - 4
apps/oozie/src/oozie/models.py

@@ -666,7 +666,7 @@ class Action(Node):
 #  - Node.get_full_node()
 #  - Node.get_full_node()
 
 
 class Mapreduce(Action):
 class Mapreduce(Action):
-  PARAM_FIELDS = ('files', 'archives', 'job_properties', 'jar_path')
+  PARAM_FIELDS = ('files', 'archives', 'job_properties', 'jar_path', 'prepares')
   node_type = 'mapreduce'
   node_type = 'mapreduce'
 
 
   files = models.CharField(max_length=PATH_MAX, default="[]",
   files = models.CharField(max_length=PATH_MAX, default="[]",
@@ -676,6 +676,7 @@ class Mapreduce(Action):
   job_properties = models.TextField(default='[]', # JSON dict
   job_properties = models.TextField(default='[]', # JSON dict
                                     help_text=_t('For the job configuration (e.g. mapred.mapper.class)'))
                                     help_text=_t('For the job configuration (e.g. mapred.mapper.class)'))
   jar_path = models.CharField(max_length=PATH_MAX, help_text=_t('Path to jar files on HDFS'))
   jar_path = models.CharField(max_length=PATH_MAX, help_text=_t('Path to jar files on HDFS'))
+  prepares = models.TextField(default="[]", help_text=_t('List of paths to delete of create before starting the job'))
 
 
   def get_properties(self):
   def get_properties(self):
     return json.loads(self.job_properties)
     return json.loads(self.job_properties)
@@ -686,6 +687,9 @@ class Mapreduce(Action):
   def get_archives(self):
   def get_archives(self):
     return json.loads(self.archives)
     return json.loads(self.archives)
 
 
+  def get_prepares(self):
+    return json.loads(self.prepares)
+
 
 
 class Streaming(Action):
 class Streaming(Action):
   PARAM_FIELDS = ('files', 'archives', 'job_properties', 'mapper', 'reducer')
   PARAM_FIELDS = ('files', 'archives', 'job_properties', 'mapper', 'reducer')
@@ -710,7 +714,7 @@ class Streaming(Action):
 
 
 class Java(Action):
 class Java(Action):
   PARAM_FIELDS = ('files', 'archives', 'jar_path', 'main_class', 'args',
   PARAM_FIELDS = ('files', 'archives', 'jar_path', 'main_class', 'args',
-                  'java_opts', 'job_properties')
+                  'java_opts', 'job_properties', 'prepares')
   node_type = "java"
   node_type = "java"
 
 
   files = models.CharField(max_length=PATH_MAX, default="[]",
   files = models.CharField(max_length=PATH_MAX, default="[]",
@@ -723,6 +727,7 @@ class Java(Action):
   java_opts = models.CharField(max_length=256, blank=True)
   java_opts = models.CharField(max_length=256, blank=True)
   job_properties = models.TextField(default='[]', # JSON dict
   job_properties = models.TextField(default='[]', # JSON dict
                                     help_text=_t('For the job configuration (e.g. mapred.mapper.class)'))
                                     help_text=_t('For the job configuration (e.g. mapred.mapper.class)'))
+  prepares = models.TextField(default="[]", help_text=_t('List of paths to delete of create before starting the job'))
 
 
   def get_properties(self):
   def get_properties(self):
     return json.loads(self.job_properties)
     return json.loads(self.job_properties)
@@ -733,13 +738,16 @@ class Java(Action):
   def get_archives(self):
   def get_archives(self):
     return json.loads(self.archives)
     return json.loads(self.archives)
 
 
+  def get_prepares(self):
+    return json.loads(self.prepares)
+
 
 
 class Pig(Action):
 class Pig(Action):
-  PARAM_FIELDS = ('files', 'archives', 'job_properties', 'params')
+  PARAM_FIELDS = ('files', 'archives', 'job_properties', 'params', 'prepares')
   node_type = 'pig'
   node_type = 'pig'
 
 
   script_path = models.CharField(max_length=256, blank=False, help_text=_t('Local path'))
   script_path = models.CharField(max_length=256, blank=False, help_text=_t('Local path'))
-  params = models.TextField(default="[]")
+  params = models.TextField(default="[]", help_text=_t('The Pig parameters of the script'))
 
 
   files = models.CharField(max_length=PATH_MAX, default="[]",
   files = models.CharField(max_length=PATH_MAX, default="[]",
       help_text=_t('List of paths to files to be added to the distributed cache'))
       help_text=_t('List of paths to files to be added to the distributed cache'))
@@ -747,6 +755,7 @@ class Pig(Action):
       help_text=_t('List of paths to archives to be added to the distributed cache'))
       help_text=_t('List of paths to archives to be added to the distributed cache'))
   job_properties = models.TextField(default='[{"name":"oozie.use.system.libpath","value":"true"}]', # JSON dict
   job_properties = models.TextField(default='[{"name":"oozie.use.system.libpath","value":"true"}]', # JSON dict
                                     help_text=_t('For the job configuration (e.g. mapred.mapper.class)'))
                                     help_text=_t('For the job configuration (e.g. mapred.mapper.class)'))
+  prepares = models.TextField(default="[]", help_text=_t('List of paths to delete of create before starting the job'))
 
 
   def get_properties(self):
   def get_properties(self):
     return json.loads(self.job_properties)
     return json.loads(self.job_properties)
@@ -760,6 +769,9 @@ class Pig(Action):
   def get_params(self):
   def get_params(self):
     return json.loads(self.params)
     return json.loads(self.params)
 
 
+  def get_prepares(self):
+    return json.loads(self.prepares)
+
 
 
 Action.types = (Mapreduce.node_type, Streaming.node_type, Java.node_type, Pig.node_type)
 Action.types = (Mapreduce.node_type, Streaming.node_type, Java.node_type, Pig.node_type)
 
 

+ 6 - 6
apps/oozie/src/oozie/templates/editor/edit_coordinator.mako

@@ -253,11 +253,11 @@ ${ layout.menubar(section='coordinators') }
                     </tr>
                     </tr>
 
 
                      <div class="hide">
                      <div class="hide">
-                        % for field in form.visible_fields():
-                            ${ field.errors }
-                            ${ field.label }: ${ field }
-                        % endfor
-                        </div>
+                       % for field in form.visible_fields():
+                          ${ field.errors }
+                          ${ field.label }: ${ field }
+                       % endfor
+                     </div>
 
 
                    % endfor
                    % endfor
                   </tbody>
                   </tbody>
@@ -387,7 +387,7 @@ ${ layout.menubar(section='coordinators') }
 
 
    $("a[data-row-selector='true']").jHueRowSelector();
    $("a[data-row-selector='true']").jHueRowSelector();
 
 
-   ko.applyBindings(window.viewModel)
+   ko.applyBindings(window.viewModel);
  });
  });
 </script>
 </script>
 
 

+ 6 - 4
apps/oozie/src/oozie/templates/editor/edit_workflow.mako

@@ -180,10 +180,10 @@ ${ layout.menubar(section='workflows') }
 
 
 
 
 <style type="text/css">
 <style type="text/css">
-#modal-window .modal-content {
-  height: 300px;
-  overflow: auto;
-}
+  #modal-window .modal-content {
+    height: 300px;
+    overflow: auto;
+  }
 </style>
 </style>
 
 
 <script src="/static/ext/js/knockout-2.0.0.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/knockout-2.0.0.js" type="text/javascript" charset="utf-8"></script>
@@ -210,6 +210,8 @@ ${ layout.menubar(section='workflows') }
       });
       });
     });
     });
 
 
+    ko.applyBindings(window.viewModel);
+
     $("a[data-row-selector='true']").jHueRowSelector();
     $("a[data-row-selector='true']").jHueRowSelector();
   });
   });
 </script>
 </script>

+ 58 - 3
apps/oozie/src/oozie/templates/editor/edit_workflow_action.mako

@@ -61,6 +61,43 @@ ${ layout.menubar(section='workflows') }
       % endif
       % endif
     % endfor
     % endfor
 
 
+    % if 'prepares' in action_form.fields:
+      <div class="control-group">
+        <label class="control-label">${ _('Prepare') }</label>
+        <div class="controls">
+          <table class="table-condensed designTable" data-bind="visible: prepares().length > 0">
+            <thead>
+              <tr>
+                <th>${ _('Type') }</th>
+                <th>${ _('Value') }</th>
+                <th/>
+              </tr>
+            </thead>
+            <tbody data-bind="foreach: prepares">
+              <tr>
+                <td>
+                  <span class="span3 required" data-bind="text: type" />
+                </td>
+                <td>
+                  <input class="input span5 required pathChooserKo" data-bind="fileChooser: $data, value: value, uniqueName: false" />
+                </td>
+                <td><a class="btn" href="#" data-bind="click: $root.removePrepare">${ _('Delete') }</a></td>
+              </tr>
+            </tbody>
+          </table>
+
+          % if len(action_form['prepares'].errors):
+            <div class="alert alert-error">
+              ${ unicode(action_form['prepares'].errors) | n }
+            </div>
+          % endif
+
+          <button class="btn" data-bind="click: addPrepareDelete">${ _('Add delete') }</button>
+          <button class="btn" data-bind="click: addPrepareMkdir">${ _('Add mkdir') }</button>
+        </div>
+      </div>
+    % endif
+
     % if 'params' in action_form.fields:
     % if 'params' in action_form.fields:
       <div class="control-group">
       <div class="control-group">
         <label class="control-label">${ _('Params') }</label>
         <label class="control-label">${ _('Params') }</label>
@@ -275,13 +312,14 @@ ${ layout.menubar(section='workflows') }
         });
         });
     };
     };
 
 
-    var ViewModel = function(properties, files, archives, params) {
+    var ViewModel = function(properties, files, archives, params, prepares) {
         var self = this;
         var self = this;
 
 
         self.properties = ko.observableArray(properties);
         self.properties = ko.observableArray(properties);
         self.files = ko.observableArray(files);
         self.files = ko.observableArray(files);
         self.archives = ko.observableArray(archives);
         self.archives = ko.observableArray(archives);
         self.params = ko.observableArray(params);
         self.params = ko.observableArray(params);
+        self.prepares = ko.observableArray(prepares);
 
 
         self.addProp = function() {
         self.addProp = function() {
             self.properties.push({ name: "", value: "" });
             self.properties.push({ name: "", value: "" });
@@ -304,6 +342,18 @@ ${ layout.menubar(section='workflows') }
             self.params.remove(val);
             self.params.remove(val);
         };
         };
 
 
+        self.addPrepareDelete = function() {
+            self.prepares.push({ value: "", type: "delete" });
+        };
+
+        self.addPrepareMkdir = function() {
+            self.prepares.push({ value: "", type: "mkdir" });
+        };
+
+        self.removePrepare = function(val) {
+            self.prepares.remove(val);
+        };
+
         self.addFile = function() {
         self.addFile = function() {
             self.files.push({ name: "", dummy: "" });
             self.files.push({ name: "", dummy: "" });
         };
         };
@@ -325,7 +375,7 @@ ${ layout.menubar(section='workflows') }
             var files_arr = dictArrayToArray(ko.toJS(self.files));
             var files_arr = dictArrayToArray(ko.toJS(self.files));
             var archives_arr = dictArrayToArray(ko.toJS(self.archives));
             var archives_arr = dictArrayToArray(ko.toJS(self.archives));
 
 
-            // Beware dirty
+            // Beware: dirty
             $("<input>").attr("type", "hidden")
             $("<input>").attr("type", "hidden")
                 .attr("name", "job_properties")
                 .attr("name", "job_properties")
                 .attr("value", ko.utils.stringifyJson(self.properties))
                 .attr("value", ko.utils.stringifyJson(self.properties))
@@ -342,6 +392,10 @@ ${ layout.menubar(section='workflows') }
                 .attr("name", "params")
                 .attr("name", "params")
                 .attr("value", ko.utils.stringifyJson(self.params))
                 .attr("value", ko.utils.stringifyJson(self.params))
                 .appendTo(form);
                 .appendTo(form);
+            $("<input>").attr("type", "hidden")
+                .attr("name", "prepares")
+                .attr("value", ko.utils.stringifyJson(self.prepares))
+                .appendTo(form);
 
 
             form.submit();
             form.submit();
         };
         };
@@ -351,7 +405,8 @@ ${ layout.menubar(section='workflows') }
               ${ job_properties },
               ${ job_properties },
               arrayToDictArray(${ files }),
               arrayToDictArray(${ files }),
               arrayToDictArray(${ archives }),
               arrayToDictArray(${ archives }),
-              ${ params });
+              ${ params },
+              ${ prepares });
 
 
     ko.bindingHandlers.fileChooser = {
     ko.bindingHandlers.fileChooser = {
           init: function(element, valueAccessor, allBindings, model) {
           init: function(element, valueAccessor, allBindings, model) {

+ 11 - 0
apps/oozie/src/oozie/templates/editor/gen/workflow-common.xml.mako

@@ -26,6 +26,17 @@ import posixpath
 <%def name="filelink(path)">${ path + '#' + posixpath.basename(path) }</%def>
 <%def name="filelink(path)">${ path + '#' + posixpath.basename(path) }</%def>
 
 
 
 
+<%def name="prepares(prepares)">
+        % if prepares:
+            <prepare>
+                % for p in prepares:
+                <${ p['type'] } path="${ p['value'] }"/>
+                % endfor
+            </prepare>
+        % endif
+</%def>
+
+
 <%def name="configuration(properties)">
 <%def name="configuration(properties)">
         % if properties:
         % if properties:
             <configuration>
             <configuration>

+ 1 - 0
apps/oozie/src/oozie/templates/editor/gen/workflow-java.xml.mako

@@ -21,6 +21,7 @@
             <job-tracker>${'${'}jobTracker}</job-tracker>
             <job-tracker>${'${'}jobTracker}</job-tracker>
             <name-node>${'${'}nameNode}</name-node>
             <name-node>${'${'}nameNode}</name-node>
 
 
+            ${ common.prepares(node.get_prepares()) }
             ${ common.configuration(node.get_properties()) }
             ${ common.configuration(node.get_properties()) }
 
 
             <main-class>${ node.main_class }</main-class>
             <main-class>${ node.main_class }</main-class>

+ 1 - 0
apps/oozie/src/oozie/templates/editor/gen/workflow-mapreduce.xml.mako

@@ -21,6 +21,7 @@
             <job-tracker>${'${'}jobTracker}</job-tracker>
             <job-tracker>${'${'}jobTracker}</job-tracker>
             <name-node>${'${'}nameNode}</name-node>
             <name-node>${'${'}nameNode}</name-node>
 
 
+            ${ common.prepares(node.get_prepares()) }
             ${ common.configuration(node.get_properties()) }
             ${ common.configuration(node.get_properties()) }
 
 
             ${ common.distributed_cache(node.get_files(), node.get_archives()) }
             ${ common.distributed_cache(node.get_files(), node.get_archives()) }

+ 1 - 0
apps/oozie/src/oozie/templates/editor/gen/workflow-pig.xml.mako

@@ -21,6 +21,7 @@
             <job-tracker>${'${'}jobTracker}</job-tracker>
             <job-tracker>${'${'}jobTracker}</job-tracker>
             <name-node>${'${'}nameNode}</name-node>
             <name-node>${'${'}nameNode}</name-node>
 
 
+            ${ common.prepares(node.get_prepares()) }
             ${ common.configuration(node.get_properties()) }
             ${ common.configuration(node.get_properties()) }
 
 
             <script>${ node.script_path }</script>
             <script>${ node.script_path }</script>

+ 5 - 0
apps/oozie/src/oozie/templates/editor/import_workflow_action.mako

@@ -45,6 +45,11 @@
               <td>${ action.description }</td>
               <td>${ action.description }</td>
             </tr>
             </tr>
           %endfor
           %endfor
+          % if not available_actions:
+            <tr class="action-row">
+              <td>${ _('N/A') }</td><td></td><td></td>
+            </tr>
+          % endif
         </tbody>
         </tbody>
       </table>
       </table>
     </div>
     </div>

+ 1 - 1
apps/oozie/src/oozie/templates/editor/job_action_properties.mako

@@ -15,7 +15,7 @@
 ## limitations under the License.
 ## limitations under the License.
 
 
 <%!
 <%!
-from django.utils.translation import ugettext as _
+  from django.utils.translation import ugettext as _
 %>
 %>
 
 
 <%namespace name="utils" file="../../utils.inc.mako" />
 <%namespace name="utils" file="../../utils.inc.mako" />

+ 14 - 4
apps/oozie/src/oozie/tests.py

@@ -131,7 +131,8 @@ class TestEditor:
 
 
 
 
   def test_find_all_parameters(self):
   def test_find_all_parameters(self):
-        assert_equal([{'name': u'SLEEP', 'value': ''}, {'name': u'market', 'value': u'US'}], self.wf.find_all_parameters())
+        assert_equal([{'name': u'output', 'value': u''}, {'name': u'SLEEP', 'value': ''}, {'name': u'market', 'value': u'US'}],
+                     self.wf.find_all_parameters())
 
 
 
 
   def test_move_up(self):
   def test_move_up(self):
@@ -342,6 +343,9 @@ class TestEditor:
         '        <map-reduce>\n'
         '        <map-reduce>\n'
         '           <job-tracker>${jobTracker}</job-tracker>\n'
         '           <job-tracker>${jobTracker}</job-tracker>\n'
         '            <name-node>${nameNode}</name-node>\n'
         '            <name-node>${nameNode}</name-node>\n'
+        '            <prepare>\n'
+        '                <delete path="${output}"/>\n'
+        '            </prepare>\n'
         '            <configuration>\n'
         '            <configuration>\n'
         '                <property>\n'
         '                <property>\n'
         '                    <name>sleep</name>\n'
         '                    <name>sleep</name>\n'
@@ -356,6 +360,9 @@ class TestEditor:
         '        <map-reduce>\n'
         '        <map-reduce>\n'
         '            <job-tracker>${jobTracker}</job-tracker>\n'
         '            <job-tracker>${jobTracker}</job-tracker>\n'
         '            <name-node>${nameNode}</name-node>\n'
         '            <name-node>${nameNode}</name-node>\n'
+        '            <prepare>\n'
+        '                <delete path="${output}"/>\n'
+        '            </prepare>\n'
         '            <configuration>\n'
         '            <configuration>\n'
         '                <property>\n'
         '                <property>\n'
         '                    <name>sleep</name>\n'
         '                    <name>sleep</name>\n'
@@ -370,6 +377,9 @@ class TestEditor:
         '        <map-reduce>\n'
         '        <map-reduce>\n'
         '            <job-tracker>${jobTracker}</job-tracker>\n'
         '            <job-tracker>${jobTracker}</job-tracker>\n'
         '            <name-node>${nameNode}</name-node>\n'
         '            <name-node>${nameNode}</name-node>\n'
+        '            <prepare>\n'
+        '                <delete path="${output}"/>\n'
+        '            </prepare>\n'
         '            <configuration>\n'
         '            <configuration>\n'
         '                <property>\n'
         '                <property>\n'
         '                    <name>sleep</name>\n'
         '                    <name>sleep</name>\n'
@@ -814,7 +824,8 @@ class TestEditor:
 
 
 
 
 # Utils
 # Utils
-WORKFLOW_DICT = {u'deployment_dir': [u''], u'name': [u'wf-name-1'], u'description': [u''], u'parameters': [u'[{"name":"market","value":"US"}]']}
+WORKFLOW_DICT = {u'deployment_dir': [u''], u'name': [u'wf-name-1'], u'description': [u''],
+                 u'parameters': [u'[{"name":"market","value":"US"}]']}
 
 
 
 
 # Beware: client not consistent with self.c in TestEditor
 # Beware: client not consistent with self.c in TestEditor
@@ -823,8 +834,7 @@ def add_action(workflow, action, name):
 
 
   response = c.post("/oozie/new_action/%s/%s/%s" % (workflow, 'mapreduce', action), {
   response = c.post("/oozie/new_action/%s/%s/%s" % (workflow, 'mapreduce', action), {
      u'files': [u'[]'], u'name': [name], u'jar_path': [u'/tmp/.file.jar'], u'job_properties': [u'[{"name":"sleep","value":"${SLEEP}"}]'],
      u'files': [u'[]'], u'name': [name], u'jar_path': [u'/tmp/.file.jar'], u'job_properties': [u'[{"name":"sleep","value":"${SLEEP}"}]'],
-     u'archives': [u'[]'], u'description': [u'']}, follow=True)
-  assert_equal(200, response.status_code)
+     u'archives': [u'[]'], u'description': [u''], u'prepares': [u'[{"type":"delete","value":"${output}"}]']}, follow=True)
   assert_true(Node.objects.filter(name=name).exists(), response)
   assert_true(Node.objects.filter(name=name).exists(), response)
   return Node.objects.get(name=name)
   return Node.objects.get(name=name)
 
 

+ 2 - 0
apps/oozie/src/oozie/views/editor.py

@@ -432,6 +432,7 @@ def new_action(request, workflow, node_type, parent_action_id):
       'files': extract_field_data(action_form['files']),
       'files': extract_field_data(action_form['files']),
       'archives': extract_field_data(action_form['archives']),
       'archives': extract_field_data(action_form['archives']),
       'params': 'params' in action_form.fields and extract_field_data(action_form['params']) or '[]',
       'params': 'params' in action_form.fields and extract_field_data(action_form['params']) or '[]',
+      'prepares': 'prepares' in action_form.fields and extract_field_data(action_form['prepares']) or '[]',
       'action_form': action_form,
       'action_form': action_form,
       'node_type': node_type,
       'node_type': node_type,
       'properties_hint': _STD_PROPERTIES_JSON,
       'properties_hint': _STD_PROPERTIES_JSON,
@@ -460,6 +461,7 @@ def edit_action(request, action):
     'files': extract_field_data(action_form['files']),
     'files': extract_field_data(action_form['files']),
     'archives': extract_field_data(action_form['archives']),
     'archives': extract_field_data(action_form['archives']),
     'params': 'params' in action_form.fields and extract_field_data(action_form['params']) or '[]',
     'params': 'params' in action_form.fields and extract_field_data(action_form['params']) or '[]',
+    'prepares': 'prepares' in action_form.fields and extract_field_data(action_form['prepares']) or '[]',
     'action_form': action_form,
     'action_form': action_form,
     'node_type': action.node_type,
     'node_type': action.node_type,
     'properties_hint': _STD_PROPERTIES_JSON,
     'properties_hint': _STD_PROPERTIES_JSON,