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-plugin-prettier": "^3.4.1",
|
||||
"eslint-plugin-vue": "^7.20.0",
|
||||
"genversion": "^3.2.0",
|
||||
"less": "^4.4.2",
|
||||
"less-loader": "^11.1.4",
|
||||
"material-symbols": "^0.39.1",
|
||||
|
|
@ -7480,39 +7479,6 @@
|
|||
"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": {
|
||||
"version": "1.3.1",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
|
|
@ -7787,40 +7743,6 @@
|
|||
"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": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
|
|
@ -8646,101 +8568,6 @@
|
|||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
|
||||
|
|
@ -10231,16 +10058,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": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
|
|
@ -10336,16 +10153,6 @@
|
|||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"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": {
|
||||
"version": "0.1.12",
|
||||
"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)",
|
||||
"scripts": {
|
||||
"gv": "genversion src/version.js",
|
||||
"serve": "npm run gv && vue-cli-service serve --mode development",
|
||||
"build": "npm run gv && vue-cli-service build --mode production",
|
||||
"serve": "vue-cli-service serve --mode development",
|
||||
"build": "vue-cli-service build --mode production",
|
||||
"lint": "vue-cli-service lint --no-fix --max-warnings 0",
|
||||
"lint:fix": "vue-cli-service lint --fix"
|
||||
},
|
||||
|
|
@ -32,7 +32,6 @@
|
|||
"eslint-config-prettier": "^8.10.2",
|
||||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"eslint-plugin-vue": "^7.20.0",
|
||||
"genversion": "^3.2.0",
|
||||
"less": "^4.4.2",
|
||||
"less-loader": "^11.1.4",
|
||||
"material-symbols": "^0.39.1",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<template>
|
||||
<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" />
|
||||
<appContent v-if="$store.getters.ready" />
|
||||
<!-- Runtime modals -->
|
||||
|
|
@ -213,8 +215,8 @@ export default {
|
|||
// TODO: should we purge existing consumedThings?
|
||||
try {
|
||||
await this.$store.dispatch("wot/fetchThingDescriptions", `${baseUri}/thing_descriptions/`);
|
||||
for (let requiredThing of ["camera", "stage"]) {
|
||||
if (!this.$store.getters["wot/thingAvailable"](requiredThing)) {
|
||||
for (let requiredThing of ["camera", "system"]) {
|
||||
if (!this.thingAvailable(requiredThing)) {
|
||||
throw new Error(`No ${requiredThing} found, the GUI won't work without one.`);
|
||||
}
|
||||
}
|
||||
|
|
@ -225,6 +227,7 @@ export default {
|
|||
} catch {
|
||||
this.$store.commit("changeMicroscopeHostname", null);
|
||||
}
|
||||
// start rendering components
|
||||
this.$store.commit("setConnected");
|
||||
this.$store.commit("setErrorMessage", null);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
:id="item.id + '-tab-icon'"
|
||||
:key="item.id + '-tab-icon'"
|
||||
:tab-i-d="item.id"
|
||||
:title="item.title"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
:class="item.class"
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
:id="item.id + '-tab-icon'"
|
||||
:key="item.id + '-tab-icon'"
|
||||
:tab-i-d="item.id"
|
||||
:title="item.title"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
:class="item.class"
|
||||
|
|
@ -81,19 +83,18 @@ import tabIcon from "./genericComponents/tabIcon";
|
|||
import tabContent from "./genericComponents/tabContent";
|
||||
|
||||
// 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 backgroundDetectContent from "./tabContentComponents/backgroundDetectContent.vue";
|
||||
import controlContent from "./tabContentComponents/controlContent.vue";
|
||||
import loggingContent from "./tabContentComponents/loggingContent.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 calibrationWizard from "./modalComponents/calibrationWizard.vue";
|
||||
import TabIcon from "./genericComponents/tabIcon.vue";
|
||||
import ScanListContent from "./tabContentComponents/scanListContent.vue";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
|
|
@ -102,15 +103,7 @@ export default {
|
|||
components: {
|
||||
tabIcon,
|
||||
tabContent,
|
||||
controlContent,
|
||||
slideScanContent,
|
||||
viewContent,
|
||||
settingsContent,
|
||||
calibrationWizard,
|
||||
aboutContent,
|
||||
loggingContent,
|
||||
TabIcon,
|
||||
powerContent,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
|
|
@ -118,26 +111,69 @@ export default {
|
|||
bottomTabs: [
|
||||
{
|
||||
id: "settings",
|
||||
title: "Settings",
|
||||
icon: "settings",
|
||||
component: settingsContent,
|
||||
class: "uk-margin-auto-top",
|
||||
},
|
||||
{
|
||||
id: "logging",
|
||||
title: "Logging",
|
||||
icon: "assignment_late",
|
||||
component: loggingContent,
|
||||
},
|
||||
{
|
||||
id: "about",
|
||||
title: "About",
|
||||
icon: "info",
|
||||
component: aboutContent,
|
||||
},
|
||||
{
|
||||
id: "power",
|
||||
title: "Power",
|
||||
icon: "power_settings_new",
|
||||
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 () {
|
||||
let tabs = [
|
||||
{
|
||||
id: "view",
|
||||
icon: "visibility",
|
||||
component: viewContent,
|
||||
},
|
||||
{
|
||||
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;
|
||||
// Filter core top tabs based on available Things. Once Things can specify a
|
||||
// custom tab those will need to be added here
|
||||
return this.coreTopTabs.filter((tab) => {
|
||||
if (!tab.requiredThings || tab.requiredThings.length === 0) return true;
|
||||
return tab.requiredThings.every((thing) => this.thingAvailable(thing));
|
||||
});
|
||||
},
|
||||
allTabs() {
|
||||
return [...this.topTabs, ...this.bottomTabs];
|
||||
|
|
|
|||
|
|
@ -209,6 +209,8 @@ export default {
|
|||
// A global signal listener to perform the action
|
||||
if (this.submitOnEvent) {
|
||||
this.$root.$on(this.submitOnEvent, () => {
|
||||
if (this.isDisabled) return;
|
||||
// Bootstrap task if button is not disabled.
|
||||
this.bootstrapTask();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,13 +78,11 @@ export default {
|
|||
const calibrateableThings = Object.keys(this.availableCalibrationTasks);
|
||||
|
||||
for (const name of calibrateableThings) {
|
||||
try {
|
||||
if (this.thingPropertyAvailable(name, "calibration_required")) {
|
||||
const thingNeedsCal = await this.readThingProperty(name, "calibration_required");
|
||||
if (thingNeedsCal) {
|
||||
needsCalibration.push(name);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`${name}: missing calibration_required property`, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
{{ $store.state.origin }}
|
||||
</div>
|
||||
<action-button
|
||||
v-if="stageType"
|
||||
thing="stage"
|
||||
action="flash_led"
|
||||
submit-label="Flash Illumination"
|
||||
|
|
@ -34,18 +35,16 @@
|
|||
<div class="uk-margin-small-bottom">
|
||||
<b>Camera:</b>
|
||||
<br />
|
||||
<div v-if="'camera' in things">
|
||||
{{ things.camera.title }}
|
||||
<div>
|
||||
{{ cameraType }}
|
||||
</div>
|
||||
<div v-else class="uk-text-danger"><b>No camera configured</b></div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="stageType">
|
||||
<b>Stage:</b>
|
||||
<br />
|
||||
<div v-if="'stage' in things">
|
||||
{{ things.stage.title }}
|
||||
<div>
|
||||
{{ stageType }}
|
||||
</div>
|
||||
<div v-else class="uk-text-danger"><b>No stage configured</b></div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
|
@ -71,8 +70,12 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
things: function () {
|
||||
return this.$store.getters["wot/thingDescriptions"];
|
||||
cameraType() {
|
||||
// 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>
|
||||
<div id="paneControl" class="uk-padding-small">
|
||||
<div v-if="setPosition">
|
||||
<ul uk-accordion="multiple: true">
|
||||
<li>
|
||||
<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>
|
||||
<positionControl v-if="stageAvailable" />
|
||||
<autofocusControl v-if="autofocusAvailable" />
|
||||
<p>Image Capture</p>
|
||||
|
||||
<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>
|
||||
<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 class="uk-margin">
|
||||
<action-button
|
||||
thing="camera"
|
||||
action="capture_jpeg"
|
||||
:submit-data="{ stream_name: 'main' }"
|
||||
:submit-label="'Low Resolution'"
|
||||
@response="handleCaptureResponse"
|
||||
@error="modalError"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="uk-text-warning">
|
||||
<p>Stage positioning is disabled since no stage is connected.</p>
|
||||
<p>Please check all data and power connections to your motor controller board.</p>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
|
|
@ -147,151 +32,28 @@
|
|||
<script>
|
||||
import axios from "axios";
|
||||
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 {
|
||||
name: "PaneControl",
|
||||
|
||||
components: {
|
||||
ActionButton,
|
||||
syncPropertyButton,
|
||||
},
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
setPosition: null,
|
||||
isAutofocusing: 0,
|
||||
moveLock: false,
|
||||
};
|
||||
positionControl,
|
||||
autofocusControl,
|
||||
},
|
||||
|
||||
computed: {
|
||||
// Note that as stepSize and invert are set based on internals we can use
|
||||
// get() and set() to interact with the store. Instead use a deep watcher to
|
||||
// update the store (see ``watch:`` below)
|
||||
stepSize() {
|
||||
return this.$store.state.navigationStepSize;
|
||||
stageAvailable() {
|
||||
return this.thingAvailable("stage");
|
||||
},
|
||||
invert() {
|
||||
return this.$store.state.navigationInvert;
|
||||
autofocusAvailable() {
|
||||
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: {
|
||||
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) {
|
||||
// Retrieve the captured image and save it
|
||||
let imageUri = response.output.href;
|
||||
|
|
@ -312,29 +74,3 @@ export default {
|
|||
},
|
||||
};
|
||||
</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>
|
||||
|
||||
<style scoped>
|
||||
.control-component {
|
||||
width: 160px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
</p>
|
||||
<div class="buttons-container">
|
||||
<button
|
||||
v-show="'shutdown' in things.system.actions"
|
||||
v-show="shutdownAvailable"
|
||||
class="uk-button uk-button-primary uk-width-1-3 shutdown-button"
|
||||
@click="systemRequest('shutdown')"
|
||||
>
|
||||
|
|
@ -42,8 +42,8 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
things: function () {
|
||||
return this.$store.getters["wot/thingDescriptions"];
|
||||
shutdownAvailable() {
|
||||
return this.thingActionAvailable("system", "shutdown");
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -119,12 +119,7 @@ export default {
|
|||
|
||||
computed: {
|
||||
scansUri() {
|
||||
return this.$store.getters["wot/thingPropertyUrl"](
|
||||
"smart_scan",
|
||||
"scans",
|
||||
"readproperty",
|
||||
true,
|
||||
);
|
||||
return this.thingPropertyUrl("smart_scan", "scans");
|
||||
},
|
||||
scansEmpty() {
|
||||
return this.scans.length == 0;
|
||||
|
|
|
|||
|
|
@ -86,10 +86,10 @@ export default {
|
|||
|
||||
computed: {
|
||||
actions() {
|
||||
return this.$store.getters["wot/thingDescription"]("camera_stage_mapping").actions;
|
||||
return this.thingDescription("camera_stage_mapping").actions;
|
||||
},
|
||||
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: {
|
||||
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">
|
||||
<ul class="uk-nav uk-nav-default">
|
||||
<li class="uk-nav-header">Application Settings</li>
|
||||
<li>
|
||||
<li v-for="item in appTabs" :key="'setting-' + item.id + '-tab-content'">
|
||||
<tabIcon
|
||||
id="settings-display-icon"
|
||||
tab-i-d="display"
|
||||
:id="'setting-' + item.id + '-tab-content'"
|
||||
:tab-i-d="item.id"
|
||||
:show-title="false"
|
||||
:show-tooltip="false"
|
||||
:require-connection="false"
|
||||
:require-connection="item.requireConnection"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
Display
|
||||
{{ item.title }}
|
||||
</tabIcon>
|
||||
</li>
|
||||
<li class="uk-nav-header">Microscope Settings</li>
|
||||
|
|
@ -27,81 +27,41 @@
|
|||
>
|
||||
Launch Calibration Wizard
|
||||
</button>
|
||||
<li>
|
||||
<li v-for="item in calibrationTabs" :key="'setting-' + item.id + '-tab-icon'">
|
||||
<tabIcon
|
||||
id="settings-camera-icon"
|
||||
tab-i-d="camera"
|
||||
:id="'setting-' + item.id + '-tab-icon'"
|
||||
:tab-i-d="item.id"
|
||||
:show-title="false"
|
||||
:show-tooltip="false"
|
||||
:require-connection="true"
|
||||
:require-connection="item.requireConnection"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
Camera
|
||||
</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
|
||||
{{ item.title }}
|
||||
</tabIcon>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="view-component uk-width-expand uk-padding-small">
|
||||
<tabContent tab-i-d="display" :require-connection="false" :current-tab="currentTab">
|
||||
<div class="settings-pane uk-padding-small">
|
||||
<appSettings />
|
||||
<streamSettings />
|
||||
</div>
|
||||
</tabContent>
|
||||
|
||||
<tabContent tab-i-d="camera" :require-connection="true" :current-tab="currentTab">
|
||||
<div class="settings-pane uk-padding-small">
|
||||
<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
|
||||
v-for="item in allTabs"
|
||||
:id="'setting-' + item.id + '-tab-content'"
|
||||
:key="'setting-' + item.id + '-tab-content'"
|
||||
:tab-i-d="item.id"
|
||||
:require-connection="item.requireConnection"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<component :is="item.component"></component>
|
||||
</tabContent>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 cameraSettings from "./settingsComponents/cameraSettings.vue";
|
||||
import appSettings from "./settingsComponents/appSettings.vue";
|
||||
import CSMSettings from "./settingsComponents/CSMSettings.vue";
|
||||
import stageSettings from "./settingsComponents/stageSettings.vue";
|
||||
// Import generic components
|
||||
|
|
@ -113,11 +73,6 @@ export default {
|
|||
name: "SettingsContent",
|
||||
|
||||
components: {
|
||||
streamSettings,
|
||||
cameraSettings,
|
||||
stageSettings,
|
||||
CSMSettings,
|
||||
appSettings,
|
||||
tabIcon,
|
||||
tabContent,
|
||||
calibrationWizard,
|
||||
|
|
@ -127,9 +82,67 @@ export default {
|
|||
return {
|
||||
selected: "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: {
|
||||
setTab: function (event, tab) {
|
||||
if (!(this.currentTab == tab)) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
<template>
|
||||
<div v-if="!backendOK" class="uk-alert-danger">No scan back-end found.</div>
|
||||
<!-- Grid managing tab content -->
|
||||
<div v-else uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
||||
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
|
||||
<div class="control-component uk-padding-small">
|
||||
<div v-show="!scanning" class="uk-padding-small">
|
||||
<ul uk-accordion="multiple: true">
|
||||
|
|
@ -183,9 +181,6 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
backendOK() {
|
||||
return this.thingAvailable("smart_scan");
|
||||
},
|
||||
cancellable() {
|
||||
return (this.taskStatus == "running") | (this.taskStatus == "pending");
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import Vue from "vue";
|
||||
import App from "./App.vue";
|
||||
import store from "./store";
|
||||
import axios from "axios";
|
||||
import UIkit from "uikit";
|
||||
|
||||
import VueObserveVisibility from "vue-observe-visibility";
|
||||
|
||||
// Import MD icons
|
||||
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.mixin(
|
||||
|
|
@ -25,176 +26,8 @@ Vue.use(VueObserveVisibility);
|
|||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
Vue.mixin({
|
||||
methods: {
|
||||
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();
|
||||
},
|
||||
},
|
||||
});
|
||||
Vue.mixin(queryMixin);
|
||||
Vue.mixin(modalMixin);
|
||||
|
||||
new Vue({
|
||||
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: "",
|
||||
autoGpuPreview: false,
|
||||
trackWindow: true,
|
||||
galleryEnabled: true,
|
||||
activeStreams: {},
|
||||
microscopeHostname: "",
|
||||
// Persistent items:
|
||||
|
|
@ -108,9 +107,6 @@ export default new Vuex.Store({
|
|||
changeAppTheme(state, theme) {
|
||||
state.appTheme = theme;
|
||||
},
|
||||
changeGalleryEnabled(state, enabled) {
|
||||
state.galleryEnabled = enabled;
|
||||
},
|
||||
resetState(state) {
|
||||
state.waiting = false;
|
||||
state.available = false;
|
||||
|
|
|
|||
|
|
@ -51,12 +51,22 @@ export const wotStoreModule = {
|
|||
thingDescriptions: (state) => {
|
||||
return state.thingDescriptions;
|
||||
},
|
||||
thingList: (state) => {
|
||||
return Object.keys(state.thingDescriptions);
|
||||
},
|
||||
thingDescription: (state) => (thingName) => {
|
||||
return state.thingDescriptions[thingName];
|
||||
},
|
||||
thingAvailable: (state) => (thingName) => {
|
||||
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:
|
||||
(state) =>
|
||||
(thing, affordanceType, affordance, op, allowUndefined = true) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue