mirror of
https://github.com/openfaas/faas.git
synced 2025-06-08 16:26:47 +00:00
Fixes https://github.com/openfaas/faas/issues/1413 Fixes https://github.com/openfaas/faas-netes/issues/707 This fixes the Gateway UI not updating the invocation count automatically without a page reload. Tested by deploying on a local cluster and making sure invocations go up with and without namespace suffix Signed-off-by: Alistair Hey <alistair@heyal.co.uk>
605 lines
22 KiB
JavaScript
605 lines
22 KiB
JavaScript
"use strict"
|
|
// 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', 'ngMessages', 'faasGateway.funcStore']);
|
|
|
|
app.controller("home", ['$scope', '$log', '$http', '$location', '$interval', '$filter', '$mdDialog', '$mdToast', '$mdSidenav',
|
|
function($scope, $log, $http, $location, $interval, $filter, $mdDialog, $mdToast, $mdSidenav) {
|
|
var FUNCSTORE_DEPLOY_TAB_INDEX = 0;
|
|
var CUSTOM_DEPLOY_TAB_INDEX = 1;
|
|
|
|
var newFuncTabIdx = FUNCSTORE_DEPLOY_TAB_INDEX;
|
|
$scope.functions = [];
|
|
$scope.invocationInProgress = false;
|
|
$scope.invocationRequest = "";
|
|
$scope.invocationResponse = "";
|
|
$scope.invocationStatus = "";
|
|
$scope.invocationStart = new Date().getTime();
|
|
$scope.roundTripDuration = "";
|
|
$scope.selectedNamespace = "";
|
|
$scope.allNamespaces = [""];
|
|
$scope.invocation = {
|
|
contentType: "text"
|
|
};
|
|
|
|
$scope.baseUrl = $location.absUrl().replace(/\ui\/$/, '');
|
|
|
|
try {
|
|
$scope.canCopyToClipboard = document.queryCommandSupported('copy');
|
|
} catch (err) {
|
|
console.error(err);
|
|
$scope.canCopyToClipboard = false;
|
|
}
|
|
$scope.copyClicked = function(e) {
|
|
e.target.parentElement.querySelector('input').select()
|
|
var copySuccessful = false;
|
|
try {
|
|
copySuccessful = document.execCommand('copy');
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
var msg = copySuccessful ? 'Copied to Clipboard' : 'Copy failed. Please copy it manually';
|
|
showPostInvokedToast(msg);
|
|
}
|
|
|
|
|
|
|
|
$scope.toggleSideNav = function() {
|
|
$mdSidenav('left').toggle();
|
|
};
|
|
|
|
$scope.functionTemplate = {
|
|
image: "",
|
|
envProcess: "",
|
|
network: "",
|
|
service: "",
|
|
envVars: {},
|
|
labels: {},
|
|
annotations: {},
|
|
secrets: [],
|
|
namespace: "",
|
|
};
|
|
|
|
$scope.invocation.request = "";
|
|
|
|
var fetchFunctionsDelay = 3500;
|
|
var queryFunctionDelay = 2500;
|
|
|
|
|
|
|
|
|
|
$http.get("../system/namespaces")
|
|
.then(function(response) {
|
|
$scope.allNamespaces = response.data;
|
|
if ($scope.selectedNamespace === "" && response.data && response.data[0] && response.data[0] !== "") {
|
|
$scope.selectedNamespace = response.data[0]
|
|
refreshData($scope.selectedNamespace)
|
|
}
|
|
})
|
|
|
|
|
|
var fetchFunctionsInterval = $interval(function() {
|
|
refreshData($scope.selectedNamespace);
|
|
}, fetchFunctionsDelay);
|
|
|
|
var queryFunctionInterval = $interval(function() {
|
|
if($scope.selectedFunction && $scope.selectedFunction.name) {
|
|
refreshFunction($scope.selectedFunction);
|
|
}
|
|
}, queryFunctionDelay);
|
|
|
|
$scope.setNamespace = function(namespace) {
|
|
$scope.selectedNamespace = namespace;
|
|
$scope.selectedFunction = undefined;
|
|
$scope.functions = [];
|
|
refreshData($scope.selectedNamespace)
|
|
}
|
|
|
|
var refreshFunction = function(functionInstance) {
|
|
const url = "/system/function/" + functionInstance.name;
|
|
$http.get(buildNamespaceAwareURL(url, $scope.selectedNamespace))
|
|
.then(function(response) {
|
|
$scope.selectedFunction.ready = (response.data && response.data.availableReplicas && response.data.availableReplicas > 0);
|
|
})
|
|
.catch(function(err) {
|
|
console.error(err);
|
|
});
|
|
};
|
|
|
|
var showPostInvokedToast = function(message, duration) {
|
|
$mdToast.show(
|
|
$mdToast.simple()
|
|
.textContent(message)
|
|
.position("top right")
|
|
.hideDelay(duration || 500)
|
|
);
|
|
};
|
|
|
|
$scope.fireRequest = function() {
|
|
var requestContentType = $scope.invocation.contentType == "json" ? "application/json" : "text/plain";
|
|
if ($scope.invocation.contentType == "binary") {
|
|
requestContentType = "binary/octet-stream";
|
|
}
|
|
|
|
var fnNamespace = ($scope.selectedNamespace && $scope.selectedNamespace.length > 0) ? "." + $scope.selectedNamespace : "";
|
|
var options = {
|
|
url: "../function/" + $scope.selectedFunction.name + fnNamespace,
|
|
data: $scope.invocation.request,
|
|
method: "POST",
|
|
headers: { "Content-Type": requestContentType },
|
|
responseType: $scope.invocation.contentType == "binary" ? "arraybuffer" : $scope.invocation.contentType
|
|
};
|
|
|
|
$scope.invocationInProgress = true;
|
|
$scope.invocationResponse = "";
|
|
$scope.invocationStatus = null;
|
|
$scope.roundTripDuration = "";
|
|
$scope.invocationStart = new Date().getTime()
|
|
|
|
|
|
var tryDownload = function(data, filename) {
|
|
var caught;
|
|
|
|
try {
|
|
var blob = new Blob([data], { type: "binary/octet-stream" });
|
|
|
|
if (window.navigator.msSaveBlob) { // // IE hack; see http://msdn.microsoft.com/en-us/library/ie/hh779016.aspx
|
|
window.navigator.msSaveOrOpenBlob(blob, filename);
|
|
}
|
|
else {
|
|
var linkElement = window.document.createElement("a");
|
|
linkElement.href = window.URL.createObjectURL(blob);
|
|
linkElement.download = filename;
|
|
document.body.appendChild(linkElement);
|
|
linkElement.click();
|
|
document.body.removeChild(linkElement);
|
|
}
|
|
|
|
} catch (ex) {
|
|
caught = ex;
|
|
}
|
|
return caught;
|
|
}
|
|
|
|
$http(options)
|
|
.then(function (response) {
|
|
var data = response.data;
|
|
var status = response.status;
|
|
|
|
if($scope.invocation.contentType == "binary") {
|
|
var filename = uuidv4();
|
|
|
|
if($scope.selectedFunction.labels) {
|
|
var ext = $scope.selectedFunction.labels["com.openfaas.ui.ext"];
|
|
if(ext && ext.length > 0 ) {
|
|
filename = filename + "." + ext;
|
|
}
|
|
}
|
|
|
|
var caught = tryDownload(data, filename);
|
|
if(caught) {
|
|
console.log(caught);
|
|
$scope.invocationResponse = caught
|
|
} else {
|
|
$scope.invocationResponse = data.byteLength + " byte(s) received";
|
|
}
|
|
|
|
} else {
|
|
|
|
if (typeof data == 'object') {
|
|
$scope.invocationResponse = JSON.stringify(data, null, 2);
|
|
} else {
|
|
$scope.invocationResponse = data;
|
|
}
|
|
}
|
|
|
|
$scope.invocationInProgress = false;
|
|
$scope.invocationStatus = status;
|
|
var now = new Date().getTime();
|
|
$scope.roundTripDuration = (now - $scope.invocationStart) / 1000;
|
|
showPostInvokedToast("Success");
|
|
|
|
}).catch(function(error1) {
|
|
$scope.invocationInProgress = false;
|
|
$scope.invocationResponse = error1.statusText + "\n" + error1.data;
|
|
$scope.invocationStatus = error1.status;
|
|
var now = new Date().getTime();
|
|
$scope.roundTripDuration = (now - $scope.invocationStart) / 1000;
|
|
|
|
showPostInvokedToast("Error");
|
|
});
|
|
};
|
|
|
|
var refreshData = function (selectedNamespace) {
|
|
$http.get(buildNamespaceAwareURL("/system/functions", selectedNamespace)).then(function (response) {
|
|
const curNamespace = ($scope.functions.length > 0 && $scope.functions[0].namespace && $scope.functions[0].namespace) ? $scope.functions[0].namespace : "";
|
|
const newNamespace = (response.data && response.data[0] && response.data[0].namespace) ? response.data[0].namespace : "";
|
|
|
|
if (response && response.data && (curNamespace !== newNamespace || $scope.functions.length != response.data.length)) {
|
|
$scope.functions = response.data;
|
|
if (!$scope.functions.indexOf($scope.selectedFunction )) {
|
|
$scope.selectedFunction = undefined;
|
|
}
|
|
}
|
|
|
|
if ($scope.selectedFunction) {
|
|
response.data.some(function(entry) {
|
|
if (entry.name === $scope.selectedFunction.name) {
|
|
$scope.selectedFunction.invocationCount = entry.invocationCount
|
|
return true
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
};
|
|
|
|
$scope.showFunction = function(fn) {
|
|
if ($scope.selectedFunction != fn) {
|
|
$scope.selectedFunction = fn;
|
|
$scope.invocation.request = "";
|
|
$scope.invocationResponse = "";
|
|
$scope.invocationStatus = "";
|
|
$scope.invocationInProgress = false;
|
|
if (fn.labels && fn.labels['com.openfaas.ui.ext']) {
|
|
$scope.invocation.contentType = "binary";
|
|
} else {
|
|
$scope.invocation.contentType = "text";
|
|
}
|
|
$scope.invocation.roundTripDuration = "";
|
|
}
|
|
};
|
|
|
|
var showDialog = function($event) {
|
|
var parentEl = angular.element(document.body);
|
|
$mdDialog.show({
|
|
parent: parentEl,
|
|
targetEvent: $event,
|
|
templateUrl: "templates/newfunction.html",
|
|
locals: {
|
|
item: $scope.functionTemplate,
|
|
selectedNamespace: $scope.selectedNamespace
|
|
},
|
|
controller: DialogController,
|
|
});
|
|
};
|
|
|
|
var DialogController = function($scope, $mdDialog, item, selectedNamespace) {
|
|
var fetchNamespaces = function () {
|
|
$http.get("../system/namespaces")
|
|
.then(function(response) {
|
|
$scope.allNamespaces = response.data;
|
|
})
|
|
}
|
|
$scope.selectedTabIdx = newFuncTabIdx;
|
|
$scope.item = {};
|
|
$scope.selectedFunc = null;
|
|
$scope.envFieldsVisible = false;
|
|
$scope.envVarInputs = [{key: "", value: ""}];
|
|
|
|
$scope.secretFieldsVisible = false;
|
|
$scope.secretInputs = [{name: ""}];
|
|
|
|
$scope.labelFieldsVisible = false;
|
|
$scope.labelInputs = [{key: "", value: ""}];
|
|
|
|
$scope.annotationFieldsVisible = false;
|
|
$scope.annotationInputs = [{key: "", value: ""}];
|
|
$scope.namespaceSelect = selectedNamespace;
|
|
fetchNamespaces();
|
|
|
|
|
|
$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.envVarsToEnvVarInputs(func.environment);
|
|
$scope.labelsToLabelInputs(func.labels);
|
|
$scope.annotationsToAnnotationInputs(func.annotations);
|
|
$scope.secretsToSecretInputs(func.secrets);
|
|
$scope.item.namespace = $scope.namespaceSelected();
|
|
|
|
|
|
$scope.selectedFunc = func;
|
|
}
|
|
|
|
$scope.onTabSelect = function(idx) {
|
|
newFuncTabIdx = idx;
|
|
}
|
|
|
|
$scope.onStoreTabDeselect = function() {
|
|
$scope.selectedFunc = null;
|
|
}
|
|
|
|
$scope.onCustomTabDeselect = function() {
|
|
$scope.item = {};
|
|
}
|
|
|
|
$scope.createFunc = function() {
|
|
$scope.item.envVars = $scope.envVarInputsToEnvVars();
|
|
$scope.item.secrets = $scope.secretInputsToSecrets();
|
|
$scope.item.labels = $scope.labelInputsToLabels();
|
|
$scope.item.annotations = $scope.annotationInputsToAnnotations();
|
|
$scope.item.namespace = $scope.namespaceSelected();
|
|
|
|
var options = {
|
|
url: "../system/functions",
|
|
data: $scope.item,
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
responseType: "text"
|
|
};
|
|
|
|
$http(options)
|
|
.then(function(response) {
|
|
item.image = "";
|
|
item.service = "";
|
|
item.envProcess = "";
|
|
item.network = "";
|
|
item.envVars = {};
|
|
item.labels = {};
|
|
item.annotations = {};
|
|
item.secrets = [];
|
|
item.namespace = "";
|
|
|
|
$scope.validationError = "";
|
|
$scope.closeDialog();
|
|
showPostInvokedToast("Function created");
|
|
}).catch(function(error1) {
|
|
showPostInvokedToast("Error");
|
|
$scope.selectedTabIdx = CUSTOM_DEPLOY_TAB_INDEX;
|
|
$scope.validationError = error1.data;
|
|
});
|
|
};
|
|
|
|
$scope.fnNamespaceSelected = function(inputNamespace) {
|
|
$scope.namespaceSelect = inputNamespace;
|
|
}
|
|
|
|
$scope.onEnvInputExpand = function() {
|
|
$scope.envFieldsVisible = !$scope.envFieldsVisible;
|
|
}
|
|
|
|
$scope.addEnvVar = function(index) {
|
|
var newInput = {key: "", value: ""};
|
|
$scope.envVarInputs.splice(index + 1, 0, newInput);
|
|
}
|
|
|
|
$scope.removeEnvVar = function($event, envVar) {
|
|
var index = $scope.envVarInputs.indexOf(envVar);
|
|
if ($event.which == 1) {
|
|
$scope.envVarInputs.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
$scope.onSecretInputExpand = function() {
|
|
$scope.secretFieldsVisible = !$scope.secretFieldsVisible;
|
|
}
|
|
|
|
$scope.addSecret = function(index) {
|
|
var newInput = {name: ""};
|
|
$scope.secretInputs.splice(index + 1, 0, newInput);
|
|
}
|
|
|
|
$scope.removeSecret = function($event, secret) {
|
|
var index = $scope.secretInputs.indexOf(secret);
|
|
if ($event.which == 1) {
|
|
$scope.secretInputs.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
$scope.onLabelInputExpand = function() {
|
|
$scope.labelFieldsVisible = !$scope.labelFieldsVisible;
|
|
}
|
|
|
|
$scope.addLabel = function(index) {
|
|
var newInput = {key: "", value: ""};
|
|
$scope.labelInputs.splice(index + 1, 0, newInput);
|
|
}
|
|
|
|
$scope.removeLabel = function($event, label) {
|
|
var index = $scope.labelInputs.indexOf(label);
|
|
if ($event.which == 1) {
|
|
$scope.labelInputs.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
$scope.onAnnotationInputExpand = function() {
|
|
$scope.annotationFieldsVisible = !$scope.annotationFieldsVisible;
|
|
}
|
|
|
|
$scope.addAnnotation = function(index) {
|
|
var newInput = {key: "", value: ""};
|
|
$scope.annotationInputs.splice(index + 1, 0, newInput);
|
|
}
|
|
|
|
$scope.removeAnnotation = function($event, annotation) {
|
|
var index = $scope.annotationInputs.indexOf(annotation);
|
|
if ($event.which == 1) {
|
|
$scope.annotationInputs.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
$scope.envVarInputsToEnvVars = function() {
|
|
var self = this;
|
|
var result = {};
|
|
for(var i = 0; i < self.envVarInputs.length; i++) {
|
|
if (self.envVarInputs[i].key && self.envVarInputs[i].value) {
|
|
result[self.envVarInputs[i].key] = self.envVarInputs[i].value;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
$scope.envVarsToEnvVarInputs = function(envVars) {
|
|
var result = [];
|
|
for (var e in envVars) {
|
|
result.push({key: e, value: envVars[e]});
|
|
}
|
|
|
|
if (result.length > 0) {
|
|
// make the fields visible if deploying from store with values
|
|
$scope.envFieldsVisible = true;
|
|
} else {
|
|
result.push({key: "", value: ""});
|
|
$scope.envFieldsVisible = false;
|
|
}
|
|
|
|
$scope.envVarInputs = result;
|
|
}
|
|
|
|
$scope.secretInputsToSecrets = function() {
|
|
var self = this;
|
|
var result = [];
|
|
for(var i = 0; i < self.secretInputs.length; i++) {
|
|
if (self.secretInputs[i].name) {
|
|
result.push(self.secretInputs[i].name);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
$scope.secretsToSecretInputs = function(secrets) {
|
|
var result = [];
|
|
for (var secret in secrets) {
|
|
result.push({name: secret});
|
|
}
|
|
|
|
if (result.length > 0) {
|
|
// make the fields visible if deploying from store with values
|
|
$scope.secretFieldsVisible = true;
|
|
} else {
|
|
result.push({name: ""});
|
|
$scope.secretFieldsVisible = false;
|
|
}
|
|
|
|
$scope.secretInputs = result;
|
|
}
|
|
|
|
$scope.labelInputsToLabels = function() {
|
|
var self = this;
|
|
var result = {};
|
|
for(var i = 0; i < self.labelInputs.length; i++) {
|
|
if (self.labelInputs[i].key && self.labelInputs[i].value) {
|
|
result[self.labelInputs[i].key] = self.labelInputs[i].value;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
$scope.labelsToLabelInputs = function(labels) {
|
|
var result = [];
|
|
for (var l in labels) {
|
|
result.push({key: l, value: labels[l]});
|
|
}
|
|
|
|
if (result.length > 0) {
|
|
// make the fields visible if deploying from store with values
|
|
$scope.labelFieldsVisible = true;
|
|
} else {
|
|
result.push({key: "", value: ""});
|
|
$scope.labelFieldsVisible = false;
|
|
}
|
|
|
|
$scope.labelInputs = result;
|
|
}
|
|
|
|
$scope.annotationInputsToAnnotations = function() {
|
|
var self = this;
|
|
var result = {};
|
|
for(var i = 0; i < self.annotationInputs.length; i++) {
|
|
if (self.annotationInputs[i].key && self.annotationInputs[i].value) {
|
|
result[self.annotationInputs[i].key] = self.annotationInputs[i].value;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
$scope.namespaceSelected = function() {
|
|
var self = this;
|
|
return self.namespaceSelect;
|
|
}
|
|
|
|
$scope.annotationsToAnnotationInputs = function(annotations) {
|
|
var result = [];
|
|
for (var a in annotations) {
|
|
result.push({key: a, value: annotations[a]});
|
|
}
|
|
|
|
if (result.length > 0) {
|
|
// make the fields visible if deploying from store with values
|
|
$scope.annotationFieldsVisible = true;
|
|
} else {
|
|
result.push({key: "", value: ""});
|
|
$scope.annotationFieldsVisible = false;
|
|
}
|
|
|
|
$scope.annotationInputs = result;
|
|
}
|
|
};
|
|
|
|
$scope.newFunction = function() {
|
|
showDialog();
|
|
};
|
|
|
|
$scope.deleteFunction = function($event) {
|
|
var confirm = $mdDialog.confirm()
|
|
.title('Delete Function')
|
|
.textContent('Are you sure you want to delete ' + $scope.selectedFunction.name + '?')
|
|
.ariaLabel('Delete function')
|
|
.targetEvent($event)
|
|
.ok('OK')
|
|
.cancel('Cancel');
|
|
|
|
$mdDialog.show(confirm)
|
|
.then(function() {
|
|
var options = {
|
|
|
|
url: buildNamespaceAwareURL("/system/functions", $scope.selectedNamespace),
|
|
data: {
|
|
functionName: $scope.selectedFunction.name
|
|
},
|
|
method: "DELETE",
|
|
headers: { "Content-Type": "application/json" },
|
|
responseType: "json"
|
|
};
|
|
|
|
return $http(options);
|
|
}).then(function() {
|
|
showPostInvokedToast("Success");
|
|
}).catch(function(err) {
|
|
if (err) {
|
|
// show error toast only if there actually is an err.
|
|
// because hitting 'Cancel' also rejects the promise.
|
|
showPostInvokedToast("Error");
|
|
}
|
|
});
|
|
};
|
|
}
|
|
]);
|
|
|
|
function uuidv4() {
|
|
var cryptoInstance = window.crypto || window.msCrypto; // for IE11
|
|
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function(c) {
|
|
return (c ^ cryptoInstance.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
|
})
|
|
}
|
|
|
|
function buildNamespaceAwareURL(path, namespace) {
|
|
let newUrl = path.startsWith("/")? ".." + path: "../" + path;
|
|
|
|
if (namespace && namespace.length > 0){
|
|
newUrl += "?namespace=" + namespace
|
|
}
|
|
return newUrl
|
|
} |