You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by ra...@apache.org on 2019/11/01 14:06:54 UTC
[openwhisk] 01/02: A UI providing playground functionality for
authoring functions and running them in the browser.
This is an automated email from the ASF dual-hosted git repository.
rabbah pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk.git
commit 81d6b4daa88efb1bdfd08b185568b75e8c7dbdac
Author: Joshua Auerbach <jo...@gmail.com>
AuthorDate: Fri Oct 11 16:45:00 2019 -0400
A UI providing playground functionality for authoring functions and running them in the browser.
---
.../playground/actions/playground-delete.js | 34 +
.../playground/actions/playground-fetch.js | 34 +
.../resources/playground/actions/playground-run.js | 72 ++
.../playground/actions/playground-userpackage.js | 60 ++
.../src/main/resources/playground/ui/index.html | 139 ++++
.../main/resources/playground/ui/playground.css | 228 ++++++
.../resources/playground/ui/playgroundFunctions.js | 799 +++++++++++++++++++++
7 files changed, 1366 insertions(+)
diff --git a/core/standalone/src/main/resources/playground/actions/playground-delete.js b/core/standalone/src/main/resources/playground/actions/playground-delete.js
new file mode 100644
index 0000000..75e4f50
--- /dev/null
+++ b/core/standalone/src/main/resources/playground/actions/playground-delete.js
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var openwhisk = require('openwhisk');
+
+// Deletes a deployed action (named according to the playgroundId and action name) if the action exists.
+function main(outerParam) {
+ let param = JSON.parse(outerParam['__ow_body'])
+ let playgroundId = param['playgroundId']
+ let actionName = param['actionName']
+ let wsk = openwhisk({ignore_certs: outerParam.__ignore_certs}) // ignores self-signed certs, necessary in some deployments
+ let fullName = 'user' + playgroundId + '/' + actionName
+ console.log("deleting action", fullName)
+ return wsk.actions.delete(fullName).then(result => {
+ console.log('deleted user action')
+ return result
+ }).catch(err => {
+ console.error('action did not exist or error occurred', err)
+ })
+}
diff --git a/core/standalone/src/main/resources/playground/actions/playground-fetch.js b/core/standalone/src/main/resources/playground/actions/playground-fetch.js
new file mode 100644
index 0000000..76540ab
--- /dev/null
+++ b/core/standalone/src/main/resources/playground/actions/playground-fetch.js
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var openwhisk = require('openwhisk');
+
+// Returns the code of a deployed action named according to the playgroundId action name
+function main(outerParam) {
+ let param = JSON.parse(outerParam['__ow_body'])
+ let playgroundId = param['playgroundId']
+ let actionName = param['actionName']
+ let wsk = openwhisk({ignore_certs: outerParam.__ignore_certs}) // ignores self-signed certs, necessary in some deployments
+ let fullName = 'user' + playgroundId + '/' + actionName
+ console.log("fetching action", fullName)
+ return wsk.actions.get(fullName).then(result => {
+ console.log('got user action')
+ return result
+ }).catch(err => {
+ console.error('error retrieving action', err)
+ })
+}
diff --git a/core/standalone/src/main/resources/playground/actions/playground-run.js b/core/standalone/src/main/resources/playground/actions/playground-run.js
new file mode 100644
index 0000000..6a4984d
--- /dev/null
+++ b/core/standalone/src/main/resources/playground/actions/playground-run.js
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var openwhisk = require('openwhisk');
+
+// Deploys code as an action and optionally runs it.
+// The input parameters are
+// code -- the code to run
+// saveOnly -- if present and true, the action is not run but only deployed
+// web-export -- if present and true, the action is deployed as a web action (annotated with web-export=true). Implies saveOnly.
+// params -- parameters to pass to the code when running it (ignored if saveOnly is present or implied)
+// playgroundId -- the identity of the browser instance submitting the code (functions as a kind of user id but not enduring
+// or authenticated). Becomes part of the name of the action.
+// action -- the name of the action as assigned by the user or one of the default sample names; combines with playgroundId to form
+// the action name as viewed by OpenWhisk
+// runtime -- the whisk runtime ('kind') value to use in running or saving the action.
+function main(outerParam) {
+ let t0 = new Date().getTime()
+ //console.log('outerParam: ', outerParam)
+ // Get parameters
+ let param = JSON.parse(outerParam['__ow_body'])
+ let saveOnly = param['saveOnly']
+ let webExport = param['web-export']
+ let code = param['code']
+ let codeParams = param['params']
+ let playgroundId = param['playgroundId']
+ let action = param['actionName']
+ let runtime = param['runtime']
+ // Deploy the action. The action is left deployed after running it, which allows playground-fetch to fetch the code back
+ // for a later edit session. In a saveOnly or web-export scenario, the code is not even run after that.
+ let wsk = openwhisk({ignore_certs: outerParam.__ignore_certs}) // ignores self-signed certs, necessary in some deployments
+ let actionName = 'user' + playgroundId + '/' + action
+ let annotations = {"web-export": webExport ? true : false }
+ var deployParams = {name: actionName, action: code, kind: runtime, annotations: annotations}
+ return wsk.actions.update(deployParams).then(uresult => {
+ // Unless saveOnly, run the action once deployed.
+ let t1 = new Date().getTime()
+ console.log('made user action')
+ if (saveOnly || webExport) {
+ return { saved: true }
+ } else {
+ return wsk.actions.invoke({ actionName: actionName, blocking: true, params: codeParams }).then(aresult => {
+ // Return the result
+ let t2 = new Date().getTime()
+ console.log('aresult: ', aresult)
+ let response = aresult['response']
+ let result = response['result']
+ return { param: param, result: result, deployTime: t1 - t0, runTime: t2 - t1 }
+ }).catch(err => {
+ console.error('error invoking action', err)
+ return {error: err}
+ })
+ }
+ }).catch(err => {
+ console.error('error creating action', err)
+ return {error: err}
+ })
+}
diff --git a/core/standalone/src/main/resources/playground/actions/playground-userpackage.js b/core/standalone/src/main/resources/playground/actions/playground-userpackage.js
new file mode 100644
index 0000000..6259b54
--- /dev/null
+++ b/core/standalone/src/main/resources/playground/actions/playground-userpackage.js
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var openwhisk = require('openwhisk');
+
+// Returns the package structure for a given user, creating it if it doesn't exist.
+// Used to initialize playground state when an existing user loads the playground page and also to begin the
+// process with an empty package for a new user.
+// This code also maintains a lastSession date as a package annotation. This denotes the last time
+// this user opened the playground and can be used to expire the package.
+function main(outerParam) {
+ let param = JSON.parse(outerParam['__ow_body'])
+ let playgroundId = param['playgroundId']
+ let wsk = openwhisk({ignore_certs: outerParam.__ignore_certs}) // ignores self-signed certs, necessary in some deployments
+ let name = "user" + playgroundId
+ let ts = new Date().toISOString()
+ let tsAnnotation = { key: "lastSession", value: ts }
+ return wsk.packages.get(name).then(result => {
+ console.log('found existing package', result)
+ let annotations = result.annotations
+ annotations.push(tsAnnotation)
+ return wsk.packages.update({"name": name, "package": {annotations: annotations}}).then(_ => {
+ // Return original response, which has the old timestamp. Client does not use the timestamp in the response.
+ // The response from the update does not include the package list.
+ return result
+ }).catch(err => {
+ console.log("could not add lastSession annotation (proceeding)", err)
+ return result // even if not updated
+ })
+ }).catch(err => {
+ console.log('package does not exist or other error')
+ if (err.statusCode === 404) {
+ // Simple not found error. Just create the package
+ return wsk.packages.create({"name": name, "package": { annotations: [ tsAnnotation ]}}).then(result => {
+ console.log('created package', result)
+ return result
+ }).catch(err => {
+ console.error('error creating package', err)
+ return { error: err }
+ })
+ } else {
+ console.error('unrecoverable error retrieving package', err)
+ return { error: err }
+ }
+ })
+}
diff --git a/core/standalone/src/main/resources/playground/ui/index.html b/core/standalone/src/main/resources/playground/ui/index.html
new file mode 100644
index 0000000..0aaba42
--- /dev/null
+++ b/core/standalone/src/main/resources/playground/ui/index.html
@@ -0,0 +1,139 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+<!DOCTYPE html>
+<html>
+
+<head>
+<meta charset="utf-8"/>
+<title>Function Playground</title>
+
+<!-- for the ACE editor component -->
+<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.2/ace.js" type="text/javascript" charset="utf-8"></script>
+
+<!-- for the Google material UI icons -->
+<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+
+<!-- begin - to enable panel resize -->
+<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
+<script src="https://rawgit.com/RickStrahl/jquery-resizable/master/src/jquery-resizable.js"></script>
+
+<script>
+$(document).ready(function() {
+ $("#panel-left").resizable({
+ handleSelector: ".splitter-vertical",
+ resizeHeight: false
+ });
+});
+</script>
+<!-- end - to enable panel resize -->
+
+<!-- The js needs to go after jquery is loaded because it uses jquery to run the init block after DOM loading.
+ The css is placed next to it to make the inlining easier.
+ -->
+<!--Start Inlining-->
+<link rel="stylesheet" type="text/css" href="playground.css">
+<script src="playgroundFunctions.js"></script>
+<!--End Inlining-->
+
+<!-- OpenWhisk and Function Playground icons (svgomg was used to compact) -->
+<svg aria-hidden="true" focusable="false" style="display:none" xmlns="http://www.w3.org/2000/svg">
+ <symbol id="svg-logo-icon" viewBox="0 0 32 32">
+ <image xlink:href="https://openwhisk.apache.org/images/logo/apache-openwhisk-logo-only.png" height="32" width="32" />
+ </symbol>
+ <symbol id="svg-logo-text" viewBox="0 0 195 32">
+ <text x="0.259234" y="29.829633" fill="#808080" font-family="Helvetica, Arial, sans-serif" font-size="10.667px" font-weight="bold" stroke="#000000" stroke-width="0" xml:space="preserve">OpenWhisk</text>
+ <text transform="matrix(.89868 0 0 1 -.044817 0)" x="-0.480163" y="16.794799" fill="#cccccc" font-family="Helvetica, Arial, sans-serif" font-size="18.667px" font-weight="bold" stroke-width="1px" xml:space="preserve">
+ <tspan x="-0.480163" y="16.794799" fill="#cccccc" font-family="Helvetica, Arial, sans-serif" font-size="18.667px" font-weight="bold">Function Playground</tspan>
+ </text>
+ </symbol>
+</svg>
+
+</head>
+
+<body id="body" class="body-container">
+ <div class="navbar">
+ <div class="nav-item">
+ <svg aria-hidden="true" focusable="false" class="logo-icon"><use xlink:href="#svg-logo-icon"/></svg>
+ <svg id="logo-text" aria-hidden="true" focusable="false" class="logo-text nav-right-spacer"><use xlink:href="#svg-logo-text"/></svg>
+ </div>
+ <div class="nav-item">
+ <button id="run" class="nav-button" type="button" onclick="runClicked()">
+ <i style="font-size:12pt !important;" class="material-icons icon-size">play_arrow</i>Run
+ </button>
+ </div>
+ <div class="nav-item">
+ <button id="publish" class="nav-button nav-right-spacer" type="button" onclick="publishClicked()">
+ <i class="material-icons icon-size icon-extra-margin">cloud_upload</i>Publish
+ </button>
+ </div>
+ <div class="nav-item">
+ <select id="languageSelector" class="nav-select" onchange="languageChanged()">
+ <option value="JavaScript" selected="selected">JavaScript</option>
+ <option value="Python">Python</option>
+ </select>
+ </div>
+ <div class="nav-item">
+ <select id="actionSelector" class="nav-select" onchange="actionChanged()" select="">
+ <option value="sampleJavaScript" selected="selected">sampleJavaScript</option>
+ <option value="--New Action--">--New Action--</option>
+ <option value="--Rename--">--Rename--</option>
+ </select>
+ <input id="nameInput" class="nav-input" onchange="processNewName()" type="text">
+ </div>
+ <div class="nav-item-last">
+ <button id="theme" class="nav-button" type="button" onclick="themeClicked()">
+ <i class="material-icons icon-size icon-extra-margin">web</i>
+ <span id="themeName">Light</span>
+ </button>
+ </div>
+ </div>
+ <div class="central-container">
+ <div id="panel-left" class="panel-left">
+ <div class="panel-header">
+ <i style="margin-left: 4px;" class="material-icons icon-size icon-extra-margin">cloud_queue</i>
+ URL: <span id="urlText">[editable, private]</span>
+ </div>
+ <div id="editor" ace-editor [(text)]="text"></div>
+ </div>
+ <div class="splitter-vertical"></div>
+ <div class="panel-right">
+ <div class="panel-header">
+ <i class="material-icons icon-size icon-extra-margin">input</i>INPUT PARAMETERS
+ </div>
+ <div class="panel-right-top">
+ <textarea id="input" spellcheck="false" class="panel-right-input">{ "name" : "openwhisk" }</textarea>
+ </div>
+ <div class="panel-header">
+ <i class="material-icons icon-size icon-extra-margin">access_time</i>EXECUTION TIME
+ </div>
+ <div class="panel-right-mid">
+ <div id="timingText" class="panel-right-box"></div>
+ </div>
+ <div class="panel-header">
+ <i class="material-icons icon-size icon-extra-margin">done</i>OUTPUT
+ </div>
+ <div class="panel-right-bottom">
+ <div id="resultText" class="panel-right-box"></div>
+ </div>
+ </div>
+ </div>
+</body>
+
+</html>
diff --git a/core/standalone/src/main/resources/playground/ui/playground.css b/core/standalone/src/main/resources/playground/ui/playground.css
new file mode 100644
index 0000000..8df6370
--- /dev/null
+++ b/core/standalone/src/main/resources/playground/ui/playground.css
@@ -0,0 +1,228 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+html, body {
+ height: 100%;
+ margin: 0px;
+}
+
+* {
+ box-sizing: border-box; /* include the border and padding in width / height calcuations */
+}
+
+#editor {
+ flex: 1 1 auto;
+}
+
+.body-container {
+ font-family: Arial, Helvetica, sans-serif;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ background-color: #26282C;
+}
+
+.navbar {
+ flex: 0 0 auto;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ margin-top: 8px;
+ margin-bottom: 8px;
+}
+
+.nav-item {
+ display: flex;
+ flex: 0 0 auto;
+ border-right: 12px solid transparent;
+}
+
+.nav-item-center {
+ display: flex;
+ align-items: center;
+}
+
+.nav-item-last {
+ flex: 1 0 auto;
+ text-align:right;
+ margin-right: 4px;
+}
+
+.logo-icon {
+ width: 32px;
+ height: 32px;
+ margin-left: 8px;
+ margin-right: 6px;
+}
+
+.logo-text {
+ width: 195px;
+ height: 32px;
+}
+
+.nav-button {
+ padding-top: 8px;
+ padding-bottom: 8px;
+ padding-left: 15px;
+ padding-right: 15px;
+ border: 2px solid #424446;
+ border-radius: 8px;
+ background-color: #26282C;
+ color: white;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ font-size: 12pt;
+ cursor: pointer;
+}
+
+.icon-size {
+ font-size:12pt !important;
+ position: relative;
+ top: 2px;
+ margin-right: 4px;
+}
+
+.icon-extra-margin {
+ margin-right: 8px;
+}
+
+.nav-select {
+ color: white;
+ border: 2px solid #424446;
+ border-radius: 8px;
+ background-color: #26282C;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ padding-left: 20px;
+ padding-right: 20px;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ font-size: 10pt;
+ cursor: pointer;
+}
+
+.nav-input {
+ padding-top: 5px;
+ padding-bottom: 5px;
+ color: white;
+ background-color: black;
+ margin-left: 6px;
+ display: none;
+}
+
+.nav-right-spacer {
+ margin-right: 30px;
+}
+
+.nav-label {
+ color:#C0C0C0;
+}
+
+.central-container {
+ flex: 1 1 auto;
+ display: flex; /* make this a flex container */
+ /* by default flex-direction is row */
+ /* by default, elements will stretch vertically to the full height of the container */
+}
+
+.panel-left {
+ width: 65%;
+ flex: 0 1 auto;
+ min-height: 160px;
+ min-width: 160px;
+ display: flex;
+ flex-direction: column;
+}
+
+.splitter-vertical {
+ flex: 0 0 auto;
+ width: 12px;
+ min-width: 12px;
+ cursor: col-resize;
+ background-color: #26282C;
+}
+
+.panel-right {
+ flex: 1 1 auto;
+ min-width: 160px;
+ display: flex;
+ flex-direction: column;
+}
+
+.panel-right-box {
+ flex: 1 1 auto;
+ font-family: "Courier New", Courier, monospace;
+ font-size: 12pt;
+ padding: 4px;
+ color: white;
+ background-color: black;
+ margin: 0px;
+}
+
+.panel-right-input {
+ flex: 1 1 auto;
+ font-family: "Courier New", Courier, monospace;
+ font-size: 12pt;
+ padding: 4px;
+ color: white;
+ background-color: black;
+ border:solid 1px grey;
+ margin: 0px;
+ resize:none;
+}
+
+.panel-header {
+ padding-top: 4px;
+ padding-bottom: 4px;
+ font-size: 10pt;
+ font-weight: bold;
+ background-color: #202020;
+ color: #9098A0;
+}
+
+.panel-right-top {
+ flex: 3 1 auto; /* grow(relative size) shrink basis */
+ display: flex;
+ flex-direction: column;
+}
+
+.panel-right-mid {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.panel-right-bottom {
+ flex: 3 1 auto;
+ display: flex;
+ flex-direction: column;
+}
+
+/* for smaller screens - get rid of text logo and shrink button margins */
+@media screen and (min-width: 0px) and (max-width: 1000px) {
+ #logo-text {
+ display: none;
+ }
+ .nav-button {
+ padding-left: 4px;
+ padding-right: 4px;
+ margin-left: 0px;
+ margin-right: 0px;
+ }
+}
diff --git a/core/standalone/src/main/resources/playground/ui/playgroundFunctions.js b/core/standalone/src/main/resources/playground/ui/playgroundFunctions.js
new file mode 100644
index 0000000..35c33ab
--- /dev/null
+++ b/core/standalone/src/main/resources/playground/ui/playgroundFunctions.js
@@ -0,0 +1,799 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+$(document).ready(function(){
+ // This is the location of the supporting API
+ // The host value may get replaced in PlaygroundLauncher to a specific host
+ window.APIHOST=window.location ? window.location.origin : ''
+
+ // To install in a different namespace, change this value
+ window.PLAYGROUND='whisk.system'
+
+ // Keys for cookies
+ window.colorKey = 'colorId'
+ window.languageKey = 'language'
+ window.playgroundIdKey = 'playgroundId'
+ window.actionKey = 'actionName'
+
+ // Initialize GUI elements
+ window.editor = initializeEditor()
+ window.colorSetting = initializeColor()
+
+ // The language table (a JS object acting as an associative array)
+ // Maps from language symbol to structure (1) repeating the symbol as 'name', (2) the editor mode,
+ // (3) the whisk runtime 'kind' to use for the language, and (4) the starting example code for that language.
+ window.languages = {
+ JavaScript: {
+ name: "JavaScript",
+ editMode: "ace/mode/javascript",
+ kind: "nodejs:default",
+ example:`function main(args) {
+ let name = args.name || 'stranger'
+ let greeting = 'Hello ' + name + '!'
+ console.log(greeting)
+ return {"body": greeting}
+}`
+ },
+
+ Python: {
+ name: "Python",
+ editMode: "ace/mode/python",
+ kind: "python:default",
+ example: `def main(args):
+ if 'name' in args:
+ name = args['name']
+ else:
+ name = "stranger"
+ greeting = "Hello " + name + "!"
+ print(greeting)
+ return {"body": greeting}
+`
+ },
+
+ Swift: {
+ name: "Swift",
+ editMode: "ace/mode/swift",
+ kind: "swift:default",
+ example:`func main(args: [String:Any]) -> [String:Any] {
+ if let name = args["name"] as? String {
+ let greeting = "Hello \\(name)!"
+ print(greeting)
+ return [ "body" : greeting ]
+ } else {
+ let greeting = "Hello stranger!"
+ print(greeting)
+ return [ "body" : greeting ]
+ }
+}`
+ },
+
+ Go: {
+ name: 'Go',
+ editMode: 'ace/mode/go',
+ kind: `go:default`,
+ example: `package main
+
+func Main(args map[string]interface{}) map[string]interface{} {
+ name, ok := args["name"].(string)
+ if !ok {
+ name = "stranger"
+ }
+ msg := make(map[string]interface{})
+ msg["body"] = "Hello, " + name + "!"
+ return msg
+}`
+ },
+
+ PHP: {
+ name: 'PHP',
+ editMode: 'ace/mode/php',
+ kind: `php:default`,
+ example: `<?php
+function main(array $args) : array {
+ $name = $args["name"] ?? "stranger";
+ $greeting = "Hello $name!";
+ echo $greeting;
+ return ["body" => $greeting];
+}`
+ }
+ }
+
+ // Other initialization
+ window.playgroundId = initializePlaygroundId()
+ window.EditSession = require("ace/edit_session").EditSession // Per ACE doc
+ window.activeSessions = [] // Contains triples {actionName, EditSession, webbiness} for actions visited in this browser session
+ window.editorContentsChanged = false // A 'dirty' flag consulted as part of autosave logic
+ window.language = initializeLanguage() // Requires languages table to exist
+ window.actionList = [] // Populated asynchronously by initializeUserPackage. Contains pairs {actionName, actionKind}
+ window.currentAction = null // Name of the action displayed in the editor and actionSelector. Initialized by initializeActionSelector.
+ window.entryFollowup = null // Function to execute when name entry completes (for renameAction and startNewAction). Null except during name entry.
+ document.onkeydown = detectEscapeKey // Examine key presses to see if they indicate a desire to cancel name input mode
+
+ initializeUserPackage().then(initializeActionSelector).then(startAutosave)
+});
+
+// Start autosave polling
+function startAutosave() {
+ window.setInterval(maybeSave, 15 * 1000)
+}
+
+// Initialize the playgroundId
+function initializePlaygroundId() {
+ let playgroundId = getCookie(window.playgroundIdKey)
+ if (playgroundId == "") {
+ playgroundId = (new Date().getTime()) % 1000000
+ console.log('New playgroundId: ', playgroundId)
+ } else {
+ console.log('Existing playgroundId: ', playgroundId)
+ }
+ setCookie(window.playgroundIdKey, playgroundId) // regardless of whether it was set before; refreshes expiration
+ return playgroundId
+}
+
+// Initialize the actionList to reflect the user's package structure stored on the server, perhaps creating a new package for a new user
+// with no actions. Returns a promise. Initialization code dependent on the action list should be in the promise chain.
+function initializeUserPackage() {
+ console.log("Initializing user", window.playgroundId)
+ return makeOpenWhiskRequest('playground-userpackage.json', { playgroundId: window.playgroundId }).then(result => {
+ console.log("userpackage raw response:", result)
+ let userPackage = JSON.parse(result)
+ if (userPackage && userPackage.actions && Array.isArray(userPackage.actions)) {
+ for (action of userPackage.actions) {
+ let kind = getAnnotation(action, "exec")
+ window.actionList.push({ name: action.name, kind: kind } )
+ }
+ }
+ return window.actionList // For definiteness, to carry on the promise chain. actionList is also global.
+ }).catch(err => {
+ console.error("Error getting user package.", err)
+ })
+}
+
+// Initialize the actions in the action selector and select one (also assigning currentAction) based on a user cookie.
+// Assumes the 'language' global variable is initialized. Only actions in that language are listed.
+// If the cookie is not set, or it denotes an action for a non-selected language, we arbitrarily select the first action
+// of the selected language and also put it into the cookie. End by calling 'imposeAction' to initialize the editor session
+// for the action, returning the result thereof which is a Promise. Editor code may be filled in asynchronously.
+function initializeActionSelector(actionList) {
+ const selector = elem("actionSelector")
+ // Determine the list of action names that should be used.
+ // Start with those that can be read from the user's package (pre-existing).
+ // Add a sample for the current language iff the user has no actions for that language.
+ let actions = actionList.filter(action => matchesLanguage(action))
+ console.log("read", actions.length, "actions from user package")
+ if (actions.length == 0) {
+ console.log("adding sample for", window.language.name)
+ actions.push({ name: "sample" + window.language.name, kind: window.language.kind} )
+ }
+ // Place the action names in the selector's options
+ selector.options.length = 0
+ for (action of actions) {
+ console.log("adding action to selector", action.name)
+ selector.options[selector.options.length] = new Option(action.name, action.name)
+ }
+ // Add other capabilities to the action list.
+ // Add --New Action-- iff the user is within his quota. Add --Delete-- iff there is more than one action.
+ // Add --Rename-- unconditionally. However, --Rename-- and --Delete-- are also enabled/disabled as part of
+ // the imposeWebbiness function (when the action isn't editable it seems illogical that you can rename and delete it)
+ if (actions.length < 10) { // quota is arbitrary
+ let other = "--New Action--"
+ console.log("adding capability", other)
+ selector.options[selector.options.length] = new Option(other, other)
+ }
+ if (actions.length > 1) {
+ let other = "--Delete--"
+ console.log("adding capability", other)
+ selector.options[selector.options.length] = new Option(other, other)
+ }
+ let other = "--Rename--"
+ console.log("adding capability", other)
+ selector.options[selector.options.length] = new Option(other, other)
+ // Now select the action according to the user's cookie (if present and applicable) else arbitrarily choose
+ // the first (or only) list element. The list has at least one action at this point.
+ const cookieVal = getCookie(window.actionKey)
+ window.currentAction = (cookieVal != "" && matchesLanguageByName(cookieVal)) ? cookieVal : actions[0].name
+ selector.value = window.currentAction
+ setCookie(window.actionKey, window.currentAction)
+ return imposeAction(window.currentAction)
+}
+
+// Initialize the editor
+function initializeEditor() {
+ editor = ace.edit("editor");
+ editor.setTheme("ace/theme/monokai");
+ editor.setShowPrintMargin(false);
+ elem('editor').style.fontSize='12pt';
+ return editor
+}
+
+// Initialize the color theme
+function initializeColor() {
+ let color = getCookie(window.colorKey)
+ if (color == "") {
+ color = "dark"
+ }
+ imposeColor(color)
+ return color
+}
+
+// Initialize the language
+function initializeLanguage() {
+ // First initialize the options of the language selector from the language table
+ var selector = elem("languageSelector")
+ selector.options.length = 0 // probably unneeded but just in case this gets done more than once
+ for (member in window.languages) {
+ let languageName = window.languages[member].name
+ console.log("Adding language " + languageName + " to selector")
+ selector.options[selector.options.length] = new Option(languageName, languageName)
+ }
+ console.log("Selector now has " + selector.options.length + " choices")
+ // Retrieve the language choice from the cookie or set to default
+ var language = window.languages.JavaScript // Default
+ let languageName = getCookie(window.languageKey)
+ if (languageName != "") {
+ language = window.languages[languageName]
+ console.log("Language " + languageName + " was retrieved from the cookie")
+ } else {
+ console.log("Language defaulted to " + language.name)
+ setCookie(window.languageKey, language.name)
+ }
+ // Set the language into the selector
+ selector.value = language.name
+ return language
+}
+
+// Examine key presses looking for esc
+function detectEscapeKey(evt) {
+ evt = evt || window.event;
+ var isEscape = false;
+ if ("key" in evt) {
+ isEscape = (evt.key == "Escape" || evt.key == "Esc");
+ } else {
+ isEscape = (evt.keyCode == 27);
+ }
+ if (isEscape && window.entryFollowup != null) {
+ console.log("Cancel detected via esc key")
+ endNameEntry()
+ }
+}
+
+// Test whether an action (from the action list) matches the current language (the action {name, kind} pair is the argument)
+function matchesLanguage(action) {
+ console.log("matching", action.name, "for kind", window.language.kind)
+ let matched = action.kind === window.language.kind
+ console.log("matched", matched)
+ return matched
+}
+
+// Test whether an action matches the current language (language name given)
+// Answers false if the action isn't found.
+function matchesLanguageByName(actionName) {
+ let action = getAction(actionName)
+ return action ? matchesLanguage(action) : false
+}
+
+// Lookup an action by name in the actionList.
+function getAction(actionName) {
+ let index = indexOfAction(actionName)
+ if (index < 0) {
+ return undefined
+ }
+ return window.actionList[index]
+}
+
+// Find the index of an action name in the action list
+function indexOfAction(actionName) {
+ for (i = 0; i < window.actionList.length; i++) {
+ if (window.actionList[i].name == actionName) {
+ return i
+ }
+ }
+ return -1
+}
+
+// Change the language in response to a change in the language selector
+function languageChanged() {
+ const newName = elem("languageSelector").value
+ if (window.language.name == newName) {
+ // Avoid disruption if not really changed (not sure if this can actually happen but just in case)
+ return
+ }
+ maybeSave() // Before language change: saves previous contents. Save is asynchronous but racing with the
+ // following is ok because the asynchronous part of save follows the network send. Once the network send
+ // has occurred, the local state is free to change (if the save fails there is no real recovery).
+ // Change the language global variable and reset the cookie
+ window.language = window.languages[newName]
+ setCookie(window.languageKey, newName)
+ // Redo action selector initialization. This returns a promise but we need not hook it because
+ // we are running in response to a UI event and things can settle in any order.
+ initializeActionSelector(window.actionList)
+}
+
+// Change the selected action or process the special options (new/rename/delete) that are handled via that selector
+function actionChanged() {
+ let newAction = elem("actionSelector").value
+ if (newAction == window.currentAction) {
+ return
+ } else if (newAction.startsWith("--")) {
+ switch (newAction.charAt(2)) {
+ case 'N':
+ nameEntry(completeNewAction)
+ break
+ case 'R':
+ nameEntry(completeRename)
+ break
+ case 'D':
+ deleteAction()
+ break
+ }
+ } else {
+ maybeSave() // Save previous contents. Save is asynchronous but racing with the following is ok because the
+ // asynchronous part of save follows the network send. Once the network send has occurred, the local state is
+ // free to change (if the save fails there is no real recovery).
+ window.currentAction = newAction
+ setCookie(window.actionKey, window.currentAction)
+ imposeAction(window.currentAction)
+ }
+}
+
+// Start a name entry sequence (for rename or new action)
+function nameEntry(followup) {
+ window.entryFollowup = followup
+ const selector = elem("actionSelector")
+ const entry = elem("nameInput")
+ selector.style.display = "none"
+ entry.style.display = "block"
+ entry.value = ""
+ entry.focus()
+}
+
+// End the name entry phase, either after processing a valid name or after cancellation
+function endNameEntry() {
+ window.entryFollowup = null
+ const selector = elem("actionSelector")
+ const entry = elem("nameInput")
+ selector.style.display = "block"
+ entry.style.display = "none"
+ console.log("Name entry ending. Setting selector to the correct action", window.currentAction)
+ selector.value = window.currentAction
+}
+
+// Followup after user enters the name of a new action
+function completeNewAction(newName) {
+ window.actionList.push({ name: newName, kind: window.language.kind })
+ window.currentAction = newName
+ endNameEntry()
+ setCookie(window.actionKey, window.currentAction)
+ initializeActionSelector(window.actionList)
+}
+
+// Followup after user renames an existing action
+function completeRename(newName) {
+ let action = getAction(window.currentAction)
+ if (action) {
+ let oldName = window.currentAction
+ // Rename locally
+ action.name = newName
+ window.currentAction = newName
+ // Resave under the new name, delete old copy on success
+ let web = elem("publish").value != 'Publish' // The presence of a Publish button means locally editable.
+ save(web).then(_ => deleteRemote(oldName))
+ // Restabilize action selector and editor
+ setCookie(window.actionKey, newName)
+ initializeActionSelector(window.actionList)
+ } else {
+ // Should not happen
+ console.log(window.currentAction, "not found in action list", window.actionList)
+ }
+ endNameEntry()
+}
+
+// Delete the current action
+function deleteAction() {
+ // Get index of current action in action list
+ let index = indexOfAction(window.currentAction)
+ if (index < 0) {
+ // Should not happen
+ console.log("current action not found in action list", window.currentAction)
+ endNameEntry()
+ return
+ }
+ // Remove locally
+ window.actionList.splice(index, 1)
+ // Remove remotely
+ deleteRemote(window.currentAction)
+ // Restabilize the action selector, window.currentAction, and current cookie based on what's left in the list
+ initializeActionSelector(window.actionList)
+ // Don't end name entry until a new currentAction has been nominated
+ endNameEntry()
+}
+
+// Delete the remote copy of an action if present. If absent, no error is indicated except on the console. Local processing
+// proceeds in either case.
+function deleteRemote(actionName) {
+ return makeOpenWhiskRequest('playground-delete.json', { playgroundId: window.playgroundId, actionName: actionName }).then(result => {
+ console.log("deleted", actionName)
+ console.log("full result", result)
+ }).catch(err => {
+ console.log("not deleted (perhaps doesn't exist)", actionName)
+ console.log("full error object", err)
+ })
+}
+
+// Fetch code from a deployed action. Returns a promise, for chaining purposes, but both the resolve and the reject path simply provide the
+// action name. Code, if retrieved, is placed directly in the editor. Failure to retrieve code is tolerated as a sometimes-expected condition.
+function getCode(actionName) {
+ return makeOpenWhiskRequest('playground-fetch.json', { playgroundId: window.playgroundId, actionName: actionName }).then(result => {
+ let response = JSON.parse(result)
+ console.log("getCode response", response)
+ if ('exec' in response) {
+ console.log("Code retrieved from deployed action")
+ let exec = response.exec
+ let code = exec.code
+ window.editor.setValue(code)
+ editorContentsChanged = false // Setting the editor contents will fire the change event but there is no need to re-save.
+ } else {
+ console.log("No deployed action, no code retrieved")
+ }
+ let webbiness = isWeb(response)
+ imposeWebbiness(webbiness)
+ return actionName
+ }).catch(err => {
+ console.error("Error retrieving code", err)
+ imposeWebbiness(false)
+ return actionName
+ })
+}
+
+// Determine if an action being fetched is a web action by examining its annotations. The argument is the response to a wsk get operation on the
+// action. If there are no annotations in the response, the answer is false.
+function isWeb(response) {
+ return getAnnotation(response, "web-export") === true // ensures boolean
+}
+
+// Get an annotation from an object that may or may not have an 'annotations' member (as whisk responses generally do). Returns undefined if
+// (1) The 'annotations' member is absent. (2) The 'annotations' member's members are not key value pairs. (3) The 'annotations' member does not
+// contain a key value pair matching the requested annotation. On a match, returns the value of the annotation.
+function getAnnotation(object, name) {
+ if ('annotations' in object && Array.isArray(object.annotations)) {
+ for (i = 0; i < object.annotations.length; i++) {
+ let member = object.annotations[i]
+ if (member.key === name) { // false if no key
+ return member.value // undefined if no value
+ }
+ }
+ }
+ return undefined
+}
+
+// Impose the local conventions for a currently published (web) action (argument is true) or a private (non-web) action (argument is false)
+function imposeWebbiness(isWeb) {
+ console.log("Webbiness being set to " + isWeb)
+ let button = elem("publish")
+ let urlText = elem("urlText")
+ let actionSelector = elem("actionSelector")
+ let mutableOptions = [] // For some reason, select.options doesn't support 'filter' (backlevel JS?)
+ for (i = 0; i < actionSelector.options.length; i++) {
+ let option = actionSelector.options[i]
+ if (option.value == "--Rename--" || option.value == "--Delete--") {
+ mutableOptions.push(option)
+ }
+ }
+ if (isWeb) {
+ button.innerHTML = '<i class="material-icons icon-size icon-extra-margin">cloud_download</i>Edit'
+ setReadOnly(true)
+ const url = window.APIHOST + '/api/v1/web/' + window.PLAYGROUND + '/user' + window.playgroundId + '/' + window.currentAction
+ urlText.innerHTML = "Readonly, public at <a style='text-decoration:none;color:#488' href='" + url + "'>" + url + "</a>"
+ for (opt of mutableOptions) {
+ opt.disabled = true
+ }
+ } else {
+ button.innerHTML = '<i class="material-icons icon-size icon-extra-margin">cloud_upload</i>Publish'
+ setReadOnly(false)
+ urlText.innerHTML = "[ editable, private ]"
+ for (opt of mutableOptions) {
+ opt.disabled = false
+ }
+ }
+ // Record the webbiness in the session record
+ getSession(window.currentAction).isWeb = isWeb
+ // Since this may be called as part of publish or edit, remove focus from the button
+ button.blur()
+}
+
+// Sets the readonly properties of the editor on or off. A thorough job, including a proper visual indication,
+// requires taggling several properties
+function setReadOnly(on) {
+ window.editor.setOptions({readOnly: on, highlightActiveLine: !on, highlightGutterLine: !on});
+ window.editor.renderer.$cursorLayer.element.style.display = on ? "none" : ""
+ if (on) {
+ window.editor.clearSelection()
+ }
+}
+
+// Parse out a specific cookie by key
+function getCookie(key) {
+ let keyPrefix = key + "=";
+ let cookie = decodeURIComponent(document.cookie)
+ let parts = cookie.split(';');
+ for(var i = 0; i <parts.length; i++) {
+ let p = parts[i].trim()
+ if (p.startsWith(keyPrefix)) {
+ return p.substring(keyPrefix.length)
+ }
+ }
+ return ""
+}
+
+// Set a specific cookie by key (note that the document.cookie field has asymmetric behavior: on reference you get all the cookies but
+// on setting you provide a single cookie and it is added to the list)
+function setCookie(key, value) {
+ let age = String(60 * 60 * 24 * 7) // one week: kind of arbitrary
+ document.cookie = key + "=" + String(value) + ";max-age=" + age
+}
+
+// Respond to click of the theme button
+function themeClicked() {
+ window.colorSetting = (window.colorSetting == "dark") ? "light" : "dark"
+ imposeColor(window.colorSetting)
+}
+
+// Impose a color scheme. Called at startup and when theme is clicked
+function imposeColor(color) {
+ let $white = 'white';
+ let $black = 'black'
+ $reverseTheme = 'Light';
+ if (color == 'light') {
+ $white = 'black';
+ $black = 'white';
+ $reverseTheme = 'Dark';
+ editor.setTheme('ace/theme/xcode');
+ } else {
+ editor.setTheme('ace/theme/terminal');
+ }
+ elem('themeName').textContent = $reverseTheme;
+ elem('input').style.color = $white;
+ elem('input').style.background = $black;
+ elem('timingText').style.color = $white;
+ elem('timingText').style.background = $black;
+ elem('resultText').style.color = $white;
+ elem('resultText').style.background = $black;
+ setCookie(window.colorKey, color)
+}
+
+// Get the active session for a given action if present
+function getSession(actionName) {
+ for (i in window.activeSessions) {
+ let candidate = window.activeSessions[i]
+ if (candidate.name == actionName) {
+ return candidate
+ }
+ }
+ return null
+}
+
+// Impose a specific action on the editor. Each action that the user has visited or created gets its own session and at most one
+// session can exist for each action. Returns a Promise, which is either the result of calling getCode (truly asynchronous)
+// or a vacuous promise that simply continues the resolve chain (if an existing session was used).
+// Assumes that the 'language' global variable is correctly initialized for the action.
+function imposeAction(actionName) {
+ // Check whether we already have an ACE EditSession going for the action. If so, just switch to it.
+ let candidate = getSession(actionName)
+ if (candidate != null) {
+ console.log("Used existing session for action " + actionName)
+ window.editor.setSession(candidate.session)
+ imposeWebbiness(candidate.isWeb)
+ return Promise.resolve(actionName)
+ }
+ // If we are making a new session, we initialize it here with example code. This may be overwritten by saved
+ // code. However, if there is no saved code, getCode will do nothing but will resolve to the action name rather
+ // than rejecting. This will leave the sample code in place
+ let session = new window.EditSession(language.example)
+ session.setMode(language.editMode)
+ session.on("change", codeChanged)
+ window.activeSessions[window.activeSessions.count] = { name: actionName, session: session, isWeb: false }
+ window.editor.setSession(session)
+ return getCode(actionName)
+}
+
+// Called when code changes
+function codeChanged(delta) {
+ window.editorContentsChanged = true
+}
+
+// Open a request session to nimbella
+function makeOpenWhiskRequest(actionName, args) {
+ return new Promise(function (resolve, reject) {
+ const xhr = new XMLHttpRequest()
+ const url = window.APIHOST + '/api/v1/web/' + window.PLAYGROUND + '/default/' + actionName
+ xhr.open('POST', url)
+ xhr.onload = function () {
+ if (this.status >= 200 && this.status < 300) {
+ resolve(xhr.responseText)
+ } else {
+ console.log("calling reject with status", this.status)
+ reject({status: this.status, statusText: xhr.statusText})
+ }
+ }
+ xhr.onerror = function () {
+ console.log("calling reject with network error")
+ reject({statusText: "Network error"})
+ }
+ xhr.send(JSON.stringify(args))
+ })
+}
+
+// Conditionally save the code from the current editor without actually running it (and only if contents of the editor
+// have changed since initialization or last save). Invoked periodically ("autosave").
+function maybeSave() {
+ if (window.editorContentsChanged) {
+ save(false)
+ }
+}
+
+// Save the code without running it, either as a standard action or a webaction. Called for autosaving iff editor contents changed
+// and when imposing webbiness or non-webbiness.
+function save(web) {
+ elem("run").disabled = true // Suppress run while saving
+ console.log("Saving editor contents")
+ let contents = window.editor.getValue()
+ let arg = { code : contents, playgroundId: window.playgroundId, actionName: window.currentAction, runtime: window.language.kind }
+ if (web) {
+ arg['web-export'] = true
+ } else {
+ arg['saveOnly'] = true
+ }
+ return makeOpenWhiskRequest('playground-run.json', arg).then(result => {
+ window.editorContentsChanged = false // regardless of error. We don't want to keep trying if it isn't going to work.
+ elem("run").disabled = false // Save is over, run is ok
+ let response = JSON.parse(result)
+ if ("error" in response) { // this is error as defined by the remote action, not xhr
+ let error = response.error
+ console.log("Error response: " + error)
+ } else if ("saved" in response) { // success
+ console.log("Saved")
+ } else {
+ console.log("Unexpected", response)
+ }
+ }).catch(err => {
+ console.error("Error performing save action", err)
+ })
+}
+
+// Set the contents of a text display area
+function setAreaContents(areaID, contents, error) {
+ let innerHTML = error ? "<p style=\"color:red\">" + contents + "</p>" : contents
+ elem(areaID).innerHTML = innerHTML
+}
+
+// Respond to click of the publish/retract button
+function publishClicked() {
+ let button = elem("publish")
+ let session = getSession(currentAction)
+ let newWebbiness = !session.isWeb
+ save(newWebbiness).then(imposeWebbiness(newWebbiness)).catch(button.blur())
+}
+
+// Process a new name entered in the nameInput area
+function processNewName() {
+ if (window.entryFollowup == null) {
+ // Can happen because of cancelling with escape key after some data was entered
+ console.log("Not processing new name due to previous cancellation")
+ return
+ }
+ let newName = elem("nameInput").value
+ if (newName.trim() == "") {
+ // Cancel request
+ console.log("Cancel detected as empty name")
+ endNameEntry()
+ return
+ }
+ console.log("Processing new name", newName)
+ if (isInvalidActionName(newName)) {
+ postNameError("Invalid name")
+ } else if (isConflictingActionName(newName)) {
+ postNameError("Conflicting Name")
+ } else {
+ console.log("Valid new name", newName)
+ window.entryFollowup(newName) // leave remainder to the individual followups
+ }
+}
+
+// Check for valid syntax of action name. Returns true IF INVALID! Rule:
+// The first character must be an alphanumeric character, or an underscore.
+// The subsequent characters can be alphanumeric, spaces, or any of the following values: _, @, ., -.
+// The last character can't be a space.
+function isInvalidActionName(newName) {
+ if (newName.trim() !== newName) {
+ return true
+ }
+ let valid = /^[0-9a-zA-Z_][ 0-9a-zA-Z_@.-]*$/
+ return !valid.test(newName)
+}
+
+// Check for conflict between a proposed action name and any existing action in the same package
+function isConflictingActionName(newName) {
+ for (action of window.actionList) {
+ if (action.name == newName) {
+ return true
+ }
+ }
+ return false
+}
+
+// Post an error over the name entry area
+function postNameError(msg) {
+ console.log("Posting name error", msg)
+ let nameInput = elem("nameInput")
+ let savedValue = nameInput.value
+ let savedColor = nameInput.style.color
+ nameInput.style.color = "red"
+ nameInput.value = msg
+ setTimeout(function() {
+ nameInput.value = savedValue
+ nameInput.style.color = savedColor
+ }, 2000)
+}
+
+// abbreviation for document.getElementById
+function elem(name) {
+ return document.getElementById(name)
+}
+
+// Respond to click of the run button
+function runClicked() {
+ window.editorContentsChanged = false // don't permit save to run in parallel
+ let contents = window.editor.getValue()
+ console.log("Contents: ", contents)
+ setAreaContents("resultText", "Running...")
+ let t0 = new Date().getTime()
+ let inputStr = elem("input").value
+ let params = JSON.parse(inputStr)
+ let arg = { code : contents, params: params, playgroundId: window.playgroundId, actionName: window.currentAction, runtime: window.language.kind }
+ return makeOpenWhiskRequest('playground-run.json', arg).then(result => {
+ let elapsed = new Date().getTime() - t0
+ let response = JSON.parse(result)
+ if ("error" in response) {
+ let msg = response.error.response.result.error // seems the more readable form of the error is buried here
+ let inx = msg.indexOf("\n")
+ let usermsg = inx > 0 ? msg.substring(0, inx) : msg
+ console.log("Error response: " + msg)
+ setAreaContents("resultText", usermsg, true)
+ setAreaContents("timingText", "", false)
+ } else {
+ console.log('response: ', response)
+ console.log('elapsed: ', elapsed)
+ let result = response['result']
+ let deploy = response['deployTime']
+ let exec = response['runTime']
+ let network = elapsed - (deploy + exec)
+
+ if (result.body && result.headers && result.headers['content-type'] == 'image/jpeg') {
+ setAreaContents("resultText", '<img src="data:image/png;base64, ' + result.body + '">', false)
+ } else {
+ setAreaContents("resultText", JSON.stringify(result, null, 4), false)
+ }
+
+ let timingStr = "Network: " + network + " ms<br>Deploy: " + deploy + " ms<br>Exec: " + exec + " ms"
+ setAreaContents("timingText", timingStr, false)
+ }
+ }).catch(err => {
+ console.log("Error contacting service", err)
+ setAreaContents("resultText", "Error contacting service, status = " + err.status, true)
+ setAreaContents("timingText", "", false)
+ });
+}