Add remote logging server

Remote logging enables the user to configure a remote
server to stream out local logs. This feature will be
available on the Event Log page. The user can add a
remote server, edit/change an existing server
configuration and remove/disable the remote server.

Resolves openbmc/phosphor-webui#68

Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: I8284cbdbdaaf85f5c95f237efc72290c66904b40
diff --git a/app/common/services/api-utils.js b/app/common/services/api-utils.js
index 6e46c9c..e796f43 100644
--- a/app/common/services/api-utils.js
+++ b/app/common/services/api-utils.js
@@ -1557,6 +1557,65 @@
           });
           return $q.all(promises);
         },
+        setRemoteLoggingServer: (data) => {
+          const ip = data.hostname;
+          const port = data.port;
+          const setIPRequest = $http({
+            method: 'PUT',
+            url: DataService.getHost() +
+                '/xyz/openbmc_project/logging/config/remote/attr/Address',
+            withCredentials: true,
+            data: {'data': ip}
+          });
+          const setPortRequest = $http({
+            method: 'PUT',
+            url: DataService.getHost() +
+                '/xyz/openbmc_project/logging/config/remote/attr/Port',
+            withCredentials: true,
+            data: {'data': port}
+          });
+          const promises = [setIPRequest, setPortRequest];
+          return $q.all(promises);
+        },
+        getRemoteLoggingServer: () => {
+          return $http({
+                   method: 'GET',
+                   url: DataService.getHost() +
+                       '/xyz/openbmc_project/logging/config/remote',
+                   withCredentials: true
+                 })
+              .then((response) => {
+                const remoteServer = response.data.data;
+                if (remoteServer === undefined) {
+                  return undefined;
+                }
+                const hostname = remoteServer.Address;
+                const port = remoteServer.Port;
+                if (hostname === '') {
+                  return undefined;
+                } else {
+                  return {
+                    hostname, port
+                  }
+                }
+              });
+        },
+        disableRemoteLoggingServer: () => {
+          return SERVICE.setRemoteLoggingServer({hostname: '', port: 0});
+        },
+        updateRemoteLoggingServer: (data) => {
+          // Recommended to disable existing configuration
+          // before updating config to new server
+          // https://github.com/openbmc/phosphor-logging#changing-the-rsyslog-server
+          return SERVICE.disableRemoteLoggingServer()
+              .then(() => {
+                return SERVICE.setRemoteLoggingServer(data);
+              })
+              .catch(() => {
+                // try updating server even if initial disable attempt fails
+                return SERVICE.setRemoteLoggingServer(data);
+              });
+        },
         getPowerConsumption: function() {
           return $http({
                    method: 'GET',
diff --git a/app/common/styles/base/forms.scss b/app/common/styles/base/forms.scss
index 21253e7..5e75bcc 100644
--- a/app/common/styles/base/forms.scss
+++ b/app/common/styles/base/forms.scss
@@ -10,6 +10,13 @@
   }
 }
 
+.label__helper-text {
+  color: $darkgrey;
+  line-height: 1.2;
+  font-size: 0.9em;
+  margin-bottom: 0.4em;
+}
+
 input[type='email'],
 input[type='number'],
 input[type='password'],
@@ -113,3 +120,7 @@
     max-height: none;
   }
 }
+.form__validation-message {
+  color: $error-color;
+  font-size: 0.9em;
+}
diff --git a/app/common/styles/base/icons.scss b/app/common/styles/base/icons.scss
index 43d3669..557c857 100644
--- a/app/common/styles/base/icons.scss
+++ b/app/common/styles/base/icons.scss
@@ -126,3 +126,18 @@
   @extend .icon__up-arrow;
   transform: rotate(180deg);
 }
+
+.icon__edit {
+  @include status-icon;
+  background-image: url(../assets/images/icon-edit-blue.svg);
+}
+
+.icon__delete {
+  @include status-icon;
+  background-image: url(../assets/images/icon-trashcan-blue.svg);
+}
+
+.icon__close {
+  @include status-icon;
+  background-image: url(../assets/images/crit-x-black.svg);
+}
diff --git a/app/common/styles/elements/modals.scss b/app/common/styles/elements/modals.scss
index 1a8b71f..0bb81d5 100644
--- a/app/common/styles/elements/modals.scss
+++ b/app/common/styles/elements/modals.scss
@@ -18,7 +18,7 @@
   @include fastTransition-all;
 }
 
-.modal {
+.modal:not(.uib-modal) {
   width: auto;
   height: auto;
   left: 50%;
@@ -79,3 +79,26 @@
     }
   }
 }
+
+.uib-modal.fade.in {
+  opacity: 1;
+}
+.uib-modal.in .modal-dialog {
+  transform: translate(0, 10vh);
+  margin-top: 50px;
+  .icon__close {
+    margin: 0;
+    padding: 0;
+  }
+  .modal-content {
+    border-radius: 0;
+    border-color: $black;
+  }
+}
+
+.modal-backdrop.in {
+  opacity: 0.5;
+}
+.uib-modal__content {
+  padding: 1em;
+}
diff --git a/app/index.js b/app/index.js
index 6303142..1d54b45 100644
--- a/app/index.js
+++ b/app/index.js
@@ -22,7 +22,6 @@
 import ngToast_animate from 'ng-toast/dist/ngToast-animations.css';
 import ngToast_style from 'ng-toast/dist/ngToast.css';
 
-
 require('./styles/index.scss');
 var config = require('../config.json');
 
@@ -77,6 +76,7 @@
 import sensors_overview_controller from './server-health/controllers/sensors-overview-controller.js';
 import syslog_controller from './server-health/controllers/syslog-controller.js';
 import syslog_filter from './common/directives/syslog-filter.js';
+import remote_logging_server from './server-health/directives/remote-logging-server.js';
 
 import redfish_index from './redfish/index.js';
 import redfish_controller from './redfish/controllers/redfish-controller.js';
@@ -100,6 +100,7 @@
             // Dependencies
             'ngRoute', 'angular-clipboard', 'ngToast', 'ngAnimate',
             'ngMessages', 'app.common.directives.dirPagination', 'ngSanitize',
+            'ui.bootstrap',
             // Basic resources
             'app.common.services', 'app.common.directives',
             'app.common.filters',
diff --git a/app/overview/controllers/system-overview-controller.html b/app/overview/controllers/system-overview-controller.html
index 31e2917..0403a85 100644
--- a/app/overview/controllers/system-overview-controller.html
+++ b/app/overview/controllers/system-overview-controller.html
@@ -204,7 +204,7 @@
 
       <form name="edit_hostname_text">
         <label for="editServerName">Hostname</label>
-        <p>Hostname must be less than 64 characters and must not contain spaces.</p>
+        <p class="label__helper-text">Hostname must be less than 64 characters and must not contain spaces.</p>
         <input id="editServerName" class="modal__edit-server-name" type="text" ng-model="newHostname" ng-trim="false"
           name="hostname" ng-pattern="/^\S{0,64}$/" required autofocus />
         <span class="modal__error" ng-show="edit_hostname_text.hostname.$error.pattern">Invalid format.
diff --git a/app/server-health/controllers/log-controller.html b/app/server-health/controllers/log-controller.html
index 0a985c5..34a2ec3 100644
--- a/app/server-health/controllers/log-controller.html
+++ b/app/server-health/controllers/log-controller.html
@@ -1,7 +1,12 @@
 <loader loading="loading"></loader>
 <div id="event-log">
   <div class="row column">
-    <h1>Event log</h1>
+   <div class="column small-6 large-7 no-padding">
+     <h1>Event log</h1>
+   </div>
+   <div class="column small-6 large-5">
+     <remote-logging-server class="remote-logging-server"></remote-logging-server>
+   </div>
   </div>
   <section class="row column">
     <div class="page-header">
diff --git a/app/server-health/directives/remote-logging-server-modal.html b/app/server-health/directives/remote-logging-server-modal.html
new file mode 100644
index 0000000..eba57af
--- /dev/null
+++ b/app/server-health/directives/remote-logging-server-modal.html
@@ -0,0 +1,42 @@
+<div role="dialog" class="uib-modal__content  remote-logging-server__modal">
+  <button type="button" class="icon  icon__close  float-right" ng-click="$close()"></button>
+  <div class="modal-header">
+    <h2 class="modal-title" id="dialog_label">{{activeModalProps.title}}</h2>
+  </div>
+  <form name="form">
+    <div ng-if="activeModal !== 2" class="modal-body">
+      <label for="remoteServerIP">Hostname or IP Address</label>
+      <input id="remoteServerIP" type="text" required name="hostname"
+        ng-model="remoteServerForm.hostname" />
+      <div ng-if="form.hostname.$invalid && form.hostname.$dirty"
+        class="form__validation-message">
+        <span ng-show="form.hostname.$error.required">Field is required</span>
+      </div>
+      <label for="remoteServerPort">Port</label>
+      <p class="label__helper-text">Value must be between 0 – 65535</p>
+      <input id="remoteServerPort" type="number" required name="port"
+        min="0" max="65535" ng-model="remoteServerForm.port"/>
+      <div ng-if="form.port.$invalid && form.port.$dirty"
+        class="form__validation-message">
+        <span ng-show="form.port.$error.required">Field is required</span>
+        <span ng-show="form.port.$error.min || form.port.$error.max">
+          Value must be between 0 – 65535
+        </span>
+      </div>
+    </div>
+    <div ng-if="activeModal === 2" class="modal-body">
+      <p>Are you sure you want to remove remote logging server
+      {{remoteServer.hostname}}?</p>
+    </div>
+    <div class="modal-footer">
+      <button class="button btn-secondary" ng-click="$close()" type="button">
+        Cancel
+      </button>
+      <button class="button btn-primary" type="submit"
+        ng-click="$close(activeModal)" ng-disabled="form.$invalid"
+        ng-class="{'disabled': form.$invalid}">
+        {{activeModalProps.actionLabel}}
+      </button>
+    </div>
+  </form>
+</div>
diff --git a/app/server-health/directives/remote-logging-server.html b/app/server-health/directives/remote-logging-server.html
new file mode 100644
index 0000000..28fc313
--- /dev/null
+++ b/app/server-health/directives/remote-logging-server.html
@@ -0,0 +1,21 @@
+<p class="content-label">Remote Logging Server</p>
+<div ng-if="!loadError && !remoteServer">
+  <button ng-click="initModal(0)" class="modal__trigger">
+    <span class="icon  icon__plus"></span>
+    Add server
+  </button>
+</div>
+<div ng-if="!loadError && remoteServer">
+  <p class="inline remote-logging-server__details">
+    {{remoteServer.hostname}}
+  </p>
+  <button ng-click="initModal(1)" class="modal__trigger">
+    <span class="icon icon__edit"></span>
+  </button>
+  <button ng-click="initModal(2)" class="modal__trigger">
+    <span class="icon icon__delete"></span>
+  </button>
+</div>
+<div class="text-right" ng-if="loadError">
+  <p>--</p>
+</div>
diff --git a/app/server-health/directives/remote-logging-server.js b/app/server-health/directives/remote-logging-server.js
new file mode 100644
index 0000000..4e8ad6f
--- /dev/null
+++ b/app/server-health/directives/remote-logging-server.js
@@ -0,0 +1,157 @@
+window.angular && (function(angular) {
+  'use strict';
+
+  angular.module('app.common.directives').directive('remoteLoggingServer', [
+    'APIUtils',
+    function(APIUtils) {
+      return {
+        'restrict': 'E', 'template': require('./remote-logging-server.html'),
+            'controller': [
+              '$scope', '$uibModal', 'toastService',
+              function($scope, $uibModal, toastService) {
+                const modalActions = {
+                  ADD: 0,
+                  EDIT: 1,
+                  REMOVE: 2,
+                  properties: {
+                    0: {
+                      title: 'Add remote logging server',
+                      actionLabel: 'Add',
+                      successMessage: 'Connected to remote logging server.',
+                      errorMessage: 'Unable to connect to server.'
+                    },
+                    1: {
+                      title: 'Edit remote logging server',
+                      actionLabel: 'Save',
+                      successMessage: 'Connected to remote logging server.',
+                      errorMessage: 'Unable to save remote logging server.'
+                    },
+                    2: {
+                      title: 'Remove remote logging server',
+                      actionLabel: 'Remove',
+                      successMessage: 'Remote logging server removed.',
+                      errorMessage: 'Unable to remove remote logging server.'
+                    }
+                  }
+                };
+
+                const modalTemplate =
+                    require('./remote-logging-server-modal.html');
+
+                $scope.activeModal;
+                $scope.activeModalProps;
+
+                $scope.remoteServer;
+                $scope.remoteServerForm;
+                $scope.loadError = true;
+
+                $scope.initModal = (type) => {
+                  if (type === undefined) {
+                    return;
+                  }
+                  $scope.activeModal = type;
+                  $scope.activeModalProps = modalActions.properties[type];
+
+                  $uibModal
+                      .open({
+                        template: modalTemplate,
+                        windowTopClass: 'uib-modal',
+                        scope: $scope,
+                        ariaLabelledBy: 'dialog_label'
+                      })
+                      .result
+                      .then((action) => {
+                        switch (action) {
+                          case modalActions.ADD:
+                            addServer();
+                            break;
+                          case modalActions.EDIT:
+                            editServer();
+                            break;
+                          case modalActions.REMOVE:
+                            removeServer();
+                            break;
+                          default:
+                            setFormValues();
+                        }
+                      })
+                      .catch(() => {
+                        // reset form when modal overlay clicked
+                        // and modal closes
+                        setFormValues();
+                      })
+                };
+
+                const addServer = () => {
+                  $scope.loading = true;
+                  APIUtils.setRemoteLoggingServer($scope.remoteServerForm)
+                      .then(() => {
+                        $scope.loading = false;
+                        $scope.remoteServer = {...$scope.remoteServerForm};
+                        toastService.success(
+                            $scope.activeModalProps.successMessage);
+                      })
+                      .catch(() => {
+                        $scope.loading = false;
+                        $scope.remoteServer = undefined;
+                        setFormValues();
+                        toastService.error(
+                            $scope.activeModalProps.errorMessage);
+                      })
+                };
+
+                const editServer = () => {
+                  $scope.loading = true;
+                  APIUtils.updateRemoteLoggingServer($scope.remoteServerForm)
+                      .then(() => {
+                        $scope.loading = false;
+                        $scope.remoteServer = {...$scope.remoteServerForm};
+                        toastService.success(
+                            $scope.activeModalProps.successMessage);
+                      })
+                      .catch(() => {
+                        $scope.loading = false;
+                        setFormValues();
+                        toastService.error(
+                            $scope.activeModalProps.errorMessage);
+                      })
+                };
+
+                const removeServer = () => {
+                  $scope.loading = true;
+                  APIUtils.disableRemoteLoggingServer()
+                      .then(() => {
+                        $scope.loading = false;
+                        $scope.remoteServer = undefined;
+                        setFormValues();
+                        toastService.success(
+                            $scope.activeModalProps.successMessage);
+                      })
+                      .catch(() => {
+                        $scope.loading = false;
+                        toastService.error(
+                            $scope.activeModalProps.errorMessage);
+                      })
+                };
+
+                const setFormValues = () => {
+                  $scope.remoteServerForm = {...$scope.remoteServer};
+                };
+
+                this.$onInit = () => {
+                  APIUtils.getRemoteLoggingServer()
+                      .then((remoteServer) => {
+                        $scope.loadError = false;
+                        $scope.remoteServer = remoteServer;
+                        setFormValues();
+                      })
+                      .catch(() => {
+                        $scope.loadError = true;
+                      })
+                };
+              }
+            ]
+      }
+    }
+  ])
+})(window.angular);
\ No newline at end of file
diff --git a/app/server-health/styles/log.scss b/app/server-health/styles/log.scss
index cc58a60..675dc26 100644
--- a/app/server-health/styles/log.scss
+++ b/app/server-health/styles/log.scss
@@ -370,6 +370,34 @@
   word-break: break-word;
 }
 
+.remote-logging-server {
+  float: right;
+  .modal__trigger {
+    padding: 0;
+    color: $primebtn__bg;
+    .icon {
+      margin: 0;
+      width: 20px;
+      height: 20px;
+      vertical-align: text-bottom;
+    }
+  }
+}
+
+.remote-logging-server__details {
+  margin-right: 0.4em;
+}
+
+.remote-logging-server__modal {
+  input {
+    margin-bottom: 30px;
+    max-width: 70%;
+    + .form__validation-message {
+      position: absolute;
+      margin-top: -30px;
+    }
+  }
+}
 //end event-log__events