ScanList gets scans from GalleryThing, no ongoing scans shown.

This commit is contained in:
Julian Stirling 2026-05-06 16:01:09 +01:00
parent 4583151345
commit a16d47ed61
5 changed files with 52 additions and 63 deletions

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

@ -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. 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 from pydantic import BaseModel
@ -42,10 +42,10 @@ class GalleryThing(lt.Thing):
all_ofm_things: Mapping[str, OFMThing] = lt.thing_slot() 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 @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.""" """All Things that provide data to the gallery."""
if self._gallery_providing_things is None: if self._gallery_providing_things is None:
raise RuntimeError( raise RuntimeError(
@ -56,6 +56,7 @@ class GalleryThing(lt.Thing):
def __enter__(self) -> Self: def __enter__(self) -> Self:
"""Check for all gallery providing things on server startup.""" """Check for all gallery providing things on server startup."""
self._set_gallery_providers() self._set_gallery_providers()
return self
def _set_gallery_providers(self) -> None: def _set_gallery_providers(self) -> None:
"""Set the mapping of gallery providing things. """Set the mapping of gallery providing things.
@ -77,4 +78,22 @@ class GalleryThing(lt.Thing):
) )
gallery_providers.pop(key) 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

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."""
@ -197,7 +187,9 @@ class SmartScanThing(OFMThing):
in any ongoing scans being read. in any ongoing scans being read.
""" """
ongoing_name = None if self._ongoing_scan is None else self.ongoing_scan.name 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 # 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.
@ -567,21 +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."""
@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( @lt.endpoint(
"get", "get",
"get_stitch/{scan_name}", "get_stitch/{scan_name}",
@ -646,7 +623,7 @@ class SmartScanThing(OFMThing):
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 # 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: if scan_info.number_of_images == 0:
self._delete_scan(scan_info.name) self._delete_scan(scan_info.name)
@ -761,6 +738,6 @@ 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(): for scan in self.get_gallery_data():
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

@ -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(