Merge branch 'fewer-ui-assumptions' into 'v3'
UI customises to available Things Closes #510 and #385 See merge request openflexure/openflexure-microscope-server!435
This commit is contained in:
commit
5074f75442
27 changed files with 886 additions and 811 deletions
193
webapp/package-lock.json
generated
193
webapp/package-lock.json
generated
|
|
@ -29,7 +29,6 @@
|
||||||
"eslint-config-prettier": "^8.10.2",
|
"eslint-config-prettier": "^8.10.2",
|
||||||
"eslint-plugin-prettier": "^3.4.1",
|
"eslint-plugin-prettier": "^3.4.1",
|
||||||
"eslint-plugin-vue": "^7.20.0",
|
"eslint-plugin-vue": "^7.20.0",
|
||||||
"genversion": "^3.2.0",
|
|
||||||
"less": "^4.4.2",
|
"less": "^4.4.2",
|
||||||
"less-loader": "^11.1.4",
|
"less-loader": "^11.1.4",
|
||||||
"material-symbols": "^0.39.1",
|
"material-symbols": "^0.39.1",
|
||||||
|
|
@ -7480,39 +7479,6 @@
|
||||||
"node": "^10.12.0 || >=12.0.0"
|
"node": "^10.12.0 || >=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/filelist": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"minimatch": "^5.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/filelist/node_modules/brace-expansion": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/filelist/node_modules/minimatch": {
|
|
||||||
"version": "5.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
|
||||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||||
|
|
@ -7566,16 +7532,6 @@
|
||||||
"url": "https://github.com/avajs/find-cache-dir?sponsor=1"
|
"url": "https://github.com/avajs/find-cache-dir?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/find-package": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/find-package/-/find-package-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-yVn71XCCaNgxz58ERTl8nA/8YYtIQDY9mHSrgFBfiFtdNNfY0h183Vh8BRkKxD8x9TUw3ec290uJKhDVxqGZBw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"parents": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/find-up": {
|
"node_modules/find-up": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
|
|
@ -7787,40 +7743,6 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/genversion": {
|
|
||||||
"version": "3.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/genversion/-/genversion-3.2.0.tgz",
|
|
||||||
"integrity": "sha512-OIYSX6XYA8PHecLDCTri30hadSZfAjZ8Iq1+BBDXqLWP4dRLuJNLoNjsSWtTpw97IccK2LDWzkEstxAB8GdN7g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"commander": "^7.2.0",
|
|
||||||
"ejs": "^3.1.9",
|
|
||||||
"find-package": "^1.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"genversion": "bin/genversion.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/genversion/node_modules/ejs": {
|
|
||||||
"version": "3.1.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
|
||||||
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"jake": "^10.8.5"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"ejs": "bin/cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/get-caller-file": {
|
"node_modules/get-caller-file": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
|
@ -8646,101 +8568,6 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jake": {
|
|
||||||
"version": "10.9.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
|
|
||||||
"integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"async": "^3.2.3",
|
|
||||||
"chalk": "^4.0.2",
|
|
||||||
"filelist": "^1.0.4",
|
|
||||||
"minimatch": "^3.1.2"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"jake": "bin/cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jake/node_modules/ansi-styles": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"color-convert": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jake/node_modules/chalk": {
|
|
||||||
"version": "4.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
|
||||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": "^4.1.0",
|
|
||||||
"supports-color": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jake/node_modules/color-convert": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"color-name": "~1.1.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jake/node_modules/color-name": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/jake/node_modules/has-flag": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jake/node_modules/supports-color": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
|
||||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"has-flag": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/javascript-stringify": {
|
"node_modules/javascript-stringify": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
|
||||||
|
|
@ -10231,16 +10058,6 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/parents": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-mXKF3xkoUt5td2DoxpLmtOmZvko9VfFpwRwkKDHSNvgmpLAeBo18YDhcPbBzJq+QLCHMbGOfzia2cX4U+0v9Mg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"path-platform": "~0.11.15"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/parse-json": {
|
"node_modules/parse-json": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||||
|
|
@ -10336,16 +10153,6 @@
|
||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/path-platform": {
|
|
||||||
"version": "0.11.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz",
|
|
||||||
"integrity": "sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.12",
|
"version": "0.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
"author": "OpenFlexure <contact@openflexure.org> (https://www.openflexure.org)",
|
"author": "OpenFlexure <contact@openflexure.org> (https://www.openflexure.org)",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gv": "genversion src/version.js",
|
"gv": "genversion src/version.js",
|
||||||
"serve": "npm run gv && vue-cli-service serve --mode development",
|
"serve": "vue-cli-service serve --mode development",
|
||||||
"build": "npm run gv && vue-cli-service build --mode production",
|
"build": "vue-cli-service build --mode production",
|
||||||
"lint": "vue-cli-service lint --no-fix --max-warnings 0",
|
"lint": "vue-cli-service lint --no-fix --max-warnings 0",
|
||||||
"lint:fix": "vue-cli-service lint --fix"
|
"lint:fix": "vue-cli-service lint --fix"
|
||||||
},
|
},
|
||||||
|
|
@ -32,7 +32,6 @@
|
||||||
"eslint-config-prettier": "^8.10.2",
|
"eslint-config-prettier": "^8.10.2",
|
||||||
"eslint-plugin-prettier": "^3.4.1",
|
"eslint-plugin-prettier": "^3.4.1",
|
||||||
"eslint-plugin-vue": "^7.20.0",
|
"eslint-plugin-vue": "^7.20.0",
|
||||||
"genversion": "^3.2.0",
|
|
||||||
"less": "^4.4.2",
|
"less": "^4.4.2",
|
||||||
"less-loader": "^11.1.4",
|
"less-loader": "^11.1.4",
|
||||||
"material-symbols": "^0.39.1",
|
"material-symbols": "^0.39.1",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app" class="uk-height-1-1 uk-margin-remove uk-padding-remove" :class="handleTheme">
|
<div id="app" class="uk-height-1-1 uk-margin-remove uk-padding-remove" :class="handleTheme">
|
||||||
|
<!-- this stops the app loading until setConnected is committed in the store, this means
|
||||||
|
other components will not load until we have Thing Descriptions. -->
|
||||||
<loadingContent v-if="!$store.getters.ready" />
|
<loadingContent v-if="!$store.getters.ready" />
|
||||||
<appContent v-if="$store.getters.ready" />
|
<appContent v-if="$store.getters.ready" />
|
||||||
<!-- Runtime modals -->
|
<!-- Runtime modals -->
|
||||||
|
|
@ -213,8 +215,8 @@ export default {
|
||||||
// TODO: should we purge existing consumedThings?
|
// TODO: should we purge existing consumedThings?
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch("wot/fetchThingDescriptions", `${baseUri}/thing_descriptions/`);
|
await this.$store.dispatch("wot/fetchThingDescriptions", `${baseUri}/thing_descriptions/`);
|
||||||
for (let requiredThing of ["camera", "stage"]) {
|
for (let requiredThing of ["camera", "system"]) {
|
||||||
if (!this.$store.getters["wot/thingAvailable"](requiredThing)) {
|
if (!this.thingAvailable(requiredThing)) {
|
||||||
throw new Error(`No ${requiredThing} found, the GUI won't work without one.`);
|
throw new Error(`No ${requiredThing} found, the GUI won't work without one.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -225,6 +227,7 @@ export default {
|
||||||
} catch {
|
} catch {
|
||||||
this.$store.commit("changeMicroscopeHostname", null);
|
this.$store.commit("changeMicroscopeHostname", null);
|
||||||
}
|
}
|
||||||
|
// start rendering components
|
||||||
this.$store.commit("setConnected");
|
this.$store.commit("setConnected");
|
||||||
this.$store.commit("setErrorMessage", null);
|
this.$store.commit("setErrorMessage", null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
:id="item.id + '-tab-icon'"
|
:id="item.id + '-tab-icon'"
|
||||||
:key="item.id + '-tab-icon'"
|
:key="item.id + '-tab-icon'"
|
||||||
:tab-i-d="item.id"
|
:tab-i-d="item.id"
|
||||||
|
:title="item.title"
|
||||||
:require-connection="true"
|
:require-connection="true"
|
||||||
:current-tab="currentTab"
|
:current-tab="currentTab"
|
||||||
:class="item.class"
|
:class="item.class"
|
||||||
|
|
@ -42,6 +43,7 @@
|
||||||
:id="item.id + '-tab-icon'"
|
:id="item.id + '-tab-icon'"
|
||||||
:key="item.id + '-tab-icon'"
|
:key="item.id + '-tab-icon'"
|
||||||
:tab-i-d="item.id"
|
:tab-i-d="item.id"
|
||||||
|
:title="item.title"
|
||||||
:require-connection="true"
|
:require-connection="true"
|
||||||
:current-tab="currentTab"
|
:current-tab="currentTab"
|
||||||
:class="item.class"
|
:class="item.class"
|
||||||
|
|
@ -81,19 +83,18 @@ import tabIcon from "./genericComponents/tabIcon";
|
||||||
import tabContent from "./genericComponents/tabContent";
|
import tabContent from "./genericComponents/tabContent";
|
||||||
|
|
||||||
// Import new content components
|
// Import new content components
|
||||||
import controlContent from "./tabContentComponents/controlContent.vue";
|
|
||||||
import slideScanContent from "./tabContentComponents/slideScanContent.vue";
|
|
||||||
import backgroundDetectContent from "./tabContentComponents/backgroundDetectContent.vue";
|
|
||||||
import viewContent from "./tabContentComponents/viewContent.vue";
|
|
||||||
import settingsContent from "./tabContentComponents/settingsContent.vue";
|
|
||||||
import aboutContent from "./tabContentComponents/aboutContent.vue";
|
import aboutContent from "./tabContentComponents/aboutContent.vue";
|
||||||
|
import backgroundDetectContent from "./tabContentComponents/backgroundDetectContent.vue";
|
||||||
|
import controlContent from "./tabContentComponents/controlContent.vue";
|
||||||
import loggingContent from "./tabContentComponents/loggingContent.vue";
|
import loggingContent from "./tabContentComponents/loggingContent.vue";
|
||||||
import powerContent from "./tabContentComponents/powerContent.vue";
|
import powerContent from "./tabContentComponents/powerContent.vue";
|
||||||
|
import scanListContent from "./tabContentComponents/scanListContent.vue";
|
||||||
|
import settingsContent from "./tabContentComponents/settingsContent.vue";
|
||||||
|
import slideScanContent from "./tabContentComponents/slideScanContent.vue";
|
||||||
|
import viewContent from "./tabContentComponents/viewContent.vue";
|
||||||
|
|
||||||
// Import modal components for device initialisation
|
// Import modal components for device initialisation
|
||||||
import calibrationWizard from "./modalComponents/calibrationWizard.vue";
|
import calibrationWizard from "./modalComponents/calibrationWizard.vue";
|
||||||
import TabIcon from "./genericComponents/tabIcon.vue";
|
|
||||||
import ScanListContent from "./tabContentComponents/scanListContent.vue";
|
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -102,15 +103,7 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
tabIcon,
|
tabIcon,
|
||||||
tabContent,
|
tabContent,
|
||||||
controlContent,
|
|
||||||
slideScanContent,
|
|
||||||
viewContent,
|
|
||||||
settingsContent,
|
|
||||||
calibrationWizard,
|
calibrationWizard,
|
||||||
aboutContent,
|
|
||||||
loggingContent,
|
|
||||||
TabIcon,
|
|
||||||
powerContent,
|
|
||||||
},
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
|
|
@ -118,26 +111,69 @@ export default {
|
||||||
bottomTabs: [
|
bottomTabs: [
|
||||||
{
|
{
|
||||||
id: "settings",
|
id: "settings",
|
||||||
|
title: "Settings",
|
||||||
icon: "settings",
|
icon: "settings",
|
||||||
component: settingsContent,
|
component: settingsContent,
|
||||||
class: "uk-margin-auto-top",
|
class: "uk-margin-auto-top",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "logging",
|
id: "logging",
|
||||||
|
title: "Logging",
|
||||||
icon: "assignment_late",
|
icon: "assignment_late",
|
||||||
component: loggingContent,
|
component: loggingContent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "about",
|
id: "about",
|
||||||
|
title: "About",
|
||||||
icon: "info",
|
icon: "info",
|
||||||
component: aboutContent,
|
component: aboutContent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "power",
|
id: "power",
|
||||||
|
title: "Power",
|
||||||
icon: "power_settings_new",
|
icon: "power_settings_new",
|
||||||
component: powerContent,
|
component: powerContent,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
coreTopTabs: [
|
||||||
|
{
|
||||||
|
id: "view",
|
||||||
|
title: "View",
|
||||||
|
icon: "visibility",
|
||||||
|
component: viewContent,
|
||||||
|
requiredThings: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "control",
|
||||||
|
title: "Control",
|
||||||
|
icon: "gamepad",
|
||||||
|
component: controlContent,
|
||||||
|
requiredThings: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "background-detect",
|
||||||
|
title: "Background Detect",
|
||||||
|
icon: "background_replace",
|
||||||
|
component: backgroundDetectContent,
|
||||||
|
// While stage isn't needed; automatic background detect has little function
|
||||||
|
// for a manual microscope.
|
||||||
|
requiredThings: ["stage"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "slide-scan",
|
||||||
|
title: "Slide Scan",
|
||||||
|
icon: "settings_overscan",
|
||||||
|
component: slideScanContent,
|
||||||
|
requiredThings: ["smart_scan"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "scan-list",
|
||||||
|
title: "Scan List",
|
||||||
|
icon: "photo_library",
|
||||||
|
component: scanListContent,
|
||||||
|
requiredThings: ["smart_scan"],
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -154,37 +190,12 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
topTabs: function () {
|
topTabs: function () {
|
||||||
let tabs = [
|
// Filter core top tabs based on available Things. Once Things can specify a
|
||||||
{
|
// custom tab those will need to be added here
|
||||||
id: "view",
|
return this.coreTopTabs.filter((tab) => {
|
||||||
icon: "visibility",
|
if (!tab.requiredThings || tab.requiredThings.length === 0) return true;
|
||||||
component: viewContent,
|
return tab.requiredThings.every((thing) => this.thingAvailable(thing));
|
||||||
},
|
});
|
||||||
{
|
|
||||||
id: "control",
|
|
||||||
icon: "gamepad",
|
|
||||||
component: controlContent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "background detect",
|
|
||||||
icon: "background_replace",
|
|
||||||
component: backgroundDetectContent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "slide scan",
|
|
||||||
icon: "settings_overscan",
|
|
||||||
component: slideScanContent,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "scan list",
|
|
||||||
icon: "photo_library",
|
|
||||||
component: ScanListContent,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
if (!this.$store.state.galleryEnabled) {
|
|
||||||
tabs = tabs.filter((tab) => tab.id != "gallery");
|
|
||||||
}
|
|
||||||
return tabs;
|
|
||||||
},
|
},
|
||||||
allTabs() {
|
allTabs() {
|
||||||
return [...this.topTabs, ...this.bottomTabs];
|
return [...this.topTabs, ...this.bottomTabs];
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,8 @@ export default {
|
||||||
// A global signal listener to perform the action
|
// A global signal listener to perform the action
|
||||||
if (this.submitOnEvent) {
|
if (this.submitOnEvent) {
|
||||||
this.$root.$on(this.submitOnEvent, () => {
|
this.$root.$on(this.submitOnEvent, () => {
|
||||||
|
if (this.isDisabled) return;
|
||||||
|
// Bootstrap task if button is not disabled.
|
||||||
this.bootstrapTask();
|
this.bootstrapTask();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,13 +78,11 @@ export default {
|
||||||
const calibrateableThings = Object.keys(this.availableCalibrationTasks);
|
const calibrateableThings = Object.keys(this.availableCalibrationTasks);
|
||||||
|
|
||||||
for (const name of calibrateableThings) {
|
for (const name of calibrateableThings) {
|
||||||
try {
|
if (this.thingPropertyAvailable(name, "calibration_required")) {
|
||||||
const thingNeedsCal = await this.readThingProperty(name, "calibration_required");
|
const thingNeedsCal = await this.readThingProperty(name, "calibration_required");
|
||||||
if (thingNeedsCal) {
|
if (thingNeedsCal) {
|
||||||
needsCalibration.push(name);
|
needsCalibration.push(name);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(`${name}: missing calibration_required property`, e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
{{ $store.state.origin }}
|
{{ $store.state.origin }}
|
||||||
</div>
|
</div>
|
||||||
<action-button
|
<action-button
|
||||||
|
v-if="stageType"
|
||||||
thing="stage"
|
thing="stage"
|
||||||
action="flash_led"
|
action="flash_led"
|
||||||
submit-label="Flash Illumination"
|
submit-label="Flash Illumination"
|
||||||
|
|
@ -34,18 +35,16 @@
|
||||||
<div class="uk-margin-small-bottom">
|
<div class="uk-margin-small-bottom">
|
||||||
<b>Camera:</b>
|
<b>Camera:</b>
|
||||||
<br />
|
<br />
|
||||||
<div v-if="'camera' in things">
|
<div>
|
||||||
{{ things.camera.title }}
|
{{ cameraType }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="uk-text-danger"><b>No camera configured</b></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="stageType">
|
||||||
<b>Stage:</b>
|
<b>Stage:</b>
|
||||||
<br />
|
<br />
|
||||||
<div v-if="'stage' in things">
|
<div>
|
||||||
{{ things.stage.title }}
|
{{ stageType }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="uk-text-danger"><b>No stage configured</b></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
@ -71,8 +70,12 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
things: function () {
|
cameraType() {
|
||||||
return this.$store.getters["wot/thingDescriptions"];
|
// No need to check as the microscope won't start up without a camera defined
|
||||||
|
return this.thingDescription("camera").title;
|
||||||
|
},
|
||||||
|
stageType() {
|
||||||
|
return this.thingAvailable("stage") ? this.thingDescription("stage").title : undefined;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<p>Autofocus</p>
|
||||||
|
<div class="uk-margin">
|
||||||
|
<action-button
|
||||||
|
thing="autofocus"
|
||||||
|
action="fast_autofocus"
|
||||||
|
:submit-data="{ dz: 2000 }"
|
||||||
|
:submit-label="'Autofocus'"
|
||||||
|
:button-primary="true"
|
||||||
|
:submit-on-event="'globalFastAutofocusEvent'"
|
||||||
|
:is-disabled="isAutofocusing"
|
||||||
|
@taskStarted="onAutofocus"
|
||||||
|
@finished="afterAutofocus"
|
||||||
|
@error="modalError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "AutofocusControl",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ActionButton,
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
isAutofocusing: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onAutofocus() {
|
||||||
|
this.isAutofocusing = true;
|
||||||
|
},
|
||||||
|
afterAutofocus() {
|
||||||
|
this.isAutofocusing = false;
|
||||||
|
this.$root.$emit("globalUpdatePositionEvent");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -1,145 +1,30 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="paneControl" class="uk-padding-small">
|
<div id="paneControl" class="uk-padding-small">
|
||||||
<div v-if="setPosition">
|
<positionControl v-if="stageAvailable" />
|
||||||
<ul uk-accordion="multiple: true">
|
<autofocusControl v-if="autofocusAvailable" />
|
||||||
<li>
|
<p>Image Capture</p>
|
||||||
<a class="uk-accordion-title" href="#">Configure</a>
|
|
||||||
<div class="uk-accordion-content">
|
|
||||||
<b>Keyboard Step Size</b>
|
|
||||||
<div class="uk-grid-small uk-child-width-1-3" uk-grid>
|
|
||||||
<div>
|
|
||||||
<label class="uk-form-label" for="form-stacked-text">x</label>
|
|
||||||
<div class="uk-form-controls">
|
|
||||||
<input v-model="stepSize.x" class="uk-input uk-form-small" type="number" />
|
|
||||||
</div>
|
|
||||||
<label class="uk-margin-small-right"
|
|
||||||
><input v-model="invert.x" class="uk-checkbox" type="checkbox" /> Invert x</label
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div class="uk-margin">
|
||||||
<label class="uk-form-label" for="form-stacked-text">y</label>
|
<action-button
|
||||||
<div class="uk-form-controls">
|
thing="camera"
|
||||||
<input v-model="stepSize.y" class="uk-input uk-form-small" type="number" />
|
action="capture_jpeg"
|
||||||
</div>
|
:submit-data="{ stream_name: 'main' }"
|
||||||
<label class="uk-margin-small-right"
|
:submit-label="'Low Resolution'"
|
||||||
><input v-model="invert.y" class="uk-checkbox" type="checkbox" /> Invert y</label
|
@response="handleCaptureResponse"
|
||||||
>
|
@error="modalError"
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="uk-form-label" for="form-stacked-text">z</label>
|
|
||||||
<div class="uk-form-controls">
|
|
||||||
<input v-model="stepSize.z" class="uk-input uk-form-small" type="number" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<action-button
|
|
||||||
thing="stage"
|
|
||||||
action="set_zero_position"
|
|
||||||
submit-label="Zero Coordinates"
|
|
||||||
:can-terminate="false"
|
|
||||||
@finished="updatePosition"
|
|
||||||
@error="modalError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="uk-open">
|
|
||||||
<a class="uk-accordion-title" href="#">Position</a>
|
|
||||||
<div class="uk-accordion-content">
|
|
||||||
<form>
|
|
||||||
<!-- Text boxes to set and view position -->
|
|
||||||
<div class="input-and-buttons-container">
|
|
||||||
<input
|
|
||||||
v-for="(_v, key) in setPosition"
|
|
||||||
:key="`setPosition_${key}`"
|
|
||||||
v-model="setPosition[key]"
|
|
||||||
class="uk-form-small numeric-setting-line-input"
|
|
||||||
type="number"
|
|
||||||
@keyup.enter="startMoveTask"
|
|
||||||
/>
|
|
||||||
<sync-property-button @click="updatePosition" />
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<action-button
|
|
||||||
ref="moveButton"
|
|
||||||
thing="stage"
|
|
||||||
action="move_absolute"
|
|
||||||
:submit-data="setPosition"
|
|
||||||
:submit-label="'Move'"
|
|
||||||
:can-terminate="true"
|
|
||||||
:poll-interval="0.05"
|
|
||||||
@taskStarted="moveLock = true"
|
|
||||||
@finished="
|
|
||||||
updatePosition();
|
|
||||||
moveLock = false;
|
|
||||||
"
|
|
||||||
@error="modalError"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!--Show autofocus if default plugin is enabled-->
|
|
||||||
<li class="uk-open">
|
|
||||||
<a class="uk-accordion-title" href="#">Autofocus</a>
|
|
||||||
<div class="uk-accordion-content">
|
|
||||||
<div class="uk-grid-small uk-child-width-expand" uk-grid>
|
|
||||||
<div v-show="!isAutofocusing || isAutofocusing == 1">
|
|
||||||
<action-button
|
|
||||||
thing="autofocus"
|
|
||||||
action="fast_autofocus"
|
|
||||||
:submit-data="{ dz: 2000 }"
|
|
||||||
:submit-label="'Autofocus'"
|
|
||||||
:button-primary="true"
|
|
||||||
:submit-on-event="'globalFastAutofocusEvent'"
|
|
||||||
@taskStarted="isAutofocusing = 1"
|
|
||||||
@finished="
|
|
||||||
updatePosition();
|
|
||||||
isAutofocusing = 0;
|
|
||||||
"
|
|
||||||
@error="modalError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li class="uk-open">
|
|
||||||
<a class="uk-accordion-title" href="#">Image Capture</a>
|
|
||||||
<div class="uk-accordion-content">
|
|
||||||
<div class="uk-margin">
|
|
||||||
<action-button
|
|
||||||
thing="camera"
|
|
||||||
action="capture_jpeg"
|
|
||||||
:submit-data="{ stream_name: 'main' }"
|
|
||||||
:submit-label="'Low Resolution'"
|
|
||||||
:submit-on-event="'globalCaptureEvent'"
|
|
||||||
@response="handleCaptureResponse"
|
|
||||||
@error="modalError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="uk-margin">
|
|
||||||
<action-button
|
|
||||||
thing="camera"
|
|
||||||
action="capture_jpeg"
|
|
||||||
:submit-data="{ stream_name: 'full' }"
|
|
||||||
submit-label="Full Resolution"
|
|
||||||
:submit-on-event="'globalCaptureEvent'"
|
|
||||||
@response="handleCaptureResponse"
|
|
||||||
@error="modalError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="uk-text-warning">
|
|
||||||
<p>Stage positioning is disabled since no stage is connected.</p>
|
<div class="uk-margin">
|
||||||
<p>Please check all data and power connections to your motor controller board.</p>
|
<action-button
|
||||||
|
thing="camera"
|
||||||
|
action="capture_jpeg"
|
||||||
|
:submit-data="{ stream_name: 'full' }"
|
||||||
|
submit-label="Full Resolution"
|
||||||
|
:submit-on-event="'globalCaptureEvent'"
|
||||||
|
@response="handleCaptureResponse"
|
||||||
|
@error="modalError"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -147,151 +32,28 @@
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
||||||
import syncPropertyButton from "../../labThingsComponents/syncPropertyButton.vue";
|
import positionControl from "./positionControl.vue";
|
||||||
|
import autofocusControl from "./autofocusControl.vue";
|
||||||
|
|
||||||
// Export main app
|
|
||||||
export default {
|
export default {
|
||||||
name: "PaneControl",
|
name: "PaneControl",
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
ActionButton,
|
ActionButton,
|
||||||
syncPropertyButton,
|
positionControl,
|
||||||
},
|
autofocusControl,
|
||||||
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
setPosition: null,
|
|
||||||
isAutofocusing: 0,
|
|
||||||
moveLock: false,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
// Note that as stepSize and invert are set based on internals we can use
|
stageAvailable() {
|
||||||
// get() and set() to interact with the store. Instead use a deep watcher to
|
return this.thingAvailable("stage");
|
||||||
// update the store (see ``watch:`` below)
|
|
||||||
stepSize() {
|
|
||||||
return this.$store.state.navigationStepSize;
|
|
||||||
},
|
},
|
||||||
invert() {
|
autofocusAvailable() {
|
||||||
return this.$store.state.navigationInvert;
|
return this.thingAvailable("autofocus");
|
||||||
},
|
},
|
||||||
baseUri: function () {
|
|
||||||
return this.$store.getters.baseUri;
|
|
||||||
},
|
|
||||||
positionStatusUri: function () {
|
|
||||||
return `${this.baseUri}/stage/position`;
|
|
||||||
},
|
|
||||||
moveInImageCoordinatesUri: function () {
|
|
||||||
return this.thingActionUrl("camera_stage_mapping", "move_in_image_coordinates");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
stepSize: {
|
|
||||||
deep: true,
|
|
||||||
handler(newVal) {
|
|
||||||
this.$store.commit("changeNavigationStepSize", newVal);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
invert: {
|
|
||||||
deep: true,
|
|
||||||
handler(newVal) {
|
|
||||||
this.$store.commit("changeNavigationInvert", newVal);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
async mounted() {
|
|
||||||
let self = this;
|
|
||||||
// A global signal listener to perform a move action
|
|
||||||
this.$root.$on("globalMoveEvent", self.move);
|
|
||||||
// A global signal listener to perform a move action in pixels
|
|
||||||
this.$root.$on("globalMoveInImageCoordinatesEvent", (x, y, absolute) => {
|
|
||||||
this.moveInImageCoordinatesRequest(x, y, absolute);
|
|
||||||
});
|
|
||||||
// A global signal listener to perform a move in multiples of a step size
|
|
||||||
this.$root.$on("globalMoveStepEvent", (x_steps, y_steps, z_steps) => {
|
|
||||||
this.$root.$emit(
|
|
||||||
"globalMoveEvent",
|
|
||||||
x_steps * this.stepSize.x * (this.invert.x ? -1 : 1),
|
|
||||||
y_steps * this.stepSize.y * (this.invert.y ? -1 : 1),
|
|
||||||
z_steps * this.stepSize.z * (this.invert.z ? -1 : 1),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
// Update the current position in text boxes
|
|
||||||
await this.updatePosition();
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeDestroy() {
|
|
||||||
// Remove global signal listener to perform a move action
|
|
||||||
this.$root.$off("globalMoveEvent");
|
|
||||||
this.$root.$off("globalMoveInImageCoordinatesEvent");
|
|
||||||
this.$root.$off("globalMoveStepEvent");
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
timeout(ms) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
},
|
|
||||||
async move(x, y, z, absolute) {
|
|
||||||
// Move the stage, by updating the controls and starting a move task
|
|
||||||
// This is equivalent to clicking the "move" button.
|
|
||||||
if (this.moveLock) return; // Discard move requests if we're already moving
|
|
||||||
// NB moveLock is just boolean flag - it's not as safe as a "proper" lock.
|
|
||||||
this.moveLock = true; // This will also be set by the task submitter, but
|
|
||||||
// setting it here avoids multiple moves being requested simultaneously.
|
|
||||||
if (absolute) {
|
|
||||||
this.setPosition = { x: x, y: y, z: z };
|
|
||||||
} else {
|
|
||||||
await this.updatePosition();
|
|
||||||
this.setPosition = {
|
|
||||||
x: this.setPosition.x + x,
|
|
||||||
y: this.setPosition.y + y,
|
|
||||||
z: this.setPosition.z + z,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
await this.timeout(1); // Wait for Vue to update the position
|
|
||||||
await this.startMoveTask();
|
|
||||||
},
|
|
||||||
async startMoveTask() {
|
|
||||||
await this.$refs.moveButton.startTask();
|
|
||||||
},
|
|
||||||
moveInImageCoordinatesRequest: function (x, y) {
|
|
||||||
// If not movement-locked
|
|
||||||
if (!this.moveLock) {
|
|
||||||
// Lock move requests
|
|
||||||
this.moveLock = true;
|
|
||||||
|
|
||||||
if (!this.moveInImageCoordinatesUri) {
|
|
||||||
this.modalError(
|
|
||||||
"Moving in image coordinates is not supported - you will need to upgrade your microscope's software.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send move request
|
|
||||||
axios
|
|
||||||
.post(this.moveInImageCoordinatesUri, {
|
|
||||||
x: x, // NB the coordinates are numpy/PIL style, meaning X and Y are swapped.
|
|
||||||
y: y,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.updatePosition(); // Update the position in text boxes
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.modalError(error); // Let mixin handle error
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.moveLock = false; // Release the move lock
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async updatePosition() {
|
|
||||||
this.setPosition = await this.readThingProperty("stage", "position");
|
|
||||||
},
|
|
||||||
|
|
||||||
handleCaptureResponse: async function (response) {
|
handleCaptureResponse: async function (response) {
|
||||||
// Retrieve the captured image and save it
|
// Retrieve the captured image and save it
|
||||||
let imageUri = response.output.href;
|
let imageUri = response.output.href;
|
||||||
|
|
@ -312,29 +74,3 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.input-and-buttons-container {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-content: stretch;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.numeric-setting-line-input {
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-right: 5px;
|
|
||||||
width: 3em;
|
|
||||||
/* Stop Firefox showing input spinners, other
|
|
||||||
browsers set with block below */
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
}
|
|
||||||
/* Chrome, Safari, Edge, Opera */
|
|
||||||
.numeric-setting-line-input::-webkit-outer-spin-button,
|
|
||||||
.numeric-setting-line-input::-webkit-inner-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
<!-- This is the main position component for the control tab.
|
||||||
|
|
||||||
|
Do not reuse this component until we remove global listeners for move.
|
||||||
|
|
||||||
|
This component shows themotor positions in a closed accordion along with the move
|
||||||
|
and zero position buttons. It also includes the d-pad.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ul uk-accordion="multiple: true">
|
||||||
|
<li class="uk-closed">
|
||||||
|
<a class="uk-accordion-title" href="#">Position</a>
|
||||||
|
<div class="uk-accordion-content">
|
||||||
|
<form>
|
||||||
|
<!-- Text boxes to set and view position -->
|
||||||
|
<div class="input-and-buttons-container">
|
||||||
|
<input
|
||||||
|
v-for="(_v, key) in setPosition"
|
||||||
|
:key="`setPosition_${key}`"
|
||||||
|
v-model="setPosition[key]"
|
||||||
|
class="uk-form-small numeric-setting-line-input"
|
||||||
|
type="number"
|
||||||
|
@keyup.enter="startMoveTask"
|
||||||
|
/>
|
||||||
|
<sync-property-button @click="updatePosition" />
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<action-button
|
||||||
|
ref="moveButton"
|
||||||
|
thing="stage"
|
||||||
|
action="move_absolute"
|
||||||
|
:submit-data="setPosition"
|
||||||
|
:submit-label="'Move'"
|
||||||
|
:can-terminate="true"
|
||||||
|
:poll-interval="0.05"
|
||||||
|
@finished="moveComplete"
|
||||||
|
@error="modalError"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<action-button
|
||||||
|
thing="stage"
|
||||||
|
action="set_zero_position"
|
||||||
|
submit-label="Zero Coordinates"
|
||||||
|
:can-terminate="false"
|
||||||
|
@finished="updatePosition"
|
||||||
|
@error="modalError"
|
||||||
|
/>
|
||||||
|
<div class="uk-flex uk-flex-center uk-margin">
|
||||||
|
<hr class="uk-divider-small" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<stageControlButtons />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
||||||
|
import syncPropertyButton from "../../labThingsComponents/syncPropertyButton.vue";
|
||||||
|
import stageControlButtons from "./stageControlButtons.vue";
|
||||||
|
export default {
|
||||||
|
name: "PaneControl",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ActionButton,
|
||||||
|
syncPropertyButton,
|
||||||
|
stageControlButtons,
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
setPosition: null,
|
||||||
|
moveLock: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
positionStatusUri: function () {
|
||||||
|
return this.thingActionUrl("stage", "position");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
let self = this;
|
||||||
|
// A global signal listener to perform a move action
|
||||||
|
this.$root.$on("globalMoveEvent", self.move);
|
||||||
|
this.$root.$on("globalUpdatePositionEvent", self.updatePosition);
|
||||||
|
// A global signal listener to perform a move action in pixels
|
||||||
|
this.$root.$on("globalMoveInImageCoordinatesEvent", (x, y, absolute) => {
|
||||||
|
this.moveInImageCoordinatesRequest(x, y, absolute);
|
||||||
|
});
|
||||||
|
// A global signal listener to perform a move in multiples of a step size
|
||||||
|
this.$root.$on("globalMoveStepEvent", (x_steps, y_steps, z_steps) => {
|
||||||
|
const navigationStepSize = this.$store.state.navigationStepSize;
|
||||||
|
const navigationInvert = this.$store.state.navigationInvert;
|
||||||
|
const x = x_steps * navigationStepSize.x * (navigationInvert.x ? -1 : 1);
|
||||||
|
const y = y_steps * navigationStepSize.y * (navigationInvert.y ? -1 : 1);
|
||||||
|
const z = z_steps * navigationStepSize.z * (navigationInvert.z ? -1 : 1);
|
||||||
|
this.$root.$emit("globalMoveEvent", x, y, z, false);
|
||||||
|
});
|
||||||
|
// Update the current position in text boxes
|
||||||
|
await this.updatePosition();
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
// Remove global signal listener to perform a move action
|
||||||
|
this.$root.$off("globalMoveEvent");
|
||||||
|
this.$root.$off("globalMoveInImageCoordinatesEvent");
|
||||||
|
this.$root.$off("globalMoveStepEvent");
|
||||||
|
this.$root.$off("globalUpdatePositionEvent");
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
timeout(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
},
|
||||||
|
async move(x, y, z, absolute) {
|
||||||
|
// Move the stage, by updating the controls and starting a move task
|
||||||
|
// This is equivalent to clicking the "move" button.
|
||||||
|
if (this.moveLock) return; // Discard move requests if we're already moving
|
||||||
|
// NB moveLock is just boolean flag - it's not as safe as a "proper" lock.
|
||||||
|
this.moveLock = true; // This will also be set by the task submitter, but
|
||||||
|
// setting it here avoids multiple moves being requested simultaneously.
|
||||||
|
if (absolute) {
|
||||||
|
this.setPosition = { x: x, y: y, z: z };
|
||||||
|
} else {
|
||||||
|
await this.updatePosition();
|
||||||
|
this.setPosition = {
|
||||||
|
x: this.setPosition.x + x,
|
||||||
|
y: this.setPosition.y + y,
|
||||||
|
z: this.setPosition.z + z,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await this.timeout(1); // Wait for Vue to update the position
|
||||||
|
await this.startMoveTask();
|
||||||
|
},
|
||||||
|
async startMoveTask() {
|
||||||
|
this.moveLock = true;
|
||||||
|
await this.$refs.moveButton.startTask();
|
||||||
|
},
|
||||||
|
moveComplete() {
|
||||||
|
this.updatePosition();
|
||||||
|
this.moveLock = false;
|
||||||
|
},
|
||||||
|
async moveInImageCoordinatesRequest(x, y) {
|
||||||
|
// If not movement-locked
|
||||||
|
if (!this.moveLock) {
|
||||||
|
// Lock move requests
|
||||||
|
this.moveLock = true;
|
||||||
|
const response = await this.invokeAction(
|
||||||
|
"camera_stage_mapping",
|
||||||
|
"move_in_image_coordinates",
|
||||||
|
{
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this.pollUntilComplete(
|
||||||
|
response.data.href,
|
||||||
|
null, // Nothing to do while ongoing
|
||||||
|
this.moveComplete, // Call move complete once done.
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updatePosition() {
|
||||||
|
this.setPosition = await this.readThingProperty("stage", "position");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.input-and-buttons-container {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-content: stretch;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.numeric-setting-line-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 5px 0px;
|
||||||
|
width: 5em;
|
||||||
|
/* Stop Firefox showing input spinners, other
|
||||||
|
browsers set with block below */
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
/* Chrome, Safari, Edge, Opera */
|
||||||
|
.numeric-setting-line-input::-webkit-outer-spin-button,
|
||||||
|
.numeric-setting-line-input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
<template>
|
||||||
|
<div class="uk-flex uk-flex-center uk-flex-middle uk-margin">
|
||||||
|
<div class="dpad-grid">
|
||||||
|
<button id="up-button" class="uk-button uk-button-primary dpad-btn" @click="move(0, 1, 0)">
|
||||||
|
<span class="material-symbols-outlined sync-icon"> arrow_upward </span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="left-button" class="uk-button uk-button-primary dpad-btn" @click="move(-1, 0, 0)">
|
||||||
|
<span class="material-symbols-outlined sync-icon"> arrow_back </span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="right-button" class="uk-button uk-button-primary dpad-btn" @click="move(1, 0, 0)">
|
||||||
|
<span class="material-symbols-outlined sync-icon"> arrow_forward </span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="down-button" class="uk-button uk-button-primary dpad-btn" @click="move(0, -1, 0)">
|
||||||
|
<span class="material-symbols-outlined sync-icon"> arrow_downward </span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="focus-out-button"
|
||||||
|
class="uk-button uk-button-primary dpad-btn"
|
||||||
|
@click="move(0, 0, -1)"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined sync-icon"> remove </span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="focus-in-button"
|
||||||
|
class="uk-button uk-button-primary dpad-btn"
|
||||||
|
@click="move(0, 0, 1)"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined sync-icon"> add </span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "StageControlButtons",
|
||||||
|
methods: {
|
||||||
|
move(x, y, z) {
|
||||||
|
this.$root.$emit("globalMoveStepEvent", x, y, z);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dpad-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 40px);
|
||||||
|
grid-template-rows: 40px 40px 40px 20px 40px;
|
||||||
|
gap: 1px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Place buttons within grid */
|
||||||
|
.dpad-grid #up-button {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
.dpad-grid #left-button {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
.dpad-grid #right-button {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
.dpad-grid #down-button {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 3;
|
||||||
|
}
|
||||||
|
.dpad-grid #focus-out-button {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 5;
|
||||||
|
}
|
||||||
|
.dpad-grid #focus-in-button {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dpad-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -23,3 +23,9 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.control-component {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
</p>
|
</p>
|
||||||
<div class="buttons-container">
|
<div class="buttons-container">
|
||||||
<button
|
<button
|
||||||
v-show="'shutdown' in things.system.actions"
|
v-show="shutdownAvailable"
|
||||||
class="uk-button uk-button-primary uk-width-1-3 shutdown-button"
|
class="uk-button uk-button-primary uk-width-1-3 shutdown-button"
|
||||||
@click="systemRequest('shutdown')"
|
@click="systemRequest('shutdown')"
|
||||||
>
|
>
|
||||||
|
|
@ -42,8 +42,8 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
things: function () {
|
shutdownAvailable() {
|
||||||
return this.$store.getters["wot/thingDescriptions"];
|
return this.thingActionAvailable("system", "shutdown");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,12 +119,7 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
scansUri() {
|
scansUri() {
|
||||||
return this.$store.getters["wot/thingPropertyUrl"](
|
return this.thingPropertyUrl("smart_scan", "scans");
|
||||||
"smart_scan",
|
|
||||||
"scans",
|
|
||||||
"readproperty",
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
scansEmpty() {
|
scansEmpty() {
|
||||||
return this.scans.length == 0;
|
return this.scans.length == 0;
|
||||||
|
|
|
||||||
|
|
@ -86,10 +86,10 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
actions() {
|
actions() {
|
||||||
return this.$store.getters["wot/thingDescription"]("camera_stage_mapping").actions;
|
return this.thingDescription("camera_stage_mapping").actions;
|
||||||
},
|
},
|
||||||
properties() {
|
properties() {
|
||||||
return this.$store.getters["wot/thingDescription"]("camera_stage_mapping").properties;
|
return this.thingDescription("camera_stage_mapping").properties;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
actions() {
|
actions() {
|
||||||
return this.$store.getters["wot/thingDescription"]("camera").actions;
|
return this.thingDescription("camera").actions;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<appSettings />
|
||||||
|
<streamSettings />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import appSettings from "./displaySetttingsComponents/appSettings.vue";
|
||||||
|
import streamSettings from "./displaySetttingsComponents/streamSettings.vue";
|
||||||
|
|
||||||
|
// Export main app
|
||||||
|
export default {
|
||||||
|
name: "DisplaySettings",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
streamSettings,
|
||||||
|
appSettings,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
<template>
|
||||||
|
<div class="uk-width-large">
|
||||||
|
<p><b>Single Move Step Size</b></p>
|
||||||
|
<p>
|
||||||
|
This sets the size (and direction) of movements made using the navigation buttons in the
|
||||||
|
control tab or using the keyboard (arrow keys, page up/down).
|
||||||
|
</p>
|
||||||
|
<p>These settings do not affect the operation of other actions your microscope performs.</p>
|
||||||
|
<div class="uk-grid-small uk-child-width-1-3" uk-grid>
|
||||||
|
<div>
|
||||||
|
<label class="uk-form-label" for="form-stacked-text">x</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<input v-model="stepSize.x" class="uk-input uk-form-small" type="number" />
|
||||||
|
</div>
|
||||||
|
<label class="uk-margin-small-right">
|
||||||
|
<input v-model="invert.x" class="uk-checkbox" type="checkbox" />
|
||||||
|
Invert x
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="uk-form-label" for="form-stacked-text">y</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<input v-model="stepSize.y" class="uk-input uk-form-small" type="number" />
|
||||||
|
</div>
|
||||||
|
<label class="uk-margin-small-right">
|
||||||
|
<input v-model="invert.y" class="uk-checkbox" type="checkbox" />
|
||||||
|
Invert y
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="uk-form-label" for="form-stacked-text">z</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<input v-model="stepSize.z" class="uk-input uk-form-small" type="number" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "StageControlSettings",
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
// Note that as stepSize and invert are mutated (i.e. we change stepSize.x not stepSize)
|
||||||
|
// rather than directly set we cannot use get() and set() computed to interact with the
|
||||||
|
// store as changed won't be detected by set(). Instead use a deep watcher to
|
||||||
|
// update the store (see ``watch:`` below)
|
||||||
|
stepSize() {
|
||||||
|
return this.$store.state.navigationStepSize;
|
||||||
|
},
|
||||||
|
invert() {
|
||||||
|
return this.$store.state.navigationInvert;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
stepSize: {
|
||||||
|
deep: true,
|
||||||
|
handler(newVal) {
|
||||||
|
this.$store.commit("changeNavigationStepSize", newVal);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
invert: {
|
||||||
|
deep: true,
|
||||||
|
handler(newVal) {
|
||||||
|
this.$store.commit("changeNavigationInvert", newVal);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -5,17 +5,17 @@
|
||||||
<div class="settings-nav">
|
<div class="settings-nav">
|
||||||
<ul class="uk-nav uk-nav-default">
|
<ul class="uk-nav uk-nav-default">
|
||||||
<li class="uk-nav-header">Application Settings</li>
|
<li class="uk-nav-header">Application Settings</li>
|
||||||
<li>
|
<li v-for="item in appTabs" :key="'setting-' + item.id + '-tab-content'">
|
||||||
<tabIcon
|
<tabIcon
|
||||||
id="settings-display-icon"
|
:id="'setting-' + item.id + '-tab-content'"
|
||||||
tab-i-d="display"
|
:tab-i-d="item.id"
|
||||||
:show-title="false"
|
:show-title="false"
|
||||||
:show-tooltip="false"
|
:show-tooltip="false"
|
||||||
:require-connection="false"
|
:require-connection="item.requireConnection"
|
||||||
:current-tab="currentTab"
|
:current-tab="currentTab"
|
||||||
@set-tab="setTab"
|
@set-tab="setTab"
|
||||||
>
|
>
|
||||||
Display
|
{{ item.title }}
|
||||||
</tabIcon>
|
</tabIcon>
|
||||||
</li>
|
</li>
|
||||||
<li class="uk-nav-header">Microscope Settings</li>
|
<li class="uk-nav-header">Microscope Settings</li>
|
||||||
|
|
@ -27,81 +27,41 @@
|
||||||
>
|
>
|
||||||
Launch Calibration Wizard
|
Launch Calibration Wizard
|
||||||
</button>
|
</button>
|
||||||
<li>
|
<li v-for="item in calibrationTabs" :key="'setting-' + item.id + '-tab-icon'">
|
||||||
<tabIcon
|
<tabIcon
|
||||||
id="settings-camera-icon"
|
:id="'setting-' + item.id + '-tab-icon'"
|
||||||
tab-i-d="camera"
|
:tab-i-d="item.id"
|
||||||
:show-title="false"
|
:show-title="false"
|
||||||
:show-tooltip="false"
|
:show-tooltip="false"
|
||||||
:require-connection="true"
|
:require-connection="item.requireConnection"
|
||||||
:current-tab="currentTab"
|
:current-tab="currentTab"
|
||||||
@set-tab="setTab"
|
@set-tab="setTab"
|
||||||
>
|
>
|
||||||
Camera
|
{{ item.title }}
|
||||||
</tabIcon>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<tabIcon
|
|
||||||
id="settings-stage-icon"
|
|
||||||
tab-i-d="stage"
|
|
||||||
:show-title="false"
|
|
||||||
:show-tooltip="false"
|
|
||||||
:require-connection="true"
|
|
||||||
:current-tab="currentTab"
|
|
||||||
@set-tab="setTab"
|
|
||||||
>
|
|
||||||
Stage
|
|
||||||
</tabIcon>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<tabIcon
|
|
||||||
id="settings-mapping-icon"
|
|
||||||
tab-i-d="mapping"
|
|
||||||
:show-title="false"
|
|
||||||
:show-tooltip="false"
|
|
||||||
:require-connection="true"
|
|
||||||
:current-tab="currentTab"
|
|
||||||
@set-tab="setTab"
|
|
||||||
>
|
|
||||||
Camera to Stage Mapping
|
|
||||||
</tabIcon>
|
</tabIcon>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="view-component uk-width-expand uk-padding-small">
|
<div class="view-component uk-width-expand uk-padding-small">
|
||||||
<tabContent tab-i-d="display" :require-connection="false" :current-tab="currentTab">
|
<tabContent
|
||||||
<div class="settings-pane uk-padding-small">
|
v-for="item in allTabs"
|
||||||
<appSettings />
|
:id="'setting-' + item.id + '-tab-content'"
|
||||||
<streamSettings />
|
:key="'setting-' + item.id + '-tab-content'"
|
||||||
</div>
|
:tab-i-d="item.id"
|
||||||
</tabContent>
|
:require-connection="item.requireConnection"
|
||||||
|
:current-tab="currentTab"
|
||||||
<tabContent tab-i-d="camera" :require-connection="true" :current-tab="currentTab">
|
>
|
||||||
<div class="settings-pane uk-padding-small">
|
<component :is="item.component"></component>
|
||||||
<cameraSettings />
|
|
||||||
</div>
|
|
||||||
</tabContent>
|
|
||||||
|
|
||||||
<tabContent tab-i-d="stage" :require-connection="true" :current-tab="currentTab">
|
|
||||||
<div class="settings-pane uk-padding-small">
|
|
||||||
<stageSettings />
|
|
||||||
</div>
|
|
||||||
</tabContent>
|
|
||||||
|
|
||||||
<tabContent tab-i-d="mapping" :require-connection="true" :current-tab="currentTab">
|
|
||||||
<div class="settings-pane uk-padding-small">
|
|
||||||
<CSMSettings />
|
|
||||||
</div>
|
|
||||||
</tabContent>
|
</tabContent>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import streamSettings from "./settingsComponents/streamSettings.vue";
|
import displaySettings from "./settingsComponents/displaySettings.vue";
|
||||||
|
import stageControlSettings from "./settingsComponents/stageControlSettings.vue";
|
||||||
import calibrationWizard from "../modalComponents/calibrationWizard.vue";
|
import calibrationWizard from "../modalComponents/calibrationWizard.vue";
|
||||||
import cameraSettings from "./settingsComponents/cameraSettings.vue";
|
import cameraSettings from "./settingsComponents/cameraSettings.vue";
|
||||||
import appSettings from "./settingsComponents/appSettings.vue";
|
|
||||||
import CSMSettings from "./settingsComponents/CSMSettings.vue";
|
import CSMSettings from "./settingsComponents/CSMSettings.vue";
|
||||||
import stageSettings from "./settingsComponents/stageSettings.vue";
|
import stageSettings from "./settingsComponents/stageSettings.vue";
|
||||||
// Import generic components
|
// Import generic components
|
||||||
|
|
@ -113,11 +73,6 @@ export default {
|
||||||
name: "SettingsContent",
|
name: "SettingsContent",
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
streamSettings,
|
|
||||||
cameraSettings,
|
|
||||||
stageSettings,
|
|
||||||
CSMSettings,
|
|
||||||
appSettings,
|
|
||||||
tabIcon,
|
tabIcon,
|
||||||
tabContent,
|
tabContent,
|
||||||
calibrationWizard,
|
calibrationWizard,
|
||||||
|
|
@ -127,9 +82,67 @@ export default {
|
||||||
return {
|
return {
|
||||||
selected: "display",
|
selected: "display",
|
||||||
currentTab: "display",
|
currentTab: "display",
|
||||||
|
coreAppTabs: [
|
||||||
|
{
|
||||||
|
id: "display",
|
||||||
|
title: "Display",
|
||||||
|
requireConnection: false,
|
||||||
|
component: displaySettings,
|
||||||
|
requiredThings: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "stage-control",
|
||||||
|
title: "Stage Control Preferences",
|
||||||
|
requireConnection: false,
|
||||||
|
component: stageControlSettings,
|
||||||
|
requiredThings: ["stage"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
coreCalibrationTabs: [
|
||||||
|
{
|
||||||
|
id: "camera",
|
||||||
|
title: "Camera",
|
||||||
|
requireConnection: true,
|
||||||
|
component: cameraSettings,
|
||||||
|
requiredThings: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "stage",
|
||||||
|
title: "Stage",
|
||||||
|
requireConnection: true,
|
||||||
|
component: stageSettings,
|
||||||
|
requiredThings: ["stage"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mapping",
|
||||||
|
title: "Camera to Stage Mapping",
|
||||||
|
requireConnection: true,
|
||||||
|
component: CSMSettings,
|
||||||
|
requiredThings: ["camera_stage_mapping"],
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
allTabs() {
|
||||||
|
return [...this.appTabs, ...this.calibrationTabs];
|
||||||
|
},
|
||||||
|
appTabs() {
|
||||||
|
// Filter core top tabs based on available Things.
|
||||||
|
return this.coreAppTabs.filter((tab) => {
|
||||||
|
if (!tab.requiredThings || tab.requiredThings.length === 0) return true;
|
||||||
|
return tab.requiredThings.every((thing) => this.thingAvailable(thing));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
calibrationTabs() {
|
||||||
|
// Filter core top tabs based on available Things.
|
||||||
|
return this.coreCalibrationTabs.filter((tab) => {
|
||||||
|
if (!tab.requiredThings || tab.requiredThings.length === 0) return true;
|
||||||
|
return tab.requiredThings.every((thing) => this.thingAvailable(thing));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setTab: function (event, tab) {
|
setTab: function (event, tab) {
|
||||||
if (!(this.currentTab == tab)) {
|
if (!(this.currentTab == tab)) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!backendOK" class="uk-alert-danger">No scan back-end found.</div>
|
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
||||||
<!-- Grid managing tab content -->
|
|
||||||
<div v-else uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
|
||||||
<div class="control-component uk-padding-small">
|
<div class="control-component uk-padding-small">
|
||||||
<div v-show="!scanning" class="uk-padding-small">
|
<div v-show="!scanning" class="uk-padding-small">
|
||||||
<ul uk-accordion="multiple: true">
|
<ul uk-accordion="multiple: true">
|
||||||
|
|
@ -183,9 +181,6 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
backendOK() {
|
|
||||||
return this.thingAvailable("smart_scan");
|
|
||||||
},
|
|
||||||
cancellable() {
|
cancellable() {
|
||||||
return (this.taskStatus == "running") | (this.taskStatus == "pending");
|
return (this.taskStatus == "running") | (this.taskStatus == "pending");
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import store from "./store";
|
import store from "./store";
|
||||||
import axios from "axios";
|
|
||||||
import UIkit from "uikit";
|
import UIkit from "uikit";
|
||||||
|
|
||||||
import VueObserveVisibility from "vue-observe-visibility";
|
import VueObserveVisibility from "vue-observe-visibility";
|
||||||
|
|
||||||
// Import MD icons
|
// Import MD icons
|
||||||
import "material-symbols/outlined.css";
|
import "material-symbols/outlined.css";
|
||||||
import version from "./version.js";
|
|
||||||
|
import queryMixin from "@/mixins/labThingsMixins.js";
|
||||||
|
import modalMixin from "@/mixins/modalMixins.js";
|
||||||
|
|
||||||
// UIKit overrides
|
// UIKit overrides
|
||||||
UIkit.mixin(
|
UIkit.mixin(
|
||||||
|
|
@ -25,176 +26,8 @@ Vue.use(VueObserveVisibility);
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
Vue.mixin({
|
Vue.mixin(queryMixin);
|
||||||
methods: {
|
Vue.mixin(modalMixin);
|
||||||
app_version() {
|
|
||||||
return version;
|
|
||||||
},
|
|
||||||
thingDescription(thing) {
|
|
||||||
return this.$store.getters["wot/thingDescription"](thing);
|
|
||||||
},
|
|
||||||
thingAvailable(thing) {
|
|
||||||
return this.$store.getters["wot/thingAvailable"](thing);
|
|
||||||
},
|
|
||||||
async readThingProperty(thing, property, silence_errors = false) {
|
|
||||||
let url = this.$store.getters["wot/thingPropertyUrl"](thing, property, "readproperty", false);
|
|
||||||
try {
|
|
||||||
let response = await axios.get(url);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (!silence_errors) this.modalError(error);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async writeThingProperty(thing, property, value) {
|
|
||||||
let url = this.$store.getters["wot/thingPropertyUrl"](
|
|
||||||
thing,
|
|
||||||
property,
|
|
||||||
"writeproperty",
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
// `false` fails because axios somehow eats it!
|
|
||||||
// Other values should not be stringified or pydantic
|
|
||||||
// can't parse them.
|
|
||||||
if ((value === false) | (value === true)) {
|
|
||||||
value = JSON.stringify(value);
|
|
||||||
}
|
|
||||||
await axios.put(url, value);
|
|
||||||
},
|
|
||||||
async invokeAction(thing, action, data) {
|
|
||||||
let url = this.$store.getters["wot/thingActionUrl"](thing, action, "invokeaction", false);
|
|
||||||
try {
|
|
||||||
let response = await axios.post(url, data);
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
this.modalError(error);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
thingActionUrl(thing, action, allow_missing = false) {
|
|
||||||
let url = this.$store.getters["wot/thingActionUrl"](
|
|
||||||
thing,
|
|
||||||
action,
|
|
||||||
"invokeaction",
|
|
||||||
allow_missing,
|
|
||||||
);
|
|
||||||
return url;
|
|
||||||
},
|
|
||||||
modalConfirm: function (modalText) {
|
|
||||||
var context = this;
|
|
||||||
|
|
||||||
// Stop GPU preview to show modal
|
|
||||||
context.$root.$emit("globalTogglePreview", false);
|
|
||||||
|
|
||||||
// force OK to be capitalised
|
|
||||||
UIkit.modal.i18n = { ok: "OK", cancel: "Cancel" };
|
|
||||||
|
|
||||||
var showModal = function (resolve, reject) {
|
|
||||||
UIkit.modal
|
|
||||||
.confirm(modalText, { stack: true })
|
|
||||||
.then(
|
|
||||||
function () {
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
function () {
|
|
||||||
reject();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.finally(function () {
|
|
||||||
// Re-enable the GPU preview, if it was active before the modal
|
|
||||||
if (context.$store.state.autoGpuPreview) {
|
|
||||||
context.$root.$emit("globalTogglePreview", true);
|
|
||||||
}
|
|
||||||
context.$root.$emit("modalClosed");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return new Promise(showModal);
|
|
||||||
},
|
|
||||||
|
|
||||||
modalNotify: function (message, status = "success") {
|
|
||||||
UIkit.notification({
|
|
||||||
message: message,
|
|
||||||
status: status,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
modalDialog: function (title, message) {
|
|
||||||
UIkit.modal.dialog(
|
|
||||||
`
|
|
||||||
<button class="uk-modal-close-default" type="button" uk-close></button>
|
|
||||||
<div class="uk-modal-header">
|
|
||||||
<h2 class="uk-modal-title">${title}</h2>
|
|
||||||
</div>
|
|
||||||
<div class="uk-modal-body">
|
|
||||||
<p>${message}</p>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
{ stack: true },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
modalError: function (error) {
|
|
||||||
var errormsg = this.getErrorMessage(error);
|
|
||||||
this.$store.commit("setErrorMessage", errormsg);
|
|
||||||
UIkit.notification({
|
|
||||||
message: `${errormsg}`,
|
|
||||||
status: "danger",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getErrorMessage: function (error) {
|
|
||||||
// Format the error.
|
|
||||||
let data = this.getErrorData(error);
|
|
||||||
// Get error data may get an object. Handle edge cases and if not try to use
|
|
||||||
// JSON to get the best string. This stops "objectObject" showing as an error.
|
|
||||||
if (data === null) return "null";
|
|
||||||
if (data === undefined) return "undefined";
|
|
||||||
if (typeof data === "string") return data;
|
|
||||||
try {
|
|
||||||
return JSON.stringify(data, null, 2);
|
|
||||||
} catch (err) {
|
|
||||||
return String(data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getErrorData: function (error) {
|
|
||||||
// If a response was obtained, extract the most specific message
|
|
||||||
if (error.response) {
|
|
||||||
// If the response is a nicely formatted JSON response from the server
|
|
||||||
if (error.response.data.message) {
|
|
||||||
return error.response.data.message;
|
|
||||||
}
|
|
||||||
if (error.response.data.detail) {
|
|
||||||
try {
|
|
||||||
return error.response.data.detail[0].msg;
|
|
||||||
} catch (err) {
|
|
||||||
return error.response.data.detail;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If the response is just some generic error response
|
|
||||||
if (error.response.data) {
|
|
||||||
return error.response.data;
|
|
||||||
}
|
|
||||||
return error.response;
|
|
||||||
}
|
|
||||||
// If we have an error object with a message, use that
|
|
||||||
if (error.message) return error.message;
|
|
||||||
// At this point just formatting the whole error object is the best we can do.
|
|
||||||
return error;
|
|
||||||
},
|
|
||||||
|
|
||||||
showModalElement: function (element) {
|
|
||||||
UIkit.modal(element).show();
|
|
||||||
},
|
|
||||||
|
|
||||||
hideModalElement: function (element) {
|
|
||||||
UIkit.modal(element).hide();
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleModalElement: function (element) {
|
|
||||||
UIkit.modal(element).toggle();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
store,
|
store,
|
||||||
|
|
|
||||||
109
webapp/src/mixins/labThingsMixins.js
Normal file
109
webapp/src/mixins/labThingsMixins.js
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
/**
|
||||||
|
* @fileoverview
|
||||||
|
* Global mixin providing access to the LabThings (WoT) API.
|
||||||
|
*
|
||||||
|
* This mixin is registered globally in `main.js` using. Do not
|
||||||
|
* manually import it in components.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
pollTimers: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
thingDescription(thing) {
|
||||||
|
return this.$store.getters["wot/thingDescription"](thing);
|
||||||
|
},
|
||||||
|
thingList() {
|
||||||
|
return this.$store.getters["wot/thingList"];
|
||||||
|
},
|
||||||
|
thingAvailable(thing) {
|
||||||
|
return this.$store.getters["wot/thingAvailable"](thing);
|
||||||
|
},
|
||||||
|
thingPropertyUrl(thing, property, allowUndefined = false) {
|
||||||
|
return this.$store.getters["wot/thingPropertyUrl"](
|
||||||
|
thing,
|
||||||
|
property,
|
||||||
|
"readproperty",
|
||||||
|
allowUndefined,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
thingActionAvailable(thing, action) {
|
||||||
|
return this.$store.getters["wot/thingAffordanceAvailable"](thing, "actions", action);
|
||||||
|
},
|
||||||
|
thingPropertyAvailable(thing, property) {
|
||||||
|
return this.$store.getters["wot/thingAffordanceAvailable"](thing, "properties", property);
|
||||||
|
},
|
||||||
|
async readThingProperty(thing, property, silenceErrors = false) {
|
||||||
|
let url = this.$store.getters["wot/thingPropertyUrl"](thing, property, "readproperty", false);
|
||||||
|
try {
|
||||||
|
let response = await axios.get(url);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (!silenceErrors) this.modalError(error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async writeThingProperty(thing, property, value) {
|
||||||
|
let url = this.$store.getters["wot/thingPropertyUrl"](
|
||||||
|
thing,
|
||||||
|
property,
|
||||||
|
"writeproperty",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
// `false` fails because axios somehow eats it!
|
||||||
|
// Other values should not be stringified or pydantic
|
||||||
|
// can't parse them.
|
||||||
|
if ((value === false) | (value === true)) {
|
||||||
|
value = JSON.stringify(value);
|
||||||
|
}
|
||||||
|
await axios.put(url, value);
|
||||||
|
},
|
||||||
|
async invokeAction(thing, action, data) {
|
||||||
|
let url = this.$store.getters["wot/thingActionUrl"](thing, action, "invokeaction", false);
|
||||||
|
try {
|
||||||
|
let response = await axios.post(url, data);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
this.modalError(error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async pollUntilComplete(taskUrl, ongoingMethod, finalMethod, interval = 500) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(taskUrl, { baseURL: this.$store.getters.baseUri });
|
||||||
|
const result = response.data.status;
|
||||||
|
|
||||||
|
if ((result == "running") | (result == "pending")) {
|
||||||
|
ongoingMethod?.(response);
|
||||||
|
this.pollTimers[taskUrl] = setTimeout(() => {
|
||||||
|
this.pollUntilComplete(taskUrl, ongoingMethod, finalMethod, interval);
|
||||||
|
}, interval);
|
||||||
|
} else {
|
||||||
|
clearTimeout(this.pollTimers[taskUrl]);
|
||||||
|
delete this.pollTimers[taskUrl];
|
||||||
|
finalMethod?.(response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(this.pollTimers[taskUrl]);
|
||||||
|
delete this.pollTimers[taskUrl];
|
||||||
|
this.modalError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
thingActionUrl(thing, action, allowUndefined = false) {
|
||||||
|
let url = this.$store.getters["wot/thingActionUrl"](
|
||||||
|
thing,
|
||||||
|
action,
|
||||||
|
"invokeaction",
|
||||||
|
allowUndefined,
|
||||||
|
);
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
127
webapp/src/mixins/modalMixins.js
Normal file
127
webapp/src/mixins/modalMixins.js
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
/**
|
||||||
|
* @fileoverview
|
||||||
|
* Global mixin providing modal functionality.
|
||||||
|
*
|
||||||
|
* This mixin is registered globally in `main.js` using. Do not
|
||||||
|
* manually import it in components.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import UIkit from "uikit";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
modalConfirm: function (modalText) {
|
||||||
|
var context = this;
|
||||||
|
|
||||||
|
// Stop GPU preview to show modal
|
||||||
|
context.$root.$emit("globalTogglePreview", false);
|
||||||
|
|
||||||
|
// force OK to be capitalised
|
||||||
|
UIkit.modal.i18n = { ok: "OK", cancel: "Cancel" };
|
||||||
|
|
||||||
|
var showModal = function (resolve, reject) {
|
||||||
|
UIkit.modal
|
||||||
|
.confirm(modalText, { stack: true })
|
||||||
|
.then(
|
||||||
|
function () {
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
reject();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.finally(function () {
|
||||||
|
// Re-enable the GPU preview, if it was active before the modal
|
||||||
|
if (context.$store.state.autoGpuPreview) {
|
||||||
|
context.$root.$emit("globalTogglePreview", true);
|
||||||
|
}
|
||||||
|
context.$root.$emit("modalClosed");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return new Promise(showModal);
|
||||||
|
},
|
||||||
|
|
||||||
|
modalNotify: function (message, status = "success") {
|
||||||
|
UIkit.notification({
|
||||||
|
message: message,
|
||||||
|
status: status,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
modalDialog: function (title, message) {
|
||||||
|
UIkit.modal.dialog(
|
||||||
|
`
|
||||||
|
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||||
|
<div class="uk-modal-header">
|
||||||
|
<h2 class="uk-modal-title">${title}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="uk-modal-body">
|
||||||
|
<p>${message}</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
{ stack: true },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
modalError: function (error) {
|
||||||
|
var errormsg = this.getErrorMessage(error);
|
||||||
|
this.$store.commit("setErrorMessage", errormsg);
|
||||||
|
UIkit.notification({
|
||||||
|
message: `${errormsg}`,
|
||||||
|
status: "danger",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getErrorMessage: function (error) {
|
||||||
|
// Format the error.
|
||||||
|
let data = this.getErrorData(error);
|
||||||
|
// Get error data may get an object. Handle edge cases and if not try to use
|
||||||
|
// JSON to get the best string. This stops "objectObject" showing as an error.
|
||||||
|
if (data === null) return "null";
|
||||||
|
if (data === undefined) return "undefined";
|
||||||
|
if (typeof data === "string") return data;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
} catch (err) {
|
||||||
|
return String(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getErrorData: function (error) {
|
||||||
|
// If a response was obtained, extract the most specific message
|
||||||
|
if (error.response) {
|
||||||
|
// If the response is a nicely formatted JSON response from the server
|
||||||
|
if (error.response.data.message) {
|
||||||
|
return error.response.data.message;
|
||||||
|
}
|
||||||
|
if (error.response.data.detail) {
|
||||||
|
try {
|
||||||
|
return error.response.data.detail[0].msg;
|
||||||
|
} catch (err) {
|
||||||
|
return error.response.data.detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the response is just some generic error response
|
||||||
|
if (error.response.data) {
|
||||||
|
return error.response.data;
|
||||||
|
}
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
// If we have an error object with a message, use that
|
||||||
|
if (error.message) return error.message;
|
||||||
|
// At this point just formatting the whole error object is the best we can do.
|
||||||
|
return error;
|
||||||
|
},
|
||||||
|
|
||||||
|
showModalElement: function (element) {
|
||||||
|
UIkit.modal(element).show();
|
||||||
|
},
|
||||||
|
|
||||||
|
hideModalElement: function (element) {
|
||||||
|
UIkit.modal(element).hide();
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleModalElement: function (element) {
|
||||||
|
UIkit.modal(element).toggle();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -66,7 +66,6 @@ export default new Vuex.Store({
|
||||||
error: "",
|
error: "",
|
||||||
autoGpuPreview: false,
|
autoGpuPreview: false,
|
||||||
trackWindow: true,
|
trackWindow: true,
|
||||||
galleryEnabled: true,
|
|
||||||
activeStreams: {},
|
activeStreams: {},
|
||||||
microscopeHostname: "",
|
microscopeHostname: "",
|
||||||
// Persistent items:
|
// Persistent items:
|
||||||
|
|
@ -108,9 +107,6 @@ export default new Vuex.Store({
|
||||||
changeAppTheme(state, theme) {
|
changeAppTheme(state, theme) {
|
||||||
state.appTheme = theme;
|
state.appTheme = theme;
|
||||||
},
|
},
|
||||||
changeGalleryEnabled(state, enabled) {
|
|
||||||
state.galleryEnabled = enabled;
|
|
||||||
},
|
|
||||||
resetState(state) {
|
resetState(state) {
|
||||||
state.waiting = false;
|
state.waiting = false;
|
||||||
state.available = false;
|
state.available = false;
|
||||||
|
|
|
||||||
|
|
@ -51,12 +51,22 @@ export const wotStoreModule = {
|
||||||
thingDescriptions: (state) => {
|
thingDescriptions: (state) => {
|
||||||
return state.thingDescriptions;
|
return state.thingDescriptions;
|
||||||
},
|
},
|
||||||
|
thingList: (state) => {
|
||||||
|
return Object.keys(state.thingDescriptions);
|
||||||
|
},
|
||||||
thingDescription: (state) => (thingName) => {
|
thingDescription: (state) => (thingName) => {
|
||||||
return state.thingDescriptions[thingName];
|
return state.thingDescriptions[thingName];
|
||||||
},
|
},
|
||||||
thingAvailable: (state) => (thingName) => {
|
thingAvailable: (state) => (thingName) => {
|
||||||
return thingName in state.thingDescriptions;
|
return thingName in state.thingDescriptions;
|
||||||
},
|
},
|
||||||
|
thingAffordanceAvailable: (state) => (thing, affordanceType, affordance) => {
|
||||||
|
let td = state.thingDescriptions[thing];
|
||||||
|
if (!td) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return affordance in td[affordanceType];
|
||||||
|
},
|
||||||
thingFormUrl:
|
thingFormUrl:
|
||||||
(state) =>
|
(state) =>
|
||||||
(thing, affordanceType, affordance, op, allowUndefined = true) => {
|
(thing, affordanceType, affordance, op, allowUndefined = true) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue