diff --git a/src/openflexure_microscope_server/scan_directories.py b/src/openflexure_microscope_server/scan_directories.py index 4f2e5215..da852438 100644 --- a/src/openflexure_microscope_server/scan_directories.py +++ b/src/openflexure_microscope_server/scan_directories.py @@ -335,14 +335,25 @@ class ScanDirectoryManager: return [f.name for f in os.scandir(self._base_scan_dir) if f.is_dir()] @requires_lock - def all_scans_info(self, ongoing: Optional[str] = None) -> list[ScanInfo]: - """Return a lists of ScanInfo objects for each scan.""" + def all_scans_info( + 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] = [] for scan_name in self.all_scans: - # If the scan is ongoing send flag to skip reading the json data - skip_json = scan_name == ongoing + scan_is_ongoing = scan_name == ongoing + if scan_is_ongoing and not include_ongoing: + continue 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) return all_info diff --git a/src/openflexure_microscope_server/things/gallery.py b/src/openflexure_microscope_server/things/gallery.py index e83c8f4f..cfb73ae1 100644 --- a/src/openflexure_microscope_server/things/gallery.py +++ b/src/openflexure_microscope_server/things/gallery.py @@ -5,7 +5,7 @@ the Things that capture the Data. This thing just provides a unified way for the front end to access the data. """ -from typing import Mapping, Optional, Protocol, Self, runtime_checkable +from typing import Any, Mapping, Optional, Protocol, Self, cast, runtime_checkable from pydantic import BaseModel @@ -42,10 +42,10 @@ class GalleryThing(lt.Thing): all_ofm_things: Mapping[str, OFMThing] = lt.thing_slot() - _gallery_providing_things: Optional[Mapping[str, OFMThing]] = None + _gallery_providing_things: Optional[Mapping[str, GalleryCompatibleThing]] = None @property - def gallery_providing_things(self) -> Mapping[str, OFMThing]: + 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( @@ -56,6 +56,7 @@ class GalleryThing(lt.Thing): 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. @@ -77,4 +78,22 @@ class GalleryThing(lt.Thing): ) gallery_providers.pop(key) - self._gallery_providing_things = gallery_providers + # 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_gallery_data()] + return data_list diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index 3a3fdc28..5b4c8f3a 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -71,16 +71,6 @@ class ActiveScanData(scan_directories.BaseScanData): 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): """A class representing a JPEG image as a LabThings FastAPI Blob.""" @@ -197,7 +187,9 @@ class SmartScanThing(OFMThing): 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) + 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 # setting is loaded from disk. @@ -567,21 +559,6 @@ class SmartScanThing(OFMThing): stitch_automatically: bool = lt.setting(default=True) """Whether to run a final stitch at the end of a successful scan.""" - @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_gallery_data(), - ongoing=None if self._ongoing_scan is None else self.ongoing_scan.name, - ) - @lt.endpoint( "get", "get_stitch/{scan_name}", @@ -646,7 +623,7 @@ class SmartScanThing(OFMThing): def purge_empty_scans(self) -> None: """Delete all scan folders containing no images at the top level.""" # JSON is ignored as it's created before any images are captured - for scan_info in self._get_all_scan_info(): + for scan_info in self.get_gallery_data(): if scan_info.number_of_images == 0: self._delete_scan(scan_info.name) @@ -761,6 +738,6 @@ class SmartScanThing(OFMThing): """ if self._scan_lock.locked(): raise RuntimeError("Can't stitch previous scans while a scan is ongoing") - for scan in self._get_all_scan_info(): + for scan in self.get_gallery_data(): if scan.dzi is None: self.stitch_scan(scan_name=scan.name) diff --git a/webapp/src/components/tabContentComponents/scanListComponents/scanCard.vue b/webapp/src/components/tabContentComponents/scanListComponents/scanCard.vue index ec56b5e3..4f1999ed 100644 --- a/webapp/src/components/tabContentComponents/scanListComponents/scanCard.vue +++ b/webapp/src/components/tabContentComponents/scanListComponents/scanCard.vue @@ -13,8 +13,7 @@

{{ scanData.name }}

-

Scan in progress

-
+
  • {{ scanData.number_of_images }} images
  • Created: {{ formatDate(scanData.created) }}
  • -
  • Duration: {{ formatDuration(scanData.duration) }}
  • -
  • Duration: Ongoing
  • +
  • Duration: {{ formatDuration(scanData.duration) }}
  • -
      +
      • Not enough images to stitch
      • @@ -102,10 +100,6 @@ export default { type: String, required: true, }, - ongoing: { - type: Boolean, - required: true, - }, }, emits: ["viewer-requested", "update-requested"], @@ -148,9 +142,7 @@ export default { }, requestViewer() { // Notify parent that thumbnail was clicked - if (!this.ongoing) { - this.$emit("viewer-requested", this.scanData); - } + this.$emit("viewer-requested", this.scanData); }, async deleteScan() { try { @@ -204,9 +196,4 @@ ul { .scan-card-title { text-align: center; } - -.ongoing-msg { - text-align: center; - padding: 2rem 0; -} diff --git a/webapp/src/components/tabContentComponents/scanListContent.vue b/webapp/src/components/tabContentComponents/scanListContent.vue index 2a451617..b8ad5de2 100644 --- a/webapp/src/components/tabContentComponents/scanListContent.vue +++ b/webapp/src/components/tabContentComponents/scanListContent.vue @@ -55,7 +55,6 @@ @@ -97,7 +96,6 @@ export default { data: function () { return { scans: [], - ongoing: null, selectedScan: null, osdViewer: null, currentPage: 1, @@ -108,7 +106,9 @@ export default { computed: { ...mapState(useSettingsStore, ["baseUri", "ready"]), 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() { return this.scans.length == 0; @@ -179,9 +179,7 @@ export default { }, async updateScans() { try { - let scans_information = await this.readThingProperty("smart_scan", "scans"); - let scans = scans_information.scans; - this.ongoing = scans_information.ongoing; + let scans = await this.readThingProperty("gallery", "list_data"); if (!scans | (scans.length == 0)) { this.scans = scans; } @@ -198,9 +196,6 @@ export default { this.scans = []; } }, - isOngoing(name) { - return name === this.ongoing; - }, async deleteAllScans() { try { await this.modalConfirm(