From aba3a8ca2d6796fdda0e2bd253a13b74fea475c8 Mon Sep 17 00:00:00 2001
From: Ken Fukuyama <kenfdev@gmail.com>
Date: Thu, 30 Nov 2017 00:27:27 +0900
Subject: [PATCH] * Added function store feature to the "Deploy New Function"  
 * This feature fetches function catalogs from openfaas/store and makes  
 one-click deploy easy   * You can switch between "From Store" or "Manually"
 by tabs

* Added icon to "Deploy New Function" button
* Added function search feature to the main UI

Signed-off-by: Ken Fukuyama <kenfdev@gmail.com>

reverted fixed tabs

Signed-off-by: Ken Fukuyama <kenfdev@gmail.com>
---
 .../img/icons/ic_info_outline_black_24px.svg  |  4 +
 .../assets/img/icons/ic_search_black_24px.svg |  4 +
 .../img/icons/ic_shop_two_black_24px.svg      |  4 +
 gateway/assets/index.html                     | 16 +++-
 gateway/assets/newfunction.html               | 61 ---------------
 gateway/assets/script/bootstrap.js            | 27 ++++++-
 gateway/assets/script/funcstore.js            | 76 +++++++++++++++++++
 gateway/assets/style/bootstrap.css            | 13 ++++
 gateway/assets/templates/funcstore.html       | 25 ++++++
 gateway/assets/templates/newfunction.html     | 71 +++++++++++++++++
 10 files changed, 235 insertions(+), 66 deletions(-)
 create mode 100644 gateway/assets/img/icons/ic_info_outline_black_24px.svg
 create mode 100644 gateway/assets/img/icons/ic_search_black_24px.svg
 create mode 100644 gateway/assets/img/icons/ic_shop_two_black_24px.svg
 delete mode 100644 gateway/assets/newfunction.html
 create mode 100644 gateway/assets/script/funcstore.js
 create mode 100644 gateway/assets/templates/funcstore.html
 create mode 100644 gateway/assets/templates/newfunction.html

diff --git a/gateway/assets/img/icons/ic_info_outline_black_24px.svg b/gateway/assets/img/icons/ic_info_outline_black_24px.svg
new file mode 100644
index 00000000..c999d630
--- /dev/null
+++ b/gateway/assets/img/icons/ic_info_outline_black_24px.svg
@@ -0,0 +1,4 @@
+<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M0 0h24v24H0z" fill="none"/>
+    <path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"/>
+</svg>
\ No newline at end of file
diff --git a/gateway/assets/img/icons/ic_search_black_24px.svg b/gateway/assets/img/icons/ic_search_black_24px.svg
new file mode 100644
index 00000000..ccc84b62
--- /dev/null
+++ b/gateway/assets/img/icons/ic_search_black_24px.svg
@@ -0,0 +1,4 @@
+<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
+    <path d="M0 0h24v24H0z" fill="none"/>
+</svg>
\ No newline at end of file
diff --git a/gateway/assets/img/icons/ic_shop_two_black_24px.svg b/gateway/assets/img/icons/ic_shop_two_black_24px.svg
new file mode 100644
index 00000000..93b75621
--- /dev/null
+++ b/gateway/assets/img/icons/ic_shop_two_black_24px.svg
@@ -0,0 +1,4 @@
+<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M3 9H1v11c0 1.11.89 2 2 2h14c1.11 0 2-.89 2-2H3V9zm15-4V3c0-1.11-.89-2-2-2h-4c-1.11 0-2 .89-2 2v2H5v11c0 1.11.89 2 2 2h14c1.11 0 2-.89 2-2V5h-5zm-6-2h4v2h-4V3zm0 12V8l5.5 3-5.5 4z"/>
+    <path d="M0 0h24v24H0z" fill="none"/>
+</svg>
\ No newline at end of file
diff --git a/gateway/assets/index.html b/gateway/assets/index.html
index da38cf30..f1300293 100644
--- a/gateway/assets/index.html
+++ b/gateway/assets/index.html
@@ -36,10 +36,19 @@
                 </md-toolbar>
 
                 <md-content layout-padding>
-                    <md-button ng-click="newFunction()" ng-disabled="isFunctionBeingCreated" class="md-primary">Deploy New Function</md-button>
+                    <md-list>
+                        <md-list-item class="primary-item" ng-disabled="isFunctionBeingCreated" ng-click="newFunction()">
+                            <md-icon style="margin-right: 16px; opacity:0.6" md-svg-icon="img/icons/ic_shop_two_black_24px.svg"></md-icon>
+                            <p>Deploy New Function</p>
+                        </md-list-item>
+                    </md-list>
 
+                    <md-input-container ng-hide="functions.length === 0" class="md-block" flex-gt-sm>
+                        <label style="padding-left: 8px">Search for Function</label>
+                        <input ng-model="search.name">
+                      </md-input-container>
                     <md-list>
-                        <md-list-item ng-switch class="md-3-line" ng-click="showFunction(function)" ng-repeat="function in functions | orderBy: '-invocationCount'" ng-class="function.name == selectedFunction.name ? 'selected' : false">
+                        <md-list-item ng-switch class="md-3-line" ng-click="showFunction(function)" ng-repeat="function in functions | filter:search | orderBy: '-invocationCount'" ng-class="function.name == selectedFunction.name ? 'selected' : false">
                             <md-icon ng-switch-when="true" style="color: blue" md-svg-icon="person"></md-icon>
                             <md-icon ng-switch-when="false" md-svg-icon="person-outline"></md-icon>
                             <p>{{function.name}}</p>
@@ -166,7 +175,8 @@
     <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-animate.min.js"></script>
     <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-aria.min.js"></script>
     <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-messages.min.js"></script>
-    <script src="https://ajax.googleapis.com/ajax/libs/angular_material/1.1.0/angular-material.min.js"></script>
+    <script src="https://ajax.googleapis.com/ajax/libs/angular_material/1.1.4/angular-material.min.js"></script>
+    <script src="script/funcstore.js"></script>
     <script src="script/bootstrap.js"></script>
 </body>
 
diff --git a/gateway/assets/newfunction.html b/gateway/assets/newfunction.html
deleted file mode 100644
index 39b8a7df..00000000
--- a/gateway/assets/newfunction.html
+++ /dev/null
@@ -1,61 +0,0 @@
-<md-dialog aria-label="List dialog" layout="column" flex="70">
-    <md-toolbar>
-        <div class="md-toolbar-tools">
-            <h2>Deploy A New Function</h2>
-            <span flex></span>
-            <md-button class="md-icon-button" ng-click="closeDialog()">
-            <md-icon md-svg-src="img/icons/ic_close_24px.svg" aria-label="Close dialog"></md-icon>
-            </md-button>
-        </div>
-    </md-toolbar>
-
-    <md-dialog-content class="md-padding">
-        <label><i>Use this form to test a function or the <a ng-href="https://github.com/openfaas/faas-cli">faas-cli</a> for more options.</i></label>
-    </md-dialog-content>
-
-    <md-dialog-content class="md-padding">
-        <label>Define the function below:</label>
-        <form name="userForm">
-            <div layout-gt-xs="row">
-                <md-input-container class="md-block" flex-gt-sm>
-                    <md-tooltip md-direction="bottom">Docker image name and tag to use for function i.e. functions/alpine:latest</md-tooltip>
-                    <label>Docker image:</label>
-                    <input name="dockerImage" ng-model="item.image" required md-maxlength="200" minlength="2">
-                </md-input-container>
-            </div>
-            <div layout-gt-xs="row">
-                <md-input-container class="md-block" flex-gt-sm>
-                    <md-tooltip md-direction="bottom">Name of the function - must be a valid DNS entry</md-tooltip>
-                    <label>Function name:</label>
-                    <input name="serviceName" ng-model="item.service" required md-maxlength="200" minlength="2">
-                </md-input-container>
-            </div>
-            <div layout-gt-xs="row">
-                <md-input-container class="md-block" flex-gt-sm>
-                    <md-tooltip md-direction="bottom">Process to run as your function i.e. 'env' or 'shasum'. Ignore if using OpenFaaS templates</md-tooltip>
-                    <label>Function process (optional):</label>
-                    <input name="envProcess" ng-model="item.envProcess" md-maxlength="200" minlength="0">
-                </md-input-container>
-            </div>
-            <div layout-gt-xs="row">
-                <md-input-container class="md-block" flex-gt-sm>
-                    <md-tooltip md-direction="bottom">Docker Swarm network, not required for other providers. Default: func_functions</md-tooltip>
-                    <label>Network (Swarm):</label>
-                    <input name="network" ng-model="item.network" md-maxlength="200" minlength="0">
-                </md-input-container>
-            </div>
-            <div class="validation-error" layout-gt-xs="row" layout-align="start end">
-                <span ng-show="validationError">{{ validationError }}</span>
-            </div>
-        </form>
-    </md-dialog-content>
-
-    <md-dialog-actions>
-        <md-button ng-click="closeDialog()" class="md-secondary">
-            Close Dialog
-        </md-button>
-        <md-button ng-click="createFunc()" class="md-primary">
-            Deploy
-        </md-button>
-    </md-dialog-actions>
-</md-dialog>
diff --git a/gateway/assets/script/bootstrap.js b/gateway/assets/script/bootstrap.js
index 3ee6992b..a706c218 100644
--- a/gateway/assets/script/bootstrap.js
+++ b/gateway/assets/script/bootstrap.js
@@ -2,10 +2,11 @@
 // Copyright (c) Alex Ellis 2017. All rights reserved.
 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
 
-var app = angular.module('faasGateway', ['ngMaterial']);
+var app = angular.module('faasGateway', ['ngMaterial', 'faasGateway.funcStore']);
 
 app.controller("home", ['$scope', '$log', '$http', '$location', '$timeout', '$mdDialog', '$mdToast', '$mdSidenav',
     function($scope, $log, $http, $location, $timeout, $mdDialog, $mdToast, $mdSidenav) {
+        var newFuncTabIdx = 0;
         $scope.functions = [];
         $scope.invocationInProgress = false;
         $scope.invocationRequest = "";
@@ -128,7 +129,7 @@ app.controller("home", ['$scope', '$log', '$http', '$location', '$timeout', '$md
             $mdDialog.show({
                 parent: parentEl,
                 targetEvent: $event,
-                templateUrl: "newfunction.html",
+                templateUrl: "templates/newfunction.html",
                 locals: {
                     item: $scope.functionTemplate
                 },
@@ -137,11 +138,33 @@ app.controller("home", ['$scope', '$log', '$http', '$location', '$timeout', '$md
         };
 
         var DialogController = function($scope, $mdDialog, item) {
+            $scope.selectedTabIdx = newFuncTabIdx;
             $scope.item = item;
+            $scope.selectedFunc = null;
             $scope.closeDialog = function() {
                 $mdDialog.hide();
             };
 
+            $scope.onFuncSelected = function(func) {
+                $scope.item.image = func.image;
+                $scope.item.service = func.name;
+                $scope.item.envProcess = func.fprocess;
+                $scope.item.network = func.network;
+                $scope.selectedFunc = func;
+            }
+            
+            $scope.onTabSelect = function(idx) {
+                newFuncTabIdx = idx;
+            }
+
+            $scope.onStoreTabDeselect = function() {
+                $scope.selectedFunc = null;
+            }
+
+            $scope.onManualTabDeselect = function() {
+                $scope.item = {};
+            }
+
             $scope.createFunc = function() {
                 var options = {
                     url: "/system/functions",
diff --git a/gateway/assets/script/funcstore.js b/gateway/assets/script/funcstore.js
new file mode 100644
index 00000000..0fd8aa88
--- /dev/null
+++ b/gateway/assets/script/funcstore.js
@@ -0,0 +1,76 @@
+var funcStoreModule = angular.module('faasGateway.funcStore', ['ngMaterial']);
+
+funcStoreModule.service('FuncStoreService', ['$http', function ($http) {
+    var self = this;
+    this.fetchStore = function (url) {
+        return $http.get(url)
+            .then(function (resp) {
+                return resp.data;
+            });
+    };
+
+}]);
+
+funcStoreModule.component('funcStore', {
+    templateUrl: 'templates/funcstore.html',
+    bindings: {
+        selectedFunc: '<',
+        onSelected: '&',
+    },
+    controller: ['FuncStoreService', '$mdDialog', function FuncStoreController(FuncStoreService, $mdDialog) {
+        var self = this;
+
+        this.storeUrl = 'https://raw.githubusercontent.com/openfaas/store/master/store.json';
+        this.selectedFunc = null;
+        this.functions = [];
+        this.message = '';
+        this.searchText = '';
+
+        this.search = function (func) {
+            // filter with title and description
+            if (!self.searchText || (func.title.toLowerCase().indexOf(self.searchText.toLowerCase()) != -1) ||
+                (func.description.toLowerCase().indexOf(self.searchText.toLowerCase()) != -1)) {
+                return true;
+            }
+            return false;
+        }
+
+        this.select = function (func, event) {
+            self.selectedFunc = func;
+            self.onSelected()(func, event);
+        };
+
+        this.loadStore = function () {
+            self.loading = true;
+            self.functions = [];
+            self.message = '';
+            FuncStoreService.fetchStore(self.storeUrl)
+                .then(function (data) {
+                    self.loading = false;
+                    self.functions = data;
+                })
+                .catch(function (err) {
+                    console.error(err);
+                    self.loading = false;
+                    self.message = 'Unable to reach GitHub.com';
+                });
+        }
+
+        this.showInfo = function (func, event) {
+            $mdDialog.show(
+                $mdDialog.alert()
+                .multiple(true)
+                .parent(angular.element(document.querySelector('#newfunction-dialog')))
+                .clickOutsideToClose(true)
+                .title(func.title)
+                .textContent(func.description)
+                .ariaLabel(func.title)
+                .ok('OK')
+                .targetEvent(event)
+            );
+        }
+
+        this.loadStore();
+
+    }]
+});
\ No newline at end of file
diff --git a/gateway/assets/style/bootstrap.css b/gateway/assets/style/bootstrap.css
index a8cf57e6..61bbddfc 100644
--- a/gateway/assets/style/bootstrap.css
+++ b/gateway/assets/style/bootstrap.css
@@ -54,3 +54,16 @@ md-input-container .md-errors-spacer {
 	color: red;
 	height: 52px;
 }
+
+.primary-item .md-list-item-inner{
+    color: rgb(63,81,181);
+    font-weight: 500;
+}
+
+span.md-avatar {
+    font-weight: bold;
+    line-height: 40px;
+    text-align: center;
+    background-color: #1398D6;
+    color: white;
+}
diff --git a/gateway/assets/templates/funcstore.html b/gateway/assets/templates/funcstore.html
new file mode 100644
index 00000000..2aceb3d3
--- /dev/null
+++ b/gateway/assets/templates/funcstore.html
@@ -0,0 +1,25 @@
+<div layout="row" layout-align="center center" ng-show="$ctrl.message">
+    <p>{{ $ctrl.message }}</p>
+</div>
+<div ng-hide="$ctrl.message">
+    <md-input-container class="md-icon-float md-block">
+        <label>Search for Function</label>
+        <md-icon md-svg-src="img/icons/ic_search_black_24px.svg"></md-icon>
+        <input ng-model="$ctrl.searchText" type="text">
+    </md-input-container>
+    <div ng-if="$ctrl.loading" layout="row" layout-sm="column" layout-align="space-around">
+        <md-progress-circular md-mode="indeterminate"></md-progress-circular>
+    </div>
+    <md-list ng-hide="$ctrl.loading">
+        <md-list-item class="md-3-line" ng-repeat="func in $ctrl.functions | filter:$ctrl.search" ng-click="$ctrl.select(func, $event)"
+            ng-class="func.name === $ctrl.selectedFunc.name ? 'selected' : false">
+            <img ng-if="func.icon" ng-src="{{func.icon}}" class="md-avatar" alt="{{func.name}}" style="border-radius: 0" />
+            <span ng-if="!func.icon" class="md-avatar">{{func.title | limitTo:1}}</span>
+            <div class="md-list-item-text" layout="column">
+                <h3>{{ func.title }}</h3>
+                <p>{{ func.description }}</p>
+            </div>
+            <md-divider md-inset ng-if="!$last"></md-divider>
+        </md-list-item>
+    </md-list>
+</div>
\ No newline at end of file
diff --git a/gateway/assets/templates/newfunction.html b/gateway/assets/templates/newfunction.html
new file mode 100644
index 00000000..659c154f
--- /dev/null
+++ b/gateway/assets/templates/newfunction.html
@@ -0,0 +1,71 @@
+<md-dialog id="newfunction-dialog" aria-label="List dialog" layout="column" flex="70">
+    <md-toolbar>
+        <div class="md-toolbar-tools">
+            <h2>Deploy A New Function</h2>
+            <span flex></span>
+            <md-button class="md-icon-button" ng-click="closeDialog()">
+            <md-icon md-svg-src="img/icons/ic_close_24px.svg" aria-label="Close dialog"></md-icon>
+            </md-button>
+        </div>
+    </md-toolbar>
+
+    <md-dialog-content class="md-padding">
+            <md-tabs md-dynamic-height md-border-bottom md-selected="selectedTabIdx">
+              <md-tab label="From Store" md-on-deselect="onStoreTabDeselect()" md-on-select="onTabSelect(0)">
+                <md-content class="md-padding">
+                  <func-store selected-func="selectedFunc" on-selected="onFuncSelected"></func-store>
+                </md-content>
+              </md-tab>
+              <md-tab label="Manually" md-on-deselect="onManualTabDeselect()" md-on-select="onTabSelect(1)">
+                <md-content class="md-padding">
+                    <div>
+                        <label><i>Use this form to test a function or the <a ng-href="https://github.com/openfaas/faas-cli">faas-cli</a> for more options.</i></label>
+                    </div>
+                    <label>Define the function below:</label>
+                    <form name="userForm">
+                        <div layout-gt-xs="row">
+                            <md-input-container class="md-block" flex-gt-sm>
+                                <md-tooltip md-direction="bottom">Docker image name and tag to use for function i.e. functions/alpine:latest</md-tooltip>
+                                <label>Docker image:</label>
+                                <input name="dockerImage" ng-model="item.image" required md-maxlength="200" minlength="2">
+                            </md-input-container>
+                        </div>
+                        <div layout-gt-xs="row">
+                            <md-input-container class="md-block" flex-gt-sm>
+                                <md-tooltip md-direction="bottom">Name of the function - must be a valid DNS entry</md-tooltip>
+                                <label>Function name:</label>
+                                <input name="serviceName" ng-model="item.service" required md-maxlength="200" minlength="2">
+                            </md-input-container>
+                        </div>
+                        <div layout-gt-xs="row">
+                            <md-input-container class="md-block" flex-gt-sm>
+                                <md-tooltip md-direction="bottom">Process to run as your function i.e. 'env' or 'shasum'. Ignore if using OpenFaaS templates</md-tooltip>
+                                <label>Function process (optional):</label>
+                                <input name="envProcess" ng-model="item.envProcess" md-maxlength="200" minlength="0">
+                            </md-input-container>
+                        </div>
+                        <div layout-gt-xs="row">
+                            <md-input-container class="md-block" flex-gt-sm>
+                                <md-tooltip md-direction="bottom">Docker Swarm network, not required for other providers. Default: func_functions</md-tooltip>
+                                <label>Network (Swarm):</label>
+                                <input name="network" ng-model="item.network" md-maxlength="200" minlength="0">
+                            </md-input-container>
+                        </div>
+                        <div class="validation-error" layout-gt-xs="row" layout-align="start end">
+                            <span ng-show="validationError">{{ validationError }}</span>
+                        </div>
+                    </form>
+                </md-content>
+              </md-tab>
+            </md-tabs>
+    </md-dialog-content>
+
+    <md-dialog-actions>
+        <md-button ng-click="closeDialog()" class="md-secondary">
+            Close Dialog
+        </md-button>
+        <md-button ng-click="createFunc()" class="md-primary">
+            Deploy
+        </md-button>
+    </md-dialog-actions>
+</md-dialog>
-- 
GitLab