瀏覽代碼

HUE-2488 [oozie] Validate the action properties

Support for modelchange and before save validation
Restyle of editing widgets (more spacing and correct alignment)
Enrico Berti 10 年之前
父節點
當前提交
142eb1d

+ 4 - 0
apps/oozie/src/oozie/static/oozie/css/common-editor.css

@@ -65,6 +65,10 @@ input[type="text"].thirty {
   width: 30%;
 }
 
+input[type="text"].with-errors, textarea.with-errors {
+  border: solid 1px #EE5F5B;
+}
+
 .fileChooserBtn {
   height: 31px;
 }

+ 13 - 0
apps/oozie/src/oozie/static/oozie/css/workflow-editor.css

@@ -284,4 +284,17 @@ ul.unstyled li {
 
 .widget-running {
   opacity: .5;
+}
+
+.airy {
+  margin-bottom: 3px;
+}
+
+.widget-label {
+  display: inline-block;
+  width: 80px;
+}
+
+.widget-label-large {
+  width: 90px;
 }

+ 1 - 0
apps/oozie/src/oozie/static/oozie/js/workflow-editor.ko.js

@@ -474,6 +474,7 @@ var WorkflowEditorViewModel = function (layout_json, workflow_json, credentials_
   };
   self.isSaving = ko.observable(false);
 
+  self.isInvalid = ko.observable(false);
   self.isRunning = ko.observable(false);
 
   self.newAction = ko.observable();

+ 83 - 76
apps/oozie/src/oozie/templates/editor2/common_workflow.mako

@@ -414,7 +414,7 @@
   </h6>
   <ul class="unstyled" data-bind="visible: properties.arguments().length > 0, foreach: properties.arguments">
     <li>
-      <input type="text" class="input-medium" data-bind="value: value, attr: { placeholder: $root.workflow_properties.arguments.help_text }"/>
+      <input type="text" class="input-medium" data-bind="value: value, attr: { placeholder: $root.workflow_properties.arguments.help_text }" validate="nonempty"/>
       <a href="#" data-bind="click: function(){ $parent.properties.arguments.remove(this); $(document).trigger('drawArrows') }">
         <i class="fa fa-minus"></i>
       </a>
@@ -432,7 +432,7 @@
   </h6>
   <ul class="unstyled" data-bind="visible: properties.arguments().length > 0, foreach: properties.arguments">
     <li style="margin-bottom: 3px">
-      <input type="text" class="span11" data-bind="value: value, attr: { placeholder: $root.workflow_properties.arguments.help_text }"/>
+      <input type="text" class="span11" data-bind="value: value, attr: { placeholder: $root.workflow_properties.arguments.help_text }" validate="nonempty"/>
       <a href="#" data-bind="click: function(){ $parent.properties.arguments.remove(this); $(document).trigger('drawArrows') }">
         <i class="fa fa-minus"></i>
       </a>
@@ -449,7 +449,7 @@
   </h6>
   <ul class="unstyled" data-bind="foreach: properties.files">
     <li style="margin-bottom: 3px">
-      <input type="text" class="filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value"/>
+      <input type="text" class="filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value" validate="nonempty"/>
       <span data-bind='template: { name: "common-fs-link", data: { path: value(), with_label: false} }'></span>
       <a href="#" data-bind="click: function(){ $parent.properties.files.remove(this); $(document).trigger('drawArrows') }">
         <i class="fa fa-minus"></i>
@@ -467,7 +467,7 @@
   </h6>
   <ul class="unstyled" data-bind="foreach: properties.parameters">
     <li style="margin-bottom: 3px">
-      <input type="text" class="filechooser-input seventy" data-bind="value: value, filechooser: value, filechooserOptions: globalFilechooserOptions, filechooserPrefixSeparator: '=', hdfsAutocomplete: value, attr: { placeholder: ' ${ _("Fill me up!") }' }, typeahead: { target: value, source: $parent.actionParametersUI, sourceSuffix: '=', triggerOnFocus: true }"/>
+      <input type="text" class="filechooser-input seventy" data-bind="value: value, filechooser: value, filechooserOptions: globalFilechooserOptions, filechooserPrefixSeparator: '=', hdfsAutocomplete: value, attr: { placeholder: ' ${ _("Fill me up!") }' }, typeahead: { target: value, source: $parent.actionParametersUI, sourceSuffix: '=', triggerOnFocus: true }"  validate="nonempty"/>
       <span data-bind='template: { name: "param-fs-link", data: {path: value()} }'></span>
       <a href="#" data-bind="click: function(){ $parent.properties.parameters.remove(this); $(document).trigger('drawArrows') }">
         <i class="fa fa-minus"></i>
@@ -556,7 +556,7 @@
 
     <div data-bind="visible: $root.isEditing">
       <div data-bind="visible: ! $parent.ooziePropertiesExpanded()" class="nowrap">
-        <input type="text" style="width:25%" class="filechooser-input" data-bind="filechooser: properties.script_path, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: properties.script_path, attr: { placeholder:  $root.workflow_properties.script_path.help_text }"/>
+        <input type="text" style="width:25%" class="filechooser-input" data-bind="filechooser: properties.script_path, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: properties.script_path, attr: { placeholder:  $root.workflow_properties.script_path.help_text }" validate="nonempty"/>
         <span data-bind='template: { name: "common-fs-link", data: {path: properties.script_path(), with_label: false}}'></span>
 
         &nbsp;&nbsp;
@@ -614,7 +614,7 @@
 
     <div data-bind="visible: $root.isEditing">
       <div data-bind="visible: ! $parent.ooziePropertiesExpanded()" class="nowrap">
-        <input type="text" class="filechooser-input" data-bind="filechooser: properties.script_path, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: properties.script_path, attr: { placeholder:  $root.workflow_properties.script_path.help_text }"/>
+        <input type="text" class="filechooser-input" data-bind="filechooser: properties.script_path, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: properties.script_path, attr: { placeholder:  $root.workflow_properties.script_path.help_text }" validate="nonempty"/>
         <span data-bind='template: { name: "common-fs-link", data: {path: properties.script_path(), with_label: false}}'></span>
 
         <div class="row-fluid">
@@ -674,7 +674,7 @@
 
     <div data-bind="visible: $root.isEditing">
       <div data-bind="visible: ! $parent.ooziePropertiesExpanded()" class="nowrap">
-        <input type="text" class="filechooser-input" data-bind="filechooser: properties.script_path, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: properties.script_path, attr: { placeholder:  $root.workflow_properties.script_path.help_text }" />
+        <input type="text" class="filechooser-input" data-bind="filechooser: properties.script_path, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: properties.script_path, attr: { placeholder:  $root.workflow_properties.script_path.help_text }" validate="nonempty"/>
         <span data-bind='template: { name: "common-fs-link", data: {path: properties.script_path(), with_label: false}}'></span>
 
         <div class="row-fluid">
@@ -735,30 +735,31 @@
     <div data-bind="visible: $root.isEditing">
       <div data-bind="visible: ! $parent.ooziePropertiesExpanded()" class="nowrap">
 
-        <div class="row-fluid">
-          <div class="span6">
-            <span data-bind="text: $root.workflow_properties.spark_master.label" style="display: inline-block"></span>
-            <input type="text" class="input-medium" data-bind="value: properties.spark_master, attr: { placeholder: $root.workflow_properties.spark_master.help_text }" />
-          </div>
-          <div class="span6">
-            <span data-bind="text: $root.workflow_properties.mode.label" style="display: inline-block"></span>
-            <input type="text" class="input-medium" data-bind="value: properties.mode, attr: { placeholder: $root.workflow_properties.mode.help_text }" />
-          </div>
+        <div class="airy">
+          <span class="widget-label" data-bind="text: $root.workflow_properties.spark_master.label"></span>
+          <input type="text" class="input-medium" data-bind="value: properties.spark_master, attr: { placeholder: $root.workflow_properties.spark_master.help_text }" />
         </div>
 
-        <span data-bind="text: $root.workflow_properties.app_name.label" style="display: inline-block; width: 75px"></span>
-        <input type="text" class="input-xlarge" data-bind="value: properties.app_name, attr: { placeholder: $root.workflow_properties.app_name.help_text }" />
-
-        <br/>
+        <div class="airy">
+          <span class="widget-label" data-bind="text: $root.workflow_properties.mode.label"></span>
+          <input type="text" class="input-medium" data-bind="value: properties.mode, attr: { placeholder: $root.workflow_properties.mode.help_text }" />
+        </div>
 
-        <span data-bind="text: $root.workflow_properties.jars.label" style="display: inline-block; width: 75px"></span>
-        <input type="text" class="filechooser-input" data-bind="filechooser: properties.jars, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: properties.jars, attr: { placeholder:  $root.workflow_properties.jars.help_text }" />
-        <span data-bind='template: { name: "common-fs-link", data: {path: properties.jars(), with_label: false}}'></span>
+        <div class="airy">
+          <span class="widget-label" data-bind="text: $root.workflow_properties.app_name.label"></span>
+          <input type="text" class="input-xlarge" data-bind="value: properties.app_name, attr: { placeholder: $root.workflow_properties.app_name.help_text }" />
+        </div>
 
-        <br/>
+        <div class="airy">
+          <span class="widget-label" data-bind="text: $root.workflow_properties.jars.label"></span>
+          <input type="text" class="filechooser-input" data-bind="filechooser: properties.jars, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: properties.jars, attr: { placeholder:  $root.workflow_properties.jars.help_text }" validate="nonempty"/>
+          <span data-bind='template: { name: "common-fs-link", data: {path: properties.jars(), with_label: false}}'></span>
+        </div>
 
-        <span data-bind="text: $root.workflow_properties.class.label" style="display: inline-block; width: 75px"></span>
-        <input type="text" class="input-xlarge" data-bind="value: properties.class, attr: { placeholder: $root.workflow_properties.class.help_text }" />
+        <div class="airy">
+          <span class="widget-label" data-bind="text: $root.workflow_properties.class.label"></span>
+          <input type="text" class="input-xlarge" data-bind="value: properties.class, attr: { placeholder: $root.workflow_properties.class.help_text }" />
+        </div>
 
         <h6>
           <a class="pointer" data-bind="click: function(){ properties.spark_arguments.push(ko.mapping.fromJS({'value': ''})); $(document).trigger('drawArrows') }">
@@ -767,7 +768,7 @@
         </h6>
         <ul class="unstyled" data-bind="visible: properties.spark_arguments().length > 0, foreach: properties.spark_arguments">
           <li>
-            <input type="text" class="input-xlarge filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, attr: { placeholder:  $root.workflow_properties.spark_arguments.help_text }" />
+            <input type="text" class="input-xlarge filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, attr: { placeholder:  $root.workflow_properties.spark_arguments.help_text }" validate="nonempty"/>
             <span data-bind='template: { name: "common-fs-link", data: {path: value, with_label: false}}'></span>
             <a href="#" data-bind="click: function(){ $parent.properties.spark_arguments.remove(this); $(document).trigger('drawArrows') }">
               <i class="fa fa-minus"></i>
@@ -787,7 +788,7 @@
       </ul>
       <div class="tab-content">
         <div class="tab-pane active" data-bind="attr: { id: 'properties-' + id() }">
-          <span data-bind="text: $root.workflow_properties.spark_opts.label" style="display: inline-block; width: 75px"></span>
+          <span class="widget-label"  data-bind="text: $root.workflow_properties.spark_opts.label"></span>
           <input type="text" class="input-xlarge" data-bind="value: properties.spark_opts, attr: { placeholder: $root.workflow_properties.spark_opts.help_text }" />
 
           <br/>
@@ -823,15 +824,16 @@
 
     <div data-bind="visible: $root.isEditing">
       <div data-bind="visible: ! $parent.ooziePropertiesExpanded()" class="nowrap">
-        <span data-bind="text: $root.workflow_properties.jar_path.label" style="display: inline-block; width: 75px"></span>
-        <input type="text" class="filechooser-input input-xlarge"
-            data-bind="filechooser: properties.jar_path, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: properties.jar_path, attr: { placeholder: $root.workflow_properties.jar_path.help_text }"
-        />
-        <span data-bind='template: { name: "common-fs-link", data: {path: properties.jar_path(), with_label: false}}'></span>
+        <div class="airy">
+          <span class="widget-label"  data-bind="text: $root.workflow_properties.jar_path.label"></span>
+          <input type="text" class="filechooser-input input-xlarge" data-bind="filechooser: properties.jar_path, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: properties.jar_path, attr: { placeholder: $root.workflow_properties.jar_path.help_text }" validate="nonempty"/>
+          <span data-bind='template: { name: "common-fs-link", data: {path: properties.jar_path(), with_label: false}}'></span>
+        </div>
 
-        <br/>
-        <span data-bind="text: $root.workflow_properties.main_class.label" style="display: inline-block; width: 75px"></span>
-        <input type="text" class="input-xlarge" data-bind="value: properties.main_class, attr: { placeholder: $root.workflow_properties.main_class.help_text }" />
+        <div class="airy">
+          <span class="widget-label" data-bind="text: $root.workflow_properties.main_class.label"></span>
+          <input type="text" class="input-xlarge" data-bind="value: properties.main_class, attr: { placeholder: $root.workflow_properties.main_class.help_text }" validate="nonempty"/>
+        </div>
 
         <div class="row-fluid">
           <div class="span6" data-bind="template: { name: 'common-properties-arguments' }"></div>
@@ -897,7 +899,7 @@
     <div data-bind="visible: $root.isEditing">
       <div data-bind="visible: ! $parent.ooziePropertiesExpanded()" class="nowrap">
         <span data-bind="text: $root.workflow_properties.command.label"></span>
-        <textarea data-bind="value: properties.command" class="input-xlarge" style="resize:both"></textarea>
+        <textarea data-bind="value: properties.command" class="input-xlarge" style="resize:both" validate="nonempty"></textarea>
 
         <div class="row-fluid">
           <div class="span6" data-bind="template: { name: 'common-properties-arguments' }"></div>
@@ -949,7 +951,7 @@
     <div data-bind="visible: $root.isEditing">
       <div data-bind="visible: ! $parent.ooziePropertiesExpanded()" class="nowrap">
         <span data-bind="text: $root.workflow_properties.jar_path.label"></span>
-        <input type="text" class="filechooser-input" data-bind="filechooser: properties.jar_path, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: properties.jar_path, value: properties.jar_path" />
+        <input type="text" class="filechooser-input" data-bind="filechooser: properties.jar_path, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: properties.jar_path, value: properties.jar_path" validate="nonempty"/>
         <span data-bind='template: { name: "common-fs-link", data: {path: properties.jar_path(), with_label: false} }'></span>
 
         <h6>
@@ -959,8 +961,8 @@
         </h6>
         <ul data-bind="visible: properties.job_properties().length > 0, foreach: properties.job_properties" class="unstyled">
           <li>
-            <input type="text" class="span4" data-bind="value: name" placeholder="${ _('name, e.g. mapred.job.queue.name') }"/>
-            <input type="text" class="filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, value: value, attr: { placeholder: $root.workflow_properties.job_properties.help_text }"/>
+            <input type="text" class="span4" data-bind="value: name" placeholder="${ _('name, e.g. mapred.job.queue.name') }" validate="nonempty"/>
+            <input type="text" class="filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, value: value, attr: { placeholder: $root.workflow_properties.job_properties.help_text }" validate="nonempty"/>
             <a href="#" data-bind="click: function(){ $parent.properties.job_properties.remove(this); $(document).trigger('drawArrows') }">
               <i class="fa fa-minus"></i>
             </a>
@@ -1064,9 +1066,9 @@
         </h6>
         <ul data-bind="visible: properties.job_properties().length > 0, foreach: properties.job_properties" class="unstyled">
           <li>
-            <input type="text" data-bind="value: name" placeholder="${ _('Name, e.g. input') }"/>
+            <input type="text" class="span4" data-bind="value: name" placeholder="${ _('Name, e.g. input') }" validate="nonempty"/>
 
-            <input type="text" class="input-xlarge filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, attr: { placeholder:  $root.workflow_properties.job_properties.help_text }" />
+            <input type="text" class="filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, attr: { placeholder:  $root.workflow_properties.job_properties.help_text }" validate="nonempty" />
             <span data-bind='template: { name: "common-fs-link", data: {path: value, with_label: false}}'></span>
 
             <a href="#" data-bind="click: function(){ $parent.properties.job_properties.remove(this); $(document).trigger('drawArrows') }">
@@ -1126,7 +1128,7 @@
   <div class="row-fluid" data-bind="with: $root.workflow.getNodeById(id())" style="padding: 10px">
     <div data-bind="visible: $root.isEditing">
       <div data-bind="visible: ! $parent.ooziePropertiesExpanded()" class="nowrap">
-        <input type="text" data-bind="value: properties.shell_command" />
+        <input type="text" data-bind="value: properties.shell_command" validate="nonempty"/>
         <span data-bind='template: { name: "common-fs-link", data: {path: properties.shell_command(), with_label: false} }'></span>
 
         <div class="row-fluid">
@@ -1195,12 +1197,14 @@
     <div data-bind="visible: $root.isEditing">
 
       <div data-bind="visible: ! $parent.ooziePropertiesExpanded()" class="nowrap">
-        <span data-bind="text: $root.workflow_properties.host.label"></span>
-        <input type="text" data-bind="value: properties.host" />
-        <br/>
-
-        <span data-bind="text: $root.workflow_properties.ssh_command.label"></span>
-        <input type="text" data-bind="value: properties.ssh_command" />
+        <div class="airy">
+          <span class="widget-label widget-label-large" data-bind="text: $root.workflow_properties.host.label"></span>
+          <input type="text" data-bind="value: properties.host" validate="nonempty"/>
+        </div>
+        <div class="airy">
+          <span class="widget-label widget-label-large" data-bind="text: $root.workflow_properties.ssh_command.label"></span>
+          <input type="text" data-bind="value: properties.ssh_command" validate="nonempty"/>
+        </div>
         <div class="row-fluid">
           <div class="span6" data-bind="template: { name: 'common-properties-arguments' }"></div>
         </div>
@@ -1313,7 +1317,7 @@
         </h6>
         <ul data-bind="foreach: properties.deletes" class="unstyled">
           <li>
-            <input type="text" class="input-xlarge filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, value: value, attr: { placeholder: $root.workflow_properties.deletes.help_text }"/>
+            <input type="text" class="input-xlarge filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, value: value, attr: { placeholder: $root.workflow_properties.deletes.help_text }" validate="nonempty"/>
             <span data-bind='template: { name: "common-fs-link", data: {path: value(), with_label: false} }, visible: value().length > 0'></span>
             <a href="#" data-bind="click: function(){ $parent.properties.deletes.remove(this); $(document).trigger('drawArrows') }">
               <i class="fa fa-minus"></i>
@@ -1328,7 +1332,7 @@
         </h6>
         <ul data-bind="foreach: properties.mkdirs" class="unstyled">
           <li>
-            <input type="text" class="input-xlarge filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, value: value, attr: { placeholder: $root.workflow_properties.mkdirs.help_text }"/>
+            <input type="text" class="input-xlarge filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, value: value, attr: { placeholder: $root.workflow_properties.mkdirs.help_text }" validate="nonempty"/>
             <span data-bind='template: { name: "common-fs-link", data: {path: value(), with_label: false} }, visible: value().length > 0'></span>
             <a href="#" data-bind="click: function(){ $parent.properties.mkdirs.remove(this); $(document).trigger('drawArrows') }">
               <i class="fa fa-minus"></i>
@@ -1343,7 +1347,7 @@
         </h6>
         <ul data-bind="foreach: properties.touchzs" class="unstyled">
           <li>
-            <input type="text" class="input-xlarge filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, value: value, attr: { placeholder: $root.workflow_properties.touchzs.help_text }"/>
+            <input type="text" class="input-xlarge filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, value: value, attr: { placeholder: $root.workflow_properties.touchzs.help_text }" validate="nonempty"/>
             <span data-bind='template: { name: "common-fs-link", data: {path: value(), with_label: false} }, visible: value().length > 0'></span>
             <a href="#" data-bind="click: function(){ $parent.properties.touchzs.remove(this); $(document).trigger('drawArrows') }">
               <i class="fa fa-minus"></i>
@@ -1358,10 +1362,10 @@
         </h6>
         <ul data-bind="foreach: properties.moves" class="unstyled">
           <li>
-            <input type="text" class="filechooser-input thirty" data-bind="filechooser: source, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: source, value: source" placeholder="${ _('Source path') }"/>
+            <input type="text" class="filechooser-input thirty" data-bind="filechooser: source, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: source, value: source" placeholder="${ _('Source path') }" validate="nonempty"/>
             <span data-bind='template: { name: "common-fs-link", data: {path: source(), with_label: false} }, visible: source().length > 0'></span>
 
-            <input type="text" class="filechooser-input thirty" data-bind="filechooser: destination, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: destination, value: destination" placeholder="${ _('New destination path') }"/>
+            <input type="text" class="filechooser-input thirty" data-bind="filechooser: destination, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: destination, value: destination" placeholder="${ _('New destination path') }" validate="nonempty"/>
             <span data-bind='template: { name: "common-fs-link", data: {path: destination(), with_label: false} }, visible: destination().length > 0'></span>
             <a href="#" data-bind="click: function(){ $parent.properties.moves.remove(this); $(document).trigger('drawArrows') }">
               <i class="fa fa-minus"></i>
@@ -1449,16 +1453,20 @@
   <div class="row-fluid" data-bind="with: $root.workflow.getNodeById(id())" style="padding: 10px">
     <div data-bind="visible: $root.isEditing">
       <div data-bind="visible: ! $parent.ooziePropertiesExpanded()" class="nowrap">
-        <span data-bind="text: $root.workflow_properties.to.label" style="display: inline-block; width: 80px"></span>
-        <input type="text" data-bind="value: properties.to, attr: { placeholder: $root.workflow_properties.to.help_text }" />
-        <br/>
+        <div class="airy">
+          <span class="widget-label" data-bind="text: $root.workflow_properties.to.label"></span>
+          <input type="text" data-bind="value: properties.to, attr: { placeholder: $root.workflow_properties.to.help_text }" validate="nonempty"/>
+        </div>
 
-        <span data-bind="text: $root.workflow_properties.subject.label" style="display: inline-block; width: 80px"></span>
-        <input type="text" data-bind="value: properties.subject, attr: { placeholder: $root.workflow_properties.subject.help_text }" />
-        <br/>
+        <div class="airy">
+          <span class="widget-label" data-bind="text: $root.workflow_properties.subject.label"></span>
+          <input type="text" data-bind="value: properties.subject, attr: { placeholder: $root.workflow_properties.subject.help_text }" />
+        </div>
 
-        <span data-bind="text: $root.workflow_properties.body.label" style="display: inline-block; width: 80px"></span>
-        <textarea data-bind="value: properties.body, attr: { placeholder: $root.workflow_properties.body.help_text }"></textarea>
+        <div class="airy">
+          <span class="widget-label" data-bind="text: $root.workflow_properties.body.label"></span>
+          <textarea data-bind="value: properties.body, attr: { placeholder: $root.workflow_properties.body.help_text }"></textarea>
+        </div>
       </div>
     </div>
 
@@ -1507,18 +1515,16 @@
   <div class="row-fluid" data-bind="with: $root.workflow.getNodeById(id())" style="padding: 10px">
     <div data-bind="visible: $root.isEditing">
       <div data-bind="visible: ! $parent.ooziePropertiesExpanded()" class="nowrap">
-        <ul class="unstyled">
-          <li>
-            <span data-bind="text: $root.workflow_properties.mapper.label"></span>
-            <input type="text" data-bind="value: properties.mapper" />
-            <span data-bind='template: { name: "common-fs-link", data: {path: properties.mapper(), with_label: false} }'></span>
-          </li>
-          <li>
-            <span data-bind="text: $root.workflow_properties.reducer.label"></span>
-            <input type="text" data-bind="value: properties.reducer" />
-            <span data-bind='template: { name: "common-fs-link", data: {path: properties.reducer(), with_label: false} }'></span>
-          </li>
-        </ul>
+        <div class="airy">
+          <span class="widget-label" data-bind="text: $root.workflow_properties.mapper.label"></span>
+          <input type="text" data-bind="value: properties.mapper" validate="nonempty"/>
+          <span data-bind='template: { name: "common-fs-link", data: {path: properties.mapper(), with_label: false} }'></span>
+        </div>
+        <div class="airy">
+          <span class="widget-label" data-bind="text: $root.workflow_properties.reducer.label"></span>
+          <input type="text" data-bind="value: properties.reducer" validate="nonempty"/>
+          <span data-bind='template: { name: "common-fs-link", data: {path: properties.reducer(), with_label: false} }'></span>
+        </div>
         <div data-bind="template: { name: 'common-properties-files' }"></div>
       </div>
     </div>
@@ -1575,7 +1581,7 @@
         </h6>
         <ul data-bind="foreach: properties.distcp_parameters" class="unstyled">
           <li>
-            <input type="text" class="input-xlarge filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, value: value, attr: { placeholder: $root.workflow_properties.distcp_parameters.help_text }"/>
+            <input type="text" class="input-xlarge filechooser-input" data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, value: value, attr: { placeholder: $root.workflow_properties.distcp_parameters.help_text }" validate="nonempty"/>
             <span data-bind='template: { name: "common-fs-link", data: {path: value(), with_label: false} }, visible: value().length > 0'></span>
             <a href="#" data-bind="click: function(){ $parent.properties.distcp_parameters.remove(this);  }">
               <i class="fa fa-minus"></i>
@@ -1609,7 +1615,8 @@
      <h6>${ _('Prepare') }</h6>
        <ul data-bind="visible: properties.prepares().length > 0, foreach: properties.prepares" class="unstyled">
          <li>
-           <div style="display: inline-block; width: 60px" data-bind="text: type"></div>
+
+'           <div style="display: inline-block; width: 60px" data-bind="text: type"></div>
            <input type="text" class="filechooser-input input-xlarge"
                 data-bind="filechooser: value, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: value, value: value, attr: { placeholder: $root.workflow_properties.prepares.help_text }"/>
             <a href="#" data-bind="click: function(){ $parent.properties.prepares.remove(this); $(document).trigger('drawArrows') }">

+ 26 - 1
apps/oozie/src/oozie/templates/editor2/workflow_editor.mako

@@ -61,7 +61,7 @@ ${ commonheader(_("Workflow Editor"), "Oozie", user, "40px") | n,unicode }
 
     &nbsp;&nbsp;&nbsp;
 
-    <a title="${ _('Save') }" rel="tooltip" data-placement="bottom" data-loading-text="${ _("Saving...") }" data-bind="click: $root.save, css: {'btn': true, 'disabled': $root.isSaving()}, visible: canEdit">
+    <a title="${ _('Save') }" rel="tooltip" data-placement="bottom" data-loading-text="${ _("Saving...") }" data-bind="click: validateAndSave, css: {'btn': true, 'disabled': $root.isSaving()}, visible: canEdit">
       <i class="fa fa-fw fa-save"></i>
     </a>
 
@@ -547,9 +547,34 @@ ${ dashboard.import_bindings() }
     $(document).trigger("drawArrows");
   }
 
+  function validateAndSave() {
+    validateFields();
+    viewModel.save();
+  }
+
+  function validateFields() {
+    var _hasErrors = false;
+    $("[validate]").each(function () {
+      if ($(this).attr("validate") == "nonempty" && $.trim($(this).val()) == "") {
+        $(this).addClass("with-errors");
+        //$(this).next(".btn").addClass("btn-danger");
+        _hasErrors = true;
+      }
+      else {
+        $(this).removeClass("with-errors");
+        //$(this).next(".btn").removeClass("btn-danger");
+      }
+    });
+    viewModel.isInvalid(_hasErrors);
+  }
+
   $(document).ready(function(){
     renderChangeables();
 
+    $(document).on("viewmodelHasChanged", function () {
+      validateFields();
+    });
+
     $("#exposeOverlay").on("click", exposeOverlayClickHandler);
 
     $(document).keyup(function(e) {

+ 1 - 0
desktop/core/src/desktop/static/desktop/js/ko.common-dashboard.js

@@ -291,6 +291,7 @@ function ChangeTracker(objectToTrack, hashFunction) {
 
   var result = {
     somethingHasChanged: ko.dependentObservable(function () {
+      $(document).trigger("viewmodelHasChanged");
       return hashFunction(objectToTrack) != lastCleanState()
     }),
     markCurrentStateAsClean: function () {