Merge branch 'start-gallery-thing' into 'v3'

Start implementing GalleryThing

See merge request openflexure/openflexure-microscope-server!576
This commit is contained in:
Joe Knapper 2026-05-07 09:51:47 +00:00
commit 906b8be82c
13 changed files with 321 additions and 104 deletions

View file

@ -8,6 +8,7 @@
}, },
"stage": "openflexure_microscope_server.things.stage.sangaboard:SangaboardThing", "stage": "openflexure_microscope_server.things.stage.sangaboard:SangaboardThing",
"autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing", "autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing",
"gallery": "openflexure_microscope_server.things.gallery.GalleryThing",
"camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", "camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper",
"system": "openflexure_microscope_server.things.system:OpenFlexureSystem", "system": "openflexure_microscope_server.things.system:OpenFlexureSystem",
"illumination": "openflexure_microscope_server.things.illumination:SangaIllumination", "illumination": "openflexure_microscope_server.things.illumination:SangaIllumination",

View file

@ -3,6 +3,7 @@
"camera": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera", "camera": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera",
"stage": "openflexure_microscope_server.things.stage.dummy:DummyStage", "stage": "openflexure_microscope_server.things.stage.dummy:DummyStage",
"autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing", "autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing",
"gallery": "openflexure_microscope_server.things.gallery.GalleryThing",
"camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", "camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper",
"illumination": "openflexure_microscope_server.things.illumination.SimulatorIllumination", "illumination": "openflexure_microscope_server.things.illumination.SimulatorIllumination",
"system": "openflexure_microscope_server.things.system:OpenFlexureSystem", "system": "openflexure_microscope_server.things.system:OpenFlexureSystem",

Binary file not shown.

View file

@ -335,14 +335,25 @@ class ScanDirectoryManager:
return [f.name for f in os.scandir(self._base_scan_dir) if f.is_dir()] return [f.name for f in os.scandir(self._base_scan_dir) if f.is_dir()]
@requires_lock @requires_lock
def all_scans_info(self, ongoing: Optional[str] = None) -> list[ScanInfo]: def all_scans_info(
"""Return a lists of ScanInfo objects for each scan.""" self, *, ongoing: Optional[str] = None, include_ongoing: bool = True
) -> list[ScanInfo]:
"""Return a lists of ScanInfo objects for each scan.
:param ongoing: The name of the ongoing scan (or None if no scan is ongoing).
:param include_ongoing: True (dfault) to include the scan info of the ongoing
scan in the return. False to exclude it.
:return: A list of ScanInfo objects for each scan.
"""
all_info: list[ScanInfo] = [] all_info: list[ScanInfo] = []
for scan_name in self.all_scans: for scan_name in self.all_scans:
# If the scan is ongoing send flag to skip reading the json data scan_is_ongoing = scan_name == ongoing
skip_json = scan_name == ongoing if scan_is_ongoing and not include_ongoing:
continue
scan_dir = ScanDirectory(scan_name, self.base_dir) scan_dir = ScanDirectory(scan_name, self.base_dir)
info = scan_dir.scan_info(skip_json=skip_json) # If the scan is ongoing send flag to skip reading the json data
info = scan_dir.scan_info(skip_json=scan_is_ongoing)
all_info.append(info) all_info.append(info)
return all_info return all_info

View file

@ -15,6 +15,13 @@ class OFMThing(lt.Thing):
_data_dir: Optional[str] = None _data_dir: Optional[str] = None
_show_data_in_gallery: bool = False
@property
def show_data_in_gallery(self) -> bool:
"""Whether to show in the Gallery."""
return self._show_data_in_gallery
def __enter__(self) -> Self: def __enter__(self) -> Self:
"""Set the data directory when the Thing is entered.""" """Set the data directory when the Thing is entered."""
# Note that the `application_config` was already validated when the # Note that the `application_config` was already validated when the

View file

@ -25,6 +25,7 @@ from pydantic import BaseModel, Field
import labthings_fastapi as lt import labthings_fastapi as lt
from labthings_fastapi.types.numpy import NDArray from labthings_fastapi.types.numpy import NDArray
from openflexure_microscope_server.things import OFMThing
from openflexure_microscope_server.things.background_detect import ( from openflexure_microscope_server.things.background_detect import (
BackgroundDetectAlgorithm, BackgroundDetectAlgorithm,
) )
@ -164,7 +165,7 @@ class CameraMemoryBuffer:
del self._storage[key] del self._storage[key]
class BaseCamera(lt.Thing): class BaseCamera(OFMThing):
"""The base class for all cameras. All cameras must directly inherit from this class. """The base class for all cameras. All cameras must directly inherit from this class.
The connection to the camera hardware should be added to the ``__enter__`` method not The connection to the camera hardware should be added to the ``__enter__`` method not

View file

@ -0,0 +1,99 @@
"""Server-side gallery functionality.
The types of data that are captured and how they display in the gallery are defined by
the Things that capture the Data. This thing just provides a unified way for the gallery
front end to access the data.
"""
from typing import Any, Mapping, Optional, Protocol, Self, cast, runtime_checkable
from pydantic import BaseModel
import labthings_fastapi as lt
from openflexure_microscope_server.things import OFMThing
@runtime_checkable
class GalleryCompatibleThing(Protocol):
"""Protocol for checking Things using the gallery are complete.
Note that runtime checkable protocols only check that the methods exist, not the
full signatures. This means that anything attempting to define the methods will
be picked up, but may throw an error when used.
"""
# Ensure it is a Thing:
_thing_server_interface: lt.ThingServerInterface
# Ensure it is an OFMThing:
show_data_in_gallery: bool
gallery_data_name: str
gallery_data_schema: type[BaseModel]
# Ignore D102: No docstrings for the protocol.
def get_data_for_gallery(self) -> list[BaseModel]: ... # noqa: D102
class GalleryThing(lt.Thing):
"""A Thing for communicating with the front end gallery."""
all_ofm_things: Mapping[str, OFMThing] = lt.thing_slot()
_gallery_providing_things: Optional[Mapping[str, GalleryCompatibleThing]] = None
@property
def gallery_providing_things(self) -> Mapping[str, GalleryCompatibleThing]:
"""All Things that provide data to the gallery."""
if self._gallery_providing_things is None:
raise RuntimeError(
"Cannot access gallery_providing_things before server has started."
)
return self._gallery_providing_things
def __enter__(self) -> Self:
"""Check for all gallery providing things on server startup."""
self._set_gallery_providers()
return self
def _set_gallery_providers(self) -> None:
"""Set the mapping of gallery providing things.
This is called on ``__enter__`` when the server starts.
"""
gallery_providers = {
name: thing
for name, thing in self.all_ofm_things.items()
if thing.show_data_in_gallery
}
# cache initial list of keys as it may change in the loop
keys = list(gallery_providers.keys())
for key in keys:
if not isinstance(gallery_providers[key], GalleryCompatibleThing):
self.logger.error(
f"Data from {key} cannot be shown in gallery as it does not "
"provide all necessary properties/methods for the gallery."
)
gallery_providers.pop(key)
# Cast the type of each thing to "GalleryCompatibleThing" as other Things have
# been popped.
self._gallery_providing_things = cast(
Mapping[str, GalleryCompatibleThing], gallery_providers
)
@lt.property
def list_data(self) -> list[dict[str, Any]]:
"""List the data from all registered things.
Currently only works with `smart_scan` as the UI cards are not customisable.
This will change to an action (or another type of endpoint) at a
later date to enable filtering, and returning only a specific page.
"""
data_list = []
for thing in self.gallery_providing_things.values():
data_list += [model.model_dump() for model in thing.get_data_for_gallery()]
return data_list

View file

@ -71,16 +71,6 @@ class ActiveScanData(scan_directories.BaseScanData):
self.scan_result = result self.scan_result = result
class ScanListInfo(BaseModel):
"""The information to be sent to the Scan List tab."""
scans: list[scan_directories.ScanInfo]
"""The list of scans as ScanInfo objects"""
ongoing: Optional[str]
"""The name of the ongoing scan or None"""
class JPEGBlob(lt.blob.Blob): class JPEGBlob(lt.blob.Blob):
"""A class representing a JPEG image as a LabThings FastAPI Blob.""" """A class representing a JPEG image as a LabThings FastAPI Blob."""
@ -176,6 +166,31 @@ class SmartScanThing(OFMThing):
In this case it doesn't need to do anything. In this case it doesn't need to do anything.
""" """
# Register with gallery.
_show_data_in_gallery = True
@property
def gallery_data_name(self) -> str:
"""Name under which data shows up in gallery."""
return "Scans"
@property
def gallery_data_schema(self) -> type[scan_directories.ScanInfo]:
"""The schema (BaseModel) for passing data to the gallery."""
return scan_directories.ScanInfo
def get_data_for_gallery(self) -> list[scan_directories.ScanInfo]:
"""Return all the information from the scan directories.
It is preferable to use the method rather than calling
_scan_dir_manager.all_scans_info() directly as it will handle stopping the json
in any ongoing scans being read.
"""
ongoing_name = None if self._ongoing_scan is None else self.ongoing_scan.name
return self._scan_dir_manager.all_scans_info(
ongoing=ongoing_name, include_ongoing=False
)
# Note that the default detector name is set at init. This is over written if # Note that the default detector name is set at init. This is over written if
# setting is loaded from disk. # setting is loaded from disk.
@lt.setting @lt.setting
@ -544,31 +559,6 @@ class SmartScanThing(OFMThing):
stitch_automatically: bool = lt.setting(default=True) stitch_automatically: bool = lt.setting(default=True)
"""Whether to run a final stitch at the end of a successful scan.""" """Whether to run a final stitch at the end of a successful scan."""
def _get_all_scan_info(self) -> list[scan_directories.ScanInfo]:
"""Return all the information from the scan directories.
It is preferable to use the method rather than calling
_scan_dir_manager.all_scans_info() directly as it will handle stopping the json
in any ongoing scans being read.
"""
ongoing_name = None if self._ongoing_scan is None else self.ongoing_scan.name
return self._scan_dir_manager.all_scans_info(ongoing=ongoing_name)
@lt.property
def scans(self) -> ScanListInfo:
"""All the available scans.
Each scan has a name (which can be used to access it), along with
its modified and created times (according to the filesystem) and
the number of items in the ``images`` folder. Note that image count
uses a regular expression, and changes to the naming scheme will
break it.
"""
return ScanListInfo(
scans=self._get_all_scan_info(),
ongoing=None if self._ongoing_scan is None else self.ongoing_scan.name,
)
@lt.endpoint( @lt.endpoint(
"get", "get",
"get_stitch/{scan_name}", "get_stitch/{scan_name}",
@ -632,8 +622,8 @@ class SmartScanThing(OFMThing):
@lt.action @lt.action
def purge_empty_scans(self) -> None: def purge_empty_scans(self) -> None:
"""Delete all scan folders containing no images at the top level.""" """Delete all scan folders containing no images at the top level."""
# JSON is ignored as it's created before any images are captured # Use the scan list (the data read by the gallery) to check for empty scans.
for scan_info in self._get_all_scan_info(): for scan_info in self.get_data_for_gallery():
if scan_info.number_of_images == 0: if scan_info.number_of_images == 0:
self._delete_scan(scan_info.name) self._delete_scan(scan_info.name)
@ -748,6 +738,8 @@ class SmartScanThing(OFMThing):
""" """
if self._scan_lock.locked(): if self._scan_lock.locked():
raise RuntimeError("Can't stitch previous scans while a scan is ongoing") raise RuntimeError("Can't stitch previous scans while a scan is ongoing")
for scan in self._get_all_scan_info(): # Use the scan list (the data read by the gallery) to find any scans that
# need stitching.
for scan in self.get_data_for_gallery():
if scan.dzi is None: if scan.dzi is None:
self.stitch_scan(scan_name=scan.name) self.stitch_scan(scan_name=scan.name)

View file

@ -1,6 +1,8 @@
"""Fixtures for all test suites.""" """Fixtures for all test suites."""
import json
import logging import logging
import os
import re import re
from collections.abc import Iterable from collections.abc import Iterable
from contextlib import contextmanager from contextlib import contextmanager
@ -10,6 +12,24 @@ import pytest
from labthings_fastapi.testing import create_thing_without_server from labthings_fastapi.testing import create_thing_without_server
from .shared_utils.lt_test_utils import LabThingsTestEnv
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.dirname(THIS_DIR)
SIM_CONFIG = os.path.join(REPO_ROOT, "ofm_config_simulation.json")
@pytest.fixture
def simulation_test_env():
"""Yield a server with the configuration from the simulation json."""
with open(SIM_CONFIG, "r", encoding="utf-8") as f_obj:
config_dict = json.load(f_obj)
with LabThingsTestEnv(
things=config_dict["things"],
application_config=config_dict["application_config"],
) as env:
yield env
@pytest.fixture @pytest.fixture
def check_side_effect(caplog): def check_side_effect(caplog):

View file

@ -12,7 +12,6 @@ it has been moved as it artificaially inflated coverage.
""" """
import json import json
import os
import numpy as np import numpy as np
import piexif import piexif
@ -21,29 +20,11 @@ from PIL import Image
import labthings_fastapi as lt import labthings_fastapi as lt
from ..shared_utils.lt_test_utils import LabThingsTestEnv
THIS_DIR = os.path.dirname(os.path.abspath(__file__)) def test_autofocus(simulation_test_env):
REPO_ROOT = os.path.dirname(os.path.dirname(THIS_DIR))
SIM_CONFIG = os.path.join(REPO_ROOT, "ofm_config_simulation.json")
@pytest.fixture
def test_env():
"""Yield a server with a very basic configuration."""
with open(SIM_CONFIG, "r", encoding="utf-8") as f_obj:
config_dict = json.load(f_obj)
with LabThingsTestEnv(
things=config_dict["things"],
application_config=config_dict["application_config"],
) as env:
yield env
def test_autofocus(test_env):
"""Test Fast Autofocus can run doesn't raise an exception.""" """Test Fast Autofocus can run doesn't raise an exception."""
stage = test_env.get_thing_client("stage") stage = simulation_test_env.get_thing_client("stage")
autofocus = test_env.get_thing_client("autofocus") autofocus = simulation_test_env.get_thing_client("autofocus")
assert stage.position["z"] == 0 assert stage.position["z"] == 0
# Autofocus 5 times and check each ends within 500 steps # Autofocus 5 times and check each ends within 500 steps
for i in range(5): for i in range(5):
@ -51,17 +32,17 @@ def test_autofocus(test_env):
assert abs(stage.position["z"]) < 500, f"Autofocus failed on iteration {i}" assert abs(stage.position["z"]) < 500, f"Autofocus failed on iteration {i}"
def test_grab_jpeg(test_env): def test_grab_jpeg(simulation_test_env):
"""Check that grab_jpeg returns a blob that can be opened.""" """Check that grab_jpeg returns a blob that can be opened."""
camera = test_env.get_thing_client("camera") camera = simulation_test_env.get_thing_client("camera")
blob = camera.grab_jpeg() blob = camera.grab_jpeg()
image = Image.open(blob.open()) image = Image.open(blob.open())
assert image.size == (820, 616) assert image.size == (820, 616)
def test_capture_jpeg_metadata(test_env): def test_capture_jpeg_metadata(simulation_test_env):
"""Check that the position is encoded into the image metadata.""" """Check that the position is encoded into the image metadata."""
camera = test_env.get_thing_client("camera") camera = simulation_test_env.get_thing_client("camera")
blob = camera.capture_jpeg() blob = camera.capture_jpeg()
image = Image.open(blob.open()) image = Image.open(blob.open())
exif_dict = piexif.load(image.info["exif"]) exif_dict = piexif.load(image.info["exif"])
@ -72,9 +53,9 @@ def test_capture_jpeg_metadata(test_env):
assert image.size == (820, 616) assert image.size == (820, 616)
def test_stage(test_env): def test_stage(simulation_test_env):
"""Test moving the stage forwards and backwards.""" """Test moving the stage forwards and backwards."""
stage = test_env.get_thing_client("stage") stage = simulation_test_env.get_thing_client("stage")
start = stage.position start = stage.position
move = {"x": 1, "y": 2, "z": 3} move = {"x": 1, "y": 2, "z": 3}
move_back = {"x": -1, "y": -2, "z": -3} move_back = {"x": -1, "y": -2, "z": -3}
@ -95,20 +76,20 @@ def test_stage(test_env):
assert start["z"] == pos["z"] assert start["z"] == pos["z"]
def test_capture_array(test_env): def test_capture_array(simulation_test_env):
"""Capture array from simulation and check the size is as expected.""" """Capture array from simulation and check the size is as expected."""
camera = test_env.get_thing_client("camera") camera = simulation_test_env.get_thing_client("camera")
array = np.asarray(camera.capture_array()) array = np.asarray(camera.capture_array())
assert array.shape == (616, 820, 3) assert array.shape == (616, 820, 3)
def test_camera_stage_mapping_calibration(test_env): def test_camera_stage_mapping_calibration(simulation_test_env):
"""Check that camera stage mapping runs and returns the expected result.""" """Check that camera stage mapping runs and returns the expected result."""
camera = test_env.get_thing_client("camera") camera = simulation_test_env.get_thing_client("camera")
# Remove camera settling time for speed. # Remove camera settling time for speed.
camera.settling_time = 0 camera.settling_time = 0
csm = test_env.get_thing_client("camera_stage_mapping") csm = simulation_test_env.get_thing_client("camera_stage_mapping")
stage = test_env.get_thing_client("stage") stage = simulation_test_env.get_thing_client("stage")
# Check it starts uncalibrated # Check it starts uncalibrated
assert csm.calibration_required assert csm.calibration_required

View file

@ -0,0 +1,122 @@
"""Tests that captures have the expected metadata."""
import logging
from pydantic import BaseModel
from labthings_fastapi.testing import create_thing_without_server
from openflexure_microscope_server.things import OFMThing
from openflexure_microscope_server.things.gallery import (
GalleryCompatibleThing,
GalleryThing,
)
from ..shared_utils.lt_test_utils import LabThingsTestEnv
class MockGalleryData(BaseModel):
"""Mock gallery data."""
mock: str
foobar: int
class MinimalGalleryClass:
"""A class that provides data to the gallery but is NOT a thing.
This should fail the protocol check as it is not an OFMThing
"""
gallery_data_name: str = "Mock"
gallery_data_schema: type[BaseModel] = MockGalleryData
def get_data_for_gallery(self) -> list[BaseModel]:
"""Return a list of Mock Gallery Data."""
return [MockGalleryData(mock="mock", foobar="foobar")]
class MinimalGallerySource(OFMThing, MinimalGalleryClass):
"""Minimal example of a Thing that can show data in the gallery.
Mixes MinimalGalleryClass into OFMThing to be recognised by the protocol.
"""
show_data_in_gallery = True
class BadGallerySource(OFMThing):
"""An OFMThing that almost defines enough to show in the gallery.
Shouldn't match the protocol as ``gallery_data_name`` is missing.
"""
show_data_in_gallery = True
gallery_data_schema: type[BaseModel] = MockGalleryData
def get_data_for_gallery(self) -> list[BaseModel]:
"""Return a list of Mock Gallery Data."""
return [MockGalleryData(mock="mock", foobar="foobar")]
def test_gallery_compatible_protocol():
"""Check the gallery compatible protocol detects compatible classes only."""
# Minimal class has the gallery specific methods but is not an OFMThing.
assert not isinstance(MinimalGalleryClass(), GalleryCompatibleThing)
# BadGallerySource is an OFM Thing but is missing one of the required properties.
assert not isinstance(
create_thing_without_server(BadGallerySource), GalleryCompatibleThing
)
# MinimalGallerySource should match!
assert isinstance(
create_thing_without_server(MinimalGallerySource), GalleryCompatibleThing
)
def test_gallery_thing_finds_all_providers(simulation_test_env):
"""Check the gallery identifies the correct Things that will provide data."""
gallery = simulation_test_env.get_thing_by_name("gallery")
snake_workflow = simulation_test_env.get_thing_by_name("snake_workflow")
smart_scan = simulation_test_env.get_thing_by_name("smart_scan")
camera = simulation_test_env.get_thing_by_name("camera")
assert snake_workflow not in gallery.all_ofm_things.values()
assert camera in gallery.all_ofm_things.values()
assert smart_scan in gallery.all_ofm_things.values()
assert snake_workflow not in gallery.gallery_providing_things.values()
assert camera not in gallery.gallery_providing_things.values()
assert smart_scan in gallery.gallery_providing_things.values()
assert not isinstance(snake_workflow, GalleryCompatibleThing)
assert isinstance(smart_scan, GalleryCompatibleThing)
def test_gallery_logs_for_bad_providers(caplog):
"""Test that an error is logged if a gallery source doesn't match the protocol."""
# Create a config with the minimal Thing and the bad Thing
things = {
"minimal_thing": MinimalGallerySource,
"bad_thing": BadGallerySource,
"gallery": GalleryThing,
}
with caplog.at_level(logging.INFO):
# Start the server
with LabThingsTestEnv(
things=things, application_config={"data_folder": "./openflexure/data/"}
) as env:
gallery = env.get_thing_by_name("gallery")
# Minimal thing is listed as a gallery provider
assert "minimal_thing" in gallery.gallery_providing_things
# Bad thing isn't as the protocol doesn't match
assert "bad_thing" not in gallery.gallery_providing_things
# There is an error logged about bad_thing
assert len(caplog.records) == 1
record = caplog.records[0]
assert "Data from bad_thing cannot be shown in gallery" in record.message
assert record.levelname == "ERROR"

View file

@ -13,8 +13,7 @@
</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 v-if="ongoing" class="ongoing-msg">Scan in progress</h4> <div class="button-container">
<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"
@ -59,10 +58,9 @@
<ul> <ul>
<li>{{ scanData.number_of_images }} images</li> <li>{{ scanData.number_of_images }} images</li>
<li>Created: {{ formatDate(scanData.created) }}</li> <li>Created: {{ formatDate(scanData.created) }}</li>
<li v-if="!ongoing">Duration: {{ formatDuration(scanData.duration) }}</li> <li>Duration: {{ formatDuration(scanData.duration) }}</li>
<li v-if="ongoing">Duration: <i>Ongoing</i></li>
</ul> </ul>
<ul v-if="!ongoing"> <ul>
<li v-if="scanData.number_of_images < 3" class="warning-msg"> <li v-if="scanData.number_of_images < 3" class="warning-msg">
Not enough images to stitch Not enough images to stitch
</li> </li>
@ -102,10 +100,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
ongoing: {
type: Boolean,
required: true,
},
}, },
emits: ["viewer-requested", "update-requested"], emits: ["viewer-requested", "update-requested"],
@ -148,9 +142,7 @@ export default {
}, },
requestViewer() { requestViewer() {
// Notify parent that thumbnail was clicked // Notify parent that thumbnail was clicked
if (!this.ongoing) { this.$emit("viewer-requested", this.scanData);
this.$emit("viewer-requested", this.scanData);
}
}, },
async deleteScan() { async deleteScan() {
try { try {
@ -204,9 +196,4 @@ ul {
.scan-card-title { .scan-card-title {
text-align: center; text-align: center;
} }
.ongoing-msg {
text-align: center;
padding: 2rem 0;
}
</style> </style>

View file

@ -55,7 +55,6 @@
<scan-card <scan-card
:scan-data="scanData" :scan-data="scanData"
:scans-uri="scansUri" :scans-uri="scansUri"
:ongoing="isOngoing(scanData.name)"
@viewer-requested="showScan" @viewer-requested="showScan"
@update-requested="updateScans" @update-requested="updateScans"
/> />
@ -97,7 +96,6 @@ export default {
data: function () { data: function () {
return { return {
scans: [], scans: [],
ongoing: null,
selectedScan: null, selectedScan: null,
osdViewer: null, osdViewer: null,
currentPage: 1, currentPage: 1,
@ -108,7 +106,9 @@ export default {
computed: { computed: {
...mapState(useSettingsStore, ["baseUri", "ready"]), ...mapState(useSettingsStore, ["baseUri", "ready"]),
scansUri() { scansUri() {
return this.thingPropertyUrl("smart_scan", "scans"); // The scans URI is currently used for creating endpoint URIs.
// The actual property does not exist. So allowUndefined=true
return this.thingPropertyUrl("smart_scan", "scans", true);
}, },
scansEmpty() { scansEmpty() {
return this.scans.length == 0; return this.scans.length == 0;
@ -179,9 +179,7 @@ export default {
}, },
async updateScans() { async updateScans() {
try { try {
let scans_information = await this.readThingProperty("smart_scan", "scans"); let scans = await this.readThingProperty("gallery", "list_data");
let scans = scans_information.scans;
this.ongoing = scans_information.ongoing;
if (!scans | (scans.length == 0)) { if (!scans | (scans.length == 0)) {
this.scans = scans; this.scans = scans;
} }
@ -198,9 +196,6 @@ export default {
this.scans = []; this.scans = [];
} }
}, },
isOngoing(name) {
return name === this.ongoing;
},
async deleteAllScans() { async deleteAllScans() {
try { try {
await this.modalConfirm( await this.modalConfirm(