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