SNMP page visual redesign

Adds form validation to SNMP page. Also adds new icons to
page for edit and delete. Validation messages for inputs
appear after input has been touched or if the user attempts
to submit the form with validation errors.

Tested: Add, remove, and update SNMP was successful and
functionality unchanged. Form did not allow invalid or
empty values to submitted. Used Chrome, Safari, and Firefox
to verify that visual design looked as expected.

Resolves openbmc/phosphor-webui#37

Change-Id: I7a97d80724e56d2f22c74ac1251041270bbc35ad
Signed-off-by: beccabroek <beccabroek@gmail.com>
diff --git a/app/assets/images/icon-edit-blue.svg b/app/assets/images/icon-edit-blue.svg
new file mode 100644
index 0000000..ba2cc40
--- /dev/null
+++ b/app/assets/images/icon-edit-blue.svg
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
+    <title>Colors/Base/Black</title>
+    <desc>Created with Sketch.</desc>
+    <defs>
+        <path d="M7.92572798,3.00428655 L1.00199753,9.34382025 L1.00199753,11.6237002 L3.30623854,11.6237002 L10.2321641,5.30721897 L7.92572798,3.00428655 Z M8.66400508,2.32830232 L10.9718396,4.63263098 L12.4234573,3.30874783 L10.1147108,1.00000135 L8.66400508,2.32830232 Z M0.00199752679,8.9035802 L9.43940417,0.262464274 C9.8341134,-0.0989409664 10.4433938,-0.0855292166 10.8218176,0.29289457 L13.1305641,2.60164105 C13.5210884,2.99216534 13.5210884,3.62533032 13.1305641,4.01585461 C13.1197237,4.02669497 13.1086355,4.03728461 13.0973082,4.04761513 L3.69376146,12.6237002 L0.00199752679,12.6237002 L0.00199752679,8.9035802 Z M0,15.6371142 L0,14.6371142 L16,14.6371142 L16,15.6371142 L0,15.6371142 Z" id="path-1"></path>
+    </defs>
+    <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Icon/Edit">
+            <g id="Colors/Base/Black">
+                <mask id="mask-2" fill="white">
+                    <use xlink:href="#path-1"></use>
+                </mask>
+                <use id="Mask" fill="#2d60e5" fill-rule="nonzero" xlink:href="#path-1"></use>
+                <g id="Colors/Base/Blue50" mask="url(#mask-2)" fill="#3c6df0">
+                    <g transform="translate(-8.377562, -8.431784)" id="Rectangle-6-Copy">
+                        <rect x="0" y="0" width="35" height="35"></rect>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/app/assets/images/icon-plus.svg b/app/assets/images/icon-plus.svg
index 21566bc..0e18b22 100644
--- a/app/assets/images/icon-plus.svg
+++ b/app/assets/images/icon-plus.svg
@@ -1,13 +1,21 @@
-<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
-	 viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
-<style type="text/css">
-	.st0{fill:#FFFFFF;}
-	.st1{fill:#2d60e5;}
-</style>
-<g id="Layer_2">
-	<rect x="3.7" y="4" class="st0" width="8.4" height="8.1"/>
-</g>
-<g id="Layer_1">
-	<path class="st1" d="M8,0C3.6,0,0,3.6,0,8s3.6,8,8,8s8-3.6,8-8S12.4,0,8,0z M12,9H9v3H7V9H4V7h3V4h2v3h3V9z"/>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="19px" height="19px" viewBox="0 0 19 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<defs>
+  <path d="M10.3189655,8.7447318 L13.822318,8.7447318 L13.822318,10.5464559 L10.3189655,10.5464559 L10.3189655,14.0498084 L8.51724138,14.0498084 L8.51724138,10.5464559 L5.01388889,10.5464559 L5.01388889,8.7447318 L8.51724138,8.7447318 L8.51724138,5.24137931 L10.3189655,5.24137931 L10.3189655,8.7447318 Z M9.5,19 C4.25329488,19 0,14.7467051 0,9.5 C0,4.25329488 4.25329488,0 9.5,0 C14.7467051,0 19,4.25329488 19,9.5 C19,14.7467051 14.7467051,19 9.5,19 Z M9.55533929,17.6650264 C14.0341958,17.6650264 17.6650264,14.0341958 17.6650264,9.55533929 C17.6650264,5.07648277 14.0341958,1.44565217 9.55533929,1.44565217 C5.07648277,1.44565217 1.44565217,5.07648277 1.44565217,9.55533929 C1.44565217,14.0341958 5.07648277,17.6650264 9.55533929,17.6650264 Z" id="path-1"></path>
+</defs>
+<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+  <g id="Icon/Add" transform="translate(-6.000000, -6.000000)">
+    <g id="Icon" transform="translate(6.000000, 6.000000)">
+      <mask id="mask-2" fill="white">
+        <use xlink:href="#path-1"></use>
+      </mask>
+      <use id="Mask" fill="#2d60e5" fill-rule="nonzero" xlink:href="#path-1"></use>
+      <g id="Colors/Base/Blue50" mask="url(#mask-2)" fill="#3c6df0">
+        <g transform="translate(-3.275862, -3.931034)" id="Rectangle-6-Copy">
+          <rect x="0" y="0" width="25" height="25"></rect>
+        </g>
+      </g>
+    </g>
+  </g>
 </g>
 </svg>
diff --git a/app/assets/images/icon-trashcan-blue.svg b/app/assets/images/icon-trashcan-blue.svg
new file mode 100644
index 0000000..87a82d8
--- /dev/null
+++ b/app/assets/images/icon-trashcan-blue.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+     viewBox="0 0 22 22" style="enable-background:new 0 0 22 22;" xml:space="preserve">
+<style type="text/css">
+  .st0{fill:#2d60e5;}
+</style>
+<g>
+  <rect x="8.1" y="1" class="st0" width="5.7" height="1.4"/>
+  <path class="st0" d="M3.9,3.1v3.6h0.7V21h12.9V6.7h0.7V3.1H3.9z M16,19.6H6V8.1h10V19.6z"/>
+  <rect x="7.4" y="9.6" class="st0" width="0.7" height="8.6"/>
+  <rect x="9.6" y="9.6" class="st0" width="0.7" height="8.6"/>
+  <rect x="11.7" y="9.6" class="st0" width="0.7" height="8.6"/>
+  <rect x="13.9" y="9.6" class="st0" width="0.7" height="8.6"/>
+</g>
+</svg>
diff --git a/app/configuration/controllers/snmp-controller.html b/app/configuration/controllers/snmp-controller.html
index ac03497..ee3d0d0 100644
--- a/app/configuration/controllers/snmp-controller.html
+++ b/app/configuration/controllers/snmp-controller.html
@@ -2,30 +2,90 @@
 <div id="configuration-snmp">
   <div class="row column">
     <h1>SNMP settings</h1>
+    <div class="small-12 snmp_description">
+      Set the Simple Network Management Protocol (SNMP) with a host name or IP address and a port.
+    </div>
+    <div class="page-header">
+      <h2>Managers</h2>
+    </div>
   </div>
-  <form class="snmp__form" role="form" action="">
-      <div class="page-header">
-        <h2>Set the Simple Network Management Protocol (SNMP)</h2>
-      </div>
-      <fieldset>
-        <div class="row column snmp__managers-wrap">
-          <div class="snmp__managers" ng-repeat="manager in managers track by $index">
-            <div class="snmp__manager-field">
-              <label>SNMP Manager Server</label>
-              <input class="inline" id="snmp-manager{{$index+1}}-address" ng-change="manager.updateAddress = true" type="text" ng-model="manager.address" ng-blur="managers[$index].address = manager.address"/>
-            </div>
-            <div class="snmp__manager-field">
-              <label>Port</label>
-              <input class="inline" id="snmp-manager{{$index+1}}-port" type="text" ng-change="manager.updatePort = true" ng-model="manager.port" ng-blur="managers[$index].port = manager.port"/>
-            </div>
-          <button class="snmp__manager-remove" ng-click="removeSNMPManager($index)">Remove</button>
-          </div>
-          <button type="button" class="btn-primary inline" ng-click="addNewSNMPManager()">Add SNMP manager</button>
+  <div class="row column">
+    <div class="small-8">
+      <div class="row column manager_group">
+        <div class="small-5 snmp__address">
+          <h3>Host name or IP Address</h3>
         </div>
-      </fieldset>
+        <div class="small-5 snmp__port label">
+          <h3>Port</h3>
+        </div>
+      </div>
+      <div class="row column manager_group empty" ng-if="managers.length <1">
+        No managers have been added yet.
+      </div>
+      <form id="snmp__form" name="snmp__form" novalidate>
+        <div class="row column manager_group" ng-form="manager_group" ng-repeat="manager in managers track by $index">
+          <div class="small-11 snmp__fields">
+            <div class="row column">
+              <div class="small-10">
+                <div class="row column">
+                  <div class="small-offset-6 small-6 help__text snmp__port">
+                    Value must be between 0-65,535
+                  </div>
+                </div>
+                <div class="row column" ng-class="{'submitted':submitted}">
+                  <div class="small-6 snmp__address">
+                    <input id="snmp-manager{{$index+1}}-address"
+                           name="snmp-manager{{$index+1}}-address"
+                           type="text"
+                           ng-change="manager.updateAddress=true"
+                           ng-model="manager.address"
+                           required />
+                    <div ng-messages="manager_group['snmp-manager'+($index+1)+'-address'].$error" class="form-error" ng-class="{'visible': manager_group['snmp-manager'+($index+1)+'-address'].$touched || submitted}">
+                      <p ng-message="required" role="alert">Field is required</p>
+                    </div>
+                  </div>
+                  <div class="small-6 snmp__port">
+                    <input id="snmp-manager{{$index+1}}-port"
+                           name="snmp-manager{{$index+1}}-port"
+                           type="number"
+                           min="0"
+                           max="65535"
+                           step="1"
+                           ng-change="manager.updatePort=true"
+                           ng-model="manager.port"
+                           required/>
+                    <div ng-messages="manager_group['snmp-manager'+($index+1)+'-port'].$error" class="form-error" ng-class="{'visible': manager_group['snmp-manager'+($index+1)+'-port'].$touched || submitted}">
+                      <div ng-message-exp="['min', 'max', 'number', 'step']">
+                        Invalid format
+                      </div>
+                      <p ng-message="required" role="alert">Field is required</p>
+                    </div>
+                  </div>
+                </div>
+              </div>
+              <div class="small-2 align-self-center snmp__buttons">
+                <button class="edit_button" type="button">
+                  <img src="../../assets/images/icon-edit-blue.svg" alt="Edit">
+                </button>
+              </div>
+            </div>
+          </div>
+          <div class="small-1 align-self-center snmp__buttons trash">
+            <button class="trash_button" type="button" ng-click="removeSNMPManager($index)">
+              <img src="../../assets/images/icon-trashcan-blue.svg" alt="Remove">
+            </button>
+          </div>
+        </div>
+        <div class="row column">
+          <button class="btn-add" type="button" ng-click="submitted=false; addNewSNMPManager();"><img class="add__icon" src="../../assets/images/icon-plus.svg" alt=""/> Add manager</button>
+        </div>
+      </form>
+    </div>
+  </div>
+  <div class="row column">
     <div class="snmp__submit-wrapper">
-      <button type="button" class="btn-primary inline" ng-click="setSNMP()">Save settings</button>
+      <button type="button" ng-click="submitted=true;  snmp__form.$valid && setSNMP();" class="btn-primary inline">Save settings</button>
       <button type="button" class="btn-secondary inline" ng-click="refresh()">Cancel</button>
     </div>
-  </form>
-</div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/app/configuration/controllers/snmp-controller.js b/app/configuration/controllers/snmp-controller.js
index 9d71765..67e210f 100644
--- a/app/configuration/controllers/snmp-controller.js
+++ b/app/configuration/controllers/snmp-controller.js
@@ -60,24 +60,19 @@
         $scope.loading = true;
         var promises = [];
 
-        // Interate in reverse so can splice
+        // Validate that no field are empty and port is valid. Port value is
+        // undefined if it is an invalid number.
+        for (var i in $scope.managers) {
+          if (!$scope.managers[i].address || !$scope.managers[i].port) {
+            $scope.loading = false;
+            toastService.error('Cannot save. Please resolve errors on page.');
+            return;
+          }
+        }
+        // Iterate in reverse so can splice
         // https://stackoverflow.com/questions/9882284/looping-through-array-and-removing-items-without-breaking-for-loop
         var i = $scope.managers.length;
         while (i--) {
-          // Remove any SNMP Manager with an empty address and port
-          if (!$scope.managers[i].address && !$scope.managers[i].port) {
-            $scope.removeSNMPManager(i);
-            continue;
-          }
-
-          // Throw an error if only 1 of the fields is filled out
-          if (!$scope.managers[i].address || !$scope.managers[i].port) {
-            // TODO: Highlight the field that is empty
-            $scope.loading = false;
-            toastService.error('All fields are required.');
-            return;
-          }
-
           // If the manager does not have a 'path', it is a new manager
           // and needs to be created
           if (!$scope.managers[i].path) {
@@ -104,7 +99,7 @@
         $q.all(promises)
             .then(
                 function() {
-                  toastService.success('SNMP Managers set.');
+                  toastService.success('SNMP settings have been saved.');
                 },
                 function(errors) {
                   toastService.error('Unable to set SNMP Managers.');
diff --git a/app/configuration/styles/snmp.scss b/app/configuration/styles/snmp.scss
index 1afefd0..47dc80f 100644
--- a/app/configuration/styles/snmp.scss
+++ b/app/configuration/styles/snmp.scss
@@ -1,51 +1,107 @@
-// SNMP SCSS
-
-.snmp__form {
-
-  fieldset {
-    padding-left: 1.8em;
+#configuration-snmp {
+  .row {
+    padding-left: 0;
   }
-   .snmp__managers {
-    padding-bottom: 1.5em;
+  .snmp_description {
+    padding:1em 0 1em 0;
   }
-
-   .snmp__manager-field {
-    display:inline-block;
-    padding-right: 4em;
-    width: 350px;
-    padding-top: .6em;
-    padding-left: 0em;
+  .help__text {
+    font-size: 0.8rem;
+    color: $disabled-row-txt;
+    padding-top: .3em;
+    display: none;
   }
-
-  .snmp__metadata-wrapper {
-    margin: 0;
+  .form-error{
+    display: none;
+  }
+  .manager_group {
+    border-bottom: 1px solid $lightbg__grey;
+    &.ng-invalid {
+      input {
+        color: black;
+        border-style: solid;
+        pointer-events: auto;
+      }
+      .help__text {
+        display: block;
+      }
+      .form-error {
+        display: block;
+      }
+    }
+    &.empty {
+      color: $disabled-row-txt;
+      padding: 1em 0 1em 0;
+    }
+  }
+  .snmp__port {
+    padding: .3em 0 .3em 1em;
+    &.label {
+      padding: .3em 0 .3em 0em;
+    }
+  }
+  .snmp__address {
+    padding: .3em 1em .3em 0;
+  }
+  .snmp__fields {
+    input {
+      border-style: none;
+      pointer-events: none;
+      color: $disabled-row-txt;
+      height: 2.4em;
+    }
+    &:focus-within {
+      input {
+        color: $black;
+        border-style: solid;
+        pointer-events: auto;
+      }
+      .form-error {
+        display: block;
+      }
+      .help__text {
+        display: block;
+      }
+    }
+  }
+  .snmp__buttons {
+    text-align: right;
+    padding-right: .5em;
+    &.trash{
+      text-align: left;
+    }
+    button{
+      margin:.3em;
+      img{
+        width: 100%;
+        height:100%;
+      }
+    }
+  }
+  .trash_button {
     padding: 0;
+    width: 1.3em;
+    height: 1.3em;
   }
-
-  .snmp__managers-wrap{
-    padding-bottom: 1em;
-    padding-top: 1em;
-    padding-left: 3em;
-    padding-right: 4em;
+  .edit_button {
+    padding: 0;
+    width: 1.3em;
+    height: 1.2em;
   }
-
+  .btn-add {
+    &:focus {
+      outline:0;
+    }
+    margin-top: 1em;
+    color: $primebtn__bg;
+    padding: 2em 0 3em 0;
+  }
   .snmp__submit-wrapper {
     width: 100%;
-    margin-top: 3em;
-    padding-top: 1em;
     border-top: 1px solid $medgrey;
     button {
       float: right;
       margin: .5em;
     }
   }
-  .snmp__manager-remove {
-    display:inline-block;
-    color: $medblue;
-    height: 2.1em;
-    opacity: 1;
-    &:hover {
-      cursor: pointer;
-    }
-  }
 }