Merge branch 'format-js' into 'v3'

Enable JS formatting checks in CI, reformat to match

See merge request openflexure/openflexure-microscope-server!422
This commit is contained in:
Julian Stirling 2025-10-28 16:27:36 +00:00
commit a89306b3b8
70 changed files with 727 additions and 1544 deletions

View file

@ -2,17 +2,17 @@ module.exports = {
root: true, root: true,
env: { env: {
node: true node: true,
}, },
parserOptions: { parserOptions: {
parser: "babel-eslint" parser: "babel-eslint",
}, },
rules: { rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off", "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
}, },
extends: ["plugin:vue/recommended", "eslint:recommended", "@vue/prettier"] extends: ["plugin:vue/recommended", "eslint:recommended", "@vue/prettier"],
}; };

7
webapp/.prettierrc.json Normal file
View file

@ -0,0 +1,7 @@
{
"trailingComma": "all",
"semi": true,
"singleQuote": false,
"printWidth": 100,
"tabWidth": 2
}

View file

@ -1,3 +1,3 @@
module.exports = { module.exports = {
presets: ["@vue/app"] presets: ["@vue/app"],
}; };

View file

@ -8,7 +8,7 @@
"gv": "genversion src/version.js", "gv": "genversion src/version.js",
"serve": "npm run gv && vue-cli-service serve --mode development", "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 --openssl-legacy-provider --mode production",
"lint": "vue-cli-service lint", "lint": "vue-cli-service lint --no-fix --max-warnings 0",
"lint:fix": "vue-cli-service lint --fix" "lint:fix": "vue-cli-service lint --fix"
}, },
"dependencies": { "dependencies": {

View file

@ -1,18 +1,9 @@
<template> <template>
<div <div id="app" class="uk-height-1-1 uk-margin-remove uk-padding-remove" :class="handleTheme">
id="app"
class="uk-height-1-1 uk-margin-remove uk-padding-remove"
:class="handleTheme"
>
<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 -->
<div <div id="modal-center" ref="keyboardManualModal" class="uk-flex-top" uk-modal>
id="modal-center"
ref="keyboardManualModal"
class="uk-flex-top"
uk-modal
>
<div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical"> <div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical">
<button class="uk-modal-close-default" type="button" uk-close></button> <button class="uk-modal-close-default" type="button" uk-close></button>
<div <div
@ -62,7 +53,7 @@ export default {
components: { components: {
appContent, appContent,
loadingContent loadingContent,
}, },
data: function() { data: function() {
@ -71,16 +62,13 @@ export default {
arrowKeysDown: {}, arrowKeysDown: {},
keyboardManual: [], keyboardManual: [],
systemDark: undefined, systemDark: undefined,
themeObserver: undefined themeObserver: undefined,
}; };
}, },
computed: { computed: {
isSystemDark: function() { isSystemDark: function() {
if ( if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
return true; return true;
} else { } else {
return false; return false;
@ -97,9 +85,9 @@ export default {
} }
return { return {
"uk-light": isDark, "uk-light": isDark,
"uk-background-secondary": isDark "uk-background-secondary": isDark,
}; };
} },
}, },
mounted() { mounted() {
@ -132,7 +120,7 @@ export default {
}, },
() => { () => {
this.checkConnection(); this.checkConnection();
} },
); );
// Keyboard shortcuts // Keyboard shortcuts
@ -147,18 +135,18 @@ export default {
this.arrowKeysDown[event.keyCode] = true; //Add key to array this.arrowKeysDown[event.keyCode] = true; //Add key to array
this.navigateKeyHandler(); this.navigateKeyHandler();
}, },
"keydown" "keydown",
); );
Mousetrap.bind( Mousetrap.bind(
["up", "down", "left", "right"], ["up", "down", "left", "right"],
event => { event => {
delete this.arrowKeysDown[event.keyCode]; //Remove key from array delete this.arrowKeysDown[event.keyCode]; //Remove key from array
}, },
"keyup" "keyup",
); );
this.keyboardManual.push({ this.keyboardManual.push({
shortcut: "←↑→↓", shortcut: "←↑→↓",
description: "Move the microscope stage" description: "Move the microscope stage",
}); });
// Focus keys // Focus keys
@ -170,7 +158,7 @@ export default {
}); });
this.keyboardManual.push({ this.keyboardManual.push({
shortcut: "pgup / pgdn", shortcut: "pgup / pgdn",
description: "Move the microscope focus" description: "Move the microscope focus",
}); });
// Capture // Capture
@ -179,7 +167,7 @@ export default {
}); });
this.keyboardManual.push({ this.keyboardManual.push({
shortcut: "c", shortcut: "c",
description: "Take a capture" description: "Take a capture",
}); });
// Autofocus // Autofocus
@ -188,7 +176,7 @@ export default {
}); });
this.keyboardManual.push({ this.keyboardManual.push({
shortcut: "a", shortcut: "a",
description: "Fast autofocus" description: "Fast autofocus",
}); });
// Increment/decrement tab // Increment/decrement tab
@ -200,7 +188,7 @@ export default {
}); });
this.keyboardManual.push({ this.keyboardManual.push({
shortcut: "shift+↑ / shift+↓", shortcut: "shift+↑ / shift+↓",
description: "Switch tab" description: "Switch tab",
}); });
}, },
@ -224,15 +212,10 @@ export default {
// TODO: more robust check - e.g. use a microscope Thing // TODO: more robust check - e.g. use a microscope Thing
// TODO: should we purge existing consumedThings? // TODO: should we purge existing consumedThings?
try { try {
await this.$store.dispatch( await this.$store.dispatch("wot/fetchThingDescriptions", `${baseUri}/thing_descriptions/`);
"wot/fetchThingDescriptions",
`${baseUri}/thing_descriptions/`
);
for (let requiredThing of ["camera", "stage"]) { for (let requiredThing of ["camera", "stage"]) {
if (!this.$store.getters["wot/thingAvailable"](requiredThing)) { if (!this.$store.getters["wot/thingAvailable"](requiredThing)) {
throw new Error( throw new Error(`No ${requiredThing} found, the GUI won't work without one.`);
`No ${requiredThing} found, the GUI won't work without one.`
);
} }
} }
try { try {
@ -291,8 +274,8 @@ export default {
// Make a position request // Make a position request
// Emit a signal to move, acted on by panelControl.vue // Emit a signal to move, acted on by panelControl.vue
this.$root.$emit("globalMoveStepEvent", x_rel, y_rel, z_rel); this.$root.$emit("globalMoveStepEvent", x_rel, y_rel, z_rel);
} },
} },
}; };
</script> </script>
@ -366,5 +349,4 @@ html {
object-fit: contain; object-fit: contain;
overflow-y: hidden; overflow-y: hidden;
} }
</style> </style>

View file

@ -1,14 +1,7 @@
<template> <template>
<div <div id="app-content" class="uk-margin-remove uk-padding-remove uk-height-1-1" uk-grid>
id="app-content"
class="uk-margin-remove uk-padding-remove uk-height-1-1"
uk-grid
>
<!-- Initialisation modals --> <!-- Initialisation modals -->
<calibrationWizard <calibrationWizard ref="calibrationWizard" @onClose="enterApp()"></calibrationWizard>
ref="calibrationWizard"
@onClose="enterApp()"
></calibrationWizard>
<!-- Vertical tab bar --> <!-- Vertical tab bar -->
<div id="switcher-left-container"> <div id="switcher-left-container">
<div <div
@ -63,10 +56,7 @@
</div> </div>
<!-- Corresponding vertical tab content --> <!-- Corresponding vertical tab content -->
<div <div id="container-left" class="uk-padding-remove uk-height-1-1 uk-width-expand">
id="container-left"
class="uk-padding-remove uk-height-1-1 uk-width-expand"
>
<!-- For each top tab --> <!-- For each top tab -->
<tabContent <tabContent
v-for="item in topTabs" v-for="item in topTabs"
@ -79,7 +69,6 @@
<component :is="item.component"></component> <component :is="item.component"></component>
</tabContent> </tabContent>
<!-- For each bottom tab --> <!-- For each bottom tab -->
<tabContent <tabContent
v-for="item in bottomTabs" v-for="item in bottomTabs"
@ -96,7 +85,6 @@
</template> </template>
<script> <script>
// Import generic components // Import generic components
import tabIcon from "./genericComponents/tabIcon"; import tabIcon from "./genericComponents/tabIcon";
import tabContent from "./genericComponents/tabContent"; import tabContent from "./genericComponents/tabContent";
@ -141,24 +129,24 @@ export default {
id: "settings", id: "settings",
icon: "settings", icon: "settings",
component: settingsContent, component: settingsContent,
class: "uk-margin-auto-top" class: "uk-margin-auto-top",
}, },
{ {
id: "logging", id: "logging",
icon: "assignment_late", icon: "assignment_late",
component: loggingContent component: loggingContent,
}, },
{ {
id: "about", id: "about",
icon: "info", icon: "info",
component: aboutContent component: aboutContent,
}, },
{ {
id: "power", id: "power",
icon: "power_settings_new", icon: "power_settings_new",
component: powerContent component: powerContent,
} },
] ],
}; };
}, },
@ -179,28 +167,28 @@ export default {
{ {
id: "view", id: "view",
icon: "visibility", icon: "visibility",
component: viewContent component: viewContent,
}, },
{ {
id: "control", id: "control",
icon: "gamepad", icon: "gamepad",
component: controlContent component: controlContent,
}, },
{ {
id: "background detect", id: "background detect",
icon: "background_replace", icon: "background_replace",
component: backgroundDetectContent component: backgroundDetectContent,
}, },
{ {
id: "slide scan", id: "slide scan",
icon: "settings_overscan", icon: "settings_overscan",
component: slideScanContent component: slideScanContent,
}, },
{ {
id: "scan list", id: "scan list",
icon: "photo_library", icon: "photo_library",
component: ScanListContent component: ScanListContent,
} },
]; ];
if (!this.$store.state.galleryEnabled) { if (!this.$store.state.galleryEnabled) {
tabs = tabs.filter(tab => tab.id != "gallery"); tabs = tabs.filter(tab => tab.id != "gallery");
@ -210,7 +198,7 @@ export default {
currentTabIndex: function() { currentTabIndex: function() {
return this.tabOrder.indexOf(this.currentTab); return this.tabOrder.indexOf(this.currentTab);
} },
}, },
mounted() { mounted() {
@ -239,8 +227,7 @@ export default {
}, },
incrementTabBy: function(n) { incrementTabBy: function(n) {
const newIndex = const newIndex =
(((this.currentTabIndex + n) % this.tabOrder.length) + (((this.currentTabIndex + n) % this.tabOrder.length) + this.tabOrder.length) %
this.tabOrder.length) %
this.tabOrder.length; this.tabOrder.length;
const newId = this.tabOrder[newIndex]; const newId = this.tabOrder[newIndex];
this.currentTab = newId; this.currentTab = newId;
@ -250,8 +237,8 @@ export default {
}, },
enterApp: function() { enterApp: function() {
// Stuff to do once connected and all init modals are finished // Stuff to do once connected and all init modals are finished
} },
} },
}; };
</script> </script>

View file

@ -1,70 +0,0 @@
<template>
<div>
<label>{{ label }}</label>
<div class="uk-form-controls">
<div v-for="option in options" :key="option">
<label>
<input
class="uk-checkbox"
type="checkbox"
:value="option"
:checked="value && value.includes(option)"
v-bind="$attrs"
@change="updateValue($event.target)"
/>
{{ option }}
</label>
</div>
</div>
</div>
</template>
<script>
export default {
name: "CheckList",
props: {
value: {
type: Array,
required: false,
default: function() {
return [];
}
},
options: {
type: Array,
required: true
},
name: {
type: String,
required: true
},
label: {
type: String,
required: true
}
},
methods: {
updateValue(target) {
var newSelected = this.value != null ? [...this.value] : []; // Clone value array
if (target.checked) {
if (!newSelected.includes(target.value)) {
newSelected.push(target.value);
}
} else {
if (newSelected.includes(target.value)) {
newSelected = newSelected.filter(function(value) {
return value != target.value;
});
}
}
this.$emit("input", newSelected);
}
}
};
</script>
<style scoped></style>

View file

@ -1,30 +0,0 @@
<template>
<div>
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="content"></p>
</div>
</template>
<script>
export default {
name: "HtmlBlock",
props: {
label: {
type: String,
required: false,
default: ""
},
name: {
type: String,
required: true
},
content: {
type: String,
required: true
}
}
};
</script>
<style scoped></style>

View file

@ -1,121 +0,0 @@
<template>
<div>
<form @submit.prevent="handleMetadataSubmit">
<div class="uk-margin-remove uk-flex uk-flex-middle">
<div
class="uk-margin-remove-top uk-padding-remove uk-grid-small uk-width-expand"
uk-grid
>
<div class="uk-margin-remove uk-width-1-2">
<input
ref="textboxKey"
v-model="newMetadata.key"
class="uk-input uk-form-width-small uk-form-small"
type="text"
name="flavor"
placeholder="Key"
/>
</div>
<div class="uk-margin-remove uk-width-1-2">
<input
v-model="newMetadata.value"
class="uk-input uk-form-width-small uk-form-small"
type="text"
name="flavor"
placeholder="Value"
@keyup.enter="handleMetadataSubmit()"
/>
</div>
</div>
<a
href="#"
class="uk-icon uk-margin-left"
@click="handleMetadataSubmit()"
><span class="material-symbols-outlined">add_circle</span></a
>
</div>
</form>
<div
v-for="(val, key) in value"
:key="key"
class="uk-width-1-1 uk-margin-small uk-margin-remove-left uk-margin-remove-right uk-flex uk-flex-middle"
>
<div class="uk-margin-remove-top uk-padding-remove uk-width-expand">
<labelInput
:name="key"
:label="key"
:value="value[key]"
@input="value[key] = $event"
/>
</div>
<a href="#" class="uk-icon uk-width-auto" @click="delMetadataKey(key)"
><span class="material-symbols-outlined">delete</span></a
>
</div>
</div>
</template>
<script>
import labelInput from "../fieldComponents/labelInput";
export default {
name: "KeyvalList",
components: {
labelInput
},
props: {
value: {
type: Object,
required: true
}
},
data: function() {
return {
newMetadata: {
key: "",
value: ""
}
};
},
methods: {
handleMetadataSubmit: function() {
var newSelected = {};
if (this.value != null) {
Object.assign(newSelected, this.value);
}
newSelected[this.newMetadata.key] = this.newMetadata.value;
this.newMetadata.key = "";
this.newMetadata.value = "";
this.$emit("input", newSelected);
// Move focus back to key textbox
if (this.$refs.textboxKey) {
this.$refs.textboxKey.focus();
}
},
delMetadataKey: function(key) {
var newSelected = {};
if (this.value != null) {
Object.assign(newSelected, this.value);
}
this.$delete(newSelected, key);
this.$emit("input", newSelected);
}
}
};
</script>
<style scoped></style>

View file

@ -1,65 +0,0 @@
<template>
<div>
<label class="uk-form-label uk-text-bold">{{ label }}</label>
<div
uk-tooltip="title: Click to edit value; delay: 250"
@click="setEditing(true)"
>
<div v-show="editing == false">
<label> {{ value }} </label>
</div>
<input
v-show="editing == true"
ref="textinput"
class="uk-input uk-form-small"
type="text"
:name="name"
:value="value"
autofocus
@blur="setEditing(false)"
@keyup.enter="setEditing(false)"
@input="$emit('input', $event.target.value)"
/>
</div>
</div>
</template>
<script>
export default {
name: "LabelInput",
props: {
label: {
type: String,
required: true
},
name: {
type: String,
required: true
},
value: {
type: String,
required: true
}
},
data: function() {
return {
editing: false
};
},
methods: {
setEditing(editing) {
this.editing = editing;
if (editing == true) {
this.$nextTick(() => this.$refs.textinput.focus());
}
}
}
};
</script>
<style scoped></style>

View file

@ -1,44 +0,0 @@
<template>
<div>
<label class="uk-form-label">{{ label }}</label>
<input
class="uk-input uk-form-small"
type="number"
:name="name"
:value="value"
:placeholder="placeholder"
v-bind="$attrs"
@input="$emit('input', Number($event.target.value))"
/>
</div>
</template>
<script>
export default {
name: "NumberInput",
props: {
value: {
type: Number,
required: false,
default: 0
},
placeholder: {
type: Number,
required: false,
default: 0
},
name: {
type: String,
required: true
},
label: {
type: String,
required: true
}
}
};
</script>
<style scoped></style>

View file

@ -1,49 +0,0 @@
<template>
<div>
<label>{{ label }}</label>
<div class="uk-form-controls">
<div v-for="option in options" :key="option">
<label
><input
class="uk-radio"
type="radio"
:name="name"
:value="option"
:checked="value == option"
@input="$emit('input', $event.target.value)"
/>
{{ option }}</label
>
</div>
</div>
</div>
</template>
<script>
export default {
name: "RadioList",
props: {
value: {
type: String,
required: false,
default: ""
},
options: {
type: Array,
required: true
},
name: {
type: String,
required: true
},
label: {
type: String,
required: true
}
}
};
</script>
<style scoped></style>

View file

@ -1,44 +0,0 @@
<template>
<div>
<label class="uk-form-label">{{ label }}</label>
<select
class="uk-select uk-form-small"
:value="value"
v-bind="$attrs"
@input="$emit('input', $event.target.value)"
>
<option v-for="option in options" :key="option">
{{ option }}
</option>
</select>
</div>
</template>
<script>
export default {
name: "SelectList",
props: {
value: {
type: String,
required: false,
default: ""
},
options: {
type: Array,
required: true
},
name: {
type: String,
required: true
},
label: {
type: String,
required: true
}
}
};
</script>
<style scoped></style>

View file

@ -1,79 +0,0 @@
<template>
<div>
<form @submit.prevent="handleTagSubmit">
<div class="uk-margin-small uk-flex uk-flex-middle">
<div class="uk-margin-remove-top uk-width-expand">
<div class="uk-inline uk-width-1-1">
<span class="uk-form-icon"
><span class="material-symbols-outlined">label</span></span
>
<input
v-model="newTag"
class="uk-input uk-form-small"
type="text"
name="flavor"
placeholder="Tag"
/>
</div>
</div>
<a href="#" class="uk-icon uk-margin-left" @click="handleTagSubmit()"
><span class="material-symbols-outlined">add_circle</span></a
>
</div>
</form>
<span
v-for="tag in value"
:key="tag"
class="uk-label uk-margin-small-right deletable-label"
@click="delTag(tag)"
>
{{ tag }}
</span>
</div>
</template>
<script>
export default {
name: "TagList",
props: {
value: {
type: Array,
required: true
}
},
data: function() {
return {
newTag: ""
};
},
methods: {
handleTagSubmit: function() {
var newSelected = this.value != null ? [...this.value] : []; // Clone value array
newSelected.push(this.newTag);
this.newTag = "";
this.$emit("input", newSelected);
},
delTag: function(tag) {
var newSelected = this.value != null ? [...this.value] : []; // Clone value array
if (newSelected.includes(tag)) {
newSelected = newSelected.filter(function(value) {
return value != tag;
});
}
this.$emit("input", newSelected);
}
}
};
</script>
<style scoped></style>

View file

@ -1,45 +0,0 @@
<template>
<div>
<label v-if="label" class="uk-form-label">{{ label }}</label>
<input
class="uk-input uk-form-small"
type="text"
:name="name"
:value="value"
:placeholder="placeholder"
v-bind="$attrs"
@input="$emit('input', $event.target.value)"
/>
</div>
</template>
<script>
export default {
name: "TextInput",
props: {
placeholder: {
type: String,
required: false,
default: null
},
label: {
type: String,
required: false,
default: null
},
name: {
type: String,
required: true
},
value: {
type: String,
required: false,
default: ""
}
}
};
</script>
<style scoped></style>

View file

@ -22,20 +22,20 @@ export default {
data: function() { data: function() {
return { return {
isVisible: false isVisible: false,
}; };
}, },
computed: { computed: {
streamImgUri: function() { streamImgUri: function() {
return `${this.$store.getters.baseUri}/camera/mjpeg_stream`; return `${this.$store.getters.baseUri}/camera/mjpeg_stream`;
} },
}, },
methods: { methods: {
visibilityChanged(isVisible) { visibilityChanged(isVisible) {
this.isVisible = isVisible; this.isVisible = isVisible;
} },
} },
}; };
</script> </script>

View file

@ -1,7 +1,5 @@
<template> <template>
<div <div class="progress uk-margin-top uk-margin-horizontal-remove uk-padding-remove">
class="progress uk-margin-top uk-margin-horizontal-remove uk-padding-remove"
>
<div class="indeterminate"></div> <div class="indeterminate"></div>
</div> </div>
</template> </template>
@ -21,16 +19,16 @@ export default {
classObject: function() { classObject: function() {
return { return {
"tabicon-active": this.currentTab == this.id, "tabicon-active": this.currentTab == this.id,
"uk-disabled": this.requireConnection && !this.$store.getters.ready "uk-disabled": this.requireConnection && !this.$store.getters.ready,
}; };
} },
}, },
methods: { methods: {
setThisTab(event) { setThisTab(event) {
this.$emit("set-tab", event, this.id); this.$emit("set-tab", event, this.id);
} },
} },
}; };
</script> </script>
@ -77,8 +75,7 @@ export default {
left: 0; left: 0;
bottom: 0; bottom: 0;
will-change: left, right; will-change: left, right;
-webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) -webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
infinite;
animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
} }
@ -90,10 +87,8 @@ export default {
left: 0; left: 0;
bottom: 0; bottom: 0;
will-change: left, right; will-change: left, right;
-webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) -webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
infinite; animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)
infinite;
-webkit-animation-delay: 1.15s; -webkit-animation-delay: 1.15s;
animation-delay: 1.15s; animation-delay: 1.15s;
} }

View file

@ -15,17 +15,17 @@ export default {
props: { props: {
tabID: { tabID: {
type: String, type: String,
required: true required: true,
}, },
currentTab: { currentTab: {
type: String, type: String,
required: true required: true,
}, },
requireConnection: Boolean requireConnection: Boolean,
}, },
computed: {}, computed: {},
methods: {} methods: {},
}; };
</script> </script>

View file

@ -1,11 +1,5 @@
<template> <template>
<a <a href="#" class="uk-link" :class="classObject" :uk-tooltip="tooltipOptions" @click="setThisTab">
href="#"
class="uk-link"
:class="classObject"
:uk-tooltip="tooltipOptions"
@click="setThisTab"
>
<slot></slot> <slot></slot>
<div v-if="showTitle" class="tabtitle"> <div v-if="showTitle" class="tabtitle">
{{ computedTitle }} {{ computedTitle }}
@ -20,33 +14,33 @@ export default {
props: { props: {
tabID: { tabID: {
type: String, type: String,
required: true required: true,
}, },
title: { title: {
type: String, type: String,
required: false, required: false,
default: undefined default: undefined,
}, },
showTitle: { showTitle: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: true,
}, },
showTooltip: { showTooltip: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: true,
}, },
currentTab: { currentTab: {
type: String, type: String,
required: true required: true,
}, },
clickCallback: { clickCallback: {
type: Function, type: Function,
required: false, required: false,
default: null default: null,
}, },
requireConnection: Boolean requireConnection: Boolean,
}, },
computed: { computed: {
@ -72,9 +66,9 @@ export default {
classObject: function() { classObject: function() {
return { return {
"tabicon-active": this.currentTab == this.tabID, "tabicon-active": this.currentTab == this.tabID,
"uk-disabled": this.requireConnection && !this.$store.getters.ready "uk-disabled": this.requireConnection && !this.$store.getters.ready,
}; };
} },
}, },
methods: { methods: {
@ -83,8 +77,8 @@ export default {
if (this.clickCallback) { if (this.clickCallback) {
this.clickCallback(); this.clickCallback();
} }
} },
} },
}; };
</script> </script>

View file

@ -1,10 +1,11 @@
<template> <template>
<div <div v-observe-visibility="visibilityChanged" class="uk-margin-remove uk-padding-remove">
v-observe-visibility="visibilityChanged"
class="uk-margin-remove uk-padding-remove"
>
<div v-if="taskStarted" ref="isPollingElement"> <div v-if="taskStarted" ref="isPollingElement">
<action-progress-bar v-if="taskStarted && hideOnRun" :progress="progress" :task-status="taskStatus" /> <action-progress-bar
v-if="taskStarted && hideOnRun"
:progress="progress"
:task-status="taskStatus"
/>
<!-- hideOnRun selects if the button hides, don't show progress bar if button doesn't hide. --> <!-- hideOnRun selects if the button hides, don't show progress bar if button doesn't hide. -->
<button <button
v-if="canTerminate && taskRunning" v-if="canTerminate && taskRunning"
@ -22,7 +23,10 @@
:disabled="isDisabled" :disabled="isDisabled"
:hidden="taskStarted && hideOnRun" :hidden="taskStarted && hideOnRun"
class="uk-button uk-width-1-1" class="uk-button uk-width-1-1"
:class="[isDisabled ? 'uk-button-disabled' : '', buttonPrimary ? 'uk-button-primary' : 'uk-button-default']" :class="[
isDisabled ? 'uk-button-disabled' : '',
buttonPrimary ? 'uk-button-primary' : 'uk-button-default',
]"
@click="bootstrapTask()" @click="bootstrapTask()"
> >
{{ submitLabel }} {{ submitLabel }}
@ -35,18 +39,12 @@
class="" class=""
uk-modal="bg-close: false; esc-close: false; stack: true;" uk-modal="bg-close: false; esc-close: false; stack: true;"
> >
<div <div id="status-modal" class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical">
id="status-modal"
class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical"
>
<h2>{{ submitLabel }}</h2> <h2>{{ submitLabel }}</h2>
<action-log-display :log="log" :task-status="taskStatus" /> <action-log-display :log="log" :task-status="taskStatus" />
<div id="progress-and-cancel-row"> <div id="progress-and-cancel-row">
<div class="stretchy"> <div class="stretchy">
<action-progress-bar <action-progress-bar :progress="progress" :task-status="taskStatus" />
:progress="progress"
:task-status="taskStatus"
/>
</div> </div>
<button <button
@ -84,67 +82,67 @@ export default {
props: { props: {
action: { action: {
type: String, type: String,
required: true required: true,
}, },
thing: { thing: {
type: String, type: String,
required: true required: true,
}, },
submitData: { submitData: {
type: [Object, Array], type: [Object, Array],
required: false, required: false,
default: () => ({}) default: () => ({}),
}, },
pollInterval: { pollInterval: {
type: Number, type: Number,
required: false, required: false,
default: 1 default: 1,
}, },
submitLabel: { submitLabel: {
type: String, type: String,
required: false, required: false,
default: "Submit" default: "Submit",
}, },
canTerminate: { canTerminate: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: true,
}, },
requiresConfirmation: { requiresConfirmation: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false,
}, },
confirmationMessage: { confirmationMessage: {
type: String, type: String,
required: false, required: false,
default: "Start task?" default: "Start task?",
}, },
buttonPrimary: { buttonPrimary: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: true,
}, },
submitOnEvent: { submitOnEvent: {
type: String, type: String,
required: false, required: false,
default: null default: null,
}, },
modalProgress: { modalProgress: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false,
}, },
isDisabled: { isDisabled: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false,
}, },
hideOnRun: { hideOnRun: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: true,
} },
}, },
data: function() { data: function() {
@ -154,14 +152,14 @@ export default {
taskStarted: false, taskStarted: false,
taskRunning: false, taskRunning: false,
log: [], log: [],
taskStatus: "" taskStatus: "",
}; };
}, },
computed: { computed: {
submitUrl() { submitUrl() {
return this.thingActionUrl(this.thing, this.action); return this.thingActionUrl(this.thing, this.action);
} },
}, },
watch: { watch: {
@ -179,7 +177,7 @@ export default {
}, },
taskStatus(newval) { taskStatus(newval) {
this.$emit("update:taskStatus", newval); this.$emit("update:taskStatus", newval);
} },
}, },
mounted() { mounted() {
@ -195,13 +193,14 @@ export default {
// Otherwise, this is just normal property access // Otherwise, this is just normal property access
return this[n]; return this[n];
} }
const TypedArray = Reflect.getPrototypeOf(Int8Array); const TypedArray = Reflect.getPrototypeOf(Int8Array);
for (const C of [Array, String, TypedArray]) { for (const C of [Array, String, TypedArray]) {
Object.defineProperty(C.prototype, "from_index", Object.defineProperty(C.prototype, "from_index", {
{ value: from_index, value: from_index,
writable: true, writable: true,
enumerable: false, enumerable: false,
configurable: true }); configurable: true,
});
} }
// Check for already running tasks // Check for already running tasks
if (this.taskStarted != true) { if (this.taskStarted != true) {
@ -234,10 +233,7 @@ export default {
if (task.status == "pending" || task.status == "running") { if (task.status == "pending" || task.status == "running") {
this.taskStarted = true; this.taskStarted = true;
this.$emit("taskStarted"); this.$emit("taskStarted");
this.startPolling( this.startPolling(task.id, task.links.find(t => t.rel == "self").href);
task.id,
task.links.find(t => t.rel == "self").href
);
} }
} }
}); });
@ -250,7 +246,7 @@ export default {
() => { () => {
this.startTask(); this.startTask();
}, },
() => {} () => {},
); );
} else { } else {
this.startTask(); this.startTask();
@ -271,10 +267,7 @@ export default {
UIkit.modal(this.$refs.statusModal).show(); UIkit.modal(this.$refs.statusModal).show();
} }
// Start the store polling TaskId for success // Start the store polling TaskId for success
response = await this.startPolling( response = await this.startPolling(response.data.id, response.data.href);
response.data.id,
response.data.href
);
if (response.status == "completed") { if (response.status == "completed") {
this.$emit("response", response); this.$emit("response", response);
this.$emit("completed", response.output); this.$emit("completed", response.output);
@ -312,43 +305,46 @@ export default {
var checkCondition = (resolve, reject) => { var checkCondition = (resolve, reject) => {
// If the condition is met, we're done! // If the condition is met, we're done!
axios axios.get(this.taskUrl, { baseURL: this.$store.getters.baseUri }).then(response => {
.get(this.taskUrl, { baseURL: this.$store.getters.baseUri }) var result = response.data.status;
.then(response => { this.taskStatus = result;
var result = response.data.status; if ((result == "running") | (result == "pending")) {
this.taskStatus = result; // If the task is still running, we update progress/log,
if ((result == "running") | (result == "pending")) { // and schedule another poll
// If the task is still running, we update progress/log, this.progress = response.data.progress;
// and schedule another poll this.log = response.data.log;
this.progress = response.data.progress; // Check again after timeout
this.log = response.data.log; setTimeout(checkCondition, interval, resolve, reject);
// Check again after timeout }
setTimeout(checkCondition, interval, resolve, reject); // If task ends with an error
else if (result == "error") {
// Pass the error string back with reject
if (!this.progress) this.progress = 1;
// Test whether the log is empty or the most recent message is not from an error
// If so, return a default message
if (
(response.data.log.length == 0) |
(response.data.log.from_index(-1).levelname != "ERROR")
) {
var message = "Unexpected error, please check the logs";
} }
// If task ends with an error // As LabThings Actions add the message from any raised exception to the log, the
else if (result == "error") { // last message in the log is the message from the Exception.
// Pass the error string back with reject //If the Exception was raised with no message, use a default.
if (!this.progress) this.progress = 1;
// Test whether the log is empty or the most recent message is not from an error
// If so, return a default message
if (response.data.log.length == 0 | response.data.log.from_index(-1).levelname != "ERROR") {
var message = "Unexpected error, please check the logs";
}
// As LabThings Actions add the message from any raised exception to the log, the
// last message in the log is the message from the Exception.
//If the Exception was raised with no message, use a default.
else {
message = response.data.log.from_index(-1).message||"Unexpected error, please check the logs";
}
// Raise an Error with the chosen message
reject(new Error(message));
}
// If task ends without reporting an error
// (NB this includes cancellation)
else { else {
resolve(response.data); message =
response.data.log.from_index(-1).message ||
"Unexpected error, please check the logs";
} }
}); // Raise an Error with the chosen message
reject(new Error(message));
}
// If task ends without reporting an error
// (NB this includes cancellation)
else {
resolve(response.data);
}
});
}; };
return new Promise(checkCondition); return new Promise(checkCondition);
@ -362,8 +358,8 @@ export default {
terminateTask: function() { terminateTask: function() {
axios.delete(this.taskUrl, { baseURL: this.$store.getters.baseUri }); axios.delete(this.taskUrl, { baseURL: this.$store.getters.baseUri });
this.$root.$emit("modalClosed"); this.$root.$emit("modalClosed");
} },
} },
}; };
</script> </script>

View file

@ -1,31 +1,24 @@
<template> <template>
<div <div class="log-wrapper" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave">
class="log-wrapper" <div ref="logContainer" class="log-container uk-margin-left uk-margin-right uk-margin">
@mouseenter="onMouseEnter" <div v-if="log">
@mouseleave="onMouseLeave" <div v-for="(item, index) in log" :key="`log_entry_${index}`">
> {{ item.message }}
<div </div>
ref="logContainer"
class="log-container uk-margin-left uk-margin-right uk-margin"
>
<div v-if="log">
<div v-for="(item, index) in log" :key="`log_entry_${index}`">
{{ item.message }}
</div> </div>
</div> <div v-if="taskStatus == 'error'" class="uk-alert uk-alert-danger">
<div v-if="taskStatus == 'error'" class="uk-alert uk-alert-danger"> The task failed due to an error. There may be more information in the log.
The task failed due to an error. There may be more information in the log.
</div>
<div v-if="taskStatus == 'cancelled'" class="uk-alert uk-alert-warning">
The task was cancelled.
</div>
<div v-if="taskStatus == 'completed'" class="uk-alert uk-alert-success">
The task completed successfully.
</div> </div>
<div v-if="taskStatus == 'cancelled'" class="uk-alert uk-alert-warning">
The task was cancelled.
</div>
<div v-if="taskStatus == 'completed'" class="uk-alert uk-alert-success">
The task completed successfully.
</div>
<!-- Paused banner outside scroll container, positioned relative to wrapper --> <!-- Paused banner outside scroll container, positioned relative to wrapper -->
<div v-if="userIsHovering" class="paused-banner"> <div v-if="userIsHovering" class="paused-banner">
Auto-scroll paused Auto-scroll paused
</div> </div>
</div> </div>
</div> </div>
@ -38,12 +31,12 @@ export default {
props: { props: {
taskStatus: { taskStatus: {
type: String, type: String,
required: true required: true,
}, },
log: { log: {
type: Array, type: Array,
required: true required: true,
} },
}, },
data() { data() {
@ -58,7 +51,7 @@ export default {
}, },
taskStatus: function() { taskStatus: function() {
this.scrollToBottom(); this.scrollToBottom();
} },
}, },
methods: { methods: {
@ -70,15 +63,15 @@ export default {
}, },
scrollToBottom() { scrollToBottom() {
/*Scroll to bottom of log unless the user is hovering over the log.*/ /*Scroll to bottom of log unless the user is hovering over the log.*/
this.$nextTick(() => { this.$nextTick(() => {
if (this.userIsHovering) return; if (this.userIsHovering) return;
const viewer = this.$refs.logContainer; const viewer = this.$refs.logContainer;
viewer.scrollTop = viewer.scrollHeight; viewer.scrollTop = viewer.scrollHeight;
}); });
} },
} },
}; };
</script> </script>

View file

@ -13,12 +13,12 @@ export default {
progress: { progress: {
type: Number, type: Number,
required: false, required: false,
default: null default: null,
}, },
taskStatus: { taskStatus: {
type: String, type: String,
required: true required: true,
} },
}, },
computed: { computed: {
@ -33,8 +33,8 @@ export default {
return true; return true;
} }
return false; return false;
} },
} },
}; };
</script> </script>
@ -81,8 +81,7 @@ export default {
left: 0; left: 0;
bottom: 0; bottom: 0;
will-change: left, right; will-change: left, right;
-webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) -webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
infinite;
animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite; animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
} }
@ -94,10 +93,8 @@ export default {
left: 0; left: 0;
bottom: 0; bottom: 0;
will-change: left, right; will-change: left, right;
-webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) -webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
infinite; animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)
infinite;
-webkit-animation-delay: 1.15s; -webkit-animation-delay: 1.15s;
animation-delay: 1.15s; animation-delay: 1.15s;
} }

View file

@ -1,15 +1,18 @@
<template> <template>
<a <a
class="uk-button" class="uk-button"
:class="[isDisabled ? 'uk-button-disabled' : '', buttonPrimary ? 'uk-button-primary' : 'uk-button-default']" :class="[
:href="URL" isDisabled ? 'uk-button-disabled' : '',
download buttonPrimary ? 'uk-button-primary' : 'uk-button-default',
> {{ buttonLabel }}</a ]"
> :href="url"
download
>
{{ buttonLabel }}</a
>
</template> </template>
<script> <script>
export default { export default {
name: "EndpointButton", name: "EndpointButton",
@ -17,27 +20,27 @@ export default {
buttonPrimary: { buttonPrimary: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: true,
}, },
URL: { url: {
type: String, type: String,
required: true, required: true,
}, },
isDisabled: { isDisabled: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false,
}, },
destinationName: { destinationName: {
type: String, type: String,
required: false, required: false,
default: null default: null,
}, },
buttonLabel: { buttonLabel: {
type: String, type: String,
required: false, required: false,
default: "Download File" default: "Download File",
} },
}, },
}; };
</script> </script>

View file

@ -51,8 +51,8 @@
<label v-if="dataType == 'number_object'" class="uk-form-label" <label v-if="dataType == 'number_object'" class="uk-form-label"
>{{ label }} >{{ label }}
<div v-for="(val, key) in value" :key="key"> <div v-for="(val, key) in value" :key="key">
<label>{{internalLabels[key]}}</label> <label>{{ internalLabels[key] }}</label>
<div class="input-and-buttons-container" > <div class="input-and-buttons-container">
<input <input
v-model="internalValue[key]" v-model="internalValue[key]"
class="uk-form-small numeric-setting-line-input" class="uk-form-small numeric-setting-line-input"
@ -89,24 +89,26 @@ export default {
name: "InputFromSchema", name: "InputFromSchema",
components: { components: {
syncPropertyButton syncPropertyButton,
}, },
props: { props: {
dataSchema: { dataSchema: {
type: Object type: Object,
required: true,
}, },
value: { value: {
type: null type: null,
required: true,
}, },
label: { label: {
type: String, type: String,
default: "" default: "",
}, },
animate: { animate: {
type: Boolean, type: Boolean,
default: false default: false,
} },
}, },
data() { data() {
@ -116,44 +118,21 @@ export default {
// (see resetInternalValue). If we do this here there is a chance we get errors // (see resetInternalValue). If we do this here there is a chance we get errors
// as internalValue is still null when rendering starts. // as internalValue is still null when rendering starts.
internalValue: Array.isArray(this.value) internalValue: Array.isArray(this.value)
? [...this.value] ? [...this.value]
: typeof this.value === 'object' : typeof this.value === "object"
? { ...this.value } ? { ...this.value }
: this.value, : this.value,
// Is edited can't be computed as we mutate internalValue // Is edited can't be computed as we mutate internalValue
isEdited: false, isEdited: false,
animateUpdate: false, animateUpdate: false,
}; };
}, },
mounted() {
if (this.value !== undefined) {
this.resetInternalValue();
}
},
watch: {
value() {
// Fire updateIsEdited on both value and internal value change,
// as change in value may not causse internalValue to change.
this.updateIsEdited();
this.resetInternalValue();
},
internalValue() {
this.updateIsEdited();
},
animate(updated) {
if (updated) {
this.animateUpdate = true;
}
}
},
computed: { computed: {
internalLabels: function() { internalLabels: function() {
if (this.dataType == "number_object") { if (this.dataType == "number_object") {
let labels = {}; let labels = {};
for (const key in this.internalValue){ for (const key in this.internalValue) {
labels[key] = this.dataSchema.properties[key].title labels[key] = this.dataSchema.properties[key].title;
} }
return labels; return labels;
} }
@ -204,6 +183,29 @@ export default {
} }
} }
return "other"; return "other";
},
},
watch: {
value() {
// Fire updateIsEdited on both value and internal value change,
// as change in value may not causse internalValue to change.
this.updateIsEdited();
this.resetInternalValue();
},
internalValue() {
this.updateIsEdited();
},
animate(updated) {
if (updated) {
this.animateUpdate = true;
}
},
},
mounted() {
if (this.value !== undefined) {
this.resetInternalValue();
} }
}, },
@ -215,10 +217,10 @@ export default {
this.internalValue = JSON.parse(JSON.stringify(this.value)); this.internalValue = JSON.parse(JSON.stringify(this.value));
}, },
requestUpdate: async function() { requestUpdate: async function() {
this.$emit("requestUpdate") this.$emit("requestUpdate");
}, },
sendValue: async function() { sendValue: async function() {
this.$emit("sendValue", this.internalValue) this.$emit("sendValue", this.internalValue);
}, },
checkboxUpdated: function() { checkboxUpdated: function() {
if (this.internalValue != this.$refs.checkbox.checked) { if (this.internalValue != this.$refs.checkbox.checked) {
@ -245,7 +247,7 @@ export default {
}, },
animationEnd: function() { animationEnd: function() {
this.animateUpdate = false; this.animateUpdate = false;
this.$emit('animationShown'); this.$emit("animationShown");
}, },
deepStringify: function(val) { deepStringify: function(val) {
// Create a json string where all internal numbers are also JSON strings. This is // Create a json string where all internal numbers are also JSON strings. This is
@ -256,16 +258,13 @@ export default {
if (Array.isArray(val)) { if (Array.isArray(val)) {
return JSON.stringify(val.map(String)); return JSON.stringify(val.map(String));
} }
if (val && typeof val === 'object') { if (val && typeof val === "object") {
const normalized = Object.fromEntries( const normalized = Object.fromEntries(Object.entries(val).map(([k, v]) => [k, String(v)]));
Object.entries(val).map(([k, v]) => [k, String(v)])
);
return JSON.stringify(normalized); return JSON.stringify(normalized);
} }
return JSON.stringify(String(val)); return JSON.stringify(String(val));
} },
},
}
}; };
</script> </script>
@ -288,8 +287,12 @@ export default {
background-color: #fff3cd; background-color: #fff3cd;
} }
@keyframes green-flash { @keyframes green-flash {
0% { background-color: #3fda63; } 0% {
100% { background-color: white; } background-color: #3fda63;
}
100% {
background-color: white;
}
} }
.flash { .flash {
animation: green-flash 0.7s ease; animation: green-flash 0.7s ease;

View file

@ -18,58 +18,56 @@ export default {
name: "PropertyControl", name: "PropertyControl",
components: { components: {
InputFromSchema InputFromSchema,
}, },
props: { props: {
label: { label: {
type: String, type: String,
default: "" default: "",
}, },
propertyName: { propertyName: {
type: String, type: String,
required: true required: true,
}, },
thingName: { thingName: {
type: String, type: String,
required: true required: true,
}, },
readBack: { readBack: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false,
}, },
readBackDelay: { readBackDelay: {
type: Number, type: Number,
default: 1000, default: 1000,
required: false required: false,
} },
}, },
data() { data() {
return { return {
value: undefined, value: undefined,
animate: false animate: false,
}; };
}, },
computed: { computed: {
propertyDescription: function() { propertyDescription: function() {
try { try {
return this.thingDescription(this.thingName).properties[ return this.thingDescription(this.thingName).properties[this.propertyName];
this.propertyName
];
} catch (error) { } catch (error) {
return undefined; return undefined;
} }
} },
}, },
watch: { watch: {
propertyDescription: function() { propertyDescription: function() {
// Ensure we read the property once the URL is known // Ensure we read the property once the URL is known
this.readProperty(); this.readProperty();
} },
}, },
mounted: function() { mounted: function() {
@ -83,21 +81,14 @@ export default {
methods: { methods: {
readProperty: async function() { readProperty: async function() {
let data = await this.readThingProperty( let data = await this.readThingProperty(this.thingName, this.propertyName);
this.thingName,
this.propertyName
);
this.value = data; this.value = data;
return data; return data;
}, },
writeProperty: async function(requestedValue) { writeProperty: async function(requestedValue) {
try { try {
this.value=requestedValue; this.value = requestedValue;
await this.writeThingProperty( await this.writeThingProperty(this.thingName, this.propertyName, requestedValue);
this.thingName,
this.propertyName,
requestedValue
);
if (this.readBack) { if (this.readBack) {
await new Promise(r => setTimeout(r, this.readBackDelay)); await new Promise(r => setTimeout(r, this.readBackDelay));
let newVal = await this.readProperty(); let newVal = await this.readProperty();
@ -106,7 +97,9 @@ export default {
} else { } else {
this.animate = true; this.animate = true;
await this.modalNotify( await this.modalNotify(
`Set ${this.label} to ${formatValue(newVal)} (closest valid value to requested ${formatValue(requestedValue)}).` `Set ${this.label} to ${formatValue(
newVal,
)} (closest valid value to requested ${formatValue(requestedValue)}).`,
); );
} }
} else { } else {
@ -120,9 +113,9 @@ export default {
} }
}, },
resetAnimate: function() { resetAnimate: function() {
this.animate = false; this.animate = false;
} },
} },
}; };
</script> </script>

View file

@ -22,15 +22,14 @@ export default {
name: "ServerSpecifiedActionButton", name: "ServerSpecifiedActionButton",
components: { components: {
ActionButton ActionButton,
}, },
props: { props: {
actionData: { actionData: {
type: Object, type: Object,
required: true, required: true,
} },
}, },
methods: { methods: {
@ -38,8 +37,8 @@ export default {
if (this.actionData.notify_on_success) { if (this.actionData.notify_on_success) {
this.modalNotify(this.actionData.success_message); this.modalNotify(this.actionData.success_message);
} }
} },
} },
}; };
</script> </script>

View file

@ -17,15 +17,15 @@ export default {
name: "ServerSpecifiedPropertyControl", name: "ServerSpecifiedPropertyControl",
components: { components: {
PropertyControl PropertyControl,
}, },
props: { props: {
propertyData: { propertyData: {
type: Object, type: Object,
required: true, required: true,
} },
} },
}; };
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<a @click.prevent="$emit('click')" class="sync-button"> <a class="sync-button" @click.prevent="$emit('click')">
<span <span
class="material-symbols-outlined sync-icon" class="material-symbols-outlined sync-icon"
title="Reread value from microscope. Required if microscope is updated externally" title="Reread value from microscope. Required if microscope is updated externally"
@ -11,7 +11,7 @@
<script> <script>
export default { export default {
name: "syncPropertyButton" name: "SyncPropertyButton",
}; };
</script> </script>
@ -24,14 +24,13 @@ export default {
cursor: pointer; cursor: pointer;
} }
.material-symbols-outlined.sync-icon{ .material-symbols-outlined.sync-icon {
color: #888; color: #888;
transition: transform 0.3s ease, color 0.3s ease; transition: transform 0.3s ease, color 0.3s ease;
} }
.material-symbols-outlined.sync-icon:hover{ .material-symbols-outlined.sync-icon:hover {
transform: rotate(-90deg); transform: rotate(-90deg);
color: #c5247f; color: #c5247f;
} }
</style> </style>

View file

@ -1,14 +1,8 @@
<template> <template>
<div class="uk-flex uk-flex-column uk-flex-center uk-height-1-1"> <div class="uk-flex uk-flex-column uk-flex-center uk-height-1-1">
<div <div v-if="$store.state.waiting" class="uk-align-center" uk-spinner="ratio: 3"></div>
v-if="$store.state.waiting"
class="uk-align-center"
uk-spinner="ratio: 3"
></div>
<div v-if="$store.state.waiting" class="uk-align-center">Loading...</div> <div v-if="$store.state.waiting" class="uk-align-center">Loading...</div>
<span class="material-symbols-outlined uk-align-center error-icon" <span class="material-symbols-outlined uk-align-center error-icon">error_outline</span>
>error_outline</span
>
<div v-if="$store.state.error" class="uk-align-center"> <div v-if="$store.state.error" class="uk-align-center">
{{ $store.state.error }} {{ $store.state.error }}
</div> </div>
@ -29,7 +23,7 @@ export default {
data: function() { data: function() {
return {}; return {};
} },
}; };
</script> </script>

View file

@ -4,13 +4,13 @@
<h2 class="uk-modal-title">Microscope Calibration</h2> <h2 class="uk-modal-title">Microscope Calibration</h2>
<component <component
v-if="currentTask"
:is="currentTask.component" :is="currentTask.component"
v-if="currentTask"
:key="taskIndex" :key="taskIndex"
v-bind="currentTask.props" v-bind="currentTask.props"
:first="isFirstTask" :first="isFirstTask"
:final="isFinalTask" :final="isFinalTask"
:startOnLast="movingBackward" :start-on-last="movingBackward"
@next="nextTask" @next="nextTask"
@back="previousTask" @back="previousTask"
/> />
@ -26,7 +26,7 @@ import cameraStageMappingTask from "./calibrationWizardComponents/cameraStageMap
import finalStep from "./calibrationWizardComponents/finalStep.vue"; import finalStep from "./calibrationWizardComponents/finalStep.vue";
export default { export default {
name: "calibrationWizard", name: "CalibrationWizard",
components: {}, components: {},
@ -36,7 +36,7 @@ export default {
availableCalibrationTasks: {}, availableCalibrationTasks: {},
tasks: [], tasks: [],
taskIndex: 0, taskIndex: 0,
movingBackward: false movingBackward: false,
}; };
}, },
@ -49,7 +49,7 @@ export default {
}, },
isFinalTask() { isFinalTask() {
return this.taskIndex === this.tasks.length - 1; return this.taskIndex === this.tasks.length - 1;
} },
}, },
mounted() { mounted() {
@ -57,12 +57,10 @@ export default {
// Check which Things are available on mount. // Check which Things are available on mount.
const allCalibrationTasks = { const allCalibrationTasks = {
camera: cameraCalibrationTask, camera: cameraCalibrationTask,
camera_stage_mapping: cameraStageMappingTask camera_stage_mapping: cameraStageMappingTask,
}; };
this.availableCalibrationTasks = Object.fromEntries( this.availableCalibrationTasks = Object.fromEntries(
Object.entries(allCalibrationTasks).filter(([thing]) => Object.entries(allCalibrationTasks).filter(([thing]) => this.thingAvailable(thing)),
this.thingAvailable(thing)
)
); );
}, },
@ -81,10 +79,7 @@ export default {
for (const name of calibrateableThings) { for (const name of calibrateableThings) {
try { try {
const thingNeedsCal = await this.readThingProperty( const thingNeedsCal = await this.readThingProperty(name, "calibration_required");
name,
"calibration_required"
);
if (thingNeedsCal) { if (thingNeedsCal) {
needsCalibration.push(name); needsCalibration.push(name);
} }
@ -111,7 +106,7 @@ export default {
if (includeWelcome) { if (includeWelcome) {
tasks.push({ tasks.push({
component: singleStepTask, component: singleStepTask,
props: { stepComponent: welcomeStep } props: { stepComponent: welcomeStep },
}); });
} }
@ -124,7 +119,7 @@ export default {
// Always include the final step // Always include the final step
tasks.push({ tasks.push({
component: singleStepTask, component: singleStepTask,
props: { stepComponent: finalStep } props: { stepComponent: finalStep },
}); });
this.tasks = tasks; this.tasks = tasks;
@ -192,7 +187,7 @@ export default {
} else { } else {
this.hide(); this.hide();
} }
} },
} },
}; };
</script> </script>

View file

@ -23,8 +23,8 @@ gets very confusing.
<div> <div>
<h3 v-if="title">{{ title }}</h3> <h3 v-if="title">{{ title }}</h3>
<component <component
v-if="currentStep"
:is="currentStep.component" :is="currentStep.component"
v-if="currentStep"
:key="stepIndex" :key="stepIndex"
v-bind="currentStep.props" v-bind="currentStep.props"
/> />
@ -37,11 +37,7 @@ gets very confusing.
> >
Back Back
</button> </button>
<button <button class="uk-button uk-button-primary uk-margin-left" type="button" @click="nextStep">
class="uk-button uk-button-primary uk-margin-left"
type="button"
@click="nextStep"
>
{{ nextButtonText }} {{ nextButtonText }}
</button> </button>
</p> </p>
@ -50,28 +46,28 @@ gets very confusing.
<script> <script>
export default { export default {
name: "calibrationWizardTask", name: "CalibrationWizardTask",
props: { props: {
title: { title: {
type: String, type: String,
default: null default: null,
}, },
first: Boolean, first: Boolean,
final: Boolean, final: Boolean,
startOnLast: { startOnLast: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
steps: { steps: {
type: Array, type: Array,
required: true required: true,
} },
}, },
data() { data() {
return { return {
stepIndex: this.startOnLast ? this.steps.length - 1 : 0 stepIndex: this.startOnLast ? this.steps.length - 1 : 0,
}; };
}, },
@ -83,10 +79,8 @@ export default {
return !this.first || this.stepIndex > 0; return !this.first || this.stepIndex > 0;
}, },
nextButtonText() { nextButtonText() {
return this.final && this.stepIndex === this.steps.length - 1 return this.final && this.stepIndex === this.steps.length - 1 ? "Finish" : "Next";
? "Finish" },
: "Next";
}
}, },
methods: { methods: {
@ -111,7 +105,7 @@ export default {
} else { } else {
this.$emit("next"); this.$emit("next");
} }
} },
} },
}; };
</script> </script>

View file

@ -14,6 +14,6 @@
<script> <script>
export default { export default {
name: "camCalibrationExplanation" name: "CamCalibrationExplanation",
}; };
</script> </script>

View file

@ -6,10 +6,7 @@
<template #below-stream> <template #below-stream>
<div class="action-button-container"> <div class="action-button-container">
<cameraCalibrationSettings <cameraCalibrationSettings :show-extra-settings="false" :camera-uri="cameraUri" />
:show-extra-settings="false"
:camera-uri="cameraUri"
/>
</div> </div>
</template> </template>
</stepTemplateWithStream> </stepTemplateWithStream>
@ -20,18 +17,18 @@ import stepTemplateWithStream from "../stepTemplateWithStream.vue";
import cameraCalibrationSettings from "../../../tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue"; import cameraCalibrationSettings from "../../../tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue";
export default { export default {
name: "cameraMainCalibrationStep", name: "CameraMainCalibrationStep",
components: { components: {
stepTemplateWithStream, stepTemplateWithStream,
cameraCalibrationSettings cameraCalibrationSettings,
}, },
computed: { computed: {
cameraUri: function() { cameraUri: function() {
return `${this.$store.getters.baseUri}/camera/`; return `${this.$store.getters.baseUri}/camera/`;
} },
} },
}; };
</script> </script>

View file

@ -3,7 +3,7 @@
title="Camera Calibration" title="Camera Calibration"
:first="first" :first="first"
:final="final" :final="final"
:startOnLast="startOnLast" :start-on-last="startOnLast"
:steps="steps" :steps="steps"
@next="$emit('next')" @next="$emit('next')"
@back="$emit('back')" @back="$emit('back')"
@ -16,7 +16,7 @@ import camCalibrationExplanation from "./cameraCalibrationSteps/camCalibrationEx
import cameraMainCalibrationStep from "./cameraCalibrationSteps/cameraMainCalibrationStep.vue"; import cameraMainCalibrationStep from "./cameraCalibrationSteps/cameraMainCalibrationStep.vue";
export default { export default {
name: "cameraCalibrationTask", name: "CameraCalibrationTask",
components: { calibrationWizardTask }, components: { calibrationWizardTask },
props: { props: {
// Standard calibrationWizardTask props below: // Standard calibrationWizardTask props below:
@ -24,17 +24,14 @@ export default {
final: Boolean, final: Boolean,
startOnLast: { startOnLast: {
type: Boolean, type: Boolean,
default: false default: false,
} },
}, },
data: function() { data: function() {
return { return {
steps: [ steps: [{ component: camCalibrationExplanation }, { component: cameraMainCalibrationStep }],
{ component: camCalibrationExplanation },
{ component: cameraMainCalibrationStep }
]
}; };
} },
}; };
</script> </script>

View file

@ -3,7 +3,7 @@
title="Camera-Stage Mapping" title="Camera-Stage Mapping"
:first="first" :first="first"
:final="final" :final="final"
:startOnLast="startOnLast" :start-on-last="startOnLast"
:steps="steps" :steps="steps"
@next="$emit('next')" @next="$emit('next')"
@back="$emit('back')" @back="$emit('back')"
@ -17,7 +17,7 @@ import focusStep from "./csmSteps/focusStep.vue";
import runCsmStep from "./csmSteps/runCsmStep.vue"; import runCsmStep from "./csmSteps/runCsmStep.vue";
export default { export default {
name: "cameraCalibrationTask", name: "CameraCalibrationTask",
components: { calibrationWizardTask }, components: { calibrationWizardTask },
props: { props: {
// Standard calibrationWizardTask props below: // Standard calibrationWizardTask props below:
@ -25,18 +25,14 @@ export default {
final: Boolean, final: Boolean,
startOnLast: { startOnLast: {
type: Boolean, type: Boolean,
default: false default: false,
} },
}, },
data: function() { data: function() {
return { return {
steps: [ steps: [{ component: csmExplanation }, { component: focusStep }, { component: runCsmStep }],
{ component: csmExplanation },
{ component: focusStep },
{ component: runCsmStep }
]
}; };
} },
}; };
</script> </script>

View file

@ -17,6 +17,6 @@
<script> <script>
export default { export default {
name: "csmExplanation" name: "CsmExplanation",
}; };
</script> </script>

View file

@ -3,10 +3,7 @@
<p> <p>
Use the buttons below to bring the sample into focus. Use the buttons below to bring the sample into focus.
</p> </p>
<p> <p>You may also adjust the z position with <b>page up</b> and <b>page down</b>.</p>
You may also adjust the z position with <b>page up</b> and
<b>page down</b>.
</p>
<template #below-stream> <template #below-stream>
<div class="action-button-container"> <div class="action-button-container">
<action-button <action-button
@ -16,7 +13,7 @@
:button-primary="false" :button-primary="false"
:submit-data="{ x: 0, y: 0, z: -100 }" :submit-data="{ x: 0, y: 0, z: -100 }"
:submit-label="' - '" :submit-label="' - '"
:hideOnRun="false" :hide-on-run="false"
:can-terminate="false" :can-terminate="false"
/> />
<action-button <action-button
@ -27,7 +24,7 @@
:submit-data="{ x: 0, y: 0, z: 100 }" :submit-data="{ x: 0, y: 0, z: 100 }"
:submit-label="'+'" :submit-label="'+'"
:can-terminate="false" :can-terminate="false"
:hideOnRun="false" :hide-on-run="false"
/> />
</div> </div>
</template> </template>
@ -38,12 +35,12 @@
import stepTemplateWithStream from "../stepTemplateWithStream.vue"; import stepTemplateWithStream from "../stepTemplateWithStream.vue";
import ActionButton from "../../../labThingsComponents/actionButton.vue"; import ActionButton from "../../../labThingsComponents/actionButton.vue";
export default { export default {
name: "cameraMainCalibrationStep", name: "CameraMainCalibrationStep",
components: { components: {
stepTemplateWithStream, stepTemplateWithStream,
ActionButton ActionButton,
} },
}; };
</script> </script>
<style scoped> <style scoped>

View file

@ -17,12 +17,12 @@ import stepTemplateWithStream from "../stepTemplateWithStream.vue";
import CSMCalibrationSettings from "../../../tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue"; import CSMCalibrationSettings from "../../../tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue";
export default { export default {
name: "cameraMainCalibrationStep", name: "CameraMainCalibrationStep",
components: { components: {
stepTemplateWithStream, stepTemplateWithStream,
CSMCalibrationSettings CSMCalibrationSettings,
} },
}; };
</script> </script>

View file

@ -14,6 +14,6 @@
<script> <script>
export default { export default {
name: "finalStep" name: "FinalStep",
}; };
</script> </script>

View file

@ -15,7 +15,7 @@ supplied by the wizard.
:title="title" :title="title"
:first="first" :first="first"
:final="final" :final="final"
:startOnLast="startOnLast" :start-on-last="startOnLast"
:steps="steps" :steps="steps"
@next="$emit('next')" @next="$emit('next')"
@back="$emit('back')" @back="$emit('back')"
@ -26,36 +26,36 @@ supplied by the wizard.
import calibrationWizardTask from "./calibrationWizardTask.vue"; import calibrationWizardTask from "./calibrationWizardTask.vue";
export default { export default {
name: "singleStepTask", name: "SingleStepTask",
components: { calibrationWizardTask }, components: { calibrationWizardTask },
props: { props: {
// This must be sent // This must be sent
stepComponent: { stepComponent: {
type: Object, type: Object,
required: true required: true,
}, },
// This is optional. // This is optional.
stepProps: { stepProps: {
type: Object, type: Object,
default: () => ({}) default: () => ({}),
}, },
// Standard calibrationWizardTask props below: // Standard calibrationWizardTask props below:
title: { title: {
type: String, type: String,
default: null default: null,
}, },
first: Boolean, first: Boolean,
final: Boolean, final: Boolean,
startOnLast: { startOnLast: {
type: Boolean, type: Boolean,
default: false default: false,
} },
}, },
data: function() { data: function() {
return { return {
steps: [{ component: this.stepComponent, props: this.stepProps }] steps: [{ component: this.stepComponent, props: this.stepProps }],
}; };
} },
}; };
</script> </script>

View file

@ -10,11 +10,11 @@
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue"; import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
export default { export default {
name: "stepTemplateWithStream", name: "StepTemplateWithStream",
components: { components: {
miniStreamDisplay miniStreamDisplay,
} },
}; };
</script> </script>

View file

@ -6,8 +6,8 @@
</b> </b>
</p> </p>
<p> <p>
Your microscope will still function, however some functionality will be Your microscope will still function, however some functionality will be limited, and image
limited, and image quality will likely suffer. quality will likely suffer.
</p> </p>
<p> <p>
<b>Click Next to begin microscope calibration.</b> <b>Click Next to begin microscope calibration.</b>
@ -17,6 +17,6 @@
<script> <script>
export default { export default {
name: "welcomeStep" name: "WelcomeStep",
}; };
</script> </script>

View file

@ -1,24 +1,10 @@
<template> <template>
<div> <div>
<form <form class="uk-form-stacked" action="" method="GET" @submit="overrideAPIHost">
class="uk-form-stacked"
action=""
method="GET"
@submit="overrideAPIHost"
>
<label class="uk-form-label">Override API origin</label> <label class="uk-form-label">Override API origin</label>
<input <input v-model="newOrigin" name="overrideOrigin" class="uk-input" type="text" />
v-model="newOrigin"
name="overrideOrigin"
class="uk-input"
type="text"
/>
<label class="uk-form-label"> <label class="uk-form-label">
<input <input v-model="reloadWhenOverridingOrigin" class="uk-input uk-checkbox" type="checkbox" />
v-model="reloadWhenOverridingOrigin"
class="uk-input uk-checkbox"
type="checkbox"
/>
Reload web app with new origin Reload web app with new origin
</label> </label>
<button class="uk-button uk-button-default uk-margin-small"> <button class="uk-button uk-button-default uk-margin-small">
@ -38,7 +24,7 @@ export default {
data: function() { data: function() {
return { return {
newOrigin: this.$store.state.origin, newOrigin: this.$store.state.origin,
reloadWhenOverridingOrigin: true reloadWhenOverridingOrigin: true,
}; };
}, },
@ -62,8 +48,8 @@ export default {
this.$store.commit("changeOrigin", this.newOrigin); this.$store.commit("changeOrigin", this.newOrigin);
event.preventDefault(); event.preventDefault();
} }
} },
} },
}; };
</script> </script>

View file

@ -53,9 +53,7 @@
<div v-else-if="$store.state.waiting"> <div v-else-if="$store.state.waiting">
Loading... Loading...
</div> </div>
<div v-else-if="$store.state.error"> <div v-else-if="$store.state.error"><b>Error:</b> {{ $store.state.error }}</div>
<b>Error:</b> {{ $store.state.error }}
</div>
<div v-else>No active connection</div> <div v-else>No active connection</div>
</div> </div>
</template> </template>
@ -70,37 +68,33 @@ export default {
data: function() { data: function() {
return { return {
version: undefined, version: undefined,
version_source: undefined version_source: undefined,
} };
},
async mounted() {
let version_data = await this.readThingProperty(
"system",
"version_data"
);
this.version = version_data.version;
this.version_source = this.truncate(version_data.version_source);
}, },
computed: { computed: {
things: function() { things: function() {
return this.$store.getters["wot/thingDescriptions"]; return this.$store.getters["wot/thingDescriptions"];
} },
},
async mounted() {
let version_data = await this.readThingProperty("system", "version_data");
this.version = version_data.version;
this.version_source = this.truncate(version_data.version_source);
}, },
methods: { methods: {
truncate(string, max_length=15) { truncate(string, max_length = 15) {
if (!(typeof string === 'string' || string instanceof String)) { if (!(typeof string === "string" || string instanceof String)) {
return string; return string;
} }
if (string.length <= max_length) { if (string.length <= max_length) {
return string; return string;
} }
return string.slice(0, max_length - 3) + "..."; return string.slice(0, max_length - 3) + "...";
} },
} },
}; };
</script> </script>

View file

@ -30,7 +30,7 @@ export default {
components: { components: {
devTools, devTools,
statusPane statusPane,
} },
}; };
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="uk-padding-small" v-observe-visibility="visibilityChanged"> <div v-observe-visibility="visibilityChanged" class="uk-padding-small">
<div> <div>
<ul uk-accordion="multiple: true"> <ul uk-accordion="multiple: true">
<li> <li>
@ -35,7 +35,7 @@
thing="camera" thing="camera"
action="image_is_sample" action="image_is_sample"
submit-label="Check Current Image" submit-label="Check Current Image"
:isDisabled="!ready" :is-disabled="!ready"
:can-terminate="false" :can-terminate="false"
:poll-interval="0.1" :poll-interval="0.1"
@response="alertImageLabel" @response="alertImageLabel"
@ -53,13 +53,13 @@ import InputFromSchema from "../../labThingsComponents/inputFromSchema.vue";
export default { export default {
components: { components: {
ActionButton, ActionButton,
InputFromSchema InputFromSchema,
}, },
data() { data() {
return { return {
backgroundDetectorStatus: undefined, backgroundDetectorStatus: undefined,
animate: false animate: false,
}; };
}, },
@ -67,7 +67,13 @@ export default {
ready() { ready() {
const status = this.backgroundDetectorStatus; const status = this.backgroundDetectorStatus;
return status && status.ready === true; return status && status.ready === true;
} },
},
async created() {
this.backgroundDetectorStatus = await this.readThingProperty(
"camera",
"background_detector_status",
);
}, },
methods: { methods: {
@ -86,27 +92,17 @@ export default {
readSettings: async function() { readSettings: async function() {
this.backgroundDetectorStatus = await this.readThingProperty( this.backgroundDetectorStatus = await this.readThingProperty(
"camera", "camera",
"background_detector_status" "background_detector_status",
); );
}, },
writeSettings: async function(requestedValue) { writeSettings: async function(requestedValue) {
await this.invokeAction( await this.invokeAction("camera", "update_detector_settings", { data: requestedValue });
"camera",
"update_detector_settings",
{"data": requestedValue}
);
this.animate = true; this.animate = true;
this.readSettings(); this.readSettings();
}, },
resetAnimate: function() { resetAnimate: function() {
this.animate = false; this.animate = false;
} },
}, },
async created() {
this.backgroundDetectorStatus = await this.readThingProperty(
"camera",
"background_detector_status"
);
}
}; };
</script> </script>

View file

@ -19,7 +19,7 @@ export default {
components: { components: {
paneBackgroundDetect, paneBackgroundDetect,
streamDisplay streamDisplay,
} },
}; };
</script> </script>

View file

@ -18,12 +18,7 @@
/> />
</div> </div>
<label class="uk-margin-small-right" <label class="uk-margin-small-right"
><input ><input v-model="invert.x" class="uk-checkbox" type="checkbox" /> Invert x</label
v-model="invert.x"
class="uk-checkbox"
type="checkbox"
/>
Invert x</label
> >
</div> </div>
@ -38,12 +33,7 @@
/> />
</div> </div>
<label class="uk-margin-small-right" <label class="uk-margin-small-right"
><input ><input v-model="invert.y" class="uk-checkbox" type="checkbox" /> Invert y</label
v-model="invert.y"
class="uk-checkbox"
type="checkbox"
/>
Invert y</label
> >
</div> </div>
@ -59,7 +49,7 @@
</div> </div>
</div> </div>
</div> </div>
<br> <br />
<action-button <action-button
thing="stage" thing="stage"
action="set_zero_position" action="set_zero_position"
@ -165,8 +155,7 @@
<div v-else class="uk-text-warning"> <div v-else class="uk-text-warning">
<p>Stage positioning is disabled since no stage is connected.</p> <p>Stage positioning is disabled since no stage is connected.</p>
<p> <p>
Please check all data and power connections to your motor controller Please check all data and power connections to your motor controller board.
board.
</p> </p>
</div> </div>
</div> </div>
@ -183,7 +172,7 @@ export default {
components: { components: {
ActionButton, ActionButton,
syncPropertyButton syncPropertyButton,
}, },
data: function() { data: function() {
@ -193,16 +182,16 @@ export default {
stepSize: { stepSize: {
x: 200, x: 200,
y: 200, y: 200,
z: 50 z: 50,
}, },
invert: { invert: {
x: false, x: false,
y: false, y: false,
z: false z: false,
}, },
setPosition: null, setPosition: null,
isAutofocusing: 0, isAutofocusing: 0,
moveLock: false moveLock: false,
}; };
}, },
@ -214,11 +203,8 @@ export default {
return `${this.baseUri}/stage/position`; return `${this.baseUri}/stage/position`;
}, },
moveInImageCoordinatesUri: function() { moveInImageCoordinatesUri: function() {
return this.thingActionUrl( return this.thingActionUrl("camera_stage_mapping", "move_in_image_coordinates");
"camera_stage_mapping", },
"move_in_image_coordinates"
);
}
}, },
watch: { watch: {
@ -226,20 +212,19 @@ export default {
deep: true, deep: true,
handler() { handler() {
this.setLocalStorageObj("navigation_stepSize", this.stepSize); this.setLocalStorageObj("navigation_stepSize", this.stepSize);
} },
}, },
invert: { invert: {
deep: true, deep: true,
handler() { handler() {
this.setLocalStorageObj("navigation_invert", this.invert); this.setLocalStorageObj("navigation_invert", this.invert);
} },
} },
}, },
async mounted() { async mounted() {
// Reload saved settings // Reload saved settings
this.stepSize = this.stepSize = this.getLocalStorageObj("navigation_stepSize") || this.stepSize;
this.getLocalStorageObj("navigation_stepSize") || this.stepSize;
this.invert = this.getLocalStorageObj("navigation_invert") || this.invert; this.invert = this.getLocalStorageObj("navigation_invert") || this.invert;
let self = this; let self = this;
// A global signal listener to perform a move action // A global signal listener to perform a move action
@ -255,7 +240,7 @@ export default {
x_steps * this.stepSize.x * (this.invert.x ? -1 : 1), x_steps * this.stepSize.x * (this.invert.x ? -1 : 1),
y_steps * this.stepSize.y * (this.invert.y ? -1 : 1), y_steps * this.stepSize.y * (this.invert.y ? -1 : 1),
z_steps * this.stepSize.z * (this.invert.z ? -1 : 1), z_steps * this.stepSize.z * (this.invert.z ? -1 : 1),
false false,
); );
}); });
// Update the current position in text boxes // Update the current position in text boxes
@ -287,7 +272,7 @@ export default {
this.setPosition = { this.setPosition = {
x: this.setPosition.x + x, x: this.setPosition.x + x,
y: this.setPosition.y + y, y: this.setPosition.y + y,
z: this.setPosition.z + z z: this.setPosition.z + z,
}; };
} }
await this.timeout(1); // Wait for Vue to update the position await this.timeout(1); // Wait for Vue to update the position
@ -304,7 +289,7 @@ export default {
if (!this.moveInImageCoordinatesUri) { if (!this.moveInImageCoordinatesUri) {
this.modalError( this.modalError(
"Moving in image coordinates is not supported - you will need to upgrade your microscope's software." "Moving in image coordinates is not supported - you will need to upgrade your microscope's software.",
); );
} }
@ -312,7 +297,7 @@ export default {
axios axios
.post(this.moveInImageCoordinatesUri, { .post(this.moveInImageCoordinatesUri, {
x: x, // NB the coordinates are numpy/PIL style, meaning X and Y are swapped. x: x, // NB the coordinates are numpy/PIL style, meaning X and Y are swapped.
y: y y: y,
}) })
.then(() => { .then(() => {
this.updatePosition(); // Update the position in text boxes this.updatePosition(); // Update the position in text boxes
@ -346,8 +331,8 @@ export default {
link.setAttribute("download", `OFM_${new Date().toISOString()}.jpeg`); link.setAttribute("download", `OFM_${new Date().toISOString()}.jpeg`);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
} },
} },
}; };
</script> </script>

View file

@ -19,7 +19,7 @@ export default {
components: { components: {
paneControl, paneControl,
streamDisplay streamDisplay,
} },
}; };
</script> </script>

View file

@ -1,17 +1,9 @@
<template> <template>
<div <div v-observe-visibility="visibilityChanged" class="uk-padding uk-padding-remove-top">
v-observe-visibility="visibilityChanged"
class="uk-padding uk-padding-remove-top"
>
<!-- Logging nav bar --> <!-- Logging nav bar -->
<nav <nav class="logging-navbar uk-navbar-container uk-navbar-transparent" uk-navbar="mode: click">
class="logging-navbar uk-navbar-container uk-navbar-transparent"
uk-navbar="mode: click"
>
<!-- Left side controls --> <!-- Left side controls -->
<div <div class="uk-navbar-left uk-padding-remove-top uk-padding-remove-bottom">
class="uk-navbar-left uk-padding-remove-top uk-padding-remove-bottom"
>
<select v-model="filterLevel" class="uk-select"> <select v-model="filterLevel" class="uk-select">
<option v-for="level in allLevels" :key="level">{{ level }}</option> <option v-for="level in allLevels" :key="level">{{ level }}</option>
</select> </select>
@ -30,11 +22,11 @@
</button> </button>
</div> </div>
<div> <div>
<EndpointButton <EndpointButton
class="uk-button uk-width-1-1" class="uk-button uk-width-1-1"
:URL="logFileURI" :url="logFileURI"
buttonLabel="Download Log File" button-label="Download Log File"
:buttonPrimary="false" :button-primary="false"
/> />
</div> </div>
</div> </div>
@ -50,7 +42,7 @@
class="logging-entry" class="logging-entry"
:class="{ :class="{
'uk-alert-warning uk-alert': item.level == 'WARNING', 'uk-alert-warning uk-alert': item.level == 'WARNING',
'uk-alert-danger uk-alert': item.level == 'ERROR' 'uk-alert-danger uk-alert': item.level == 'ERROR',
}" }"
> >
<b>{{ formatDateTime(item.timestamp) }}: {{ item.level }}</b> <b>{{ formatDateTime(item.timestamp) }}: {{ item.level }}</b>
@ -64,11 +56,7 @@
More info... More info...
</a> </a>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div <div v-if="item.expanded" class="logging-message" v-html="item.message"></div>
v-if="item.expanded"
class="logging-message"
v-html="item.message"
></div>
<!-- eslint-enable --> <!-- eslint-enable -->
</div> </div>
</div> </div>
@ -101,7 +89,7 @@ export default {
components: { components: {
Paginate, Paginate,
EndpointButton EndpointButton,
}, },
data: function() { data: function() {
@ -110,7 +98,7 @@ export default {
page: 1, page: 1,
logs: [], logs: [],
allLevels: ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], allLevels: ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
filterLevel: "WARNING" filterLevel: "WARNING",
}; };
}, },
@ -142,7 +130,7 @@ export default {
}, },
numberOfPages: function() { numberOfPages: function() {
return Math.floor(this.filteredItems.length / this.maxitems); return Math.floor(this.filteredItems.length / this.maxitems);
} },
}, },
methods: { methods: {
@ -172,7 +160,7 @@ export default {
summary: m[3], summary: m[3],
message: this.escapeText(m[3]), message: this.escapeText(m[3]),
sequence: logs.length, sequence: logs.length,
expanded: false expanded: false,
}); });
} else if (logs) { } else if (logs) {
// If a line does not look like a log entry, append it to the last // If a line does not look like a log entry, append it to the last
@ -198,9 +186,7 @@ export default {
} else { } else {
// if there's no existing log message to append to, discard lines // if there's no existing log message to append to, discard lines
// until we find one. // until we find one.
console.log( console.log("Ignored non-matching lines at the start of the log file.");
"Ignored non-matching lines at the start of the log file."
);
continue; continue;
} }
} }
@ -216,8 +202,8 @@ export default {
let div = document.createElement("div"); let div = document.createElement("div");
div.innerText = unsafeText; div.innerText = unsafeText;
return div.innerHTML; return div.innerHTML;
} },
} },
}; };
</script> </script>

View file

@ -1,11 +1,12 @@
<template> <template>
<!-- Grid managing tab content --> <!-- Grid managing tab content -->
<div class="container"> <div class="container">
<h1 id="power-title"> Power</h1> <h1 id="power-title">Power</h1>
<p id="power-msg"> It's essential to turn off your OpenFlexure Microscope here before unplugging it. <p id="power-msg">
<br> It's essential to turn off your OpenFlexure Microscope here before unplugging it.
<br> <br />
Unplugging the microscope unexpectedly can damage the SD card or onboard computer. <br />
Unplugging the microscope unexpectedly can damage the SD card or onboard computer.
</p> </p>
<div class="buttons-container"> <div class="buttons-container">
<button <button
@ -37,44 +38,38 @@ export default {
data: function() { data: function() {
return { return {
isRaspberrypi: undefined, isRaspberrypi: undefined,
} };
}, },
computed: { computed: {
things: function() { things: function() {
return this.$store.getters["wot/thingDescriptions"]; return this.$store.getters["wot/thingDescriptions"];
} },
}, },
async mounted() { async mounted() {
this.isRaspberrypi = await this.readThingProperty( this.isRaspberrypi = await this.readThingProperty("system", "is_raspberrypi");
"system",
"is_raspberrypi"
);
}, },
methods: { methods: {
systemRequest: function(action) { systemRequest: function(action) {
let message = ""; let message = "";
if (action == "reboot") { if (action == "reboot") {
message = "Restart microscope?" message = "Restart microscope?";
} } else {
else { message = "Shutdown microscope?";
message = "Shutdown microscope?"
} }
this.modalConfirm(message).then( this.modalConfirm(message).then(
() => { () => {
this.$store.commit("resetState"); this.$store.commit("resetState");
this.$store.commit("wot/deleteAllThingDescriptions"); this.$store.commit("wot/deleteAllThingDescriptions");
// Post and silence errors // Post and silence errors
axios axios.post(this.thingActionUrl("system", action)).catch(() => {});
.post(this.thingActionUrl("system", action))
.catch(() => {});
}, },
() => {} () => {},
); );
} },
} },
}; };
</script> </script>
@ -82,7 +77,6 @@ export default {
// Custom UIkit CSS modifications // Custom UIkit CSS modifications
@import "../../assets/less/theme.less"; @import "../../assets/less/theme.less";
.container { .container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -109,7 +103,7 @@ export default {
.shutdown-button { .shutdown-button {
display: inline; display: inline;
text-align:center; text-align: center;
margin: 30px 30px 30px 30px; margin: 30px 30px 30px 30px;
} }
</style> </style>

View file

@ -1,7 +1,7 @@
<template> <template>
<div v-observe-visibility="visibilityChanged"> <div v-observe-visibility="visibilityChanged">
<div id="openseadragon"></div> <div id="openseadragon"></div>
</div> </div>
</template> </template>
<script> <script>
@ -13,26 +13,25 @@ export default {
props: { props: {
src: { src: {
type: String, type: String,
required: true required: true,
}, },
brightness: { brightness: {
type: Number, type: Number,
required: true required: true,
}, },
contrast: { contrast: {
type: Number, type: Number,
required: true required: true,
}, },
saturation: { saturation: {
type: Number, type: Number,
required: true required: true,
} },
}, },
data: function() { data: function() {
return { return {
osdViewer: null osdViewer: null,
}; };
}, },
@ -41,7 +40,7 @@ export default {
immediate: true, immediate: true,
handler(newVal) { handler(newVal) {
this.loadOpenSeaDragon(newVal); this.loadOpenSeaDragon(newVal);
} },
}, },
brightness() { brightness() {
this.updateFilter(); this.updateFilter();
@ -51,7 +50,7 @@ export default {
}, },
saturation() { saturation() {
this.updateFilter(); this.updateFilter();
} },
}, },
async mounted() { async mounted() {
@ -69,7 +68,7 @@ export default {
visibilityChanged(isVisible) { visibilityChanged(isVisible) {
if (isVisible) { if (isVisible) {
this.loadOpenSeaDragon(); this.loadOpenSeaDragon();
}else{ } else {
this.osdViewer.destroy(); this.osdViewer.destroy();
} }
}, },
@ -79,13 +78,13 @@ export default {
} }
this.osdViewer = OpenSeaDragon({ this.osdViewer = OpenSeaDragon({
id: "openseadragon", id: "openseadragon",
crossOriginPolicy: 'Anonymous', crossOriginPolicy: "Anonymous",
tileSources: this.src, tileSources: this.src,
showNavigationControl: false, showNavigationControl: false,
maxZoomPixelRatio: 2, maxZoomPixelRatio: 2,
gestureSettingsMouse: { gestureSettingsMouse: {
clickToZoom: false clickToZoom: false,
} },
}); });
this.updateFilter(); this.updateFilter();
@ -106,7 +105,7 @@ export default {
this.osdViewer.setFullScreen(true); this.osdViewer.setFullScreen(true);
} }
}, },
} },
}; };
</script> </script>
@ -117,4 +116,4 @@ export default {
background-color: black; background-color: black;
z-index: 1; z-index: 1;
} }
</style> </style>

View file

@ -2,9 +2,7 @@
<div class="uk-card"> <div class="uk-card">
<div class="uk-card-body"> <div class="uk-card-body">
<div class="uk-card-media-top"> <div class="uk-card-media-top">
<div <div class="view-image uk-padding-remove uk-height-1-1">
class="view-image uk-padding-remove uk-height-1-1"
>
<img <img
id="thumbnail-stitched-image" id="thumbnail-stitched-image"
class="thumbnail-fit" class="thumbnail-fit"
@ -15,39 +13,36 @@
</div> </div>
</div> </div>
<h3 class="uk-card-title scan-card-title">{{ scanData.name }}</h3> <h3 class="uk-card-title scan-card-title">{{ scanData.name }}</h3>
<h4 class="ongoing-msg" v-if="ongoing">Scan in progress</h4> <h4 v-if="ongoing" class="ongoing-msg">Scan in progress</h4>
<div class="button-container" v-if="!ongoing"> <div v-if="!ongoing" class="button-container">
<div class="uk-button-group scan-card-buttons"> <div class="uk-button-group scan-card-buttons">
<action-button <action-button
class="uk-width-1-2" class="uk-width-1-2"
thing="smart_scan" thing="smart_scan"
action="download_zip" action="download_zip"
submit-label="Download All" submit-label="Download All"
:can-terminate="false" :can-terminate="false"
:submit-data="{ scan_name: scanData.name }" :submit-data="{ scan_name: scanData.name }"
:button-primary="true" :button-primary="true"
@response="downloadZipFile" @response="downloadZipFile"
@error="modalError" @error="modalError"
/> />
<EndpointButton <EndpointButton
class="uk-width-1-2" class="uk-width-1-2"
:buttonPrimary=true :button-primary="true"
:isDisabled=!scanData.stitch_available :is-disabled="!scanData.stitch_available"
:URL="downloadStitchFile" :url="downloadStitchFile"
buttonLabel="Download JPEG" button-label="Download JPEG"
/> />
</div> </div>
<button <button class="uk-button uk-button-default uk-width-1-1" @click="deleteScan">
class="uk-button uk-button-default uk-width-1-1"
@click="deleteScan"
>
Delete Delete
</button> </button>
<action-button <action-button
v-if="scanData.can_stitch | (scanData.stitch_available & !scanData.dzi)"
submit-label="Stitch Images" submit-label="Stitch Images"
thing="smart_scan" thing="smart_scan"
action="stitch_scan" action="stitch_scan"
v-if="scanData.can_stitch | (scanData.stitch_available & !scanData.dzi)"
:can-terminate="true" :can-terminate="true"
:submit-data="{ scan_name: scanData.name }" :submit-data="{ scan_name: scanData.name }"
:button-primary="false" :button-primary="false"
@ -55,10 +50,11 @@
@error="modalError" @error="modalError"
/> />
<button <button
v-if="scanData.dzi" class="uk-button uk-button-default uk-width-1-1" v-if="scanData.dzi"
class="uk-button uk-button-default uk-width-1-1"
@click="requestViewer" @click="requestViewer"
> >
Show Stitched Scan Show Stitched Scan
</button> </button>
</div> </div>
<div class="scan-info"> <div class="scan-info">
@ -68,9 +64,15 @@
<li>duration: {{ formatDuration(scanData.duration) }}</li> <li>duration: {{ formatDuration(scanData.duration) }}</li>
</ul> </ul>
<ul v-if="!ongoing"> <ul v-if="!ongoing">
<li v-if="scanData.number_of_images<3" class="warning-msg">Not enough images to stitch</li> <li v-if="scanData.number_of_images < 3" class="warning-msg">
<li v-else-if="!scanData.dzi && scanData.stitch_available" class="alert-msg">Interactive preview not available</li> Not enough images to stitch
<li v-else-if="!scanData.stitch_available" class="alert-msg">High quality stitch not available</li> </li>
<li v-else-if="!scanData.dzi && scanData.stitch_available" class="alert-msg">
Interactive preview not available
</li>
<li v-else-if="!scanData.stitch_available" class="alert-msg">
High quality stitch not available
</li>
</ul> </ul>
</div> </div>
</div> </div>
@ -90,16 +92,16 @@ export default {
props: { props: {
scanData: { scanData: {
type: Object, type: Object,
required: true required: true,
}, },
scansUri: { scansUri: {
type: String, type: String,
required: true required: true,
}, },
ongoing: { ongoing: {
type: Boolean, type: Boolean,
required: true required: true,
} },
}, },
computed: { computed: {
@ -114,7 +116,7 @@ export default {
methods: { methods: {
formatDate(timestamp) { formatDate(timestamp) {
// Multiply by 1000 as JS uses ms not s // Multiply by 1000 as JS uses ms not s
let d = new Date(timestamp*1000); let d = new Date(timestamp * 1000);
// Convert to a string in a very javascript way! // Convert to a string in a very javascript way!
let yyyy = d.getFullYear(); let yyyy = d.getFullYear();
let mm = d.getMonth() + 1; let mm = d.getMonth() + 1;
@ -146,14 +148,14 @@ export default {
requestViewer() { requestViewer() {
// Notify parent that thumbnail was clicked // Notify parent that thumbnail was clicked
if (!this.ongoing) { if (!this.ongoing) {
this.$emit('viewer-requested', this.scanData); this.$emit("viewer-requested", this.scanData);
} }
}, },
async deleteScan() { async deleteScan() {
try { try {
await this.modalConfirm(`Are you sure you want to delete ${this.scanData.name}?`); await this.modalConfirm(`Are you sure you want to delete ${this.scanData.name}?`);
await axios.delete(`${this.scansUri}/${this.scanData.name}`); await axios.delete(`${this.scansUri}/${this.scanData.name}`);
this.$emit('update-requested'); this.$emit("update-requested");
this.modalNotify(`Deleted ${this.scanData.name}`); this.modalNotify(`Deleted ${this.scanData.name}`);
} catch (e) { } catch (e) {
// if the confirmation was cancelled, it's rejected with null error // if the confirmation was cancelled, it's rejected with null error
@ -170,15 +172,15 @@ export default {
console.log(link); console.log(link);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
} },
} },
} };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
ul { ul {
display: block; display: block;
text-align: center; text-align: center;
list-style-type:none; list-style-type: none;
margin: 5px 0px 10px 0px; margin: 5px 0px 10px 0px;
padding: 0; padding: 0;
} }
@ -207,5 +209,4 @@ ul {
text-align: center; text-align: center;
padding: 2rem 0; padding: 2rem 0;
} }
</style>
</style>

View file

@ -1,10 +1,6 @@
<template> <template>
<div id="scan-modal" ref="scanModal" uk-modal> <div id="scan-modal" ref="scanModal" uk-modal>
<div <div v-if="selectedScan" id="scan-modal-body" class="uk-modal-dialog uk-modal-body">
id="scan-modal-body"
class="uk-modal-dialog uk-modal-body"
v-if="selectedScan"
>
<h2 id="scan-modal-title" class="uk-modal-title"> <h2 id="scan-modal-title" class="uk-modal-title">
{{ selectedScan.name }} {{ selectedScan.name }}
<button class="uk-modal-close uk-float-right" type="button"> <button class="uk-modal-close uk-float-right" type="button">
@ -16,11 +12,7 @@
</h2> </h2>
<!-- Viewer --> <!-- Viewer -->
<div <div v-if="selectedScanDZI" id="viewer_container" class="viewer_container">
v-if="selectedScanDZI"
id="viewer_container"
class="viewer_container"
>
<OpenSeadragonViewer <OpenSeadragonViewer
id="openseadragon" id="openseadragon"
ref="openseadragon" ref="openseadragon"
@ -32,40 +24,19 @@
</div> </div>
<!-- Controls --> <!-- Controls -->
<div <div v-if="selectedScanDZI" class="viewer-controls">
v-if="selectedScanDZI"
class="viewer-controls"
>
<div class="controlsContainer"> <div class="controlsContainer">
<label> <label>
Brightness Brightness
<input <input v-model.number="brightness" type="range" min="0.2" max="1.8" step="0.01" />
type="range"
min="0.2"
max="1.8"
step="0.01"
v-model.number="brightness"
/>
</label> </label>
<label> <label>
Contrast Contrast
<input <input v-model.number="contrast" type="range" min="0.2" max="1.8" step="0.01" />
type="range"
min="0.2"
max="1.8"
step="0.01"
v-model.number="contrast"
/>
</label> </label>
<label> <label>
Saturation Saturation
<input <input v-model.number="saturation" type="range" min="0" max="2" step="0.01" />
type="range"
min="0"
max="2"
step="0.01"
v-model.number="saturation"
/>
</label> </label>
</div> </div>
@ -155,26 +126,26 @@ input[type="range"] {
} }
.controlsContainer { .controlsContainer {
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 1.5rem; gap: 1.5rem;
} }
.viewer-controls { .viewer-controls {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.viewer_container { .viewer_container {
flex: 1 1 auto; flex: 1 1 auto;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.reset-button{ .reset-button {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
</style> </style>

View file

@ -1,12 +1,10 @@
<template> <template>
<div <div
v-observe-visibility="visibilityChanged" v-observe-visibility="visibilityChanged"
class="galleryDisplay uk-padding uk-padding-remove-top"> class="galleryDisplay uk-padding uk-padding-remove-top"
>
<!-- Gallery nav bar --> <!-- Gallery nav bar -->
<nav <nav class="gallery-navbar uk-navbar-container uk-navbar-transparent" uk-navbar="mode: click">
class="gallery-navbar uk-navbar-container uk-navbar-transparent"
uk-navbar="mode: click"
>
<!-- Right side buttons --> <!-- Right side buttons -->
<div class="uk-navbar-right"> <div class="uk-navbar-right">
<div class="uk-grid"> <div class="uk-grid">
@ -50,18 +48,17 @@
<ScanViewerModal <ScanViewerModal
ref="scanViewer" ref="scanViewer"
:selectedScan="selectedScan" :selected-scan="selectedScan"
:baseUri="$store.getters.baseUri" :base-uri="$store.getters.baseUri"
/> />
<!-- Gallery --> <!-- Gallery -->
<div <div
v-if="$store.getters.ready" v-if="$store.getters.ready"
class="uk-padding-remove-top" class="uk-padding-remove-top"
uk-lightbox="toggle: .lightbox-link" uk-lightbox="toggle: .lightbox-link"
> >
<!-- Gallery capture cards --> <!-- Gallery capture cards -->
<div class="gallery-grid uk-grid-match" uk-grid> <div class="gallery-grid uk-grid-match" uk-grid>
<div v-if="scansEmpty"> <div v-if="scansEmpty">
<h2>No scans available</h2> <h2>No scans available</h2>
@ -99,7 +96,7 @@ export default {
scans: [], scans: [],
ongoing: null, ongoing: null,
selectedScan: null, selectedScan: null,
osdViewer: null osdViewer: null,
}; };
}, },
@ -109,19 +106,19 @@ export default {
"smart_scan", "smart_scan",
"scans", "scans",
"readproperty", "readproperty",
true true,
); );
}, },
scansEmpty() { scansEmpty() {
return this.scans.length == 0; return this.scans.length == 0;
}, },
selectedScanDZI() { selectedScanDZI() {
if (this.selectedScan && this.selectedScan.dzi!="") { if (this.selectedScan && this.selectedScan.dzi != "") {
return `${this.$store.getters.baseUri}/scans/${this.selectedScan.name}/images/${this.selectedScan.dzi}`; return `${this.$store.getters.baseUri}/scans/${this.selectedScan.name}/images/${this.selectedScan.dzi}`;
} else { } else {
return null; return null;
} }
} },
}, },
async mounted() { async mounted() {
@ -132,7 +129,7 @@ export default {
this.updateScans(); this.updateScans();
}); });
this.$root.$on("modalClosed", () => { this.$root.$on("modalClosed", () => {
// Handle the modal closed event here // Handle the modal closed event here
this.updateScans(); this.updateScans();
}); });
}, },
@ -151,7 +148,7 @@ export default {
// If the connection is now disconnected, empty capture list // If the connection is now disconnected, empty capture list
this.captures = {}; this.captures = {};
} }
} },
); );
}, },
@ -175,8 +172,8 @@ export default {
async updateScans() { async updateScans() {
try { try {
let scans_information = await this.readThingProperty("smart_scan", "scans"); let scans_information = await this.readThingProperty("smart_scan", "scans");
let scans = scans_information.scans let scans = scans_information.scans;
this.ongoing = scans_information.ongoing this.ongoing = scans_information.ongoing;
if (!scans | (scans.length == 0)) { if (!scans | (scans.length == 0)) {
this.scans = scans; this.scans = scans;
} }
@ -194,13 +191,13 @@ export default {
} }
}, },
isOngoing(name) { isOngoing(name) {
return name === this.ongoing return name === this.ongoing;
}, },
async deleteAllScans() { async deleteAllScans() {
try { try {
await this.modalConfirm( await this.modalConfirm(
"Are you sure you want to delete all scans from the microscope? " + "Are you sure you want to delete all scans from the microscope? " +
"This is <b>irreversible</b>!" "This is <b>irreversible</b>!",
); );
await axios.delete(`${this.scansUri}`); await axios.delete(`${this.scansUri}`);
await this.updateScans(); await this.updateScans();
@ -217,9 +214,9 @@ export default {
} else { } else {
this.modalError("Scan not stitched for viewing in webapp, please download or stitch"); this.modalError("Scan not stitched for viewing in webapp, please download or stitch");
} }
} },
} },
} };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -238,4 +235,4 @@ export default {
margin-top: 5px; margin-top: 5px;
margin-bottom: 2px; margin-bottom: 2px;
} }
</style> </style>

View file

@ -1,15 +1,10 @@
<template> <template>
<div <div id="CSMSettings" class="uk-grid uk-grid-divider uk-child-width-expand" uk-grid>
id="CSMSettings"
class="uk-grid uk-grid-divider uk-child-width-expand"
uk-grid
>
<div class="uk-width-xlarge"> <div class="uk-width-xlarge">
<h3>Camera to Stage Mapping</h3> <h3>Camera to Stage Mapping</h3>
<p> <p>
Camera-stage mapping allows the stage to move relative to the camera Camera-stage mapping allows the stage to move relative to the camera view. This enables
view. This enables functions like click-to-move, and more precise tile functions like click-to-move, and more precise tile scans.
scans.
</p> </p>
<CSMCalibrationSettings /> <CSMCalibrationSettings />
</div> </div>
@ -29,8 +24,8 @@ export default {
components: { components: {
CSMCalibrationSettings, CSMCalibrationSettings,
miniStreamDisplay miniStreamDisplay,
} },
}; };
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<div v-observe-visibility="visibilityChanged" id="CSMCalibrationSettings"> <div id="CSMCalibrationSettings" v-observe-visibility="visibilityChanged">
<!--Show auto calibrate if default plugin is enabled--> <!--Show auto calibrate if default plugin is enabled-->
<div v-if="'calibrate_xy' in actions" class="uk-margin-small"> <div v-if="'calibrate_xy' in actions" class="uk-margin-small">
<action-button <action-button
@ -26,15 +26,18 @@
> >
Download Calibration Data Download Calibration Data
</button> </button>
<div v-if="this.csmMatrix!='undefined'" style="margin:10px;"> <div v-if="csmMatrix != 'undefined'" style="margin:10px;">
<details><summary>Calibration Details</summary> <details
<strong>CSM calculated for images with a resolution of {{ csmResolution }}</strong> ><summary>Calibration Details</summary>
<ul> <strong>CSM calculated for images with a resolution of {{ csmResolution }}</strong>
<li>Current CSM Matrix: <tt>{{ csmMatrix }}</tt></li> <ul>
<li>Pixels per motor step: {{ csmRatio }}</li> <li>
<li>Full field of view: {{ csmFOV }} motor steps</li> Current CSM Matrix: <tt>{{ csmMatrix }}</tt>
</ul> </li>
</details> <li>Pixels per motor step: {{ csmRatio }}</li>
<li>Full field of view: {{ csmFOV }} motor steps</li>
</ul>
</details>
</div> </div>
</div> </div>
</template> </template>
@ -47,36 +50,32 @@ export default {
name: "CSMCalibrationSettings", name: "CSMCalibrationSettings",
components: { components: {
ActionButton ActionButton,
}, },
props: { props: {
showExtraSettings: { showExtraSettings: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: true,
} },
}, },
data() { data() {
return { return {
csmMatrix: "undefined", csmMatrix: "undefined",
csmResolution: "undefined", csmResolution: "undefined",
csmRatio: "undefined" csmRatio: "undefined",
}; };
}, },
computed: { computed: {
actions() { actions() {
return this.$store.getters["wot/thingDescription"]("camera_stage_mapping") return this.$store.getters["wot/thingDescription"]("camera_stage_mapping").actions;
.actions;
}, },
properties() { properties() {
return this.$store.getters["wot/thingDescription"]("camera_stage_mapping") return this.$store.getters["wot/thingDescription"]("camera_stage_mapping").properties;
.properties; },
}
}, },
methods: { methods: {
@ -87,10 +86,7 @@ export default {
}, },
getCalibrationData: async function() { getCalibrationData: async function() {
try { try {
let data = await this.readThingProperty( let data = await this.readThingProperty("camera_stage_mapping", "last_calibration");
"camera_stage_mapping",
"last_calibration"
);
if (data == {}) { if (data == {}) {
throw "No calibration data available."; throw "No calibration data available.";
} }
@ -107,33 +103,29 @@ export default {
}, },
updateDisplayedCSM: async function() { updateDisplayedCSM: async function() {
let csmMatrix = await this.readThingProperty( let csmMatrix = await this.readThingProperty(
"camera_stage_mapping",
"image_to_stage_displacement_matrix"
);
this.csmResolution = await this.readThingProperty(
"camera_stage_mapping", "camera_stage_mapping",
"image_resolution" "image_to_stage_displacement_matrix",
);
let streamResolution = await this.readThingProperty(
"camera",
"stream_resolution"
); );
this.csmResolution = await this.readThingProperty("camera_stage_mapping", "image_resolution");
let streamResolution = await this.readThingProperty("camera", "stream_resolution");
csmMatrix[0][0] = Number(csmMatrix[0][0].toFixed(3)); csmMatrix[0][0] = Number(csmMatrix[0][0].toFixed(3));
csmMatrix[0][1] = Number(csmMatrix[0][1].toFixed(3)); csmMatrix[0][1] = Number(csmMatrix[0][1].toFixed(3));
csmMatrix[1][0] = Number(csmMatrix[1][0].toFixed(3)); csmMatrix[1][0] = Number(csmMatrix[1][0].toFixed(3));
csmMatrix[1][1] = Number(csmMatrix[1][1].toFixed(3)); csmMatrix[1][1] = Number(csmMatrix[1][1].toFixed(3));
this.csmMatrix = csmMatrix; this.csmMatrix = csmMatrix;
this.csmRatio = Number((Math.abs(csmMatrix[1][0]) + Math.abs(csmMatrix[0][1])) / 2).toFixed(3); this.csmRatio = Number((Math.abs(csmMatrix[1][0]) + Math.abs(csmMatrix[0][1])) / 2).toFixed(
3,
);
this.csmFOV = [ this.csmFOV = [
Number((streamResolution[0]**2 * this.csmRatio / this.csmResolution[1]).toFixed(0)), Number(((streamResolution[0] ** 2 * this.csmRatio) / this.csmResolution[1]).toFixed(0)),
Number((streamResolution[1]**2 * this.csmRatio / this.csmResolution[0]).toFixed(0)), Number(((streamResolution[1] ** 2 * this.csmRatio) / this.csmResolution[0]).toFixed(0)),
] ];
}, },
onRecalibrateResponse: function() { onRecalibrateResponse: function() {
this.modalNotify("Finished stage-to-camera calibration."); this.modalNotify("Finished stage-to-camera calibration.");
this.updateDisplayedCSM(); this.updateDisplayedCSM();
} },
} },
}; };
</script> </script>

View file

@ -34,8 +34,19 @@ export default {
}, },
set(value) { set(value) {
this.$store.commit("changeAppTheme", value); this.$store.commit("changeAppTheme", value);
} },
} },
},
watch: {
appTheme: function() {
this.setLocalStorageObj("appTheme", this.appTheme);
},
},
mounted() {
// Try loading settings from localStorage. If null, don't change.
this.appTheme = this.getLocalStorageObj("appTheme") || this.appTheme;
}, },
methods: { methods: {
@ -45,19 +56,8 @@ export default {
} else { } else {
await document.exitFullscreen(); await document.exitFullscreen();
} }
} },
}, },
watch: {
appTheme: function() {
this.setLocalStorageObj("appTheme", this.appTheme);
}
},
mounted() {
// Try loading settings from localStorage. If null, don't change.
this.appTheme = this.getLocalStorageObj("appTheme") || this.appTheme;
}
}; };
</script> </script>

View file

@ -33,7 +33,7 @@ export default {
components: { components: {
cameraCalibrationSettings, cameraCalibrationSettings,
miniStreamDisplay, miniStreamDisplay,
ServerSpecifiedPropertyControl ServerSpecifiedPropertyControl,
}, },
data() { data() {
@ -45,15 +45,12 @@ export default {
computed: { computed: {
cameraUri: function() { cameraUri: function() {
return `${this.$store.getters.baseUri}/camera/`; return `${this.$store.getters.baseUri}/camera/`;
} },
}, },
async created() { async created() {
this.manualCameraSettings = await this.readThingProperty( this.manualCameraSettings = await this.readThingProperty("camera", "manual_camera_settings");
"camera", },
"manual_camera_settings"
);
}
}; };
</script> </script>

View file

@ -28,44 +28,44 @@ export default {
name: "CameraCalibrationSettings", name: "CameraCalibrationSettings",
components: { components: {
ServerSpecifiedActionButton ServerSpecifiedActionButton,
}, },
props: { props: {
showExtraSettings: { showExtraSettings: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: true,
}, },
cameraUri: { cameraUri: {
type: String, type: String,
required: true required: true,
} },
}, },
data() { data() {
return { return {
primaryCalibrationActions: [], primaryCalibrationActions: [],
secondaryCalibrationActions: [] secondaryCalibrationActions: [],
}; };
}, },
computed: { computed: {
actions() { actions() {
return this.$store.getters["wot/thingDescription"]("camera").actions; return this.$store.getters["wot/thingDescription"]("camera").actions;
} },
}, },
async created() { async created() {
this.primaryCalibrationActions = await this.readThingProperty( this.primaryCalibrationActions = await this.readThingProperty(
"camera", "camera",
"primary_calibration_actions" "primary_calibration_actions",
); );
this.secondaryCalibrationActions = await this.readThingProperty( this.secondaryCalibrationActions = await this.readThingProperty(
"camera", "camera",
"secondary_calibration_actions" "secondary_calibration_actions",
); );
} },
}; };
</script> </script>

View file

@ -1,31 +1,30 @@
<template> <template>
<div id="stageSettings" class="uk-width-large" v-observe-visibility="visibilityChanged"> <div id="stageSettings" v-observe-visibility="visibilityChanged" class="uk-width-large">
The microscope stage is a <b>{{ stageType }}</b> The microscope stage is a <b>{{ stageType }}</b>
<div> <div>
<div class="uk-margin"> <div class="uk-margin">
<p>Your z motor is currently {{ z_inverted }} inverted.</p>
<p> <p>
<br> We expect that moving in +z:
Your z motor is currently {{ this.z_inverted }} inverted. </p>
<br>
<br>
We expect that moving in +z:
<ul> <ul>
<li> Moves your objective up, towards the sample and illumination</li> <li>Moves your objective up, towards the sample and illumination</li>
<li> Turns the exposed z gear anti-clockwise (when viewed from above)</li> <li>Turns the exposed z gear anti-clockwise (when viewed from above)</li>
</ul> </ul>
If this is not the case, click the button below to switch. <p>
If this is not the case, click the button below to switch.
</p> </p>
<div class="uk-margin"> <div class="uk-margin">
<div class="uk-margin"> <div class="uk-margin">
<action-button <action-button
class="uk-width-1-2" class="uk-width-1-2"
thing="stage" thing="stage"
action="invert_axis_direction" action="invert_axis_direction"
:submit-data="{ axis: 'z' }" :submit-data="{ axis: 'z' }"
submit-label="Invert z" submit-label="Invert z"
@response=readAxis() @response="readAxis()"
/> />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@ -48,26 +47,23 @@ export default {
}; };
}, },
methods:{ computed: {
stageType: function() {
return this.thingDescription("stage").title;
},
},
methods: {
visibilityChanged(isVisible) { visibilityChanged(isVisible) {
if (isVisible) { if (isVisible) {
this.readAxis(); this.readAxis();
} }
}, },
async readAxis() { async readAxis() {
let axes_inverted = await this.readThingProperty( let axes_inverted = await this.readThingProperty("stage", "axis_inverted");
"stage", this.z_inverted = axes_inverted["z"] ? "" : "not ";
"axis_inverted" },
);
this.z_inverted = axes_inverted['z'] ? "" : "not ";
}
}, },
computed: {
stageType: function() {
return this.thingDescription("stage").title;
}
}
}; };
</script> </script>

View file

@ -3,8 +3,8 @@
<div> <div>
<h3>Stream Settings</h3> <h3>Stream Settings</h3>
<label <label
><input v-model="disableStream" class="uk-checkbox" type="checkbox" /> ><input v-model="disableStream" class="uk-checkbox" type="checkbox" /> Disable Web
Disable Web Stream</label Stream</label
> >
<p class="uk-margin-small"> <p class="uk-margin-small">
This will disable the embedded web stream of the camera. This will disable the embedded web stream of the camera.
@ -29,8 +29,8 @@ export default {
}, },
set(value) { set(value) {
this.$store.commit("changeDisableStream", value); this.$store.commit("changeDisableStream", value);
} },
} },
}, },
watch: { watch: {
@ -38,8 +38,8 @@ export default {
// (the next 3 functions all relate to this) // (the next 3 functions all relate to this)
disableStream: function(newValue) { disableStream: function(newValue) {
this.setLocalStorageObj("disableStream", newValue); this.setLocalStorageObj("disableStream", newValue);
} },
} },
}; };
</script> </script>

View file

@ -1,9 +1,7 @@
<template> <template>
<!-- Grid managing tab content --> <!-- Grid managing tab content -->
<div 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">
<calibrationWizard <calibrationWizard ref="calibrationWizard"></calibrationWizard>
ref="calibrationWizard"
></calibrationWizard>
<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>
@ -22,11 +20,13 @@
</li> </li>
<li class="uk-nav-header">Microscope Settings</li> <li class="uk-nav-header">Microscope Settings</li>
<button <button
submit-label="Launch Calibration Wizard" submit-label="Launch Calibration Wizard"
type="button" type="button"
class="uk-button uk-button-default uk-width-1-1" class="uk-button uk-button-default uk-width-1-1"
@click="startModals" @click="startModals"
>Launch Calibration Wizard</button> >
Launch Calibration Wizard
</button>
<li> <li>
<tabIcon <tabIcon
id="settings-camera-icon" id="settings-camera-icon"
@ -69,42 +69,26 @@
</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 <tabContent tab-i-d="display" :require-connection="false" :current-tab="currentTab">
tab-i-d="display"
:require-connection="false"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small"> <div class="settings-pane uk-padding-small">
<appSettings /> <appSettings />
<streamSettings /> <streamSettings />
</div> </div>
</tabContent> </tabContent>
<tabContent <tabContent tab-i-d="camera" :require-connection="true" :current-tab="currentTab">
tab-i-d="camera"
:require-connection="true"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small"> <div class="settings-pane uk-padding-small">
<cameraSettings /> <cameraSettings />
</div> </div>
</tabContent> </tabContent>
<tabContent <tabContent tab-i-d="stage" :require-connection="true" :current-tab="currentTab">
tab-i-d="stage"
:require-connection="true"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small"> <div class="settings-pane uk-padding-small">
<stageSettings /> <stageSettings />
</div> </div>
</tabContent> </tabContent>
<tabContent <tabContent tab-i-d="mapping" :require-connection="true" :current-tab="currentTab">
tab-i-d="mapping"
:require-connection="true"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small"> <div class="settings-pane uk-padding-small">
<CSMSettings /> <CSMSettings />
</div> </div>
@ -136,13 +120,13 @@ export default {
appSettings, appSettings,
tabIcon, tabIcon,
tabContent, tabContent,
calibrationWizard calibrationWizard,
}, },
data: function() { data: function() {
return { return {
selected: "display", selected: "display",
currentTab: "display" currentTab: "display",
}; };
}, },
@ -154,8 +138,8 @@ export default {
}, },
startModals: function() { startModals: function() {
this.$refs.calibrationWizard.force_show(); this.$refs.calibrationWizard.force_show();
} },
} },
}; };
</script> </script>

View file

@ -29,7 +29,7 @@
thing-name="autofocus" thing-name="autofocus"
property-name="stack_dz" property-name="stack_dz"
label="Stack dz (steps)" label="Stack dz (steps)"
/> />
</div> </div>
<div class="uk-margin"> <div class="uk-margin">
<propertyControl <propertyControl
@ -83,12 +83,7 @@
</ul> </ul>
<label class="uk-form-label" for="form-stacked-text">Sample ID</label> <label class="uk-form-label" for="form-stacked-text">Sample ID</label>
<div class="uk-form-controls"> <div class="uk-form-controls">
<input <input v-model="scan_name" class="uk-input uk-form-small" type="text" name="Scan Name" />
v-model="scan_name"
class="uk-input uk-form-small"
type="text"
name="Scan Name"
/>
</div> </div>
<div class="uk-margin"> <div class="uk-margin">
<action-button <action-button
@ -110,11 +105,7 @@
Live stitching preview Live stitching preview
</h2> </h2>
<mini-stream-display v-if="displayImageOnRight" /> <mini-stream-display v-if="displayImageOnRight" />
<action-log-display <action-log-display id="log-display" :log="log" :task-status="taskStatus" />
id="log-display"
:log="log"
:task-status="taskStatus"
/>
<action-progress-bar :progress="progress" :task-status="taskStatus" /> <action-progress-bar :progress="progress" :task-status="taskStatus" />
<button <button
v-if="cancellable" v-if="cancellable"
@ -124,11 +115,7 @@
> >
Cancel Cancel
</button> </button>
<div <div v-if="!cancellable" class="uk-margin uk-grid-small uk-child-width-expand" uk-grid>
v-if="!cancellable"
class="uk-margin uk-grid-small uk-child-width-expand"
uk-grid
>
<button <button
type="button" type="button"
class="uk-button" class="uk-button"
@ -182,7 +169,7 @@ export default {
actionLogDisplay, actionLogDisplay,
actionProgressBar, actionProgressBar,
MiniStreamDisplay, MiniStreamDisplay,
ActionButton ActionButton,
}, },
data() { data() {
@ -195,7 +182,7 @@ export default {
progress: null, progress: null,
log: [], log: [],
lastStitchedImage: null, lastStitchedImage: null,
scan_name: "" scan_name: "",
}; };
}, },
@ -208,7 +195,7 @@ export default {
}, },
displayImageOnRight() { displayImageOnRight() {
return this.scanning & (this.lastStitchedImage !== null); return this.scanning & (this.lastStitchedImage !== null);
} },
}, },
methods: { methods: {
@ -225,17 +212,11 @@ export default {
async pollScan() { async pollScan() {
if (this.cancellable) { if (this.cancellable) {
// while the scan is running // while the scan is running
let mtime = await this.readThingProperty( let mtime = await this.readThingProperty("smart_scan", "latest_preview_stitch_time");
"smart_scan",
"latest_preview_stitch_time"
);
if (mtime !== null) { if (mtime !== null) {
this.lastStitchedImage = `${this.$store.getters.baseUri}/smart_scan/latest_preview_stitch.jpg?t=${mtime}`; this.lastStitchedImage = `${this.$store.getters.baseUri}/smart_scan/latest_preview_stitch.jpg?t=${mtime}`;
} }
this.lastScanName = await this.readThingProperty( this.lastScanName = await this.readThingProperty("smart_scan", "latest_scan_name");
"smart_scan",
"latest_scan_name"
);
setTimeout(this.pollScan, 1000); // keep rescheduling until it's stopped setTimeout(this.pollScan, 1000); // keep rescheduling until it's stopped
} }
}, },
@ -249,8 +230,8 @@ export default {
console.log(link); console.log(link);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
} },
} },
}; };
</script> </script>

View file

@ -44,7 +44,7 @@ export default {
isVisible: false, isVisible: false,
displaySize: [0, 0], displaySize: [0, 0],
displayPosition: [0, 0], displayPosition: [0, 0],
resizeTimeoutId: setTimeout(this.doneResizing, 500) resizeTimeoutId: setTimeout(this.doneResizing, 500),
}; };
}, },
@ -58,7 +58,7 @@ export default {
}, },
streamImgUri: function() { streamImgUri: function() {
return `${this.$store.getters.baseUri}/camera/mjpeg_stream`; return `${this.$store.getters.baseUri}/camera/mjpeg_stream`;
} },
}, },
mounted() { mounted() {
@ -119,18 +119,14 @@ export default {
// factor. // factor.
let scale = Math.max( let scale = Math.max(
event.target.naturalWidth / event.target.offsetWidth, event.target.naturalWidth / event.target.offsetWidth,
event.target.naturalHeight / event.target.offsetHeight event.target.naturalHeight / event.target.offsetHeight,
); );
let xRelative = (0.5 * event.target.offsetWidth - xCoordinate) * scale; let xRelative = (0.5 * event.target.offsetWidth - xCoordinate) * scale;
let yRelative = (0.5 * event.target.offsetHeight - yCoordinate) * scale; let yRelative = (0.5 * event.target.offsetHeight - yCoordinate) * scale;
// Emit a signal to move, acted on by paneControl.vue // Emit a signal to move, acted on by paneControl.vue
this.$root.$emit( this.$root.$emit("globalMoveInImageCoordinatesEvent", -xRelative, -yRelative);
"globalMoveInImageCoordinatesEvent",
-xRelative,
-yRelative
);
}, },
handleResize: function() { handleResize: function() {
@ -167,18 +163,13 @@ export default {
let windowChromeHeight = window.outerHeight - window.innerHeight; let windowChromeHeight = window.outerHeight - window.innerHeight;
let elementPositionOnDisplay = [ let elementPositionOnDisplay = [
Math.max(0, windowPositionOnDisplay[0] + elementPositionOnWindow[0]), Math.max(0, windowPositionOnDisplay[0] + elementPositionOnWindow[0]),
Math.max( Math.max(0, windowPositionOnDisplay[1] + elementPositionOnWindow[1] + windowChromeHeight),
0,
windowPositionOnDisplay[1] +
elementPositionOnWindow[1] +
windowChromeHeight
)
]; ];
this.displaySize = elementSize; this.displaySize = elementSize;
this.displayPosition = elementPositionOnDisplay; this.displayPosition = elementPositionOnDisplay;
} },
} },
}; };
</script> </script>

View file

@ -14,7 +14,7 @@ export default {
name: "ViewContent", name: "ViewContent",
components: { components: {
streamDisplay streamDisplay,
} },
}; };
</script> </script>

View file

@ -6,19 +6,18 @@ 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 version from "./version.js";
// UIKit overrides // UIKit overrides
UIkit.mixin( UIkit.mixin(
{ {
data: { data: {
animation: false animation: false,
} },
}, },
"accordion" "accordion",
); );
// Use visibility observer // Use visibility observer
@ -38,12 +37,7 @@ Vue.mixin({
return this.$store.getters["wot/thingAvailable"](thing); return this.$store.getters["wot/thingAvailable"](thing);
}, },
async readThingProperty(thing, property, silence_errors = false) { async readThingProperty(thing, property, silence_errors = false) {
let url = this.$store.getters["wot/thingPropertyUrl"]( let url = this.$store.getters["wot/thingPropertyUrl"](thing, property, "readproperty", false);
thing,
property,
"readproperty",
false
);
try { try {
let response = await axios.get(url); let response = await axios.get(url);
return response.data; return response.data;
@ -57,7 +51,7 @@ Vue.mixin({
thing, thing,
property, property,
"writeproperty", "writeproperty",
false false,
); );
// `false` fails because axios somehow eats it! // `false` fails because axios somehow eats it!
// Other values should not be stringified or pydantic // Other values should not be stringified or pydantic
@ -68,12 +62,7 @@ Vue.mixin({
await axios.put(url, value); await axios.put(url, value);
}, },
async invokeAction(thing, action, data) { async invokeAction(thing, action, data) {
let url = this.$store.getters["wot/thingActionUrl"]( let url = this.$store.getters["wot/thingActionUrl"](thing, action, "invokeaction", false);
thing,
action,
"invokeaction",
false
);
try { try {
let response = await axios.post(url, data); let response = await axios.post(url, data);
return response; return response;
@ -87,7 +76,7 @@ Vue.mixin({
thing, thing,
action, action,
"invokeaction", "invokeaction",
allow_missing allow_missing,
); );
return url; return url;
}, },
@ -109,14 +98,14 @@ Vue.mixin({
}, },
function() { function() {
reject(); reject();
} },
) )
.finally(function() { .finally(function() {
// Re-enable the GPU preview, if it was active before the modal // Re-enable the GPU preview, if it was active before the modal
if (context.$store.state.autoGpuPreview) { if (context.$store.state.autoGpuPreview) {
context.$root.$emit("globalTogglePreview", true); context.$root.$emit("globalTogglePreview", true);
} }
context.$root.$emit("modalClosed"); context.$root.$emit("modalClosed");
}); });
}; };
return new Promise(showModal); return new Promise(showModal);
@ -125,7 +114,7 @@ Vue.mixin({
modalNotify: function(message, status = "success") { modalNotify: function(message, status = "success") {
UIkit.notification({ UIkit.notification({
message: message, message: message,
status: status status: status,
}); });
}, },
@ -140,7 +129,7 @@ Vue.mixin({
<p>${message}</p> <p>${message}</p>
</div> </div>
`, `,
{ stack: true } { stack: true },
); );
}, },
@ -149,25 +138,25 @@ Vue.mixin({
this.$store.commit("setErrorMessage", errormsg); this.$store.commit("setErrorMessage", errormsg);
UIkit.notification({ UIkit.notification({
message: `${errormsg}`, message: `${errormsg}`,
status: "danger" status: "danger",
}); });
}, },
getErrorMessage: function(error) { getErrorMessage: function(error) {
// Format the error. // Format the error.
let data = this.getErrorData(error) let data = this.getErrorData(error);
// Get error data may get an object. Handle edge cases and if not try to use // 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. // JSON to get the best string. This stops "objectObject" showing as an error.
if (data === null) return "null"; if (data === null) return "null";
if (data === undefined) return "undefined"; if (data === undefined) return "undefined";
if (typeof data === 'string') return data; if (typeof data === "string") return data;
try { try {
return JSON.stringify(data, null, 2); return JSON.stringify(data, null, 2);
} catch (err) { } catch (err) {
return String(data); return String(data);
} }
}, },
getErrorData: function(error){ getErrorData: function(error) {
// If a response was obtained, extract the most specific message // If a response was obtained, extract the most specific message
if (error.response) { if (error.response) {
// If the response is a nicely formatted JSON response from the server // If the response is a nicely formatted JSON response from the server
@ -176,7 +165,7 @@ Vue.mixin({
} }
if (error.response.data.detail) { if (error.response.data.detail) {
try { try {
return error.response.data.detail[0].msg return error.response.data.detail[0].msg;
} catch (err) { } catch (err) {
return error.response.data.detail; return error.response.data.detail;
} }
@ -219,11 +208,11 @@ Vue.mixin({
setLocalStorageObj: function(keyName, object) { setLocalStorageObj: function(keyName, object) {
const parsed = JSON.stringify(object); const parsed = JSON.stringify(object);
localStorage.setItem(keyName, parsed); localStorage.setItem(keyName, parsed);
} },
} },
}); });
new Vue({ new Vue({
store, store,
render: h => h(App) render: h => h(App),
}).$mount("#app"); }).$mount("#app");

View file

@ -19,7 +19,7 @@ function getOriginFromLocation() {
export default new Vuex.Store({ export default new Vuex.Store({
modules: { modules: {
wot: wotStoreModule wot: wotStoreModule,
}, },
state: { state: {
origin: getOriginFromLocation(), origin: getOriginFromLocation(),
@ -32,7 +32,7 @@ export default new Vuex.Store({
galleryEnabled: true, galleryEnabled: true,
appTheme: "system", appTheme: "system",
activeStreams: {}, activeStreams: {},
microscopeHostname: "" microscopeHostname: "",
}, },
mutations: { mutations: {
@ -77,13 +77,13 @@ export default new Vuex.Store({
}, },
changeMicroscopeHostname(state, value) { changeMicroscopeHostname(state, value) {
state.microscopeHostname = value; state.microscopeHostname = value;
} },
}, },
actions: {}, actions: {},
getters: { getters: {
baseUri: state => state.origin, baseUri: state => state.origin,
ready: state => state.available ready: state => state.available,
} },
}); });

View file

@ -5,7 +5,7 @@ export const wotStoreModule = {
state: () => ({ state: () => ({
thingDescriptions: {}, thingDescriptions: {},
servient: null, servient: null,
helpers: null helpers: null,
}), }),
mutations: { mutations: {
addThingDescription(state, { thingName, thingDescription }) { addThingDescription(state, { thingName, thingDescription }) {
@ -16,7 +16,7 @@ export const wotStoreModule = {
}, },
removeAllThingDescriptions(state) { removeAllThingDescriptions(state) {
state.thingDescriptions = {}; state.thingDescriptions = {};
} },
}, },
actions: { actions: {
async start() { async start() {
@ -36,7 +36,7 @@ export const wotStoreModule = {
.pop(); .pop();
commit("addThingDescription", { commit("addThingDescription", {
thingName: thing_name, thingName: thing_name,
thingDescription: td thingDescription: td,
}); });
}, },
async fetchThingDescriptions({ commit }, uri) { async fetchThingDescriptions({ commit }, uri) {
@ -47,10 +47,10 @@ export const wotStoreModule = {
let thing_name = k.replace(/\/$/, "").replace(/^\//, ""); let thing_name = k.replace(/\/$/, "").replace(/^\//, "");
commit("addThingDescription", { commit("addThingDescription", {
thingName: thing_name, thingName: thing_name,
thingDescription: response.data[k] thingDescription: response.data[k],
}); });
} }
} },
}, },
getters: { getters: {
thingDescriptions: state => { thingDescriptions: state => {
@ -62,13 +62,7 @@ export const wotStoreModule = {
thingAvailable: state => thingName => { thingAvailable: state => thingName => {
return thingName in state.thingDescriptions; return thingName in state.thingDescriptions;
}, },
thingFormUrl: state => ( thingFormUrl: state => (thing, affordanceType, affordance, op, allowUndefined = true) => {
thing,
affordanceType,
affordance,
op,
allowUndefined = true
) => {
// Find the URL for a particular operation // Find the URL for a particular operation
let td = state.thingDescriptions[thing]; let td = state.thingDescriptions[thing];
if (!td) { if (!td) {
@ -91,31 +85,15 @@ export const wotStoreModule = {
} }
return href; return href;
}, },
thingPropertyUrl: (_state, getters) => ( thingPropertyUrl: (_state, getters) => (thing, property, op, allowUndefined) => {
thing,
property,
op,
allowUndefined
) => {
// Find the URL for a particular property // Find the URL for a particular property
return getters.thingFormUrl( return getters.thingFormUrl(thing, "properties", property, op, allowUndefined);
thing,
"properties",
property,
op,
allowUndefined
);
}, },
thingActionUrl: (_state, getters) => ( thingActionUrl: (_state, getters) => (thing, action, op, allowUndefined) => {
thing,
action,
op,
allowUndefined
) => {
// Find the URL for a particular action // Find the URL for a particular action
return getters.thingFormUrl(thing, "actions", action, op, allowUndefined); return getters.thingFormUrl(thing, "actions", action, op, allowUndefined);
} },
} },
}; };
export function findFormHref(affordance, op) { export function findFormHref(affordance, op) {

View file

@ -1,3 +1,3 @@
module.exports = { module.exports = {
outputDir: "../src/openflexure_microscope_server/static" outputDir: "../src/openflexure_microscope_server/static",
}; };