Merge branch 'update-js-toolchain' into 'v3'

Update js toolchain

See merge request openflexure/openflexure-microscope-server!431
This commit is contained in:
Julian Stirling 2025-11-04 17:10:28 +00:00
commit c2b8706fb5
48 changed files with 6656 additions and 9629 deletions

View file

@ -161,7 +161,6 @@ build:
- .javascript_webapp
- .rules_common
script:
- export NODE_OPTIONS=--openssl-legacy-provider
# Build JS application for production
- npm run build
artifacts:

View file

@ -5,14 +5,19 @@ module.exports = {
node: true,
},
parser: "vue-eslint-parser",
parserOptions: {
parser: "babel-eslint",
parser: "@babel/eslint-parser",
requireConfigFile: false,
ecmaVersion: 2020,
sourceType: "module",
},
extends: ["plugin:vue/recommended", "eslint:recommended", "@vue/prettier"],
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
},
extends: ["plugin:vue/recommended", "eslint:recommended", "@vue/prettier"],
};

View file

@ -18,12 +18,9 @@
_Note: If you are using a Windows OS, the following commands must be executed in a Powershell terminal._
* Install Node.js (and npm)
* Install dependencies with `npm install`
* Node v18 changes SSL, and so you need a legacy version for compatibility:
* On Windows: `$env:NODE_OPTIONS = "--openssl-legacy-provider"`
* On Linux/MacOS `export NODE_OPTIONS=--openssl-legacy-provider`
* Install Node.js v18 (and npm)
* Navigate into the webapp directory with `cd webapp`
* Install dependencies with `npm install`
* Build the static web app with `npm run build`
# Developing

15759
webapp/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,34 +7,37 @@
"scripts": {
"gv": "genversion src/version.js",
"serve": "npm run gv && vue-cli-service serve --mode development",
"build": "npm run gv && vue-cli-service build --openssl-legacy-provider --mode production",
"build": "npm run gv && vue-cli-service build --mode production",
"lint": "vue-cli-service lint --no-fix --max-warnings 0",
"lint:fix": "vue-cli-service lint --fix"
},
"dependencies": {
"material-design-icons": "^3.0",
"material-symbols": "^0.14.3",
"mousetrap": "^1.6.5",
"openseadragon": "^5.0.0",
"vue-observe-visibility": "^0.4"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.12",
"@vue/cli-plugin-eslint": "^4.5",
"@vue/cli-service": "^4.5",
"@vue/eslint-config-prettier": "^6.0",
"axios": "^0.21",
"babel-eslint": "^10.1",
"@babel/eslint-parser": "^7.28.5",
"@vue/cli-plugin-babel": "^5.0.9",
"@vue/cli-plugin-eslint": "^5.0.9",
"@vue/cli-plugin-router": "^5.0.9",
"@vue/cli-plugin-vuex": "^5.0.9",
"@vue/cli-service": "^5.0.9",
"@vue/eslint-config-prettier": "^7.1.0",
"axios": "^1.13.1",
"core-js": "^3.46.0",
"css-loader": "^3.6",
"eslint": "^6.8",
"eslint-plugin-prettier": "^3.4",
"eslint-plugin-vue": "^6.2",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.10.2",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-vue": "^7.20.0",
"genversion": "^3.2.0",
"less": "^3.13",
"less-loader": "^5.0",
"mdns-js": "^1.0",
"prettier": "^1.19",
"uikit": "^3.17",
"less": "^4.4.2",
"less-loader": "^11.1.4",
"material-symbols": "^0.39.1",
"prettier": "^2.8.8",
"uikit": "^3.24.2",
"vue": "^2.7",
"vue-template-compiler": "^2.7",
"vuejs-paginate": "^2.1",

View file

@ -98,7 +98,7 @@ export default {
this.systemDark = true;
}
// Create a theme observer to watch for changes
this.themeObserver = mql.addListener(e => {
this.themeObserver = mql.addListener((e) => {
if (e.matches) {
this.systemDark = true;
} else {
@ -131,7 +131,7 @@ export default {
// Arrow keys
Mousetrap.bind(
["up", "down", "left", "right"],
event => {
(event) => {
this.arrowKeysDown[event.keyCode] = true; //Add key to array
this.navigateKeyHandler();
},
@ -139,7 +139,7 @@ export default {
);
Mousetrap.bind(
["up", "down", "left", "right"],
event => {
(event) => {
delete this.arrowKeysDown[event.keyCode]; //Remove key from array
},
"keyup",

View file

@ -22,7 +22,7 @@
>
<img
v-if="item.iconURL"
style="filter: grayscale(100%);width: 22px;margin-top: 5px;margin-bottom: 8px;"
style="filter: grayscale(100%); width: 22px; margin-top: 5px; margin-bottom: 8px"
:src="item.iconURL"
/>
<span v-if="!item.iconURL" class="material-symbols-outlined">
@ -182,7 +182,7 @@ export default {
},
];
if (!this.$store.state.galleryEnabled) {
tabs = tabs.filter(tab => tab.id != "gallery");
tabs = tabs.filter((tab) => tab.id != "gallery");
}
return tabs;
},
@ -196,7 +196,7 @@ export default {
mounted() {
// A global signal listener to switch tab
this.$root.$on("globalSwitchTab", tabID => {
this.$root.$on("globalSwitchTab", (tabID) => {
this.currentTab = tabID;
});
// A global signal listener to increment tab

View file

@ -245,13 +245,13 @@ export default {
}
// Check for a task that is ongoing.
// We can't handle multiple tasks ongoing, so this picks the first.
const ongoingTask = response.data.find(t => ["pending", "running"].includes(t.status));
const ongoingTask = response.data.find((t) => ["pending", "running"].includes(t.status));
if (ongoingTask) {
// There is a started task
this.taskStarted = true;
this.$emit("taskStarted");
// Find its URL
const taskUrl = ongoingTask.links.find(t => t.rel == "self").href;
const taskUrl = ongoingTask.links.find((t) => t.rel == "self").href;
try {
await this.pollOngoingTask(ongoingTask.id, taskUrl);
} catch (error) {
@ -332,7 +332,7 @@ export default {
var checkCondition = (resolve, reject) => {
// If the condition is met, we're done!
axios.get(this.taskUrl, { baseURL: this.$store.getters.baseUri }).then(response => {
axios.get(this.taskUrl, { baseURL: this.$store.getters.baseUri }).then((response) => {
var result = response.data.status;
this.taskStatus = result;
if ((result == "running") | (result == "pending")) {

View file

@ -17,9 +17,7 @@
</div>
<!-- Paused banner outside scroll container, positioned relative to wrapper -->
<div v-if="userIsHovering" class="paused-banner">
Auto-scroll paused
</div>
<div v-if="userIsHovering" class="paused-banner">Auto-scroll paused</div>
</div>
</div>
</template>

View file

@ -162,7 +162,7 @@ export default {
return "number_array";
}
if (Array.isArray(prop.items)) {
if (prop.items.every(t => num_types.includes(t.type))) {
if (prop.items.every((t) => num_types.includes(t.type))) {
return "number_array";
}
}

View file

@ -90,7 +90,7 @@ export default {
this.value = requestedValue;
await this.writeThingProperty(this.thingName, this.propertyName, requestedValue);
if (this.readBack) {
await new Promise(r => setTimeout(r, this.readBackDelay));
await new Promise((r) => setTimeout(r, this.readBackDelay));
let newVal = await this.readProperty();
if (newVal == requestedValue) {
this.animate = true;

View file

@ -1,9 +1,7 @@
<template>
<div>
<p>
<b>
Before starting camera calibration:
</b>
<b> Before starting camera calibration: </b>
</p>
<ul class="uk-list uk-list-bullet">
<li>Remove any samples from your microscope</li>

View file

@ -1,12 +1,8 @@
<template>
<div>
<p>Camera-stage mapping will calibrate the stage movement using the camera.</p>
<p>
Camera-stage mapping will calibrate the stage movement using the camera.
</p>
<p>
<b>
Before starting camera-stage mapping:
</b>
<b> Before starting camera-stage mapping: </b>
</p>
<ul class="uk-list uk-list-bullet">
<li>Add a sample with dense features to the microscope.</li>

View file

@ -1,8 +1,6 @@
<template>
<stepTemplateWithStream>
<p>
Use the buttons below to bring the sample into focus.
</p>
<p>Use the buttons below to bring the sample into focus.</p>
<p>You may also adjust the z position with <b>page up</b> and <b>page down</b>.</p>
<template #below-stream>
<div class="action-button-container">

View file

@ -3,12 +3,8 @@
<p>
<b>Calibration complete</b>
</p>
<p>
You'll need to repeat these steps from the Settings tab if you swap your objective.
</p>
<p>
Click Finish to return to your microscope.
</p>
<p>You'll need to repeat these steps from the Settings tab if you swap your objective.</p>
<p>Click Finish to return to your microscope.</p>
</div>
</template>

View file

@ -1,9 +1,7 @@
<template>
<div>
<p>
<b>
Some important microscope calibration data is currently missing.
</b>
<b> Some important microscope calibration data is currently missing. </b>
</p>
<p>
Your microscope will still function, however some functionality will be limited, and image

View file

@ -7,9 +7,7 @@
<input v-model="reloadWhenOverridingOrigin" class="uk-input uk-checkbox" type="checkbox" />
Reload web app with new origin
</label>
<button class="uk-button uk-button-default uk-margin-small">
Apply
</button>
<button class="uk-button uk-button-default uk-margin-small">Apply</button>
</form>
</div>
</template>

View file

@ -50,9 +50,7 @@
<hr />
</div>
<div v-else-if="$store.state.waiting">
Loading...
</div>
<div v-else-if="$store.state.waiting">Loading...</div>
<div v-else-if="$store.state.error"><b>Error:</b> {{ $store.state.error }}</div>
<div v-else>No active connection</div>
</div>

View file

@ -139,9 +139,7 @@
</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>
<p>Please check all data and power connections to your motor controller board.</p>
</div>
</div>
</template>
@ -235,7 +233,7 @@ export default {
methods: {
timeout(ms) {
return new Promise(resolve => setTimeout(resolve, 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
@ -281,7 +279,7 @@ export default {
.then(() => {
this.updatePosition(); // Update the position in text boxes
})
.catch(error => {
.catch((error) => {
this.modalError(error); // Let mixin handle error
})
.then(() => {

View file

@ -1,6 +1,6 @@
<template>
<div v-observe-visibility="visibilityChanged">
<div id="openseadragon"></div>
<div id="openseadragon" ref="osdContainer"></div>
</div>
</template>
@ -76,8 +76,9 @@ export default {
if (this.osdViewer) {
this.osdViewer.destroy();
}
await this.$nextTick();
this.osdViewer = OpenSeaDragon({
id: "openseadragon",
element: this.$refs.osdContainer,
crossOriginPolicy: "Anonymous",
tileSources: this.src,
showNavigationControl: false,

View file

@ -35,9 +35,7 @@
button-label="Download JPEG"
/>
</div>
<button class="uk-button uk-button-default uk-width-1-1" @click="deleteScan">
Delete
</button>
<button class="uk-button uk-button-default uk-width-1-1" @click="deleteScan">Delete</button>
<action-button
v-if="scanData.can_stitch | (scanData.stitch_available & !scanData.dzi)"
submit-label="Stitch Images"
@ -121,14 +119,8 @@ export default {
let yyyy = d.getFullYear();
let mm = d.getMonth() + 1;
let dd = d.getDate();
let HH = d
.getHours()
.toString()
.padStart(2, "0");
let MM = d
.getMinutes()
.toString()
.padStart(2, "0");
let HH = d.getHours().toString().padStart(2, "0");
let MM = d.getMinutes().toString().padStart(2, "0");
return `${HH}:${MM} ${dd}/${mm}/${yyyy}`;
},
formatDuration(duration) {

View file

@ -18,9 +18,7 @@
:button-primary="false"
:modal-progress="true"
:requires-confirmation="true"
:confirmation-message="
'<h3>Stitch all unstitched scans?</h3><br>Depending on the number and size of scans, this may be slow, and your microscope should not be used during the stitching.'
"
:confirmation-message="'<h3>Stitch all unstitched scans?</h3><br>Depending on the number and size of scans, this may be slow, and your microscope should not be used during the stitching.'"
@error="modalError"
/>
</div>
@ -62,9 +60,7 @@
<div class="gallery-grid uk-grid-match" uk-grid>
<div v-if="scansEmpty">
<h2>No scans available</h2>
<p>
There are no scans available to show.
</p>
<p>There are no scans available to show.</p>
</div>
<div v-for="scanData in paginatedScans" :key="scanData.id">
<scan-card
@ -168,7 +164,7 @@ export default {
(state, getters) => {
return getters.ready;
},
ready => {
(ready) => {
if (ready) {
// If the connection is now ready, update capture list
this.updateScans();
@ -205,7 +201,7 @@ export default {
if (!scans | (scans.length == 0)) {
this.scans = scans;
}
scans.forEach(scan => {
scans.forEach((scan) => {
scan.can_stitch = !scan.stitch_available && scan.number_of_images > 3;
});
scans.sort((a, b) => {

View file

@ -6,9 +6,7 @@
:button-primary="true"
:can-terminate="true"
:requires-confirmation="true"
:confirmation-message="
'Start recalibration of the stage to the camera? This may take a while, and the microscope will be locked during this time.'
"
:confirmation-message="'Start recalibration of the stage to the camera? This may take a while, and the microscope will be locked during this time.'"
thing="camera_stage_mapping"
action="calibrate_xy"
:submit-label="'Auto-Calibrate Using Camera'"
@ -26,9 +24,9 @@
>
Download Calibration Data
</button>
<div v-if="csmMatrix != 'undefined'" style="margin:10px;">
<details
><summary>Calibration Details</summary>
<div v-if="csmMatrix != 'undefined'" style="margin: 10px">
<details>
<summary>Calibration Details</summary>
<strong>CSM calculated for images with a resolution of {{ csmResolution }}</strong>
<ul>
<li>

View file

@ -4,16 +4,12 @@
<div>
<div class="uk-margin">
<p>Your z motor is currently {{ z_inverted }} inverted.</p>
<p>
We expect that moving in +z:
</p>
<p>We expect that moving in +z:</p>
<ul>
<li>Moves your objective up, towards the sample and illumination</li>
<li>Turns the exposed z gear anti-clockwise (when viewed from above)</li>
</ul>
<p>
If this is not the case, click the button below to switch.
</p>
<p>If this is not the case, click the button below to switch.</p>
<div class="uk-margin">
<div class="uk-margin">
<action-button

View file

@ -6,9 +6,7 @@
><input v-model="disableStream" class="uk-checkbox" type="checkbox" /> Disable Web
Stream</label
>
<p class="uk-margin-small">
This will disable the embedded web stream of the camera.
</p>
<p class="uk-margin-small">This will disable the embedded web stream of the camera.</p>
</div>
</div>
</template>

View file

@ -1,7 +1,5 @@
<template>
<div v-if="!backendOK" class="uk-alert-danger">
No scan back-end found.
</div>
<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 class="control-component uk-padding-small">
@ -101,9 +99,7 @@
</div>
</div>
<div v-show="scanning">
<h2 v-if="displayImageOnRight" style="text-align: center;">
Live stitching preview
</h2>
<h2 v-if="displayImageOnRight" style="text-align: center">Live stitching preview</h2>
<mini-stream-display v-if="displayImageOnRight" />
<action-log-display id="log-display" :log="log" :task-status="taskStatus" />
<action-progress-bar :progress="progress" :task-status="taskStatus" />

View file

@ -198,5 +198,5 @@ Vue.mixin({
new Vue({
store,
render: h => h(App),
render: (h) => h(App),
}).$mount("#app");

View file

@ -146,14 +146,14 @@ export default new Vuex.Store({
actions: {},
getters: {
baseUri: state => state.origin,
ready: state => state.available,
baseUri: (state) => state.origin,
ready: (state) => state.available,
},
plugins: [
store => {
(store) => {
// Load initial state from localStorage
LOCALSTORAGE_KEYS.forEach(key => {
LOCALSTORAGE_KEYS.forEach((key) => {
console.log(key);
const saved = localStorage.getItem(key);
if (saved !== null) {

View file

@ -28,12 +28,7 @@ export const wotStoreModule = {
// Deduplication should be done elsewhere.
let response = await axios.get(uri);
let td = response.data;
let thing_name =
name |
uri
.replace(/\/$/, "")
.split("/")
.pop();
let thing_name = name | uri.replace(/\/$/, "").split("/").pop();
commit("addThingDescription", {
thingName: thing_name,
thingDescription: td,
@ -53,16 +48,18 @@ export const wotStoreModule = {
},
},
getters: {
thingDescriptions: state => {
thingDescriptions: (state) => {
return state.thingDescriptions;
},
thingDescription: state => thingName => {
thingDescription: (state) => (thingName) => {
return state.thingDescriptions[thingName];
},
thingAvailable: state => thingName => {
thingAvailable: (state) => (thingName) => {
return thingName in state.thingDescriptions;
},
thingFormUrl: state => (thing, affordanceType, affordance, op, allowUndefined = true) => {
thingFormUrl:
(state) =>
(thing, affordanceType, affordance, op, allowUndefined = true) => {
// Find the URL for a particular operation
let td = state.thingDescriptions[thing];
if (!td) {
@ -100,7 +97,7 @@ export function findFormHref(affordance, op) {
// Find the form in the affordance that matches the given operation type
if (affordance == undefined) return undefined;
let forms = affordance.forms;
let matchingForm = forms.find(f => f.op == op || f.op.includes(op));
let matchingForm = forms.find((f) => f.op == op || f.op.includes(op));
if (matchingForm == undefined) return undefined;
return matchingForm.href;
}