Update Thing Settings to use the new format provided by PR #110 of Labthings FastAPI

This commit is contained in:
Julian Stirling 2025-06-18 17:28:38 -04:00
parent fd4d51401f
commit 840ed7f20e
10 changed files with 169 additions and 203 deletions

2
.gitignore vendored
View file

@ -30,7 +30,7 @@ var/
.installed.cfg .installed.cfg
*.egg *.egg
pip-wheel-metadata/ pip-wheel-metadata/
/openflexure/
# PyInstaller # PyInstaller
# Usually these files are written by a python script from a template # Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it. # before PyInstaller builds the exe, so as to inject date/other infos into it.

View file

@ -19,7 +19,8 @@ from pydantic import BaseModel
from labthings_fastapi.thing import Thing from labthings_fastapi.thing import Thing
from labthings_fastapi.dependencies.blocking_portal import BlockingPortal from labthings_fastapi.dependencies.blocking_portal import BlockingPortal
from labthings_fastapi.decorators import thing_action, thing_property from labthings_fastapi.decorators import thing_action
from labthings_fastapi.descriptors import ThingSetting
from labthings_fastapi.types.numpy import NDArray from labthings_fastapi.types.numpy import NDArray
from .camera import RawCameraDependency as Camera from .camera import RawCameraDependency as Camera
@ -396,37 +397,36 @@ class AutofocusThing(Thing):
stage.move_absolute(z=peak_height) stage.move_absolute(z=peak_height)
return heights.tolist(), sizes.tolist() return heights.tolist(), sizes.tolist()
@thing_property stack_images_to_save = ThingSetting(
def stack_images_to_save(self) -> int: initial_value=1,
"""The number of images to capture and save in a stack model=int,
Defaults to 1 unless you need to see either side of focus""" description="""The number of images to save in a stack.
return self.thing_settings.get("stack_images_to_save", 1)
@stack_images_to_save.setter Defaults to 1 unless you need to see either side of focus""",
def stack_images_to_save(self, value: int) -> None: )
self.thing_settings["stack_images_to_save"] = value
@thing_property stack_min_images_to_test = ThingSetting(
def stack_min_images_to_test(self) -> int: initial_value=9,
"""The number of images to test for successful focusing in a stack model=int,
Defaults to 9, which balances reliability and speed""" description="""The minimum number of images to capture in a stack.
return self.thing_settings.get("stack_min_images_to_test", 9)
@stack_min_images_to_test.setter This many images are captures and tested for focus, if the focus
def stack_min_images_to_test(self, value: int) -> None: is not central enough more images may be captured. After new images
self.thing_settings["stack_min_images_to_test"] = value are captured the number sets the number of images used for checking
if focus is central.
@thing_property Defaults to 9 which balances reliability and speed
def stack_dz(self) -> int: """,
"""Space in steps between images in a z-stack )
stack_dz = ThingSetting(
initial_value=50,
model=int,
description="""Space in steps between images in a z-stack
Suggested is 50 for 60-100x Suggested is 50 for 60-100x
100 for 40x 100 for 40x
200 for 20x""" 200 for 20x""",
return self.thing_settings.get("stack_dz", 50) )
@stack_dz.setter
def stack_dz(self, value: int) -> None:
self.thing_settings["stack_dz"] = value
@thing_action @thing_action
def run_smart_stack( def run_smart_stack(

View file

@ -6,7 +6,8 @@ from pydantic import BaseModel
from scipy.stats import norm from scipy.stats import norm
from labthings_fastapi.thing import Thing from labthings_fastapi.thing import Thing
from labthings_fastapi.decorators import thing_action, thing_property from labthings_fastapi.decorators import thing_action, thing_setting
from labthings_fastapi.descriptors import ThingSetting
from .camera import CameraDependency as CamDep from .camera import CameraDependency as CamDep
@ -17,38 +18,44 @@ class ChannelDistributions(BaseModel):
class BackgroundDetectThing(Thing): class BackgroundDetectThing(Thing):
@thing_property # Requires a getter and a setter to support being a BaseModel but being
# saved to file as a dict
_background_distributions: Optional[ChannelDistributions] = None
@thing_setting
def background_distributions(self) -> Optional[ChannelDistributions]: def background_distributions(self) -> Optional[ChannelDistributions]:
"""The statistics of the background image""" """The statistics of the background image"""
bd = self.thing_settings.get("background_distributions", None) bd = self._background_distributions
if bd: if bd is None:
return ChannelDistributions(**bd)
return None return None
return ChannelDistributions(**bd)
@background_distributions.setter @background_distributions.setter
def background_distributions(self, value: Optional[ChannelDistributions]) -> None: def background_distributions(
try: self, value: Optional[ChannelDistributions | dict]
self.thing_settings["background_distributions"] = value.model_dump() ) -> None:
except AttributeError: if value is None:
self.thing_settings["background_distributions"] = None self._background_distributions = None
elif isinstance(value, ChannelDistributions):
self._background_distributions = value.model_dump()
elif isinstance(value, dict):
self._background_distributions = value
else:
raise TypeError(
f"Cannot set background_distributions with an object of type {type(value)}"
)
@thing_property tolerance = ThingSetting(
def tolerance(self) -> float: initial_value=7.0,
"""How many standard deviations to allow for the background""" model=float,
return self.thing_settings.get("tolerance", 7) description="How many standard deviations to allow for the background",
)
@tolerance.setter fraction = ThingSetting(
def tolerance(self, value: float) -> None: initial_value=25.0,
self.thing_settings["tolerance"] = value model=float,
description="How much of the image needs to be not background to label as sample",
@thing_property )
def fraction(self) -> float:
"""How much of the image needs to be not background to label as sample"""
return self.thing_settings.get("fraction", 25)
@fraction.setter
def fraction(self, value: float) -> None:
self.thing_settings["fraction"] = value
def background_mask(self, image: np.ndarray) -> np.ndarray: def background_mask(self, image: np.ndarray) -> np.ndarray:
"""Calculate a binary image, showing whether each pixel is background """Calculate a binary image, showing whether each pixel is background

View file

@ -26,7 +26,7 @@ from tempfile import TemporaryDirectory
from pydantic import BaseModel, BeforeValidator from pydantic import BaseModel, BeforeValidator
from labthings_fastapi.descriptors.property import PropertyDescriptor from labthings_fastapi.descriptors.property import ThingProperty
from labthings_fastapi.decorators import thing_action, thing_property from labthings_fastapi.decorators import thing_action, thing_property
from labthings_fastapi.outputs.mjpeg_stream import MJPEGStream from labthings_fastapi.outputs.mjpeg_stream import MJPEGStream
from labthings_fastapi.utilities import get_blocking_portal from labthings_fastapi.utilities import get_blocking_portal
@ -46,14 +46,12 @@ from . import picamera_recalibrate_utils as recalibrate_utils
from . import BaseCamera, JPEGBlob, ArrayModel from . import BaseCamera, JPEGBlob, ArrayModel
class PicameraControl(PropertyDescriptor): class PicameraControl(ThingProperty):
def __init__( def __init__(
self, control_name: str, model: type = float, description: Optional[str] = None self, control_name: str, model: type = float, description: Optional[str] = None
): ):
"""A property descriptor controlling a picamera control""" """A property descriptor controlling a picamera control"""
PropertyDescriptor.__init__( super().__init__(model, observable=False, description=description)
self, model, observable=False, description=description
)
self.control_name = control_name self.control_name = control_name
def _getter(self, obj: StreamingPiCamera2): def _getter(self, obj: StreamingPiCamera2):
@ -213,19 +211,19 @@ class StreamingPiCamera2(BaseCamera):
except KeyError: except KeyError:
pass # If controls are missing, leave at default pass # If controls are missing, leave at default
stream_resolution = PropertyDescriptor( stream_resolution = ThingProperty(
tuple[int, int], tuple[int, int],
initial_value=(820, 616), initial_value=(820, 616),
description="Resolution to use for the MJPEG stream", description="Resolution to use for the MJPEG stream",
) )
mjpeg_bitrate = PropertyDescriptor( mjpeg_bitrate = ThingProperty(
Optional[int], Optional[int],
initial_value=100000000, initial_value=100000000,
description="Bitrate for MJPEG stream (None for default)", description="Bitrate for MJPEG stream (None for default)",
) )
stream_active = PropertyDescriptor( stream_active = ThingProperty(
bool, bool,
initial_value=False, initial_value=False,
description="Whether the MJPEG stream is active", description="Whether the MJPEG stream is active",
@ -284,7 +282,7 @@ class StreamingPiCamera2(BaseCamera):
with self.picamera() as cam: with self.picamera() as cam:
return cam.sensor_resolution return cam.sensor_resolution
tuning = PropertyDescriptor(Optional[dict], None, readonly=True) tuning = ThingProperty(Optional[dict], None, readonly=True)
def settings_to_properties(self): def settings_to_properties(self):
"""Set the values of properties based on the settings dict""" """Set the values of properties based on the settings dict"""

View file

@ -115,9 +115,11 @@ class SimulatedCamera(BaseCamera):
) )
return image return image
def attach_to_server(self, server: ThingServer, path: str): def attach_to_server(
self, server: ThingServer, path: str, setting_storage_path: str
):
self._server = server self._server = server
return super().attach_to_server(server, path) return super().attach_to_server(server, path, setting_storage_path)
def get_stage_position(self): def get_stage_position(self):
if not self._stage and self._server: if not self._stage and self._server:

View file

@ -37,8 +37,9 @@ from labthings_fastapi.dependencies.invocation import (
InvocationCancelledError, InvocationCancelledError,
InvocationLogger, InvocationLogger,
) )
from labthings_fastapi.types.numpy import NDArray, denumpify, DenumpifyingDict from labthings_fastapi.types.numpy import NDArray, DenumpifyingDict
from labthings_fastapi.decorators import thing_action, thing_property from labthings_fastapi.decorators import thing_action, thing_property
from labthings_fastapi.descriptors import ThingSetting
from labthings_fastapi.thing import Thing from labthings_fastapi.thing import Thing
from camera_stage_mapping.camera_stage_tracker import Tracker from camera_stage_mapping.camera_stage_tracker import Tracker
from .camera import CameraDependency as Camera from .camera import CameraDependency as Camera
@ -181,12 +182,6 @@ class CSMUncalibratedError(HTTPException):
class CameraStageMapper(Thing): class CameraStageMapper(Thing):
"""A Thing to manage mapping between image and stage coordinates""" """A Thing to manage mapping between image and stage coordinates"""
def __enter__(self):
pass
def __exit__(self, exc_type, exc_value, traceback):
self.thing_settings.write_to_file()
@thing_action @thing_action
def calibrate_1d( def calibrate_1d(
self, self,
@ -244,8 +239,6 @@ class CameraStageMapper(Thing):
corrected_resolution = tuple( corrected_resolution = tuple(
r * hw.grab_image_downsampling for r in cal_x["image_resolution"] r * hw.grab_image_downsampling for r in cal_x["image_resolution"]
) )
self.thing_settings.update(denumpify(cal_xy))
self.thing_settings["image_resolution"] = corrected_resolution
csm_matrix = cal_xy["image_to_stage_displacement"] csm_matrix = cal_xy["image_to_stage_displacement"]
csm_as_string = f"[{round(csm_matrix[0][0], 2)}, {round(csm_matrix[0][1], 2)},],[{round(csm_matrix[1][0], 2)}, {round(csm_matrix[1][1], 2)}]" csm_as_string = f"[{round(csm_matrix[0][0], 2)}, {round(csm_matrix[0][1], 2)},],[{round(csm_matrix[1][0], 2)}, {round(csm_matrix[1][1], 2)}]"
@ -260,7 +253,7 @@ class CameraStageMapper(Thing):
"downsampling": hw.grab_image_downsampling, "downsampling": hw.grab_image_downsampling,
} }
self.thing_settings["last_calibration"] = DenumpifyingDict(data).model_dump() self.last_calibration = DenumpifyingDict(data).model_dump()
return data return data
@ -285,15 +278,28 @@ class CameraStageMapper(Thing):
) )
``` ```
""" """
displacement_matrix = self.thing_settings.get("image_to_stage_displacement") if self.last_calibration is None:
if not displacement_matrix:
return None return None
displacement_matrix = self.last_calibration["camera_stage_mapping_calibration"][
"image_to_stage_displacement"
]
return np.array(displacement_matrix).tolist() return np.array(displacement_matrix).tolist()
last_calibration = ThingSetting(
initial_value=None,
model=Optional[dict],
readonly=True,
description="The most recent CSM calibration",
)
@thing_property @thing_property
def image_resolution(self) -> Optional[Tuple[float, float]]: def image_resolution(self) -> Optional[Tuple[float, float]]:
"""The image size used to calibrate the image_to_stage_displacement_matrix""" """The image size used to calibrate the image_to_stage_displacement_matrix"""
return self.thing_settings.get("image_resolution", None) if self.last_calibration is None:
return None
return self.last_calibration["camera_stage_mapping_calibration"][
"image_resolution"
]
def assert_calibrated(self): def assert_calibrated(self):
"""Raise an exception if the image_to_stage_displacement matrix is not set""" """Raise an exception if the image_to_stage_displacement matrix is not set"""
@ -302,11 +308,6 @@ class CameraStageMapper(Thing):
# added by CSMUncalibratedError # added by CSMUncalibratedError
raise CSMUncalibratedError() # noqa: RSE102 raise CSMUncalibratedError() # noqa: RSE102
@thing_property
def last_calibration(self) -> Optional[Dict]:
"""The results of the last calibration that was run"""
return self.thing_settings.get("last_calibration", None)
@thing_action @thing_action
def move_in_image_coordinates( def move_in_image_coordinates(
self, self,

View file

@ -13,10 +13,10 @@ from uuid import UUID, uuid4
from fastapi import Depends, HTTPException, Request from fastapi import Depends, HTTPException, Request
from labthings_fastapi.dependencies.metadata import GetThingStates from labthings_fastapi.dependencies.metadata import GetThingStates
from labthings_fastapi.thing import Thing from labthings_fastapi.thing import Thing
from labthings_fastapi.decorators import thing_action, thing_property from labthings_fastapi.decorators import thing_action, thing_property, thing_setting
from labthings_fastapi.descriptors import ThingSetting
from labthings_fastapi.dependencies.thing_server import find_thing_server from labthings_fastapi.dependencies.thing_server import find_thing_server
from labthings_fastapi.server import ThingServer from labthings_fastapi.server import ThingServer
from labthings_fastapi.dependencies.invocation import InvocationLogger
def thing_server_from_request(request: Request) -> ThingServer: def thing_server_from_request(request: Request) -> ThingServer:
@ -49,10 +49,17 @@ def nested_dict_get(data: dict, key: Sequence[str], create=False) -> Any:
class SettingsManager(Thing): class SettingsManager(Thing):
@thing_property external_metadata = ThingSetting(
def external_metadata(self) -> Mapping: initial_value={},
"""External metadata stored in the server's settings""" model=Mapping,
return self.thing_settings.get("external_metadata", {}) description="External metadata stored in the server's settings",
)
external_metadata_in_state = ThingSetting(
initial_value=[],
model=Sequence[str],
description='A list of strings that are included in the "state" metadata',
)
@thing_action @thing_action
def update_external_metadata( def update_external_metadata(
@ -75,7 +82,7 @@ class SettingsManager(Thing):
if key: if key:
subdict = nested_dict_get(subdict, key.split("/"), create=True) subdict = nested_dict_get(subdict, key.split("/"), create=True)
recursive_update(subdict, data) recursive_update(subdict, data)
self.thing_settings["external_metadata"] = metadata self.external_metadata = metadata
@thing_action @thing_action
def delete_external_metadata(self, key: str) -> None: def delete_external_metadata(self, key: str) -> None:
@ -93,14 +100,21 @@ class SettingsManager(Thing):
raise HTTPException( raise HTTPException(
status_code=404, detail="The specified key '{key}' was not found" status_code=404, detail="The specified key '{key}' was not found"
) )
self.thing_settings["external_metadata"] = metadata self.external_metadata = metadata
@thing_property _microscope_id: Optional[str] = None
@thing_setting
def microscope_id(self) -> UUID: def microscope_id(self) -> UUID:
"""A unique identifier for this microscope""" """A unique identifier for this microscope"""
if "microscope_id" not in self.thing_settings: if self._microscope_id is None:
self.thing_settings["microscope_id"] = str(uuid4()) self._microscope_id = str(uuid4())
return UUID(self.thing_settings["microscope_id"]) return UUID(self._microscope_id)
@microscope_id.setter
def microscope_id(self, uuid: UUID):
# TODO make read only but still settable from disk
self._microscope_id = uuid
@thing_property @thing_property
def hostname(self) -> str: def hostname(self) -> str:
@ -112,16 +126,6 @@ class SettingsManager(Thing):
"""Metadata summarising the current state of all Things in the server""" """Metadata summarising the current state of all Things in the server"""
return metadata_getter() return metadata_getter()
@thing_property
def external_metadata_in_state(self) -> Sequence[str]:
"""A list of strings that are included in the "state" metadata"""
return self.thing_settings.get("external_metadata_in_state", [])
@external_metadata_in_state.setter
def external_metadata_in_state(self, keys: Sequence[str]):
"""Set the keys from external metadata that are returned in state"""
self.thing_settings["external_metadata_in_state"] = keys
@property @property
def thing_state(self) -> Mapping: def thing_state(self) -> Mapping:
state = { state = {
@ -138,29 +142,3 @@ class SettingsManager(Thing):
except KeyError: except KeyError:
continue continue
return state return state
@thing_action
def save_all_thing_settings(
self, thing_server: ThingServerDep, logger: InvocationLogger
) -> None:
"""Ensure all the Things sync their settings to disk.
Normally, each Thing has a `thing_settings` attribute that can be
used to save settings. That dictionary is saved to a JSON file
when the server shuts down cleanly.
This action causes all the `thing_settings` dictionaries to be
saved to files immediately, meaning that settings will not be
lost in the event of a server crash, power outage, etc.
"""
for name, thing in thing_server.things.items():
try:
if thing._labthings_thing_settings:
thing._labthings_thing_settings.write_to_file()
logger.info(f"Wrote {name} settings to disk.")
except PermissionError:
logger.warning(
f"Could not write {name} settings to disk: permission error."
)
except FileNotFoundError:
logger.warning(f"Could not write {name} settings, folder not found")

View file

@ -14,12 +14,17 @@ from PIL import Image
from labthings_fastapi.thing import Thing from labthings_fastapi.thing import Thing
from labthings_fastapi.dependencies.metadata import GetThingStates from labthings_fastapi.dependencies.metadata import GetThingStates
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
from labthings_fastapi.descriptors import ThingSetting
from labthings_fastapi.dependencies.invocation import ( from labthings_fastapi.dependencies.invocation import (
CancelHook, CancelHook,
InvocationLogger, InvocationLogger,
InvocationCancelledError, InvocationCancelledError,
) )
from labthings_fastapi.decorators import thing_action, thing_property, fastapi_endpoint from labthings_fastapi.decorators import (
thing_action,
thing_property,
fastapi_endpoint,
)
from labthings_fastapi.outputs.blob import blob_type from labthings_fastapi.outputs.blob import blob_type
from openflexure_microscope_server.utilities import ErrorCapturingThread from openflexure_microscope_server.utilities import ErrorCapturingThread
@ -590,72 +595,54 @@ class SmartScanThing(Thing):
raise HTTPException(404, "File not found") raise HTTPException(404, "File not found")
return FileResponse(preview_path) return FileResponse(preview_path)
@thing_property save_resolution = ThingSetting(
def save_resolution(self) -> tuple[int, int]: initial_value=(1640, 1232),
"""A tuple of the image resolution to capture. Should be in a model=tuple[int, int],
4:3 aspect ratio""" description=("A tuple of the image resolution to capture."),
return self.thing_settings.get("save_resolution", ((1640, 1232))) )
@save_resolution.setter max_range = ThingSetting(
def save_resolution(self, value: tuple[int, int]) -> None: initial_value=45000,
self.thing_settings["save_resolution"] = value model=int,
description=(
"The maximum distance from the centre of the scan before we break in steps"
),
)
@thing_property stitch_tiff = ThingSetting(
def max_range(self) -> int: initial_value=False,
"""The maximum distance from the centre of the scan before we break in steps""" model=bool,
return self.thing_settings.get("max_range", 45000) description="Whether or not to also produce a pyramidal tiff",
)
@max_range.setter skip_background = ThingSetting(
def max_range(self, value: int) -> None: initial_value=True,
self.thing_settings["max_range"] = value model=bool,
description="""Whether to detect and skip empty fields of view
@thing_property This uses the settings from the `background_detect` Thing.""",
def stitch_tiff(self) -> bool: )
"""Whether or not to also produce a pyramidal tiff"""
return self.thing_settings.get("stitch_tiff", False)
@stitch_tiff.setter autofocus_dz = ThingSetting(
def stitch_tiff(self, value: bool) -> None: initial_value=1000,
self.thing_settings["stitch_tiff"] = value model=int,
description="The z distance to perform an autofocus in steps",
)
@thing_property overlap = ThingSetting(
def skip_background(self) -> bool: initial_value=0.45,
"""Whether to detect and skip empty fields of view model=float,
description="The fraction (0-1) that adjacent images should overlap in x or y",
)
This uses the settings from the `background_detect` Thing. stitch_automatically = ThingSetting(
""" initial_value=True,
return self.thing_settings.get("skip_background", True) model=bool,
description=(
@skip_background.setter "Whether to run a final stitch at the end of the scan (assuming scan "
def skip_background(self, value: bool) -> None: "success)"
self.thing_settings["skip_background"] = value ),
)
@thing_property
def autofocus_dz(self) -> int:
"""The z distance to perform an autofocus in steps"""
return self.thing_settings.get("autofocus_dz", 1000)
@autofocus_dz.setter
def autofocus_dz(self, value: int) -> None:
self.thing_settings["autofocus_dz"] = value
@thing_property
def overlap(self) -> float:
"""The fraction (0-1) that adjacent images should overlap in x or y"""
return self.thing_settings.get("overlap", 0.45)
@overlap.setter
def overlap(self, value: float) -> None:
self.thing_settings["overlap"] = value
@thing_property
def stitch_automatically(self) -> bool:
"""Whether to run a final stitch at the end of the scan (assuming scan success)"""
return self.thing_settings.get("stitch_automatically", True)
@stitch_automatically.setter
def stitch_automatically(self, value: bool) -> None:
self.thing_settings["stitch_automatically"] = value
@thing_property @thing_property
def scans(self) -> list[scan_directories.ScanInfo]: def scans(self) -> list[scan_directories.ScanInfo]:

View file

@ -2,7 +2,7 @@ from __future__ import annotations
from typing import TypeAlias from typing import TypeAlias
from collections.abc import Sequence, Mapping from collections.abc import Sequence, Mapping
from labthings_fastapi.descriptors.property import PropertyDescriptor from labthings_fastapi.descriptors.property import ThingProperty
from labthings_fastapi.thing import Thing from labthings_fastapi.thing import Thing
from labthings_fastapi.decorators import thing_action, thing_property from labthings_fastapi.decorators import thing_action, thing_property
from labthings_fastapi.dependencies.invocation import CancelHook from labthings_fastapi.dependencies.invocation import CancelHook
@ -25,7 +25,7 @@ class BaseStage(Thing):
"""The names of the stage's axes, in order.""" """The names of the stage's axes, in order."""
return self._axis_names return self._axis_names
position = PropertyDescriptor( position = ThingProperty(
Mapping[str, int], Mapping[str, int],
dict.fromkeys(_axis_names, 0), dict.fromkeys(_axis_names, 0),
description="Current position of the stage", description="Current position of the stage",
@ -33,7 +33,7 @@ class BaseStage(Thing):
observable=True, observable=True,
) )
moving = PropertyDescriptor( moving = ThingProperty(
bool, bool,
False, False,
description="Whether the stage is in motion", description="Whether the stage is in motion",

View file

@ -58,11 +58,6 @@
</tabIcon> </tabIcon>
</li> </li>
</ul> </ul>
<action-button
thing="settings"
action="save_all_thing_settings"
submit-label="Save All Settings"
/>
</div> </div>
<div class="view-component uk-width-expand uk-padding-small"> <div class="view-component uk-width-expand uk-padding-small">
<tabContent <tabContent
@ -118,7 +113,6 @@ import stageSettings from "./settingsComponents/stageSettings.vue";
// 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";
import ActionButton from "../labThingsComponents/actionButton.vue";
// Export main app // Export main app
export default { export default {
@ -131,8 +125,7 @@ export default {
CSMSettings, CSMSettings,
appSettings, appSettings,
tabIcon, tabIcon,
tabContent, tabContent
ActionButton
}, },
data: function() { data: function() {