From a9d02f2d3898e87a57a3c57ce1925c451cb85259 Mon Sep 17 00:00:00 2001 From: imanjra Date: Wed, 8 May 2024 11:00:22 -0400 Subject: [PATCH 001/126] tweak zoom and workspace right alignment --- app/packages/core/src/components/ImageContainerHeader.tsx | 4 ++-- app/packages/spaces/src/components/Workspaces/index.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/packages/core/src/components/ImageContainerHeader.tsx b/app/packages/core/src/components/ImageContainerHeader.tsx index c2b7296d00..d623c8b0c1 100644 --- a/app/packages/core/src/components/ImageContainerHeader.tsx +++ b/app/packages/core/src/components/ImageContainerHeader.tsx @@ -52,8 +52,8 @@ const RightContainer = styled.div` const SliderContainer = styled.div` display: flex; align-items: center; - width: 8rem; - padding-right: 1rem; + width: 7.375rem; + padding-right: 0.375rem; `; const ImageContainerHeader = () => { diff --git a/app/packages/spaces/src/components/Workspaces/index.tsx b/app/packages/spaces/src/components/Workspaces/index.tsx index 40eb0de074..aabd1b2eb0 100644 --- a/app/packages/spaces/src/components/Workspaces/index.tsx +++ b/app/packages/spaces/src/components/Workspaces/index.tsx @@ -61,7 +61,7 @@ export default function Workspaces() { zIndex: 1, color: (theme) => theme.palette.text.secondary, fontSize: 14, - pr: "19px", + pr: "0.75rem", }} endIcon={} > From c98f36b288bf8356a49de42bc1f70547a632449f Mon Sep 17 00:00:00 2001 From: minhtuevo Date: Thu, 9 May 2024 12:47:39 -0700 Subject: [PATCH 002/126] Remove tutorials and recipes from setup.py --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index fc6cca0a33..9139e629ba 100644 --- a/setup.py +++ b/setup.py @@ -138,8 +138,7 @@ def get_install_requirements(install_requires, choose_install_requires): long_description_content_type="text/markdown", packages=find_packages( exclude=["app", "eta", "package", "requirements", "tests", "tools"] - ) - + ["fiftyone.recipes", "fiftyone.tutorials"], + ), package_dir={ "fiftyone.recipes": "docs/source/recipes", "fiftyone.tutorials": "docs/source/tutorials", From 0b908f492545b162792a0c5fbc57fa202514c738 Mon Sep 17 00:00:00 2001 From: minhtuevo Date: Thu, 9 May 2024 22:07:36 -0700 Subject: [PATCH 003/126] Remove package_dir --- setup.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/setup.py b/setup.py index 9139e629ba..11d982e661 100644 --- a/setup.py +++ b/setup.py @@ -139,10 +139,6 @@ def get_install_requirements(install_requires, choose_install_requires): packages=find_packages( exclude=["app", "eta", "package", "requirements", "tests", "tools"] ), - package_dir={ - "fiftyone.recipes": "docs/source/recipes", - "fiftyone.tutorials": "docs/source/tutorials", - }, install_requires=get_install_requirements( INSTALL_REQUIRES, CHOOSE_INSTALL_REQUIRES ), From 86125ab7851a1656fa46e0719edde8dd94f3c3eb Mon Sep 17 00:00:00 2001 From: Minh-Tue Vo Date: Mon, 13 May 2024 08:59:31 -0700 Subject: [PATCH 004/126] Adding a clean step to Makefile (#4393) * Adding a clean step to Makefile so that we don't bundle existing dist content * Add prune dist to Manifest.in --- MANIFEST.in | 1 + Makefile | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 0747620071..06fe67a977 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,6 +10,7 @@ prune package prune requirements prune tools prune tests +prune dist global-exclude .* diff --git a/Makefile b/Makefile index 95dbc80b41..cff0660167 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,10 @@ app: @cd app && yarn && yarn build && cd .. -python: app +clean: + @rm -rf ./dist/* + +python: app clean @python -Im build docker: python From 957780226e544c2370c708f4c3a6c3b01e02fe21 Mon Sep 17 00:00:00 2001 From: Theo Patron Date: Thu, 25 Apr 2024 00:51:00 +0200 Subject: [PATCH 005/126] fix: coco category ids can now be not sequential - avoiding memory leak --- fiftyone/utils/coco.py | 99 ++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 51 deletions(-) diff --git a/fiftyone/utils/coco.py b/fiftyone/utils/coco.py index 906a779f23..8ebeefae64 100644 --- a/fiftyone/utils/coco.py +++ b/fiftyone/utils/coco.py @@ -194,6 +194,8 @@ def add_coco_labels( view.compute_metadata() widths, heights = view.values(["metadata.width", "metadata.height"]) + classes_map = {i: label for i, label in enumerate(classes)} + labels = [] for _coco_objects, width, height in zip(coco_objects, widths, heights): frame_size = (width, height) @@ -202,7 +204,7 @@ def add_coco_labels( _labels = _coco_objects_to_detections( _coco_objects, frame_size, - classes, + classes_map, None, False, include_annotation_id, @@ -212,7 +214,7 @@ def add_coco_labels( _labels = _coco_objects_to_polylines( _coco_objects, frame_size, - classes, + classes_map, None, tolerance, include_annotation_id, @@ -221,7 +223,7 @@ def add_coco_labels( _labels = _coco_objects_to_detections( _coco_objects, frame_size, - classes, + classes_map, None, True, include_annotation_id, @@ -230,7 +232,7 @@ def add_coco_labels( _labels = _coco_objects_to_keypoints( _coco_objects, frame_size, - classes, + classes_map, None, include_annotation_id, ) @@ -548,7 +550,7 @@ def setup(self): if self.labels_path is not None and os.path.isfile(self.labels_path): ( info, - classes, + classes_map, supercategory_map, images, annotations, @@ -556,6 +558,10 @@ def setup(self): self.labels_path, extra_attrs=self.extra_attrs ) + classes = None + if classes_map is not None: + classes = list(classes_map.values()) + if classes is not None: info["classes"] = classes @@ -582,7 +588,7 @@ def setup(self): } else: info = {} - classes = None + classes_map = None supercategory_map = None image_dicts_map = {} annotations = None @@ -597,7 +603,7 @@ def setup(self): license_map = None self._info = info - self._classes = classes + self._classes = classes_map self._license_map = license_map self._supercategory_map = supercategory_map self._image_paths_map = image_paths_map @@ -960,7 +966,7 @@ def __init__( def to_polyline( self, frame_size, - classes=None, + classes_map=None, supercategory_map=None, tolerance=None, include_id=False, @@ -970,7 +976,7 @@ def to_polyline( Args: frame_size: the ``(width, height)`` of the image - classes (None): the list of classes + classes_map (None): a dict mapping class IDs to class labels supercategory_map (None): a dict mapping class names to category dicts tolerance (None): a tolerance, in pixels, when generating @@ -987,7 +993,7 @@ def to_polyline( return None label, attributes = self._get_object_label_and_attributes( - classes, supercategory_map, include_id + classes_map, supercategory_map, include_id ) attributes.update(self.attributes) @@ -1007,7 +1013,7 @@ def to_polyline( def to_keypoints( self, frame_size, - classes=None, + classes_map=None, supercategory_map=None, include_id=False, ): @@ -1016,7 +1022,7 @@ def to_keypoints( Args: frame_size: the ``(width, height)`` of the image - classes (None): the list of classes + classes_map (None): a dict mapping class IDs to class labels supercategory_map (None): a dict mapping class names to category dicts include_id (False): whether to include the COCO ID of the object as @@ -1030,7 +1036,7 @@ def to_keypoints( return None label, attributes = self._get_object_label_and_attributes( - classes, supercategory_map, include_id + classes_map, supercategory_map, include_id ) attributes.update(self.attributes) @@ -1058,7 +1064,7 @@ def to_keypoints( def to_detection( self, frame_size, - classes=None, + classes_map=None, supercategory_map=None, load_segmentation=False, include_id=False, @@ -1068,7 +1074,7 @@ def to_detection( Args: frame_size: the ``(width, height)`` of the image - classes (None): the list of classes + classes_map (None): a dict mapping class IDs to class labels supercategory_map (None): a dict mapping class names to category dicts load_segmentation (False): whether to load the segmentation mask @@ -1084,7 +1090,7 @@ def to_detection( return None label, attributes = self._get_object_label_and_attributes( - classes, supercategory_map, include_id + classes_map, supercategory_map, include_id ) attributes.update(self.attributes) @@ -1310,10 +1316,10 @@ def _get_label(self, classes): return str(self.category_id) def _get_object_label_and_attributes( - self, classes, supercategory_map, include_id + self, classes_map, supercategory_map, include_id ): - if classes: - label = classes[self.category_id] + if classes_map: + label = classes_map[self.category_id] else: label = str(self.category_id) @@ -1354,7 +1360,7 @@ def load_coco_detection_annotations(json_path, extra_attrs=True): a tuple of - info: a dict of dataset info - - classes: a list of classes + - classes_map: a dict mapping class IDs to labels - supercategory_map: a dict mapping class labels to category dicts - images: a dict mapping image IDs to image dicts - annotations: a dict mapping image IDs to list of @@ -1381,9 +1387,9 @@ def _parse_coco_detection_annotations(d, extra_attrs=True): # Load classes if categories is not None: - classes, supercategory_map = parse_coco_categories(categories) + classes_map, supercategory_map = parse_coco_categories(categories) else: - classes = None + classes_map = None supercategory_map = None # Load image metadata @@ -1402,17 +1408,14 @@ def _parse_coco_detection_annotations(d, extra_attrs=True): else: annotations = None - return info, classes, supercategory_map, images, annotations + return info, classes_map, supercategory_map, images, annotations def parse_coco_categories(categories): """Parses the COCO categories list. - The returned ``classes`` contains all class IDs from ``[0, max_id]``, - inclusive. - Args: - categories: a dict of the form:: + categories: a list of dict of the form:: [ ... @@ -1429,25 +1432,15 @@ def parse_coco_categories(categories): Returns: a tuple of - - classes: a list of classes + - classes_map: a dict mapping class ids to labels - supercategory_map: a dict mapping class labels to category dicts """ - cat_map = {c["id"]: c for c in categories} - - classes = [] - supercategory_map = {} - for cat_id in range(max(cat_map, default=-1) + 1): - category = cat_map.get(cat_id, None) - try: - name = category["name"] - except: - name = str(cat_id) - - classes.append(name) - if category is not None: - supercategory_map[name] = category + classes_map = { + c["id"]: c["name"] if "name" in c else str(c["id"]) for c in categories + } + supercategory_map = {c["name"]: c for c in categories} - return classes, supercategory_map + return classes_map, supercategory_map def download_coco_dataset_split( @@ -1623,12 +1616,15 @@ def download_coco_dataset_split( d = etas.load_json(full_anno_path) ( _, - all_classes, + all_classes_map, _, images, annotations, ) = _parse_coco_detection_annotations(d, extra_attrs=True) + if all_classes_map is not None: + all_classes = list(all_classes_map.values()) + if image_ids is not None: # Start with specific images image_ids = _parse_image_ids(image_ids, images, split=split) @@ -1717,7 +1713,8 @@ def download_coco_dataset_split( categories = d.get("categories", None) if categories is not None: - all_classes, _ = parse_coco_categories(categories) + all_classes_map, _ = parse_coco_categories(categories) + all_classes = list(all_classes_map.values()) else: all_classes = None @@ -2002,7 +1999,7 @@ def _get_matching_objects(coco_objects, target_classes, all_classes): def _coco_objects_to_polylines( coco_objects, frame_size, - classes, + classes_map, supercategory_map, tolerance, include_id, @@ -2011,7 +2008,7 @@ def _coco_objects_to_polylines( for coco_obj in coco_objects: polyline = coco_obj.to_polyline( frame_size, - classes=classes, + classes_map=classes_map, supercategory_map=supercategory_map, tolerance=tolerance, include_id=include_id, @@ -2029,7 +2026,7 @@ def _coco_objects_to_polylines( def _coco_objects_to_detections( coco_objects, frame_size, - classes, + classes_map, supercategory_map, load_segmentations, include_id, @@ -2038,7 +2035,7 @@ def _coco_objects_to_detections( for coco_obj in coco_objects: detection = coco_obj.to_detection( frame_size, - classes=classes, + classes_map=classes_map, supercategory_map=supercategory_map, load_segmentation=load_segmentations, include_id=include_id, @@ -2058,7 +2055,7 @@ def _coco_objects_to_detections( def _coco_objects_to_keypoints( coco_objects, frame_size, - classes, + classes_map, supercategory_map, include_id, ): @@ -2066,7 +2063,7 @@ def _coco_objects_to_keypoints( for coco_obj in coco_objects: keypoint = coco_obj.to_keypoints( frame_size, - classes=classes, + classes_map=classes_map, supercategory_map=supercategory_map, include_id=include_id, ) From 56a14e2e138d7e63f5da504351f0b0bdb5fc1d66 Mon Sep 17 00:00:00 2001 From: Theo Patron Date: Thu, 25 Apr 2024 17:46:14 +0200 Subject: [PATCH 006/126] chore: rename self._classes to self._classes_map for consistency --- fiftyone/utils/coco.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/fiftyone/utils/coco.py b/fiftyone/utils/coco.py index 8ebeefae64..948a7732c3 100644 --- a/fiftyone/utils/coco.py +++ b/fiftyone/utils/coco.py @@ -410,7 +410,7 @@ def __init__( self._label_types = _label_types self._info = None - self._classes = None + self._classes_map = None self._license_map = None self._supercategory_map = None self._image_paths_map = None @@ -453,15 +453,16 @@ def __next__(self): frame_size = (width, height) if self.classes is not None and self.only_matching: + all_classes = list(self._classes_map.values()) coco_objects = _get_matching_objects( - coco_objects, self.classes, self._classes + coco_objects, self.classes, all_classes ) if "detections" in self._label_types: detections = _coco_objects_to_detections( coco_objects, frame_size, - self._classes, + self._classes_map, self._supercategory_map, False, # no segmentations self.include_annotation_id, @@ -474,7 +475,7 @@ def __next__(self): segmentations = _coco_objects_to_polylines( coco_objects, frame_size, - self._classes, + self._classes_map, self._supercategory_map, self.tolerance, self.include_annotation_id, @@ -483,7 +484,7 @@ def __next__(self): segmentations = _coco_objects_to_detections( coco_objects, frame_size, - self._classes, + self._classes_map, self._supercategory_map, True, # load segmentations self.include_annotation_id, @@ -496,7 +497,7 @@ def __next__(self): keypoints = _coco_objects_to_keypoints( coco_objects, frame_size, - self._classes, + self._classes_map, self._supercategory_map, self.include_annotation_id, ) @@ -603,7 +604,7 @@ def setup(self): license_map = None self._info = info - self._classes = classes_map + self._classes_map = classes_map self._license_map = license_map self._supercategory_map = supercategory_map self._image_paths_map = image_paths_map From ba60fc2776ab419f622467bf0011d7f197eef1cd Mon Sep 17 00:00:00 2001 From: Theo Patron Date: Thu, 25 Apr 2024 17:50:11 +0200 Subject: [PATCH 007/126] feat: parameter classes of add_coco_labels can now be either a list of classes or a dict mapping ids to labels --- fiftyone/utils/coco.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/fiftyone/utils/coco.py b/fiftyone/utils/coco.py index 948a7732c3..a6319b9363 100644 --- a/fiftyone/utils/coco.py +++ b/fiftyone/utils/coco.py @@ -129,7 +129,8 @@ def add_coco_labels( will be created if necessary labels_or_path: a list of COCO annotations or the path to a JSON file containing such data on disk - classes: the list of class label strings + classes: the list of class label strings or a dict mapping class IDs to + class labels label_type ("detections"): the type of labels to load. Supported values are ``("detections", "segmentations", "keypoints")`` coco_id_field (None): this parameter determines how to map the @@ -194,7 +195,10 @@ def add_coco_labels( view.compute_metadata() widths, heights = view.values(["metadata.width", "metadata.height"]) - classes_map = {i: label for i, label in enumerate(classes)} + if isinstance(classes, dict): + classes_map = classes + else: + classes_map = {i: label for i, label in enumerate(classes)} labels = [] for _coco_objects, width, height in zip(coco_objects, widths, heights): @@ -1433,7 +1437,7 @@ def parse_coco_categories(categories): Returns: a tuple of - - classes_map: a dict mapping class ids to labels + - classes_map: a dict mapping class IDs to labels - supercategory_map: a dict mapping class labels to category dicts """ classes_map = { From 70a9c428af1cdeb799f8bff70eddad002aab2b3b Mon Sep 17 00:00:00 2001 From: brimoor Date: Sun, 5 May 2024 22:54:03 -0400 Subject: [PATCH 008/126] use existing COCO categories during export, if available --- fiftyone/utils/coco.py | 116 +++++++++++++++++++++--------- fiftyone/zoo/datasets/__init__.py | 3 +- 2 files changed, 83 insertions(+), 36 deletions(-) diff --git a/fiftyone/utils/coco.py b/fiftyone/utils/coco.py index a6319b9363..ef75adb662 100644 --- a/fiftyone/utils/coco.py +++ b/fiftyone/utils/coco.py @@ -415,6 +415,7 @@ def __init__( self._label_types = _label_types self._info = None self._classes_map = None + self._class_ids = None self._license_map = None self._supercategory_map = None self._image_paths_map = None @@ -456,10 +457,9 @@ def __next__(self): coco_objects = self._annotations.get(image_id, []) frame_size = (width, height) - if self.classes is not None and self.only_matching: - all_classes = list(self._classes_map.values()) + if self.only_matching and self._class_ids is not None: coco_objects = _get_matching_objects( - coco_objects, self.classes, all_classes + coco_objects, self._class_ids ) if "detections" in self._label_types: @@ -565,7 +565,7 @@ def setup(self): classes = None if classes_map is not None: - classes = list(classes_map.values()) + classes = _to_classes(classes_map) if classes is not None: info["classes"] = classes @@ -607,8 +607,14 @@ def setup(self): else: license_map = None + if self.only_matching and self.classes is not None: + class_ids = _get_class_ids(self.classes, classes_map) + else: + class_ids = None + self._info = info self._classes_map = classes_map + self._class_ids = class_ids self._license_map = license_map self._supercategory_map = supercategory_map self._image_paths_map = image_paths_map @@ -760,7 +766,7 @@ def __init__( self._images = None self._annotations = None self._classes = None - self._dynamic_classes = classes is None + self._dynamic_classes = None self._labels_map_rev = None self._has_labels = None self._media_exporter = None @@ -793,6 +799,8 @@ def setup(self): def log_collection(self, sample_collection): if self.info is None: self.info = sample_collection.info + if "categories" in self.info: + self._parse_classes() def export_sample(self, image_or_path, label, metadata=None): out_image_path, uuid = self._media_exporter.export(image_or_path) @@ -881,30 +889,29 @@ def close(self, *args): else: classes = self.classes - date_created = datetime.now().replace(microsecond=0).isoformat() + _info = self.info or {} + _date_created = datetime.now().replace(microsecond=0).isoformat() + info = { - "year": self.info.get("year", ""), - "version": self.info.get("version", ""), - "contributor": self.info.get("contributor", ""), - "url": self.info.get("url", "https://voxel51.com/fiftyone"), - "date_created": self.info.get("date_created", date_created), + "year": _info.get("year", ""), + "version": _info.get("version", ""), + "contributor": _info.get("contributor", ""), + "url": _info.get("url", "https://voxel51.com/fiftyone"), + "date_created": _info.get("date_created", _date_created), } - licenses = self.info.get("licenses", []) - - supercategory_map = { - c["name"]: c.get("supercategory", None) - for c in self.info.get("categories", []) - } + licenses = _info.get("licenses", []) - categories = [ - { - "id": i, - "name": l, - "supercategory": supercategory_map.get(l, None), - } - for i, l in enumerate(classes) - ] + categories = _info.get("categories", None) + if categories is None: + categories = [ + { + "id": i, + "name": l, + "supercategory": None, + } + for i, l in enumerate(classes) + ] labels = { "info": info, @@ -921,10 +928,20 @@ def close(self, *args): self._media_exporter.close() def _parse_classes(self): - if self._dynamic_classes: + if self.info is not None: + labels_map_rev = _parse_categories(self.info, self.classes) + else: + labels_map_rev = None + + if labels_map_rev is not None: + self._labels_map_rev = labels_map_rev + self._dynamic_classes = False + elif self.classes is None: self._classes = set() + self._dynamic_classes = True else: self._labels_map_rev = _to_labels_map_rev(self.classes) + self._dynamic_classes = False class COCOObject(object): @@ -1058,7 +1075,8 @@ def to_keypoints( visible.append(v) if "visible" in attributes: logger.debug( - "Found a custom attribute named 'visible' which is a reserved name. Ignoring the custom attribute" + "Found a custom attribute named 'visible' which is a " + "reserved name. Ignoring the custom attribute" ) attributes.pop("visible") @@ -1628,7 +1646,7 @@ def download_coco_dataset_split( ) = _parse_coco_detection_annotations(d, extra_attrs=True) if all_classes_map is not None: - all_classes = list(all_classes_map.values()) + all_classes = _to_classes(all_classes_map) if image_ids is not None: # Start with specific images @@ -1719,7 +1737,7 @@ def download_coco_dataset_split( categories = d.get("categories", None) if categories is not None: all_classes_map, _ = parse_coco_categories(categories) - all_classes = list(all_classes_map.values()) + all_classes = _to_classes(all_classes_map) else: all_classes = None @@ -1991,16 +2009,46 @@ def _to_labels_map_rev(classes): return {c: i for i, c in enumerate(classes)} -def _get_matching_objects(coco_objects, target_classes, all_classes): - if etau.is_str(target_classes): - target_classes = [target_classes] +def _to_classes(classes_map): + return [classes_map[i] for i in sorted(classes_map.keys())] - labels_map_rev = _to_labels_map_rev(all_classes) - class_ids = {labels_map_rev[c] for c in target_classes} +def _get_class_ids(classes, classes_map): + if etau.is_str(classes): + classes = [classes] + + labels_map_rev = {c: i for i, c in classes_map.items()} + class_ids = {labels_map_rev[c] for c in classes} + + return class_ids + + +def _get_matching_objects(coco_objects, class_ids): return [obj for obj in coco_objects if obj.category_id in class_ids] +def _parse_categories(info, classes): + categories = info.get("categories", None) + if categories is None: + return None + + try: + classes_map, _ = parse_coco_categories(categories) + except: + logger.debug("Failed to parse categories from info") + return None + + if classes is None: + return {c: i for i, c in classes_map.items()} + + if etau.is_str(classes): + classes = {classes} + else: + classes = set(classes) + + return {c: i for i, c in classes_map.items() if c in classes} + + def _coco_objects_to_polylines( coco_objects, frame_size, diff --git a/fiftyone/zoo/datasets/__init__.py b/fiftyone/zoo/datasets/__init__.py index b55e6d61eb..44fc3f53ed 100644 --- a/fiftyone/zoo/datasets/__init__.py +++ b/fiftyone/zoo/datasets/__init__.py @@ -363,9 +363,8 @@ def load_zoo_dataset( progress=progress, ) - if info.classes is not None: + if info.classes is not None and not dataset.default_classes: dataset.default_classes = info.classes - dataset.save() logger.info("Dataset '%s' created", dataset.name) From 0c3400259ac54e489c69001e0eaa1f0f0e9baee4 Mon Sep 17 00:00:00 2001 From: brimoor Date: Sun, 5 May 2024 23:01:51 -0400 Subject: [PATCH 009/126] robustness --- fiftyone/utils/coco.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fiftyone/utils/coco.py b/fiftyone/utils/coco.py index ef75adb662..d185d48bde 100644 --- a/fiftyone/utils/coco.py +++ b/fiftyone/utils/coco.py @@ -902,7 +902,12 @@ def close(self, *args): licenses = _info.get("licenses", []) - categories = _info.get("categories", None) + try: + categories = _info.get("categories", None) + parse_coco_categories(categories) + except: + categories = None + if categories is None: categories = [ { From 0c9442cc862d7bd6447060a3d53d920546d25b56 Mon Sep 17 00:00:00 2001 From: brimoor Date: Mon, 6 May 2024 00:22:30 -0400 Subject: [PATCH 010/126] require categories to be explicitly passed --- docs/source/user_guide/export_datasets.rst | 9 ++--- fiftyone/utils/coco.py | 40 ++++++++-------------- tests/unittests/import_export_tests.py | 28 +++++++++++++++ 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/docs/source/user_guide/export_datasets.rst b/docs/source/user_guide/export_datasets.rst index 850e9d47d6..1c1731a5f5 100644 --- a/docs/source/user_guide/export_datasets.rst +++ b/docs/source/user_guide/export_datasets.rst @@ -1742,11 +1742,12 @@ format as follows: .. note:: - You can pass the optional `classes` parameter to + You can pass the optional `classes` or `categories` parameters to :meth:`export() ` to - explicitly define the class list to use in the exported labels. Otherwise, - the strategy outlined in :ref:`this section ` will be - used to populate the class list. + explicitly define the class list/category IDs to use in the exported + labels. Otherwise, the strategy outlined in + :ref:`this section ` will be used to populate the class + list. You can also perform labels-only exports of COCO-formatted labels by providing the `labels_path` parameter instead of `export_dir`: diff --git a/fiftyone/utils/coco.py b/fiftyone/utils/coco.py index d185d48bde..43547193a5 100644 --- a/fiftyone/utils/coco.py +++ b/fiftyone/utils/coco.py @@ -693,6 +693,9 @@ class COCODetectionDatasetExporter( images to disk. By default, ``fiftyone.config.default_image_ext`` is used classes (None): the list of possible class labels + categories (None): a list of category dicts in the format of + :meth:`parse_coco_categories` specifying the classes and their + category IDs info (None): a dict of info as returned by :meth:`load_coco_detection_annotations` to include in the exported JSON. If not provided, this info will be extracted when @@ -725,6 +728,7 @@ def __init__( abs_paths=False, image_format=None, classes=None, + categories=None, info=None, extra_attrs=True, annotation_id=None, @@ -754,6 +758,7 @@ def __init__( self.abs_paths = abs_paths self.image_format = image_format self.classes = classes + self.categories = categories self.info = info self.extra_attrs = extra_attrs self.annotation_id = annotation_id @@ -799,8 +804,6 @@ def setup(self): def log_collection(self, sample_collection): if self.info is None: self.info = sample_collection.info - if "categories" in self.info: - self._parse_classes() def export_sample(self, image_or_path, label, metadata=None): out_image_path, uuid = self._media_exporter.export(image_or_path) @@ -902,13 +905,9 @@ def close(self, *args): licenses = _info.get("licenses", []) - try: - categories = _info.get("categories", None) - parse_coco_categories(categories) - except: - categories = None - - if categories is None: + if self.categories is not None: + categories = self.categories + else: categories = [ { "id": i, @@ -933,13 +932,10 @@ def close(self, *args): self._media_exporter.close() def _parse_classes(self): - if self.info is not None: - labels_map_rev = _parse_categories(self.info, self.classes) - else: - labels_map_rev = None - - if labels_map_rev is not None: - self._labels_map_rev = labels_map_rev + if self.categories is not None: + self._labels_map_rev = _parse_categories( + self.categories, classes=self.classes + ) self._dynamic_classes = False elif self.classes is None: self._classes = set() @@ -2032,16 +2028,8 @@ def _get_matching_objects(coco_objects, class_ids): return [obj for obj in coco_objects if obj.category_id in class_ids] -def _parse_categories(info, classes): - categories = info.get("categories", None) - if categories is None: - return None - - try: - classes_map, _ = parse_coco_categories(categories) - except: - logger.debug("Failed to parse categories from info") - return None +def _parse_categories(categories, classes=None): + classes_map, _ = parse_coco_categories(categories) if classes is None: return {c: i for i, c in classes_map.items()} diff --git a/tests/unittests/import_export_tests.py b/tests/unittests/import_export_tests.py index 66b14d7df1..d04d645a58 100644 --- a/tests/unittests/import_export_tests.py +++ b/tests/unittests/import_export_tests.py @@ -1287,6 +1287,34 @@ def test_coco_detection_dataset(self): # data/_images/ self.assertEqual(len(relpath.split(os.path.sep)), 3) + # Non-sequential categories + + export_dir = self._new_dir() + + categories = [ + {"supercategory": "animal", "id": 10, "name": "cat"}, + {"supercategory": "vehicle", "id": 20, "name": "dog"}, + ] + + dataset.export( + export_dir=export_dir, + dataset_type=fo.types.COCODetectionDataset, + categories=categories, + ) + + dataset2 = fo.Dataset.from_dir( + dataset_dir=export_dir, + dataset_type=fo.types.COCODetectionDataset, + label_types="detections", + label_field="predictions", + ) + categories2 = dataset2.info["categories"] + + self.assertSetEqual( + {c["id"] for c in categories}, + {c["id"] for c in categories2}, + ) + @drop_datasets def test_voc_detection_dataset(self): dataset = self._make_dataset() From f68f2c6d013b6170c061b0e45f7a5ced0f1706d6 Mon Sep 17 00:00:00 2001 From: brimoor Date: Wed, 8 May 2024 13:18:41 -0400 Subject: [PATCH 011/126] handle errors, validation, add support for tracking on individual samples --- fiftyone/utils/tracking/deepsort.py | 190 ++++++++++++++++------------ 1 file changed, 109 insertions(+), 81 deletions(-) diff --git a/fiftyone/utils/tracking/deepsort.py b/fiftyone/utils/tracking/deepsort.py index da54e98f3c..bf5ff5e73a 100644 --- a/fiftyone/utils/tracking/deepsort.py +++ b/fiftyone/utils/tracking/deepsort.py @@ -5,105 +5,143 @@ | `voxel51.com `_ | """ -# pylint: disable=no-member - import logging -import cv2 + +import eta.core.video as etav import fiftyone as fo -import fiftyone.zoo as foz import fiftyone.core.utils as fou +import fiftyone.core.validation as fov + +dsrt = fou.lazy_import( + "deep_sort_realtime.deepsort_tracker", + callback=lambda: fou.ensure_package("deep-sort-realtime"), +) -dsrt = fou.lazy_import("deep_sort_realtime.deepsort_tracker") logger = logging.getLogger(__name__) -class DeepSort: +class DeepSort(object): @staticmethod def track( - dataset, + sample_collection, in_field, out_field="frames.ds_tracks", max_age=5, keep_confidence=False, + skip_failures=True, progress=None, ): - """Performs object tracking using the DeepSort algorithm on a video dataset. + """Performs object tracking using the DeepSort algorithm on the given + video samples. DeepSort is an algorithm for tracking multiple objects in video streams based on deep learning techniques. It associates bounding boxes between frames and maintains tracks of objects over time. Args: - dataset: a FiftyOne dataset - in_field: the name of the field containing detections in each frame - out_field ("frames.ds_tracks"): the name of the field to store tracking - information of the detections - max_age (5): the maximum number of missed misses before a track - is deleted. + sample_collection: a + :class:`fiftyone.core.collections.SampleCollection` + in_field: the name of a frame field containing + :class:`fiftyone.core.labels.Detections` to track. The + ``"frames."`` prefix is optional + out_field ("frames.ds_tracks"): the name of a frame field to store + the output :class:`fiftyone.core.labels.Detections` with + tracking information. The ``"frames."`` prefix is optional + max_age (5): the maximum number of missed misses before a track is + deleted keep_confidence (False): whether to store the detection confidence - of the tracked objects in the out_field - progress (None): whether to display a progress bar (True/False) + of the tracked objects in the ``out_field`` + skip_failures (True): whether to gracefully continue without + raising an error if tracking fails for a video + progress (False): whether to render a progress bar (True/False), + use the default value ``fiftyone.config.show_progress_bars`` + (None), or a progress callback function to invoke instead """ - if not in_field.startswith("frames.") or not out_field.startswith( - "frames." + in_field, _ = sample_collection._handle_frame_field(in_field) + out_field, _ = sample_collection._handle_frame_field(out_field) + _in_field = sample_collection._FRAMES_PREFIX + in_field + + fov.validate_video_collection(sample_collection) + fov.validate_collection_label_fields( + sample_collection, _in_field, fo.Detections + ) + + for sample in sample_collection.iter_samples( + autosave=True, progress=progress ): - raise ValueError( - "in_field and out_field must not be empty and must start with 'frames.'" - ) + try: + DeepSort.track_sample( + sample, + in_field, + out_field=out_field, + max_age=max_age, + keep_confidence=keep_confidence, + ) + except Exception as e: + if not skip_failures: + raise e - for sample in dataset.iter_samples(autosave=True, progress=progress): - tracker = dsrt.DeepSort(max_age=max_age) + logger.warning("Sample: %s\nError: %s\n", sample.id, e) - cap = cv2.VideoCapture(sample.filepath) - frames_list = [] + @staticmethod + def track_sample( + sample, + in_field, + out_field="ds_tracks", + max_age=5, + keep_confidence=False, + ): + """Performs object tracking using the DeepSort algorithm on the given + video sample. - while True: - ret, frame = cap.read() - if not ret: - break - frames_list.append(frame) + DeepSort is an algorithm for tracking multiple objects in video streams + based on deep learning techniques. It associates bounding boxes between + frames and maintains tracks of objects over time. - cap.release() + Args: + sample: a :class:`fiftyone.core.sample.Sample` + in_field: the name of the frame field containing + :class:`fiftyone.core.labels.Detections` to track + out_field ("ds_tracks"): the name of a frame field to store the + output :class:`fiftyone.core.labels.Detections` with tracking + information. The ``"frames."`` prefix is optional + max_age (5): the maximum number of missed misses before a track is + deleted + keep_confidence (False): whether to store the detection confidence + of the tracked objects in the ``out_field`` + """ + tracker = dsrt.DeepSort(max_age=max_age) - if len(frames_list) != len(sample.frames): - logger.error( - "Unable to align the captured frames with the encoded frames!" - ) - return + with etav.FFmpegVideoReader(sample.filepath) as video_reader: + for img in video_reader: + frame = sample.frames[video_reader.frame_number] + frame_width = img.shape[1] + frame_height = img.shape[0] - for frame_idx, frame in sample.frames.items(): - frame_detections = frame[in_field[len("frames.") :]] bbs = [] - extracted_detections = foz.deepcopy( - frame_detections.detections - ) - frame_width = frames_list[frame_idx - 1].shape[1] - frame_height = frames_list[frame_idx - 1].shape[0] - - for detection in extracted_detections: - coordinates = detection.bounding_box - coordinates[0] *= frame_width - coordinates[1] *= frame_height - coordinates[2] *= frame_width - coordinates[3] *= frame_height - confidence = ( - detection.confidence if detection.confidence else 0 - ) - detection_class = detection.label - - bbs.append(((coordinates), confidence, detection_class)) - tracks = tracker.update_tracks( - bbs, frame=frames_list[frame_idx - 1] - ) + if frame[in_field] is not None: + for detection in frame[in_field].detections: + bbox = detection.bounding_box + coordinates = [ + bbox[0] * frame_width, + bbox[1] * frame_height, + bbox[2] * frame_width, + bbox[3] * frame_height, + ] + confidence = detection.confidence or 0 + label = detection.label + bbs.append(((coordinates), confidence, label)) + + tracks = tracker.update_tracks(bbs, frame=img) tracked_detections = [] - - for _, track in enumerate(tracks): + for track in tracks: if not track.is_confirmed(): continue + ltrb = track.to_ltrb() x1, y1, x2, y2 = ltrb w, h = x2 - x1, y2 - y1 @@ -113,24 +151,14 @@ def track( rel_w = min(w / frame_width, 1 - rel_x) rel_h = min(h / frame_height, 1 - rel_y) + detection = fo.Detection( + label=track.get_det_class(), + bounding_box=[rel_x, rel_y, rel_w, rel_h], + index=track.track_id, + ) if keep_confidence: - tracked_detections.append( - fo.Detection( - label=track.get_det_class(), - confidence=track.get_det_conf(), - bounding_box=[rel_x, rel_y, rel_w, rel_h], - index=track.track_id, - ) - ) - else: - tracked_detections.append( - fo.Detection( - label=track.get_det_class(), - bounding_box=[rel_x, rel_y, rel_w, rel_h], - index=track.track_id, - ) - ) - - frame[out_field[len("frames.") :]] = fo.Detections( - detections=tracked_detections - ) + detection.confidence = track.get_det_conf() + + tracked_detections.append(detection) + + frame[out_field] = fo.Detections(detections=tracked_detections) From 4b2f865aa2e678ee8d2b8fc9f9e20260e8d2f855 Mon Sep 17 00:00:00 2001 From: brimoor Date: Wed, 8 May 2024 13:23:30 -0400 Subject: [PATCH 012/126] use select_fields() to optimize --- fiftyone/utils/tracking/deepsort.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fiftyone/utils/tracking/deepsort.py b/fiftyone/utils/tracking/deepsort.py index bf5ff5e73a..0c688cf0f6 100644 --- a/fiftyone/utils/tracking/deepsort.py +++ b/fiftyone/utils/tracking/deepsort.py @@ -68,9 +68,9 @@ def track( sample_collection, _in_field, fo.Detections ) - for sample in sample_collection.iter_samples( - autosave=True, progress=progress - ): + view = sample_collection.select_fields(_in_field) + + for sample in view.iter_samples(autosave=True, progress=progress): try: DeepSort.track_sample( sample, From 80735430291beb146d0a6cae79d952188d3d74cd Mon Sep 17 00:00:00 2001 From: Jacob Marks Date: Wed, 8 May 2024 13:43:12 -0400 Subject: [PATCH 013/126] Update index.rst Adding custom runs plugin and minor tweaks --- docs/source/plugins/index.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/source/plugins/index.rst b/docs/source/plugins/index.rst index 7fe88eded2..401006eb39 100644 --- a/docs/source/plugins/index.rst +++ b/docs/source/plugins/index.rst @@ -39,13 +39,15 @@ these plugins available in the +-------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ | `@voxel51/annotation `_ | ✏️ Utilities for integrating FiftyOne with annotation tools | +-------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ - | `@voxel51/brain `_ | 🧠 Utilities for working with the FiftyOne Brain | + | `@voxel51/brain `_ | 🧠 Utilities for working with the FiftyOne Brain | +-------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ - | `@voxel51/evaluation `_ | ✅ Utilities for evaluating models with FiftyOne | + | `@voxel51/evaluation `_ | ✅ Utilities for evaluating models with FiftyOne | +-------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ | `@voxel51/io `_ | 📁 A collection of import/export utilities | +-------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ - | `@voxel51/indexes `_ | 📈 Utilities working with FiftyOne database indexes | + | `@voxel51/indexes `_ | 📈 Utilities for working with FiftyOne database indexes | + +-------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | `@voxel51/runs `_ | 📈 Utilities for working with custom runs | +-------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ | `@voxel51/utils `_ | ⚒️ Call your favorite SDK utilities from the App | +-------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ @@ -54,13 +56,13 @@ these plugins available in the | `@voxel51/zoo `_ | 🌎 Download datasets and run inference with models from the FiftyOne Zoo, all without leaving the App | +-------------------------------------------------------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------+ -For example, wish you could import data from within the App? With the +For example, do you wish you could import data from within the App? With the `@voxel51/io `_, -plugin you can! +plugin, you can! .. image:: /images/plugins/operators/examples/import.gif -Want to send data for annotation from within the App? Sure thing, just install the +Want to send data for annotation from within the App? Sure thing! Just install the `@voxel51/annotation `_ plugin: From 9103bce940a3c0407baa94ed2dc3e60c663363af Mon Sep 17 00:00:00 2001 From: Benjamin Kane Date: Wed, 8 May 2024 18:33:46 -0400 Subject: [PATCH 014/126] fix absolute css position for view bar icons --- app/packages/core/src/components/ViewBar/ViewBar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/packages/core/src/components/ViewBar/ViewBar.tsx b/app/packages/core/src/components/ViewBar/ViewBar.tsx index 7e601815a8..bdb3ec5332 100644 --- a/app/packages/core/src/components/ViewBar/ViewBar.tsx +++ b/app/packages/core/src/components/ViewBar/ViewBar.tsx @@ -43,6 +43,7 @@ const IconsContainer = styled.div` display: flex; align-items: center; position: absolute; + top: 2px; z-index: 1; height: 100%; border-radius: 3px; From 3deede5630372da9e9dedae904d9ca41e25db887 Mon Sep 17 00:00:00 2001 From: Stuart Date: Wed, 8 May 2024 21:53:27 -0400 Subject: [PATCH 015/126] Fix DateField for GMT+ users (#4371) * convert to timezone only if datetime is tzaware; add exposing test --- fiftyone/core/fields.py | 10 ++++++++-- fiftyone/core/odm/database.py | 2 +- tests/unittests/dataset_tests.py | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/fiftyone/core/fields.py b/fiftyone/core/fields.py index 64eea2ff4d..c3b5b0de37 100644 --- a/fiftyone/core/fields.py +++ b/fiftyone/core/fields.py @@ -516,8 +516,14 @@ def to_python(self, value): # Explicitly converting to UTC is important here because PyMongo loads # everything as `datetime`, which will respect `fo.config.timezone`, - # but we always need UTC here for the conversion back to `date` - return value.astimezone(pytz.utc).date() + # but we always need UTC here for the conversion back to `date` because + # we write to the DB in UTC time. + # If value is timezone unaware, then do not convert because it will be + # assumed to be in local timezone and date will be wrong for GMT+ + # localities. + if value.tzinfo is not None: + value = value.astimezone(pytz.utc) + return value.date() def validate(self, value): if not isinstance(value, date): diff --git a/fiftyone/core/odm/database.py b/fiftyone/core/odm/database.py index b875fc4631..8f0d40f13f 100644 --- a/fiftyone/core/odm/database.py +++ b/fiftyone/core/odm/database.py @@ -422,7 +422,7 @@ def get_async_db_conn(use_global=False): def _apply_options(db): timezone = fo.config.timezone - if not timezone or timezone.lower() == "utc": + if not timezone: return db if timezone.lower() == "local": diff --git a/tests/unittests/dataset_tests.py b/tests/unittests/dataset_tests.py index e581b3ecfc..ef69954f97 100644 --- a/tests/unittests/dataset_tests.py +++ b/tests/unittests/dataset_tests.py @@ -5,6 +5,7 @@ | `voxel51.com `_ | """ +import time from copy import deepcopy, copy from datetime import date, datetime, timedelta import gc @@ -558,6 +559,22 @@ def test_date_fields(self): self.assertEqual(type(sample.date), date) self.assertEqual(int((sample.date - date1).total_seconds()), 0) + # Now change system time to something GMT+ + system_timezone = os.environ.get("TZ") + try: + os.environ["TZ"] = "Europe/Madrid" + time.tzset() + dataset.reload() + finally: + if system_timezone is None: + del os.environ["TZ"] + else: + os.environ["TZ"] = system_timezone + time.tzset() + + self.assertEqual(type(sample.date), date) + self.assertEqual(int((sample.date - date1).total_seconds()), 0) + @drop_datasets def test_datetime_fields(self): dataset = fo.Dataset() From a4d377578dbd56294ff01c1ee73cc3d0d049f8a5 Mon Sep 17 00:00:00 2001 From: Stuart Date: Thu, 9 May 2024 11:13:47 -0400 Subject: [PATCH 016/126] 3D export fixes and tests (#4368) * WIP 3D export fixes and tests * print * errant copypasta * move rewrite asset logic to scene_3d * cleanup 3d import/export tests a bit --- fiftyone/core/threed/scene_3d.py | 44 ++++ fiftyone/utils/data/exporters.py | 46 +--- tests/unittests/import_export_tests.py | 307 +++++++++++++++++++++++++ 3 files changed, 359 insertions(+), 38 deletions(-) diff --git a/fiftyone/core/threed/scene_3d.py b/fiftyone/core/threed/scene_3d.py index df32d13761..82d94ded6f 100644 --- a/fiftyone/core/threed/scene_3d.py +++ b/fiftyone/core/threed/scene_3d.py @@ -248,6 +248,10 @@ def traverse(self, include_self=False): Args: include_self: whether to include the current node in the traversal + + Yields: + :class:`Object3D` + """ if include_self: yield self @@ -255,6 +259,46 @@ def traverse(self, include_self=False): for child in self.children: yield from child.traverse(include_self=True) + def update_asset_paths(self, asset_rewrite_paths: dict): + """Update asset paths in this scene according to an input dict mapping. + + Asset path is unchanged if it does not exist in ``asset_rewrite_paths`` + + Args: + asset_rewrite_paths: ``dict`` mapping asset path to new asset path + + Returns: + ``True`` if the scene was modified. + """ + scene_modified = False + for node in self.traverse(): + for path_attribute in node._asset_path_fields: + asset_path = getattr(node, path_attribute, None) + new_asset_path = asset_rewrite_paths.get(asset_path) + + if asset_path is not None and asset_path != new_asset_path: + setattr(node, path_attribute, new_asset_path) + scene_modified = True + + # modify scene background paths, if any + if self.background is not None: + if self.background.image is not None: + new_asset_path = asset_rewrite_paths.get(self.background.image) + if new_asset_path != self.background.image: + self.background.image = new_asset_path + scene_modified = True + + if self.background.cube is not None: + new_cube = [ + asset_rewrite_paths.get(face) + for face in self.background.cube + ] + if new_cube != self.background.cube: + self.background.cube = new_cube + scene_modified = True + + return scene_modified + def get_scene_summary(self): """Returns a summary of the scene.""" node_types = Counter(map(type, self.traverse())) diff --git a/fiftyone/utils/data/exporters.py b/fiftyone/utils/data/exporters.py index 8cb4b85b0c..3d5ca399fc 100644 --- a/fiftyone/utils/data/exporters.py +++ b/fiftyone/utils/data/exporters.py @@ -1171,6 +1171,7 @@ def _handle_fo3d_file(self, fo3d_path, fo3d_output_path, export_mode): scene = fo3d.Scene.from_fo3d(fo3d_path) asset_paths = scene.get_asset_paths() + input_to_output_paths = {} for asset_path in asset_paths: if not os.path.isabs(asset_path): absolute_asset_path = os.path.join( @@ -1181,12 +1182,15 @@ def _handle_fo3d_file(self, fo3d_path, fo3d_output_path, export_mode): seen = self._filename_maker.seen_input_path(absolute_asset_path) - if seen: - continue - asset_output_path = self._filename_maker.get_output_path( absolute_asset_path ) + input_to_output_paths[asset_path] = os.path.relpath( + asset_output_path, os.path.dirname(fo3d_output_path) + ) + + if seen: + continue if export_mode is True: etau.copy_file(absolute_asset_path, asset_output_path) @@ -1195,41 +1199,7 @@ def _handle_fo3d_file(self, fo3d_path, fo3d_output_path, export_mode): elif export_mode == "symlink": etau.symlink_file(absolute_asset_path, asset_output_path) - is_scene_modified = False - - for node in scene.traverse(): - path_attribute = next( - ( - attr - for attr in fo3d.fo3d_path_attributes - if hasattr(node, attr) - ), - None, - ) - - if path_attribute is not None: - asset_path = getattr(node, path_attribute) - - is_nested_path = os.path.split(asset_path)[0] != "" - - if asset_path is not None and is_nested_path: - setattr(node, path_attribute, os.path.basename(asset_path)) - is_scene_modified = True - - # modify scene background paths, if any - if scene.background is not None: - if scene.background.image is not None: - scene.background.image = os.path.basename( - scene.background.image - ) - is_scene_modified = True - - if scene.background.cube is not None: - scene.background.cube = [ - os.path.basename(face_path) - for face_path in scene.background.cube - ] - is_scene_modified = True + is_scene_modified = scene.update_asset_paths(input_to_output_paths) if is_scene_modified: # note: we can't have different behavior for "symlink" because diff --git a/tests/unittests/import_export_tests.py b/tests/unittests/import_export_tests.py index d04d645a58..72180cfb2a 100644 --- a/tests/unittests/import_export_tests.py +++ b/tests/unittests/import_export_tests.py @@ -6,8 +6,10 @@ | """ import os +import pathlib import random import string +import tempfile import unittest import cv2 @@ -4676,6 +4678,311 @@ def test_media_directory(self): self.assertEqual(len(relpath.split(os.path.sep)), 2) +class ThreeDMediaTests(unittest.TestCase): + """Tests mostly for proper media export. Labels are tested + properly elsewhere, 3D should be no different in that regard. + """ + + def _build_flat_relative(self, temp_dir): + # Scene has relative asset paths + # Data layout: + # data/ + # image.jpeg + # pcd.pcd + # obj.obj + # mtl.mtl + # s1.fo3d + root_data_dir = os.path.join(temp_dir, "data") + s = fo.Scene() + s.background = fo.SceneBackground(image="image.jpeg") + s.add(fo.PointCloud("pcd", "pcd.pcd")) + s.add(fo.ObjMesh("obj", "obj.obj", "mtl.mtl")) + scene_path = os.path.join(root_data_dir, "s1.fo3d") + s.write(scene_path) + for file in s.get_asset_paths(): + with open(os.path.join(root_data_dir, file), "w") as f: + f.write(file) + dataset = fo.Dataset() + dataset.add_sample(fo.Sample(scene_path)) + return s, dataset + + def _build_flat_absolute(self, temp_dir): + # Scene has absolute asset paths + # Data layout: + # data/ + # image.jpeg + # pcd.pcd + # obj.obj + # mtl.mtl + # s1.fo3d + root_data_dir = os.path.join(temp_dir, "data") + s = fo.Scene() + s.background = fo.SceneBackground( + image=os.path.join(root_data_dir, "image.jpeg") + ) + s.add(fo.PointCloud("pcd", os.path.join(root_data_dir, "pcd.pcd"))) + s.add( + fo.ObjMesh( + "obj", + os.path.join(root_data_dir, "obj.obj"), + os.path.join(root_data_dir, "mtl.mtl"), + ) + ) + scene_path = os.path.join(root_data_dir, "s1.fo3d") + s.write(scene_path) + for file in s.get_asset_paths(): + with open(os.path.join(root_data_dir, file), "w") as f: + f.write(os.path.basename(file)) + + dataset = fo.Dataset() + dataset.add_sample(fo.Sample(scene_path)) + return s, dataset + + def _build_nested_relative(self, temp_dir): + # Scene has relative asset paths + # Data layout: + # data/ + # image.jpeg + # label1/ + # test/ + # s.fo3d + # sub/ + # pcd.pcd + # obj.obj + # mtl.mtl + # label2/ + # test/ + # s.fo3d + # sub/ + # pcd.pcd + # obj.obj + # mtl.mtl + root_data_dir = os.path.join(temp_dir, "data") + scene1_dir = os.path.join(root_data_dir, "label1", "test") + + s = fo.Scene() + s.background = fo.SceneBackground(image="../../image.jpeg") + s.add(fo.PointCloud("pcd", "sub/pcd.pcd")) + s.add( + fo.ObjMesh( + "obj", + "sub/obj.obj", + "sub/mtl.mtl", + ) + ) + scene_path = os.path.join(scene1_dir, "s.fo3d") + s.write(scene_path) + + scene2_dir = os.path.join(root_data_dir, "label2", "test") + + scene_path2 = os.path.join(scene2_dir, "s.fo3d") + + # Scene2 is the same except change something small so we know which + # is which. + s.background.color = "red" + s.write(scene_path2) + + # Write content as filename (with 2 suffix for files from scene 2) + for file in s.get_asset_paths(): + f = pathlib.Path(os.path.join(scene1_dir, file)) + f.parent.mkdir(parents=True, exist_ok=True) + f.write_text(os.path.basename(file)) + + if file.endswith("image.jpeg"): + continue + + f = pathlib.Path(os.path.join(scene2_dir, file)) + f.parent.mkdir(parents=True, exist_ok=True) + f.write_text(os.path.basename(file) + "2") + + dataset = fo.Dataset() + dataset.add_samples([fo.Sample(scene_path), fo.Sample(scene_path2)]) + return dataset + + def _assert_scene_content(self, original_scene, scene, export_dir=None): + self.assertEqual(original_scene, scene) + for file in scene.get_asset_paths(): + if export_dir: + file = os.path.join(export_dir, file) + with open(file) as f: + self.assertEqual(f.read(), os.path.basename(file)) + + @drop_datasets + def test_flat_relative(self): + """Tests a simple flat and relative-addressed scene""" + with tempfile.TemporaryDirectory() as temp_dir: + s, dataset = self._build_flat_relative(temp_dir) + + # Export + export_dir = os.path.join(temp_dir, "export") + dataset.export( + export_dir=export_dir, + dataset_type=fo.types.MediaDirectory, + export_media=True, + ) + + # All files flat in export_dir + fileset = set(os.listdir(export_dir)) + self.assertSetEqual( + fileset, + {"image.jpeg", "pcd.pcd", "obj.obj", "mtl.mtl", "s1.fo3d"}, + ) + + # Same file content + scene2 = fo.Scene.from_fo3d(os.path.join(export_dir, "s1.fo3d")) + self._assert_scene_content(s, scene2, export_dir) + + @drop_datasets + def test_flat_absolute(self): + """Tests a simple flat and absolute-addressed scene""" + with tempfile.TemporaryDirectory() as temp_dir: + s, dataset = self._build_flat_absolute(temp_dir) + + # Export it + export_dir = os.path.join(temp_dir, "export") + dataset.export( + export_dir=export_dir, + dataset_type=fo.types.MediaDirectory, + export_media=True, + ) + + # All files flat in export_dir + fileset = set(os.listdir(export_dir)) + self.assertSetEqual( + fileset, + {"image.jpeg", "pcd.pcd", "obj.obj", "mtl.mtl", "s1.fo3d"}, + ) + + # Write temp scene with resolving relative paths, so we can test + # that scenes are equal if relative paths are resolved + tmp_scene = fo.Scene.from_fo3d(os.path.join(export_dir, "s1.fo3d")) + tmp_scene.write( + os.path.join(export_dir, "test.fo3d"), + resolve_relative_paths=True, + ) + scene2 = fo.Scene.from_fo3d(os.path.join(export_dir, "test.fo3d")) + + self._assert_scene_content(s, scene2) + + @drop_datasets + def test_relative_nested_flatten(self): + """Tests nested structure is flattened to export dir. Will require + rename of duplicate asset file names and change of relative asset path + in fo3d file. + """ + with tempfile.TemporaryDirectory() as temp_dir: + dataset = self._build_nested_relative(temp_dir) + + # Export it and flatten (no rel_dir) + export_dir = os.path.join(temp_dir, "export") + dataset.export( + export_dir=export_dir, + dataset_type=fo.types.MediaDirectory, + export_media=True, + ) + + # Flattening should mean duplicate file names gain a '-2' + fileset = set(os.listdir(export_dir)) + self.assertSetEqual( + fileset, + { + "image.jpeg", + "pcd.pcd", + "obj.obj", + "mtl.mtl", + "s.fo3d", + "image.jpeg", + "pcd-2.pcd", + "obj-2.obj", + "mtl-2.mtl", + "s-2.fo3d", + }, + ) + + # Scene 1 + scene1_2 = fo.Scene.from_fo3d(os.path.join(export_dir, "s.fo3d")) + self.assertSetEqual( + set(scene1_2.get_asset_paths()), + { + "image.jpeg", + "pcd.pcd", + "obj.obj", + "mtl.mtl", + }, + ) + # Scene 2 + scene2_2 = fo.Scene.from_fo3d(os.path.join(export_dir, "s-2.fo3d")) + self.assertSetEqual( + set(scene2_2.get_asset_paths()), + { + "image.jpeg", + "pcd-2.pcd", + "obj-2.obj", + "mtl-2.mtl", + }, + ) + + # Make sure we align on scene number from before - remember, scene2 + # has a red background! Swap if necessary + if scene1_2.background.color == "red": + scene2_2, scene1_2 = scene1_2, scene2_2 + + for file in scene1_2.get_asset_paths(): + with open(os.path.join(export_dir, file)) as f: + self.assertEqual(f.read(), os.path.basename(file)) + + for file in scene2_2.get_asset_paths(): + if file.endswith("image.jpeg"): + continue + with open(os.path.join(export_dir, file)) as f: + self.assertEqual( + f.read(), + os.path.basename(file).replace("-2", "") + "2", + ) + + @drop_datasets + def test_relative_nested_maintain(self): + """Tests nested structure is maintained in export dir. No change in + relative asset paths in fo3d file. + """ + with tempfile.TemporaryDirectory() as temp_dir: + dataset = self._build_nested_relative(temp_dir) + + # Export it - with root data dir as rel_dir + root_data_dir = os.path.join(temp_dir, "data") + export_dir = os.path.join(temp_dir, "export") + + dataset.export( + export_dir=export_dir, + dataset_type=fo.types.MediaDirectory, + export_media=True, + rel_dir=root_data_dir, + ) + + scene1 = fo.Scene.from_fo3d( + os.path.join(export_dir, "label1/test/s.fo3d") + ) + self.assertEqual(scene1.background.image, "../../image.jpeg") + + for file in scene1.get_asset_paths(): + with open(os.path.join(export_dir, "label1/test/", file)) as f: + self.assertEqual(f.read(), os.path.basename(file)) + + scene2 = fo.Scene.from_fo3d( + os.path.join(export_dir, "label2/test/s.fo3d") + ) + self.assertEqual(scene2.background.image, "../../image.jpeg") + + for file in scene2.get_asset_paths(): + with open(os.path.join(export_dir, "label2/test/", file)) as f: + if file.endswith("image.jpeg"): + continue + self.assertEqual( + f.read(), + os.path.basename(file) + "2", + ) + + def _relpath(path, start): # Avoids errors related to symlinks in `/tmp` directories return os.path.relpath(os.path.realpath(path), os.path.realpath(start)) From 1f1903b8d9f84169fc3ae513362236d1fc498940 Mon Sep 17 00:00:00 2001 From: Benjamin Kane Date: Thu, 9 May 2024 11:30:12 -0400 Subject: [PATCH 017/126] Fix compute visualization results from operators (#4324) * add np casting to json encoder * isinstance * add test * add await --- fiftyone/server/decorators.py | 25 +++++++++++++++-- tests/unittests/server_decorators_tests.py | 32 ++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 tests/unittests/server_decorators_tests.py diff --git a/fiftyone/server/decorators.py b/fiftyone/server/decorators.py index 1d7e434e42..6f013b2894 100644 --- a/fiftyone/server/decorators.py +++ b/fiftyone/server/decorators.py @@ -5,11 +5,14 @@ | `voxel51.com `_ | """ + +from json import JSONEncoder import traceback import typing as t import logging from bson import json_util +import numpy as np from fiftyone.core.utils import run_sync_task @@ -18,6 +21,23 @@ from starlette.requests import Request +class Encoder(JSONEncoder): + def default(self, o): + if isinstance(o, np.floating): + return float(o) + + if isinstance(o, np.integer): + return int(o) + + return JSONEncoder.default(self, o) + + +async def create_response(response: dict): + return Response( + await run_sync_task(lambda: json_util.dumps(response, cls=Encoder)) + ) + + def route(func): async def wrapper( endpoint: HTTPEndpoint, request: Request, *args @@ -30,9 +50,8 @@ async def wrapper( if isinstance(response, Response): return response - return Response( - await run_sync_task(lambda: json_util.dumps(response)) - ) + return await create_response(response) + except Exception as e: logging.exception(e) return JSONResponse( diff --git a/tests/unittests/server_decorators_tests.py b/tests/unittests/server_decorators_tests.py new file mode 100644 index 0000000000..a281ad1a2c --- /dev/null +++ b/tests/unittests/server_decorators_tests.py @@ -0,0 +1,32 @@ +""" +FiftyOne Server decorators. + +| Copyright 2017-2024, Voxel51, Inc. +| `voxel51.com `_ +| +""" + +import unittest + +import numpy as np + +from fiftyone.server.decorators import create_response + + +class TestNumPyResponse(unittest.IsolatedAsyncioTestCase): + async def test_numpy_response(self): + await create_response( + { + "float16": np.array([16.0], dtype=np.float16), + "float32": np.array([32.0], dtype=np.float32), + "float64": np.array([64.0], dtype=np.float64), + "int8": np.array([8], dtype=np.int8), + "int16": np.array([8], dtype=np.int16), + "int32": np.array([8], dtype=np.int32), + "int64": np.array([8], dtype=np.int64), + "uint8": np.array([8], dtype=np.uint8), + "uint6": np.array([8], dtype=np.uint16), + "uint32": np.array([8], dtype=np.uint32), + "uint64": np.array([8], dtype=np.uint64), + } + ) From 887f47af8ca18b0d0d724e81ddf150339fb295c8 Mon Sep 17 00:00:00 2001 From: Benjamin Kane Date: Thu, 9 May 2024 13:02:49 -0400 Subject: [PATCH 018/126] Add state processing to the refresh event (#4347) * handle state payload in useRefresh * cleanup * skip e2e refresh * cleanup, comment --- app/packages/app/src/useEvents/useRefresh.ts | 35 ++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/app/packages/app/src/useEvents/useRefresh.ts b/app/packages/app/src/useEvents/useRefresh.ts index 3ae7bea9fa..54bf9e3174 100644 --- a/app/packages/app/src/useEvents/useRefresh.ts +++ b/app/packages/app/src/useEvents/useRefresh.ts @@ -1,8 +1,37 @@ -import { useRefresh as useRefreshState } from "@fiftyone/state"; +import { subscribe } from "@fiftyone/relay"; +import * as fos from "@fiftyone/state"; +import { useCallback } from "react"; +import { resolveURL } from "../utils"; import { EventHandlerHook } from "./registerEvent"; +import { processState } from "./utils"; -const useRefresh: EventHandlerHook = () => { - return useRefreshState(); +/** + * Handles a session state refresh event from the server and reloads the page + * query and aggregation requests + */ +const useRefresh: EventHandlerHook = ({ router, session }) => { + return useCallback( + (payload: any) => { + const state = processState(session.current, payload.state); + const path = resolveURL({ + currentPathname: router.history.location.pathname, + currentSearch: router.history.location.search, + nextDataset: payload.state.dataset ?? null, + nextView: payload.state.saved_view_slug, + extra: { + workspace: state.workspace?._name || null, + }, + }); + + const unsubscribe = subscribe((_, { set }) => { + set(fos.refresher, (cur) => cur + 1); + unsubscribe(); + }); + + router.history.replace(path, state); + }, + [router, session] + ); }; export default useRefresh; From 461679dc2d537cba1d88930fc315c3ed17ce8611 Mon Sep 17 00:00:00 2001 From: Sashank Aryal <66688606+sashankaryal@users.noreply.github.com> Date: Thu, 9 May 2024 13:51:04 -0500 Subject: [PATCH 019/126] fix two failing e2e tests (#4380) * fix multi pcd e2e * fix media visibility toggler e2e --- app/packages/looker-3d/src/fo3d/Leva.tsx | 5 ++ .../src/oss/poms/fo3d/assets-panel/index.ts | 36 +++++++++++++ .../oss/specs/groups/modal-multi-pcd.spec.ts | 51 ++++++++++++------- .../smoke-tests/quickstart-groups.spec.ts | 14 ++++- 4 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 e2e-pw/src/oss/poms/fo3d/assets-panel/index.ts diff --git a/app/packages/looker-3d/src/fo3d/Leva.tsx b/app/packages/looker-3d/src/fo3d/Leva.tsx index 2a45b4a3db..2d5bd12450 100644 --- a/app/packages/looker-3d/src/fo3d/Leva.tsx +++ b/app/packages/looker-3d/src/fo3d/Leva.tsx @@ -69,9 +69,14 @@ function Leva() { const levaContainer = levaParentContainer.firstElementChild as HTMLDivElement; + + levaContainer.setAttribute("data-cy", "leva-container"); + const levaContainerHeader = levaContainer.firstElementChild as HTMLDivElement; + levaContainerHeader.setAttribute("data-cy", "leva-container-header"); + levaContainerHeader.addEventListener("mousedown", mouseDownEventHandler); levaContainerHeader.addEventListener("mouseup", mouseUpEventHandler); levaContainerHeader.addEventListener("mousemove", mouseMoveEventHandler); diff --git a/e2e-pw/src/oss/poms/fo3d/assets-panel/index.ts b/e2e-pw/src/oss/poms/fo3d/assets-panel/index.ts new file mode 100644 index 0000000000..f1dbb61645 --- /dev/null +++ b/e2e-pw/src/oss/poms/fo3d/assets-panel/index.ts @@ -0,0 +1,36 @@ +import { Locator, Page } from "src/oss/fixtures"; + +export class Asset3dPanelPom { + readonly locator: Locator; + readonly headerLocator: Locator; + + constructor(private readonly page: Page) { + this.locator = this.page.getByTestId("leva-container"); + this.headerLocator = this.page.getByTestId("leva-container-header"); + } + + async dragToToLeftCorner() { + await this.headerLocator.waitFor({ state: "visible" }); + + const levaContainerBox = await this.locator.boundingBox(); + + if (!levaContainerBox) { + throw new Error("Unable to find bounding box on leva container"); + } + + const dragXOffset = -this.page.viewportSize().width / 2; + const dragYOffset = -10; + + const levaCenterX = levaContainerBox.x + levaContainerBox.width / 2; + const levaCenterY = levaContainerBox.y + levaContainerBox.height / 2; + + await this.headerLocator.hover(); + + await this.page.mouse.down(); + await this.page.mouse.move( + levaCenterX + dragXOffset, + levaCenterY + dragYOffset + ); + await this.page.mouse.up(); + } +} diff --git a/e2e-pw/src/oss/specs/groups/modal-multi-pcd.spec.ts b/e2e-pw/src/oss/specs/groups/modal-multi-pcd.spec.ts index 8d22d71662..5276ffb92a 100644 --- a/e2e-pw/src/oss/specs/groups/modal-multi-pcd.spec.ts +++ b/e2e-pw/src/oss/specs/groups/modal-multi-pcd.spec.ts @@ -14,18 +14,33 @@ const test = base.extend<{ grid: GridPom; modal: ModalPom }>({ const datasetName = getUniqueDatasetNameWithPrefix(`modal-multi-pcd`); -test.beforeAll(async ({ fiftyoneLoader }) => { +const pcd1Path = `/tmp/test-pcd1-${datasetName}.pcd`; +const pcd2Path = `/tmp/test-pcd2-${datasetName}.pcd`; + +test.beforeAll(async ({ fiftyoneLoader, mediaFactory }) => { + mediaFactory.createPcd({ + outputPath: pcd1Path, + shape: "cube", + numPoints: 100, + }); + + mediaFactory.createPcd({ + outputPath: pcd2Path, + shape: "diagonal", + numPoints: 5, + }); + await fiftyoneLoader.executePythonCode(` - import fiftyone.zoo as foz + import fiftyone as fo - dataset = foz.load_zoo_dataset( - "quickstart-groups", dataset_name="${datasetName}", max_samples=3 - ) + dataset = fo.Dataset("${datasetName}") dataset.persistent = True - dataset.group_slice = "pcd" - extra = dataset.first().copy() - extra.group.name = "extra" - dataset.add_sample(extra)`); + + group = fo.Group() + sample1 = fo.Sample(filepath="${pcd1Path}", group=group.element("pcd1")) + sample2 = fo.Sample(filepath="${pcd2Path}", group=group.element("pcd2")) + + dataset.add_samples([sample1, sample2])`); }); test.beforeEach(async ({ page, fiftyoneLoader }) => { @@ -35,20 +50,22 @@ test.beforeEach(async ({ page, fiftyoneLoader }) => { test.describe("multi-pcd", () => { test("multi-pcd slice in modal", async ({ grid, modal }) => { await grid.openFirstSample(); - await modal.group.toggleMedia("carousel"); - await modal.group.toggleMedia("viewer"); + await modal.clickOnLooker3d(); - await modal.toggleLooker3dSlice("extra"); + await modal.toggleLooker3dSlice("pcd2"); - await modal.sidebar.assert.verifySidebarEntryText("pcd-group.name", "pcd"); await modal.sidebar.assert.verifySidebarEntryText( - "extra-group.name", - "extra" + "pcd1-group.name", + "pcd1" + ); + await modal.sidebar.assert.verifySidebarEntryText( + "pcd2-group.name", + "pcd2" ); - await modal.toggleLooker3dSlice("pcd"); + await modal.toggleLooker3dSlice("pcd1"); - await modal.sidebar.assert.verifySidebarEntryText("group.name", "extra"); + await modal.sidebar.assert.verifySidebarEntryText("group.name", "pcd2"); }); }); diff --git a/e2e-pw/src/oss/specs/smoke-tests/quickstart-groups.spec.ts b/e2e-pw/src/oss/specs/smoke-tests/quickstart-groups.spec.ts index 7fecc043f9..24eaa714fa 100644 --- a/e2e-pw/src/oss/specs/smoke-tests/quickstart-groups.spec.ts +++ b/e2e-pw/src/oss/specs/smoke-tests/quickstart-groups.spec.ts @@ -1,4 +1,5 @@ import { test as base, expect } from "src/oss/fixtures"; +import { Asset3dPanelPom } from "src/oss/poms/fo3d/assets-panel"; import { GridPom } from "src/oss/poms/grid"; import { ModalPom } from "src/oss/poms/modal"; import { SidebarPom } from "src/oss/poms/sidebar"; @@ -13,6 +14,7 @@ const test = base.extend<{ grid: GridPom; modal: ModalPom; sidebar: SidebarPom; + asset3dPanel: Asset3dPanelPom; }>({ grid: async ({ page, eventUtils }, use) => { await use(new GridPom(page, eventUtils)); @@ -23,6 +25,9 @@ const test = base.extend<{ sidebar: async ({ page }, use) => { await use(new SidebarPom(page)); }, + asset3dPanel: async ({ page }, use) => { + await use(new Asset3dPanelPom(page)); + }, }); test.describe("quickstart-groups", () => { @@ -89,9 +94,16 @@ test.describe("quickstart-groups", () => { ); }); - test("group media visibility toggle works", async ({ modal }) => { + test("group media visibility toggle works", async ({ + modal, + asset3dPanel, + }) => { + // need to drag the asset3d panel to the left corner to make sure it doesn't overlap with the popout + await asset3dPanel.dragToToLeftCorner(); + // make sure popout is right aligned to the toggle button await modal.group.toggleMediaButton.click(); + const popoutBoundingBox = await modal.group.groupMediaVisibilityPopout.boundingBox(); const toggleButtonBoundingBox = From ecc02422a9e37372dd0f6ee63761e1d9822a2100 Mon Sep 17 00:00:00 2001 From: imanjra Date: Thu, 9 May 2024 14:52:15 -0400 Subject: [PATCH 020/126] add set_spaces operation (#4381) --- app/packages/operators/src/built-in-operators.ts | 10 +++++++++- fiftyone/operators/builtin.py | 3 +-- fiftyone/operators/operations.py | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/app/packages/operators/src/built-in-operators.ts b/app/packages/operators/src/built-in-operators.ts index 7bd3963423..e7ef771694 100644 --- a/app/packages/operators/src/built-in-operators.ts +++ b/app/packages/operators/src/built-in-operators.ts @@ -8,6 +8,7 @@ import { import * as fos from "@fiftyone/state"; import * as types from "./types"; +import { LOAD_WORKSPACE_OPERATOR } from "@fiftyone/spaces/src/components/Workspaces/constants"; import { toSlug } from "@fiftyone/utilities"; import copyToClipboard from "copy-to-clipboard"; import { useSetRecoilState } from "recoil"; @@ -771,7 +772,14 @@ class SetSpaces extends Operator { return { setSessionSpacesState }; } async execute(ctx: ExecutionContext) { - ctx.hooks.setSessionSpacesState(ctx.params.spaces); + const { name, spaces } = ctx.params || {}; + if (spaces) { + ctx.hooks.setSessionSpacesState(spaces); + } else if (name) { + executeOperator(LOAD_WORKSPACE_OPERATOR, { name }, { skipOutput: true }); + } else { + throw new Error('Param "spaces" or "name" is required to set a space'); + } } } diff --git a/fiftyone/operators/builtin.py b/fiftyone/operators/builtin.py index 5610516dd7..7f7ac09548 100644 --- a/fiftyone/operators/builtin.py +++ b/fiftyone/operators/builtin.py @@ -446,8 +446,7 @@ def resolve_input(self, ctx): def execute(self, ctx): name = ctx.params.get("name", None) - spaces = ctx.dataset.load_workspace(name) - ctx.trigger("set_spaces", {"spaces": spaces.to_dict()}) + ctx.ops.set_spaces(name=name) return {} diff --git a/fiftyone/operators/operations.py b/fiftyone/operators/operations.py index 6ba974b706..35051f23b0 100644 --- a/fiftyone/operators/operations.py +++ b/fiftyone/operators/operations.py @@ -5,6 +5,7 @@ | `voxel51.com `_ | """ + import json from bson import json_util @@ -316,6 +317,21 @@ def clear_selected_labels(self): """Clear the selected labels in the App.""" return self._ctx.trigger("clear_selected_labels") + def set_spaces(self, spaces=None, name=None): + """Set space in the App by name or :class:`fiftyone.core.odm.workspace.Space`. + + Args: + spaces: the spaces (:class:`fiftyone.core.odm.workspace.Space`) to load + name: the name of the workspace to load + """ + params = {} + if spaces is not None: + params["spaces"] = spaces.to_dict() + elif name is not None: + params["spaces"] = self._ctx.dataset.load_workspace(name).to_dict() + + return self._ctx.trigger("set_spaces", params=params) + def _serialize_view(view): return json.loads(json_util.dumps(view._serialize())) From be236de87a7c81e3211ca436c4510ac4419c3b86 Mon Sep 17 00:00:00 2001 From: Jacob Marks Date: Fri, 10 May 2024 17:35:01 -0400 Subject: [PATCH 021/126] Typo fix (#4389) --- docs/source/integrations/huggingface.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/integrations/huggingface.rst b/docs/source/integrations/huggingface.rst index 919a7d865f..e888cf5113 100644 --- a/docs/source/integrations/huggingface.rst +++ b/docs/source/integrations/huggingface.rst @@ -1709,7 +1709,7 @@ specify the `detection_fields` as `"digits"`: Loading segmentation datasets from the Hub is also a breeze. For example, to load the "instance_segmentation" subset from -`SceneParse150 `_, all you +`SceneParse150 `_, all you need to do is specify the `mask_fields` as `"annotation"`: .. code-block:: python From 516f7691d3ad6d583dfcf5732717aacbde47f0c0 Mon Sep 17 00:00:00 2001 From: Sashank Aryal <66688606+sashankaryal@users.noreply.github.com> Date: Mon, 13 May 2024 17:25:48 -0500 Subject: [PATCH 022/126] misc 3d bug fixes (#4395) * bug: use name arg in lights * add null checking when computing camera props * upgrade three.js deps * default name arg to class name in base class --- app/packages/looker-3d/package.json | 6 +- .../looker-3d/src/fo3d/MediaTypeFo3d.tsx | 19 +++-- app/yarn.lock | 79 ++++++++++++++++--- fiftyone/core/threed/lights.py | 17 +++- 4 files changed, 99 insertions(+), 22 deletions(-) diff --git a/app/packages/looker-3d/package.json b/app/packages/looker-3d/package.json index 5b14994001..a15ba47622 100644 --- a/app/packages/looker-3d/package.json +++ b/app/packages/looker-3d/package.json @@ -15,14 +15,14 @@ "dependencies": { "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", - "@react-three/drei": "^9.105.4", - "@react-three/fiber": "^8.16.2", + "@react-three/drei": "^9.105.6", + "@react-three/fiber": "^8.16.6", "leva": "^0.9.35", "lodash": "^4.17.21", "r3f-perf": "^7.2.1", "react-color": "^2.19.3", "styled-components": "^6.1.8", - "three": "^0.163.0", + "three": "^0.164.1", "tunnel-rat": "^0.1.2" }, "devDependencies": { diff --git a/app/packages/looker-3d/src/fo3d/MediaTypeFo3d.tsx b/app/packages/looker-3d/src/fo3d/MediaTypeFo3d.tsx index a74b8377d9..4f4102be19 100644 --- a/app/packages/looker-3d/src/fo3d/MediaTypeFo3d.tsx +++ b/app/packages/looker-3d/src/fo3d/MediaTypeFo3d.tsx @@ -266,14 +266,23 @@ export const MediaTypeFo3dComponent = () => { }, [isSceneInitialized, resetActiveNode]); const canvasCameraProps = useMemo(() => { - return { + const cameraProps = { position: defaultCameraPositionComputed, up: upVector, - aspect: foScene?.cameraProps.aspect, - fov: foScene?.cameraProps.fov && 50, - near: foScene?.cameraProps.near && 0.1, - far: foScene?.cameraProps.far && 2500, + fov: foScene?.cameraProps.fov ?? 50, + near: foScene?.cameraProps.near ?? 0.1, + far: foScene?.cameraProps.far ?? 2500, }; + + if (foScene?.cameraProps.lookAt) { + cameraProps["lookAt"] = foScene.cameraProps.lookAt; + } + + if (foScene?.cameraProps.aspect) { + cameraProps["aspect"] = foScene.cameraProps.aspect; + } + + return cameraProps; }, [foScene, upVector, defaultCameraPositionComputed]); const onChangeView = useCallback( diff --git a/app/yarn.lock b/app/yarn.lock index f4e6dd57b2..d36348f2a7 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -2557,8 +2557,8 @@ __metadata: dependencies: "@emotion/react": ^11.11.3 "@emotion/styled": ^11.11.0 - "@react-three/drei": ^9.105.4 - "@react-three/fiber": ^8.16.2 + "@react-three/drei": ^9.105.6 + "@react-three/fiber": ^8.16.6 "@types/node": ^20.11.10 "@types/react": ^18.2.48 "@types/react-dom": ^18.2.18 @@ -2571,7 +2571,7 @@ __metadata: react-color: ^2.19.3 rollup-plugin-external-globals: ^0.6.1 styled-components: ^6.1.8 - three: ^0.163.0 + three: ^0.164.1 tunnel-rat: ^0.1.2 typescript: ^5.4.5 vite: ^3.2.10 @@ -4380,7 +4380,7 @@ __metadata: languageName: node linkType: hard -"@react-three/drei@npm:^9.103.0, @react-three/drei@npm:^9.105.4": +"@react-three/drei@npm:^9.103.0": version: 9.105.4 resolution: "@react-three/drei@npm:9.105.4" dependencies: @@ -4419,9 +4419,48 @@ __metadata: languageName: node linkType: hard -"@react-three/fiber@npm:^8.16.2": - version: 8.16.2 - resolution: "@react-three/fiber@npm:8.16.2" +"@react-three/drei@npm:^9.105.6": + version: 9.105.6 + resolution: "@react-three/drei@npm:9.105.6" + dependencies: + "@babel/runtime": ^7.11.2 + "@mediapipe/tasks-vision": 0.10.8 + "@monogrid/gainmap-js": ^3.0.5 + "@react-spring/three": ~9.6.1 + "@use-gesture/react": ^10.2.24 + camera-controls: ^2.4.2 + cross-env: ^7.0.3 + detect-gpu: ^5.0.28 + glsl-noise: ^0.0.0 + hls.js: 1.3.5 + maath: ^0.10.7 + meshline: ^3.1.6 + react-composer: ^5.0.3 + stats-gl: ^2.0.0 + stats.js: ^0.17.0 + suspend-react: ^0.1.3 + three-mesh-bvh: ^0.7.0 + three-stdlib: ^2.29.9 + troika-three-text: ^0.49.0 + tunnel-rat: ^0.1.2 + utility-types: ^3.10.0 + uuid: ^9.0.1 + zustand: ^3.7.1 + peerDependencies: + "@react-three/fiber": ">=8.0" + react: ">=18.0" + react-dom: ">=18.0" + three: ">=0.137" + peerDependenciesMeta: + react-dom: + optional: true + checksum: 3799627337dce4597e61893702b7781c1af83526315f476d1a60028267798d288b0e4a269c3f2770d8a5c2298c89eacee5cf8ece2cc3f3fd8aafcdfc1055ef13 + languageName: node + linkType: hard + +"@react-three/fiber@npm:^8.16.6": + version: 8.16.6 + resolution: "@react-three/fiber@npm:8.16.6" dependencies: "@babel/runtime": ^7.17.8 "@types/react-reconciler": ^0.26.7 @@ -4456,7 +4495,7 @@ __metadata: optional: true react-native: optional: true - checksum: 48ae05c834125d746c91d3242178319a872f438a890e83a2cde5ea605b380dc200d3f15079548f6b54d6ce833477647425ceec118e3ea59462e8e76c6561cc92 + checksum: b398a8f6c287c3d61d2d19f4c5308b9e3d95a04c65d56574bdf0b459e1234962e64cfa2d378fd07f25027c8272a4a5e2f8a278ab4bfaa20d90fe47fcd809ee12 languageName: node linkType: hard @@ -17687,10 +17726,26 @@ __metadata: languageName: node linkType: hard -"three@npm:^0.163.0": - version: 0.163.0 - resolution: "three@npm:0.163.0" - checksum: 572c16264512f8d094929f585de0092cfaebe9306d7ee45acbf6a78e20e142424a4898679a79fbfe6c76cd3ea048b28f3f821e2daaf243a2e8f8a0d8c00c27dd +"three-stdlib@npm:^2.29.9": + version: 2.30.0 + resolution: "three-stdlib@npm:2.30.0" + dependencies: + "@types/draco3d": ^1.4.0 + "@types/offscreencanvas": ^2019.6.4 + "@types/webxr": ^0.5.2 + draco3d: ^1.4.1 + fflate: ^0.6.9 + potpack: ^1.0.1 + peerDependencies: + three: ">=0.128.0" + checksum: 2497386bd6b9e559453491764a06e1c9b74394280b08b0cb356fd170a531b4e6cd5ee905c8234cc2e86d176d5ff35ac1d8111ba3aec9cfecf6634bf8c90f2cdb + languageName: node + linkType: hard + +"three@npm:^0.164.1": + version: 0.164.1 + resolution: "three@npm:0.164.1" + checksum: 64a1b713cd14b53af12858ad1b89a9daaa493ee2a42384480e408ec3add6cd474811b34a5faa4a3611398db39ed86afb3a96b4d0a54a2feaacc9c5dc54cf5c8a languageName: node linkType: hard diff --git a/fiftyone/core/threed/lights.py b/fiftyone/core/threed/lights.py index 5ce2367541..5b5a666581 100644 --- a/fiftyone/core/threed/lights.py +++ b/fiftyone/core/threed/lights.py @@ -7,7 +7,7 @@ """ from math import pi as PI -from typing import Optional +from typing import Optional, Union from .object_3d import Object3D @@ -35,6 +35,7 @@ class Light(Object3D): def __init__( self, + name: Union[str, None] = None, color: str = COLOR_DEFAULT_WHITE, intensity: float = 1.0, visible=True, @@ -43,7 +44,7 @@ def __init__( quaternion: Optional[Quaternion] = None, ): super().__init__( - name=self.__class__.__name__, + name=name or self.__class__.__name__, visible=visible, position=position, scale=scale, @@ -68,6 +69,7 @@ class AmbientLight(Light): This light globally illuminates all objects in the scene equally. Args: + name ("AmbientLight"): the name of the light intensity (0.1): the intensity of the light in the range ``[0, 1]`` color ("#ffffff"): the color of the light visible (True): default visibility of the object in the scene @@ -78,6 +80,7 @@ class AmbientLight(Light): def __init__( self, + name: str = "AmbientLight", intensity: float = 0.1, color: str = COLOR_DEFAULT_WHITE, visible=True, @@ -86,6 +89,7 @@ def __init__( quaternion: Optional[Quaternion] = None, ): super().__init__( + name=name, intensity=intensity, color=color, visible=visible, @@ -113,6 +117,7 @@ class DirectionalLight(Light): parallel. Args: + name ("DirectionalLight"): the name of the light target ([0,0,0]): the target of the light color ("#ffffff"): the color of the light intensity (1.0): the intensity of the light in the range ``[0, 1]`` @@ -126,6 +131,7 @@ class DirectionalLight(Light): def __init__( self, + name: str = "DirectionalLight", target: Vec3UnionType = Vector3(0, 0, 0), color: str = COLOR_DEFAULT_WHITE, intensity: float = 1.0, @@ -135,6 +141,7 @@ def __init__( quaternion: Optional[Quaternion] = None, ): super().__init__( + name=name, color=color, intensity=intensity, visible=visible, @@ -166,6 +173,7 @@ class PointLight(Light): """Represents a point light. Args: + name ("PointLight"): the name of the light distance (0.0): the distance at which the light's intensity is zero decay (2.0): the amount the light dims along the distance of the light color ("#ffffff"): the color of the light @@ -178,6 +186,7 @@ class PointLight(Light): def __init__( self, + name: str = "PointLight", distance: float = 0.0, decay: float = 2.0, color: str = COLOR_DEFAULT_WHITE, @@ -188,6 +197,7 @@ def __init__( quaternion: Optional[Quaternion] = None, ): super().__init__( + name=name, color=color, intensity=intensity, visible=visible, @@ -224,6 +234,7 @@ class SpotLight(Light): """Represents a spot light. Args: + name ("SpotLight"): the name of the light target ([0,0,0]): the target of the light distance (0.0): the distance at which the light's intensity is zero decay (2.0): the amount the light dims along the distance of the light @@ -238,6 +249,7 @@ class SpotLight(Light): def __init__( self, + name: str = "SpotLight", target: Vec3UnionType = None, distance: float = 0.0, decay: float = 2.0, @@ -251,6 +263,7 @@ def __init__( quaternion: Optional[Quaternion] = None, ): super().__init__( + name=name, color=color, intensity=intensity, visible=visible, From 44675b27cc13038d752c94c0b5ad688c504d1097 Mon Sep 17 00:00:00 2001 From: cupcakesprinkle3 <79061264+cupcakesprinkle3@users.noreply.github.com> Date: Fri, 17 May 2024 13:10:55 -0400 Subject: [PATCH 023/126] fixed typo --- docs/source/teams/roles_and_permissions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/teams/roles_and_permissions.rst b/docs/source/teams/roles_and_permissions.rst index a4a07b4bdb..4f53e8c9b8 100644 --- a/docs/source/teams/roles_and_permissions.rst +++ b/docs/source/teams/roles_and_permissions.rst @@ -10,7 +10,7 @@ as possible for engineers, data scientists, and stakeholders to work together to build high quality datasets and computer vision models. Accordingly, FiftyOne Teams gives you the flexibility to configure user roles, -user groups and fine-grained permissions so that you can safely and securly +user groups and fine-grained permissions so that you can safely and securely collaborate both inside and outside your organization at all stages of your workflows. From 448e56668acdf634c6d336846d8c0dc8477e1a93 Mon Sep 17 00:00:00 2001 From: "fatih c. akyon" <34196005+fcakyon@users.noreply.github.com> Date: Mon, 20 May 2024 18:12:17 +0300 Subject: [PATCH 024/126] add ultralytics yolov8 open images v7 pretrained models into zoo (#4398) --- fiftyone/zoo/models/manifest-torch.json | 160 ++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/fiftyone/zoo/models/manifest-torch.json b/fiftyone/zoo/models/manifest-torch.json index e154afe1ef..bff58f24fe 100644 --- a/fiftyone/zoo/models/manifest-torch.json +++ b/fiftyone/zoo/models/manifest-torch.json @@ -3087,6 +3087,166 @@ "tags": ["detection", "torch", "yolo", "polylines", "obb"], "date_added": "2024-04-05 19:22:51" }, + { + "base_name": "yolov8n-oiv7-torch", + "base_filename": "yolov8n-oiv7.pt", + "description": "Ultralytics YOLOv8n model trained on Open Images v7", + "source": "https://docs.ultralytics.com/datasets/detect/open-images-v7", + "size_bytes": 6534387, + "manager": { + "type": "fiftyone.core.models.ModelManager", + "config": { + "url": "https://github.com/ultralytics/assets/releases/download/v8.2.0/yolov8n-oiv7.pt" + } + }, + "default_deployment_config_dict": { + "type": "fiftyone.utils.ultralytics.FiftyOneYOLODetectionModel", + "config": {} + }, + "requirements": { + "packages": [ + "torch>=1.7.0", + "torchvision>=0.8.1", + "ultralytics" + ], + "cpu": { + "support": true + }, + "gpu": { + "support": true + } + }, + "tags": ["detection", "oiv7", "torch", "yolo"], + "date_added": "2024-05-20 19:22:51" + }, + { + "base_name": "yolov8s-oiv7-torch", + "base_filename": "yolov8s-oiv7.pt", + "description": "Ultralytics YOLOv8s model trained on Open Images v7", + "source": "https://docs.ultralytics.com/datasets/detect/open-images-v7", + "size_bytes": 22573363, + "manager": { + "type": "fiftyone.core.models.ModelManager", + "config": { + "url": "https://github.com/ultralytics/assets/releases/download/v8.2.0/yolov8s-oiv7.pt" + } + }, + "default_deployment_config_dict": { + "type": "fiftyone.utils.ultralytics.FiftyOneYOLODetectionModel", + "config": {} + }, + "requirements": { + "packages": [ + "torch>=1.7.0", + "torchvision>=0.8.1", + "ultralytics" + ], + "cpu": { + "support": true + }, + "gpu": { + "support": true + } + }, + "tags": ["detection", "oiv7", "torch", "yolo"], + "date_added": "2024-05-20 19:22:51" + }, + { + "base_name": "yolov8m-oiv7-torch", + "base_filename": "yolov8m-oiv7.pt", + "description": "Ultralytics YOLOv8m model trained Open Images v7", + "source": "https://docs.ultralytics.com/datasets/detect/open-images-v7", + "size_bytes": 52117635, + "manager": { + "type": "fiftyone.core.models.ModelManager", + "config": { + "url": "https://github.com/ultralytics/assets/releases/download/v8.2.0/yolov8m-oiv7.pt" + } + }, + "default_deployment_config_dict": { + "type": "fiftyone.utils.ultralytics.FiftyOneYOLODetectionModel", + "config": {} + }, + "requirements": { + "packages": [ + "torch>=1.7.0", + "torchvision>=0.8.1", + "ultralytics" + ], + "cpu": { + "support": true + }, + "gpu": { + "support": true + } + }, + "tags": ["detection", "oiv7", "torch", "yolo"], + "date_added": "2024-05-20 19:22:51" + }, + { + "base_name": "yolov8l-oiv7-torch", + "base_filename": "yolov8l-oiv7.pt", + "description": "Ultralytics YOLOv8l model trained Open Images v7", + "source": "https://docs.ultralytics.com/datasets/detect/open-images-v7", + "size_bytes": 87769683, + "manager": { + "type": "fiftyone.core.models.ModelManager", + "config": { + "url": "https://github.com/ultralytics/assets/releases/download/v8.2.0/yolov8l-oiv7.pt" + } + }, + "default_deployment_config_dict": { + "type": "fiftyone.utils.ultralytics.FiftyOneYOLODetectionModel", + "config": {} + }, + "requirements": { + "packages": [ + "torch>=1.7.0", + "torchvision>=0.8.1", + "ultralytics" + ], + "cpu": { + "support": true + }, + "gpu": { + "support": true + } + }, + "tags": ["detection", "oiv7", "torch", "yolo"], + "date_added": "2024-05-20 19:22:51" + }, + { + "base_name": "yolov8x-oiv7-torch", + "base_filename": "yolov8x-oiv7.pt", + "description": "Ultralytics YOLOv8x model trained Open Images v7", + "source": "https://docs.ultralytics.com/datasets/detect/open-images-v7", + "size_bytes": 136867539, + "manager": { + "type": "fiftyone.core.models.ModelManager", + "config": { + "url": "https://github.com/ultralytics/assets/releases/download/v8.2.0/yolov8x-oiv7.pt" + } + }, + "default_deployment_config_dict": { + "type": "fiftyone.utils.ultralytics.FiftyOneYOLODetectionModel", + "config": {} + }, + "requirements": { + "packages": [ + "torch>=1.7.0", + "torchvision>=0.8.1", + "ultralytics" + ], + "cpu": { + "support": true + }, + "gpu": { + "support": true + } + }, + "tags": ["detection", "oiv7", "torch", "yolo"], + "date_added": "2024-05-20 19:22:51" + }, { "base_name": "yolo-nas-torch", "base_filename": null, From d14390bcc73337610f7679138ff62370a5fec0c3 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Mon, 20 May 2024 14:19:42 -0700 Subject: [PATCH 025/126] Fix operators eslint issues (#4388) * add operator specific eslint * fix some js operator module eslint issues * undo exhaustive-deps fix * more lint fixes for operators * rm 2204 install --------- Co-authored-by: Benjamin Kane --- .github/workflows/e2e.yml | 1 - .github/workflows/test.yml | 1 - app/packages/operators/.eslintrc | 5 ++ app/packages/operators/babel.config.js | 1 + .../src/OperatorInvocationRequestExecutor.tsx | 1 + .../operators/src/OperatorPalette.tsx | 2 +- .../operators/src/built-in-operators.ts | 39 ++++++------ .../src/components/OperatorPromptFooter.tsx | 3 +- app/packages/operators/src/operators.ts | 60 +++++++++++++------ app/packages/operators/src/types.ts | 7 ++- 10 files changed, 78 insertions(+), 42 deletions(-) create mode 100644 app/packages/operators/.eslintrc diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 722386ddc4..7f8983c18a 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -60,7 +60,6 @@ jobs: - name: Install fiftyone run: | pip install . - pip install fiftyone-db-ubuntu2204 - name: Configure id: test_config diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6d6944363e..08a80a4fe5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,7 +72,6 @@ jobs: - name: Install fiftyone run: | pip install . - pip install fiftyone-db-ubuntu2204 - name: Configure id: test_config run: | diff --git a/app/packages/operators/.eslintrc b/app/packages/operators/.eslintrc new file mode 100644 index 0000000000..b3c5d866e5 --- /dev/null +++ b/app/packages/operators/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "react-hooks/rules-of-hooks": "off" + } +} diff --git a/app/packages/operators/babel.config.js b/app/packages/operators/babel.config.js index dd242dc902..5cb73d1e3a 100644 --- a/app/packages/operators/babel.config.js +++ b/app/packages/operators/babel.config.js @@ -1,3 +1,4 @@ +/* global module */ module.exports = { presets: [ ["@babel/preset-env", { targets: { node: "current" } }], diff --git a/app/packages/operators/src/OperatorInvocationRequestExecutor.tsx b/app/packages/operators/src/OperatorInvocationRequestExecutor.tsx index 1faee7b57b..2919bece32 100644 --- a/app/packages/operators/src/OperatorInvocationRequestExecutor.tsx +++ b/app/packages/operators/src/OperatorInvocationRequestExecutor.tsx @@ -33,6 +33,7 @@ function RequestExecutor({ queueItem, onSuccess, onError }) { callback: queueItem.callback, ...(queueItem?.request?.options || {}), }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return null; diff --git a/app/packages/operators/src/OperatorPalette.tsx b/app/packages/operators/src/OperatorPalette.tsx index 28db5c2ddb..6379c1d897 100644 --- a/app/packages/operators/src/OperatorPalette.tsx +++ b/app/packages/operators/src/OperatorPalette.tsx @@ -109,7 +109,7 @@ export default function OperatorPalette(props: OperatorPaletteProps) { ); } -type SubmitButtonOption = { +export type SubmitButtonOption = { id: string; label: string; }; diff --git a/app/packages/operators/src/built-in-operators.ts b/app/packages/operators/src/built-in-operators.ts index e7ef771694..9ada7f5c41 100644 --- a/app/packages/operators/src/built-in-operators.ts +++ b/app/packages/operators/src/built-in-operators.ts @@ -48,7 +48,7 @@ class ReloadDataset extends Operator { label: "Reload the dataset", }); } - async execute({ state }: ExecutionContext) { + async execute() { // TODO - improve this... this is a temp. workaround for the fact that // there is no way to force reload just the dataset window.location.reload(); @@ -110,7 +110,7 @@ class OpenPanel extends Operator { unlisted: true, }); } - async resolveInput(ctx: ExecutionContext): Promise { + async resolveInput(): Promise { const inputs = new types.Object(); inputs.str("name", { view: new types.AutocompleteView({ @@ -139,7 +139,7 @@ class OpenPanel extends Operator { const openedPanels = useSpaceNodes(FIFTYONE_SPACE_ID); return { availablePanels, openedPanels, spaces }; } - findFirstPanelContainer(node: SpaceNode): SpaceNode { + findFirstPanelContainer(node: SpaceNode): SpaceNode | null { if (node.isPanelContainer()) { return node; } @@ -147,6 +147,8 @@ class OpenPanel extends Operator { if (node.hasChildren()) { return this.findFirstPanelContainer(node.firstChild()); } + + return null; } async execute({ hooks, params }: ExecutionContext) { const { spaces, openedPanels, availablePanels } = hooks; @@ -208,7 +210,7 @@ class ClosePanel extends Operator { unlisted: true, }); } - async resolveInput(ctx: ExecutionContext): Promise { + async resolveInput(): Promise { const inputs = new types.Object(); inputs.str("name", { view: new types.AutocompleteView({ @@ -311,7 +313,7 @@ class OpenDataset extends Operator { unlisted: true, }); } - async resolveInput(ctx: ExecutionContext): Promise { + async resolveInput(): Promise { const inputs = new types.Object(); inputs.str("dataset", { label: "Dataset name", required: true }); return new types.Property(inputs); @@ -359,7 +361,7 @@ class ClearAllStages extends Operator { label: "Clear all selections, filters, and view", }); } - useHooks(): {} { + useHooks(): object { return { resetExtended: fos.useResetExtendedSelection(), }; @@ -413,7 +415,7 @@ class ConvertExtendedSelectionToSelectedSamples extends Operator { label: "Convert extended selection to selected samples", }); } - useHooks(): {} { + useHooks(): object { return { resetExtended: fos.useResetExtendedSelection(), }; @@ -438,7 +440,7 @@ class SetSelectedSamples extends Operator { unlisted: true, }); } - useHooks(): {} { + useHooks(): object { return { setSelected: fos.useSetSelected(), }; @@ -460,7 +462,7 @@ class SetView extends Operator { unlisted: true, }); } - useHooks(ctx: ExecutionContext): {} { + useHooks(): object { const refetchableSavedViews = useRefetchableSavedViews(); return { @@ -469,7 +471,7 @@ class SetView extends Operator { setViewName: useSetRecoilState(fos.viewName), }; } - async resolveInput(ctx: ExecutionContext): Promise { + async resolveInput(): Promise { const inputs = new types.Object(); inputs.list("view", new types.Object(), { view: new types.HiddenView({}) }); inputs.str("name", { label: "Name or slug of a saved view" }); @@ -508,7 +510,7 @@ class ShowSamples extends Operator { unlisted: true, }); } - async resolveInput(ctx: ExecutionContext): Promise { + async resolveInput(): Promise { const inputs = new types.Object(); inputs.list("samples", new types.String(), { label: "Samples", @@ -520,7 +522,7 @@ class ShowSamples extends Operator { }); return new types.Property(inputs); } - useHooks(ctx: ExecutionContext): {} { + useHooks(): object { return { setView: fos.useSetView(), }; @@ -574,11 +576,10 @@ class ConsoleLog extends Operator { unlisted: true, }); } - async resolveInput(ctx: ExecutionContext): Promise { + async resolveInput(): Promise { const inputs = new types.Object(); inputs.defineProperty("message", new types.String(), { label: "Message", - required: true, }); return new types.Property(inputs); } @@ -596,7 +597,7 @@ class ShowOutput extends Operator { unlisted: true, }); } - async resolveInput(ctx: ExecutionContext): Promise { + async resolveInput(): Promise { const inputs = new types.Object(); inputs.defineProperty("outputs", new types.Object(), { label: "Outputs", @@ -608,7 +609,7 @@ class ShowOutput extends Operator { }); return new types.Property(inputs); } - useHooks(ctx: ExecutionContext): {} { + useHooks(): object { return { io: useShowOperatorIO(), }; @@ -631,7 +632,7 @@ class SetProgress extends Operator { unlisted: true, }); } - async resolveInput(ctx: ExecutionContext): Promise { + async resolveInput(): Promise { const inputs = new types.Object(); inputs.defineProperty("label", new types.String(), { label: "Label" }); inputs.defineProperty("variant", new types.Enum(["linear", "circular"]), { @@ -642,7 +643,7 @@ class SetProgress extends Operator { }); return new types.Property(inputs); } - useHooks(ctx: ExecutionContext): {} { + useHooks(): object { return { io: useShowOperatorIO(), }; @@ -730,7 +731,7 @@ class SetSelectedLabels extends Operator { unlisted: true, }); } - useHooks(ctx: ExecutionContext): {} { + useHooks(): object { return { setSelected: fos.useSetSelectedLabels(), }; diff --git a/app/packages/operators/src/components/OperatorPromptFooter.tsx b/app/packages/operators/src/components/OperatorPromptFooter.tsx index 5436318a43..88c22ae9d6 100644 --- a/app/packages/operators/src/components/OperatorPromptFooter.tsx +++ b/app/packages/operators/src/components/OperatorPromptFooter.tsx @@ -4,6 +4,7 @@ import { useCallback } from "react"; import SplitButton from "../SplitButton"; import { BaseStylesProvider } from "../styled-components"; import { onEnter } from "../utils"; +import { SubmitButtonOption } from "../OperatorPalette"; export default function OperatorPromptFooter(props: OperatorFooterProps) { const { @@ -91,7 +92,7 @@ type OperatorFooterProps = { disableSubmit?: boolean; disabledReason?: string; loading?: boolean; - submitButtonOptions: any[]; + submitButtonOptions: SubmitButtonOption[]; hasSubmitButtonOptions: boolean; submitOptionsLoading: boolean; showWarning?: boolean; diff --git a/app/packages/operators/src/operators.ts b/app/packages/operators/src/operators.ts index 186171c2ea..326ea3ca3c 100644 --- a/app/packages/operators/src/operators.ts +++ b/app/packages/operators/src/operators.ts @@ -5,13 +5,20 @@ import { stringifyError } from "./utils"; import { ValidationContext, ValidationError } from "./validation"; import { ExecutionCallback, OperatorExecutorOptions } from "./types-internal"; +type RawInvocationRequest = { + operator_uri?: string; + operator_name?: string; + params: object; + options: object; +}; + class InvocationRequest { constructor( public operatorURI: string, public params: unknown = {}, public options?: OperatorExecutorOptions ) {} - static fromJSON(json: any) { + static fromJSON(json: RawInvocationRequest): InvocationRequest { return new InvocationRequest( json.operator_uri || json.operator_name, json.params, @@ -42,13 +49,13 @@ export class Executor { queue.add(request); } } - static fromJSON(json: any) { + static fromJSON(json: { requests: RawInvocationRequest[]; logs: string[] }) { return new Executor( json.requests.map((r: any) => InvocationRequest.fromJSON(r)), json.logs ); } - trigger(operatorURI: string, params: any = {}) { + trigger(operatorURI: string, params: object = {}) { operatorURI = resolveOperatorURI(operatorURI); this.requests.push(new InvocationRequest(operatorURI, params)); } @@ -57,19 +64,32 @@ export class Executor { } } +export type RawContext = { + datasetName: string; + extended: boolean; + view: string; + filters: object; + selectedSamples: Set; + selectedLabels: any[]; + currentSample: string; + viewName: string; + delegationTarget: string; + requestDelegation: boolean; + state: CallbackInterface; +}; export class ExecutionContext { public state: CallbackInterface; constructor( - public params: any = {}, - public _currentContext: any, - public hooks: any = {}, + public params: object = {}, + public _currentContext: RawContext, + public hooks: object = {}, public executor: Executor = null ) { this.state = _currentContext.state; } public delegationTarget: string = null; - public requestDelegation: boolean = false; - trigger(operatorURI: string, params: any = {}) { + public requestDelegation = false; + trigger(operatorURI: string, params: object = {}) { if (!this.executor) { throw new Error( "Cannot trigger operator from outside of an execution context" @@ -93,9 +113,9 @@ function isObjWithContent(obj: any) { export class OperatorResult { constructor( public operator: Operator, - public result: any = {}, + public result: object = {}, public executor: Executor = null, - public error: any, + public error: string, public delegated: boolean = false ) {} hasOutputContent() { @@ -227,7 +247,7 @@ export class Operator { } return false; } - useHooks(ctx: ExecutionContext) { + useHooks(): object { // This can be overridden to use hooks in the execute function return {}; } @@ -243,19 +263,23 @@ export class Operator { } return null; } - async resolvePlacement( - ctx: ExecutionContext - ): Promise {} - async execute(ctx: ExecutionContext) { + async resolvePlacement(): Promise { + return null; + } + async execute() { throw new Error(`Operator ${this.uri} does not implement execute`); } - public isRemote: boolean = false; - static fromRemoteJSON(json: any) { + public isRemote = false; + static fromRemoteJSON(json: object) { const operator = this.fromJSON(json); operator.isRemote = true; return operator; } - static fromJSON(json: any) { + static fromJSON(json: { + plugin_name: string; + _builtin: boolean; + config: object; + }) { const config = OperatorConfig.fromJSON(json.config); const operator = new Operator(json.plugin_name, json._builtin, config); return operator; diff --git a/app/packages/operators/src/types.ts b/app/packages/operators/src/types.ts index 5f3a50e359..ab6616cdc8 100644 --- a/app/packages/operators/src/types.ts +++ b/app/packages/operators/src/types.ts @@ -220,7 +220,7 @@ export class Property { * set to `true`. * view: view options for the property. Refer to {@link View} */ - constructor(type: ANY_TYPE, options?) { + constructor(type: ANY_TYPE, options?: PropertyOptions) { this.type = type; this.defaultValue = options?.defaultValue || options?.default; this.required = options?.required; @@ -1246,6 +1246,11 @@ type PropertyOptions = { label?: string; description?: string; view?: View; + required?: boolean; + defaultValue?: any; + default?: any; + invalid?: boolean; + errorMessage?: string; }; type ObjectProperties = Map; From 8205caf7646e5e7cb38041a94efb97f6524c1db6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 May 2024 17:53:50 -0400 Subject: [PATCH 026/126] --- (#4402) updated-dependencies: - dependency-name: tar dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- app/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/yarn.lock b/app/yarn.lock index d36348f2a7..de07dae5b5 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -17650,8 +17650,8 @@ __metadata: linkType: hard "tar@npm:^6.1.11, tar@npm:^6.1.12, tar@npm:^6.1.2": - version: 6.2.0 - resolution: "tar@npm:6.2.0" + version: 6.2.1 + resolution: "tar@npm:6.2.1" dependencies: chownr: ^2.0.0 fs-minipass: ^2.0.0 @@ -17659,7 +17659,7 @@ __metadata: minizlib: ^2.1.1 mkdirp: ^1.0.3 yallist: ^4.0.0 - checksum: db4d9fe74a2082c3a5016630092c54c8375ff3b280186938cfd104f2e089c4fd9bad58688ef6be9cf186a889671bf355c7cda38f09bbf60604b281715ca57f5c + checksum: f1322768c9741a25356c11373bce918483f40fa9a25c69c59410c8a1247632487edef5fe76c5f12ac51a6356d2f1829e96d2bc34098668a2fc34d76050ac2b6c languageName: node linkType: hard From 6fd73094034971db634eb55735d3ce0a2a2fb817 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 21 May 2024 07:42:56 -0700 Subject: [PATCH 027/126] require eslint for PRs (#4391) * add operator specific eslint * fix some js operator module eslint issues * undo exhaustive-deps fix * require eslint for PRs * fix package path * remove lint push trigger * more lint fixes for operators * lint app, use workflow call * mv changes to lint-app * add lint requirement * no max warnings * rm 2204 install --------- Co-authored-by: Benjamin Kane --- .github/workflows/lint-app.yml | 43 ++++++++++++++++++++++++++++++++++ .github/workflows/pr.yml | 7 ++++-- app/eslint-packages.txt | 3 +++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/lint-app.yml create mode 100644 app/eslint-packages.txt diff --git a/.github/workflows/lint-app.yml b/.github/workflows/lint-app.yml new file mode 100644 index 0000000000..f53907a5e7 --- /dev/null +++ b/.github/workflows/lint-app.yml @@ -0,0 +1,43 @@ +name: Lint App + +on: workflow_call + +jobs: + eslint: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + changes: + - 'app/**' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "16" + + - name: Cache Node Modules + id: node-cache + uses: actions/cache@v3 + with: + path: | + app/node_modules + app/.yarn/cache + key: node-modules-${{ hashFiles('app/yarn.lock') }} + + - name: Install Dependencies + if: steps.node-cache.outputs.cache-hit != 'true' + run: cd app && yarn install + + - name: Read ESLint Packages List and Lint + if: steps.changes.outputs.changes == 'true' + run: | + cd app + ESLINT_PACKAGES=$(grep -v '^#' ./eslint-packages.txt | xargs) + yarn eslint $ESLINT_PACKAGES diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index bcd3c19946..e9e5c86d3d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -10,6 +10,9 @@ on: - release/v[0-9]+.[0-9]+.[0-9]+ jobs: + lint: + uses: ./.github/workflows/lint-app.yml + build: uses: ./.github/workflows/build.yml @@ -21,7 +24,7 @@ jobs: all-tests: runs-on: ubuntu-latest - needs: [build, test] + needs: [build, lint, test] if: always() steps: - - run: sh -c ${{ needs.build.result == 'success' && needs.test.result == 'success' }} + - run: sh -c ${{ needs.build.result == 'success' && needs.lint.result == 'success' && needs.test.result == 'success' }} diff --git a/app/eslint-packages.txt b/app/eslint-packages.txt new file mode 100644 index 0000000000..9b6dd8978e --- /dev/null +++ b/app/eslint-packages.txt @@ -0,0 +1,3 @@ +# require these paths to pass linting +# for PRs to be merge-able +packages/operators From 00885d166c0fe4a5a2e5338432dd092fec8b6a28 Mon Sep 17 00:00:00 2001 From: Lanny W Date: Thu, 9 May 2024 11:19:20 -0500 Subject: [PATCH 028/126] add flag canCreateNewField permission for similarity search --- app/packages/app/src/useWriters/registerWriter.ts | 2 +- .../core/src/components/Actions/similar/Similar.tsx | 3 ++- app/packages/state/src/recoil/atoms.ts | 5 +++++ app/packages/state/src/session.ts | 9 ++++++++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/packages/app/src/useWriters/registerWriter.ts b/app/packages/app/src/useWriters/registerWriter.ts index d277e7e7de..1ab04f65e6 100644 --- a/app/packages/app/src/useWriters/registerWriter.ts +++ b/app/packages/app/src/useWriters/registerWriter.ts @@ -6,7 +6,7 @@ import { RoutingContext } from "../routing"; type WriterKeys = keyof Omit< Session, - "canEditCustomColors" | "canEditSavedViews" | "readOnly" + "canCreateNewField" | "canEditCustomColors" | "canEditSavedViews" | "readOnly" >; type WriterContext = { diff --git a/app/packages/core/src/components/Actions/similar/Similar.tsx b/app/packages/core/src/components/Actions/similar/Similar.tsx index 932c7c33f9..7deb4b428d 100644 --- a/app/packages/core/src/components/Actions/similar/Similar.tsx +++ b/app/packages/core/src/components/Actions/similar/Similar.tsx @@ -93,6 +93,7 @@ const SortBySimilarity = ({ [] ); const isLoading = useRecoilValue(fos.similaritySorting); + const canCreateNewField = useRecoilValue(fos.canCreateNewField); const isReadOnly = useRecoilValue(fos.readOnly); useLayoutEffect(() => { @@ -281,7 +282,7 @@ const SortBySimilarity = ({ Optional: store the distance between each sample and the query in this field !value.startsWith("_")} value={state.distField ?? ""} diff --git a/app/packages/state/src/recoil/atoms.ts b/app/packages/state/src/recoil/atoms.ts index 53afb2d8d9..06cb53d0b4 100644 --- a/app/packages/state/src/recoil/atoms.ts +++ b/app/packages/state/src/recoil/atoms.ts @@ -318,6 +318,11 @@ export const canEditCustomColors = sessionAtom({ default: true, }); +export const canCreateNewField = sessionAtom({ + key: "canCreateNewField", + default: true, +}); + export const readOnly = sessionAtom({ key: "readOnly", default: false, diff --git a/app/packages/state/src/session.ts b/app/packages/state/src/session.ts index 3b45c3732f..00e36a6603 100644 --- a/app/packages/state/src/session.ts +++ b/app/packages/state/src/session.ts @@ -29,6 +29,7 @@ export interface Session { canEditCustomColors: boolean; canEditSavedViews: boolean; canEditWorkspaces: boolean; + canCreateNewField: boolean; colorScheme: ColorSchemeInput; readOnly: boolean; selectedSamples: Set; @@ -40,6 +41,7 @@ export interface Session { export const SESSION_DEFAULT: Session = { canEditCustomColors: true, + canCreateNewField: true, canEditSavedViews: true, canEditWorkspaces: true, readOnly: false, @@ -63,7 +65,11 @@ export const SESSION_DEFAULT: Session = { type SetterKeys = keyof Omit< Session, - "canEditCustomColors" | "canEditSavedViews" | "canEditWorkspaces" | "readOnly" + | "canCreateNewField" + | "canEditCustomColors" + | "canEditSavedViews" + | "canEditWorkspaces" + | "readOnly" >; type Setter = (key: K, value: Session[K]) => void; @@ -171,6 +177,7 @@ export function sessionAtom( } if ( + options.key === "canCreateNewField" || options.key === "canEditCustomColors" || options.key === "readOnly" || options.key === "canEditSavedViews" || From 1e5c637d94a077387b924b821a8805efee69a1a9 Mon Sep 17 00:00:00 2001 From: Lanny W Date: Thu, 9 May 2024 11:35:33 -0500 Subject: [PATCH 029/126] add canAddSidebarGroup permission flag for creating/dragging sidebar group --- app/packages/app/src/useWriters/registerWriter.ts | 6 +++++- .../core/src/components/Sidebar/Entries/AddGroupEntry.tsx | 6 ++++-- .../core/src/components/Sidebar/Entries/Draggable.tsx | 8 +++++--- .../core/src/components/Sidebar/ViewSelection/index.tsx | 6 +++--- app/packages/state/src/recoil/atoms.ts | 5 +++++ app/packages/state/src/session.ts | 6 +++++- 6 files changed, 27 insertions(+), 10 deletions(-) diff --git a/app/packages/app/src/useWriters/registerWriter.ts b/app/packages/app/src/useWriters/registerWriter.ts index 1ab04f65e6..394233f437 100644 --- a/app/packages/app/src/useWriters/registerWriter.ts +++ b/app/packages/app/src/useWriters/registerWriter.ts @@ -6,7 +6,11 @@ import { RoutingContext } from "../routing"; type WriterKeys = keyof Omit< Session, - "canCreateNewField" | "canEditCustomColors" | "canEditSavedViews" | "readOnly" + | "canAddSidebarGroup" + | "canCreateNewField" + | "canEditCustomColors" + | "canEditSavedViews" + | "readOnly" >; type WriterContext = { diff --git a/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx b/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx index da1cbd7292..3eddb40b83 100644 --- a/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx @@ -7,7 +7,9 @@ import { InputDiv } from "./utils"; const AddGroup = () => { const [value, setValue] = useState(""); const isFieldVisibilityApplied = useRecoilValue(fos.isFieldVisibilityActive); - const readOnly = useRecoilValue(fos.readOnly); + const isSnapShot = useRecoilValue(fos.readOnly); + const canAddSidebarGroup = useRecoilValue(fos.canAddSidebarGroup); + const isReadOnly = isSnapShot || !canAddSidebarGroup; const addGroup = useRecoilCallback( ({ set, snapshot }) => @@ -40,7 +42,7 @@ const AddGroup = () => { [] ); - if (isFieldVisibilityApplied || readOnly) { + if (isFieldVisibilityApplied || isReadOnly) { return null; } diff --git a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx index 8545352bbf..4d4fca137e 100644 --- a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx @@ -1,5 +1,5 @@ import { useTheme } from "@fiftyone/components"; -import { isFieldVisibilityActive, readOnly } from "@fiftyone/state"; +import * as fos from "@fiftyone/state"; import { DragIndicator } from "@mui/icons-material"; import { animated, useSpring } from "@react-spring/web"; import React, { useMemo, useState } from "react"; @@ -19,8 +19,10 @@ const Draggable: React.FC< const theme = useTheme(); const [hovering, setHovering] = useState(false); const [dragging, setDragging] = useState(false); - const isReadOnly = useRecoilValue(readOnly); - const isFieldVisibilityApplied = useRecoilValue(isFieldVisibilityActive); + const isSnapShot = useRecoilValue(fos.readOnly); + const canAddSidebarGroup = useRecoilValue(fos.canAddSidebarGroup); + const isReadOnly = isSnapShot || !canAddSidebarGroup; + const isFieldVisibilityApplied = useRecoilValue(fos.isFieldVisibilityActive); const disableDrag = !entryKey || diff --git a/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx b/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx index e06c031bdd..912161cb7c 100644 --- a/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx +++ b/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx @@ -43,10 +43,10 @@ export default function ViewSelection() { const setEditView = useSetRecoilState(viewDialogContent); const resetView = useResetRecoilState(fos.view); const [viewSearch, setViewSearch] = useRecoilState(viewSearchTerm); - const isReadOnly = useRecoilValue(fos.readOnly); + const isSnapShot = useRecoilValue(fos.readOnly); const canEdit = useMemo( - () => canEditSavedViews && !isReadOnly, - [canEditSavedViews, isReadOnly] + () => canEditSavedViews && !isSnapShot, + [canEditSavedViews, isSnapShot] ); const [data, refetch] = useRefetchableSavedViews(); diff --git a/app/packages/state/src/recoil/atoms.ts b/app/packages/state/src/recoil/atoms.ts index 06cb53d0b4..5b3baa7056 100644 --- a/app/packages/state/src/recoil/atoms.ts +++ b/app/packages/state/src/recoil/atoms.ts @@ -323,6 +323,11 @@ export const canCreateNewField = sessionAtom({ default: true, }); +export const canAddSidebarGroup = sessionAtom({ + key: "canAddSidebarGroup", + default: true, +}); + export const readOnly = sessionAtom({ key: "readOnly", default: false, diff --git a/app/packages/state/src/session.ts b/app/packages/state/src/session.ts index 00e36a6603..af5d1d4c25 100644 --- a/app/packages/state/src/session.ts +++ b/app/packages/state/src/session.ts @@ -30,6 +30,7 @@ export interface Session { canEditSavedViews: boolean; canEditWorkspaces: boolean; canCreateNewField: boolean; + canAddSidebarGroup: boolean; colorScheme: ColorSchemeInput; readOnly: boolean; selectedSamples: Set; @@ -41,9 +42,10 @@ export interface Session { export const SESSION_DEFAULT: Session = { canEditCustomColors: true, - canCreateNewField: true, canEditSavedViews: true, canEditWorkspaces: true, + canCreateNewField: true, + canAddSidebarGroup: true, readOnly: false, selectedSamples: new Set(), selectedLabels: [], @@ -66,6 +68,7 @@ export const SESSION_DEFAULT: Session = { type SetterKeys = keyof Omit< Session, | "canCreateNewField" + | "canAddSidebarGroup" | "canEditCustomColors" | "canEditSavedViews" | "canEditWorkspaces" @@ -178,6 +181,7 @@ export function sessionAtom( if ( options.key === "canCreateNewField" || + options.key === "canAddSidebarGroup" || options.key === "canEditCustomColors" || options.key === "readOnly" || options.key === "canEditSavedViews" || From 223be5c3daf93b83898785b74521536417a1a761 Mon Sep 17 00:00:00 2001 From: Lanny W Date: Thu, 9 May 2024 11:41:02 -0500 Subject: [PATCH 030/126] add canTagSamples permission flag for using tagger --- app/packages/app/src/useWriters/registerWriter.ts | 1 + app/packages/core/src/components/Actions/ActionsRow.tsx | 4 +++- app/packages/state/src/recoil/atoms.ts | 5 +++++ app/packages/state/src/session.ts | 4 ++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/packages/app/src/useWriters/registerWriter.ts b/app/packages/app/src/useWriters/registerWriter.ts index 394233f437..a08bb42a15 100644 --- a/app/packages/app/src/useWriters/registerWriter.ts +++ b/app/packages/app/src/useWriters/registerWriter.ts @@ -8,6 +8,7 @@ type WriterKeys = keyof Omit< Session, | "canAddSidebarGroup" | "canCreateNewField" + | "canTagSamples" | "canEditCustomColors" | "canEditSavedViews" | "readOnly" diff --git a/app/packages/core/src/components/Actions/ActionsRow.tsx b/app/packages/core/src/components/Actions/ActionsRow.tsx index 3fe844eb48..6082b11939 100644 --- a/app/packages/core/src/components/Actions/ActionsRow.tsx +++ b/app/packages/core/src/components/Actions/ActionsRow.tsx @@ -162,7 +162,9 @@ const Tag = ({ const [available, setAvailable] = useState(true); const labels = useRecoilValue(fos.selectedLabelIds); const samples = useRecoilValue(fos.selectedSamples); - const readOnly = useRecoilValue(fos.readOnly); + const canTag = useRecoilValue(fos.canTagSamples); + const isSnapshot = useRecoilValue(fos.readOnly); + const readOnly = isSnapshot || !canTag; const selected = labels.size > 0 || samples.size > 0; const tagging = useRecoilValue(fos.anyTagging); diff --git a/app/packages/state/src/recoil/atoms.ts b/app/packages/state/src/recoil/atoms.ts index 5b3baa7056..9c31435c7e 100644 --- a/app/packages/state/src/recoil/atoms.ts +++ b/app/packages/state/src/recoil/atoms.ts @@ -328,6 +328,11 @@ export const canAddSidebarGroup = sessionAtom({ default: true, }); +export const canTagSamples = sessionAtom({ + key: "canTagSamples", + default: true, +}); + export const readOnly = sessionAtom({ key: "readOnly", default: false, diff --git a/app/packages/state/src/session.ts b/app/packages/state/src/session.ts index af5d1d4c25..3007223891 100644 --- a/app/packages/state/src/session.ts +++ b/app/packages/state/src/session.ts @@ -31,6 +31,7 @@ export interface Session { canEditWorkspaces: boolean; canCreateNewField: boolean; canAddSidebarGroup: boolean; + canTagSamples: boolean; colorScheme: ColorSchemeInput; readOnly: boolean; selectedSamples: Set; @@ -46,6 +47,7 @@ export const SESSION_DEFAULT: Session = { canEditWorkspaces: true, canCreateNewField: true, canAddSidebarGroup: true, + canTagSamples: true, readOnly: false, selectedSamples: new Set(), selectedLabels: [], @@ -70,6 +72,7 @@ type SetterKeys = keyof Omit< | "canCreateNewField" | "canAddSidebarGroup" | "canEditCustomColors" + | "canTagSamples" | "canEditSavedViews" | "canEditWorkspaces" | "readOnly" @@ -183,6 +186,7 @@ export function sessionAtom( options.key === "canCreateNewField" || options.key === "canAddSidebarGroup" || options.key === "canEditCustomColors" || + options.key === "canTagSamples" || options.key === "readOnly" || options.key === "canEditSavedViews" || options.key === "canEditWorkspaces" || From 5a684c354ca58961ce7dff934bd1395f6867b5b6 Mon Sep 17 00:00:00 2001 From: Lanny W Date: Thu, 9 May 2024 14:00:33 -0500 Subject: [PATCH 031/126] Update app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../core/src/components/Sidebar/Entries/AddGroupEntry.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx b/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx index 3eddb40b83..5af646273a 100644 --- a/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx @@ -7,9 +7,7 @@ import { InputDiv } from "./utils"; const AddGroup = () => { const [value, setValue] = useState(""); const isFieldVisibilityApplied = useRecoilValue(fos.isFieldVisibilityActive); - const isSnapShot = useRecoilValue(fos.readOnly); - const canAddSidebarGroup = useRecoilValue(fos.canAddSidebarGroup); - const isReadOnly = isSnapShot || !canAddSidebarGroup; + const isReadOnly = useRecoilValue(fos.readOnly) || !useRecoilValue(fos.canAddSidebarGroup); const addGroup = useRecoilCallback( ({ set, snapshot }) => From 703f91a936906bf1144d2091c0be5f070b6aa9f8 Mon Sep 17 00:00:00 2001 From: Lanny W Date: Thu, 9 May 2024 14:00:55 -0500 Subject: [PATCH 032/126] Update app/packages/core/src/components/Sidebar/Entries/Draggable.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../core/src/components/Sidebar/Entries/Draggable.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx index 4d4fca137e..27752c0f95 100644 --- a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx @@ -19,9 +19,7 @@ const Draggable: React.FC< const theme = useTheme(); const [hovering, setHovering] = useState(false); const [dragging, setDragging] = useState(false); - const isSnapShot = useRecoilValue(fos.readOnly); - const canAddSidebarGroup = useRecoilValue(fos.canAddSidebarGroup); - const isReadOnly = isSnapShot || !canAddSidebarGroup; + const isReadOnly = useRecoilValue(fos.readOnly) || !useRecoilValue(fos.canAddSidebarGroup); const isFieldVisibilityApplied = useRecoilValue(fos.isFieldVisibilityActive); const disableDrag = From 4b24aecbd8e75d3c80199db414fb0eb2af6c4116 Mon Sep 17 00:00:00 2001 From: Lanny W Date: Thu, 9 May 2024 14:01:12 -0500 Subject: [PATCH 033/126] Update app/packages/core/src/components/Actions/ActionsRow.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- app/packages/core/src/components/Actions/ActionsRow.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/packages/core/src/components/Actions/ActionsRow.tsx b/app/packages/core/src/components/Actions/ActionsRow.tsx index 6082b11939..962eabc59f 100644 --- a/app/packages/core/src/components/Actions/ActionsRow.tsx +++ b/app/packages/core/src/components/Actions/ActionsRow.tsx @@ -162,9 +162,7 @@ const Tag = ({ const [available, setAvailable] = useState(true); const labels = useRecoilValue(fos.selectedLabelIds); const samples = useRecoilValue(fos.selectedSamples); - const canTag = useRecoilValue(fos.canTagSamples); - const isSnapshot = useRecoilValue(fos.readOnly); - const readOnly = isSnapshot || !canTag; + const readOnly = useRecoilValue(fos.readOnly) || !useRecoilValue(fos.canTagSamples); const selected = labels.size > 0 || samples.size > 0; const tagging = useRecoilValue(fos.anyTagging); From 53f96bd575ad7cffff7381b9b6649975fee343dc Mon Sep 17 00:00:00 2001 From: Lanny W Date: Thu, 9 May 2024 14:16:32 -0500 Subject: [PATCH 034/126] update names --- .../app/src/useWriters/registerWriter.ts | 4 ++-- .../core/src/components/Actions/ActionsRow.tsx | 3 ++- .../components/Sidebar/Entries/AddGroupEntry.tsx | 3 ++- .../src/components/Sidebar/Entries/Draggable.tsx | 3 ++- app/packages/state/src/recoil/atoms.ts | 8 ++++---- app/packages/state/src/session.ts | 16 ++++++++-------- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/app/packages/app/src/useWriters/registerWriter.ts b/app/packages/app/src/useWriters/registerWriter.ts index a08bb42a15..b481c3436c 100644 --- a/app/packages/app/src/useWriters/registerWriter.ts +++ b/app/packages/app/src/useWriters/registerWriter.ts @@ -6,9 +6,9 @@ import { RoutingContext } from "../routing"; type WriterKeys = keyof Omit< Session, - | "canAddSidebarGroup" + | "canModifySidebarGroup" | "canCreateNewField" - | "canTagSamples" + | "canTagSamplesOrLabels" | "canEditCustomColors" | "canEditSavedViews" | "readOnly" diff --git a/app/packages/core/src/components/Actions/ActionsRow.tsx b/app/packages/core/src/components/Actions/ActionsRow.tsx index 962eabc59f..6d0f1d8f88 100644 --- a/app/packages/core/src/components/Actions/ActionsRow.tsx +++ b/app/packages/core/src/components/Actions/ActionsRow.tsx @@ -162,7 +162,8 @@ const Tag = ({ const [available, setAvailable] = useState(true); const labels = useRecoilValue(fos.selectedLabelIds); const samples = useRecoilValue(fos.selectedSamples); - const readOnly = useRecoilValue(fos.readOnly) || !useRecoilValue(fos.canTagSamples); + const readOnly = + useRecoilValue(fos.readOnly) || !useRecoilValue(fos.canTagSamplesOrLabels); const selected = labels.size > 0 || samples.size > 0; const tagging = useRecoilValue(fos.anyTagging); diff --git a/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx b/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx index 5af646273a..cecba7c126 100644 --- a/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx @@ -7,7 +7,8 @@ import { InputDiv } from "./utils"; const AddGroup = () => { const [value, setValue] = useState(""); const isFieldVisibilityApplied = useRecoilValue(fos.isFieldVisibilityActive); - const isReadOnly = useRecoilValue(fos.readOnly) || !useRecoilValue(fos.canAddSidebarGroup); + const isReadOnly = + useRecoilValue(fos.readOnly) || !useRecoilValue(fos.canModifySidebarGroup); const addGroup = useRecoilCallback( ({ set, snapshot }) => diff --git a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx index 27752c0f95..e6d706a914 100644 --- a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx @@ -19,7 +19,8 @@ const Draggable: React.FC< const theme = useTheme(); const [hovering, setHovering] = useState(false); const [dragging, setDragging] = useState(false); - const isReadOnly = useRecoilValue(fos.readOnly) || !useRecoilValue(fos.canAddSidebarGroup); + const isReadOnly = + useRecoilValue(fos.readOnly) || !useRecoilValue(fos.canModifySidebarGroup); const isFieldVisibilityApplied = useRecoilValue(fos.isFieldVisibilityActive); const disableDrag = diff --git a/app/packages/state/src/recoil/atoms.ts b/app/packages/state/src/recoil/atoms.ts index 9c31435c7e..d3ac4bf685 100644 --- a/app/packages/state/src/recoil/atoms.ts +++ b/app/packages/state/src/recoil/atoms.ts @@ -323,13 +323,13 @@ export const canCreateNewField = sessionAtom({ default: true, }); -export const canAddSidebarGroup = sessionAtom({ - key: "canAddSidebarGroup", +export const canModifySidebarGroup = sessionAtom({ + key: "canModifySidebarGroup", default: true, }); -export const canTagSamples = sessionAtom({ - key: "canTagSamples", +export const canTagSamplesOrLabels = sessionAtom({ + key: "canTagSamplesOrLabels", default: true, }); diff --git a/app/packages/state/src/session.ts b/app/packages/state/src/session.ts index 3007223891..dd4221bda0 100644 --- a/app/packages/state/src/session.ts +++ b/app/packages/state/src/session.ts @@ -30,8 +30,8 @@ export interface Session { canEditSavedViews: boolean; canEditWorkspaces: boolean; canCreateNewField: boolean; - canAddSidebarGroup: boolean; - canTagSamples: boolean; + canModifySidebarGroup: boolean; + canTagSamplesOrLabels: boolean; colorScheme: ColorSchemeInput; readOnly: boolean; selectedSamples: Set; @@ -46,8 +46,8 @@ export const SESSION_DEFAULT: Session = { canEditSavedViews: true, canEditWorkspaces: true, canCreateNewField: true, - canAddSidebarGroup: true, - canTagSamples: true, + canModifySidebarGroup: true, + canTagSamplesOrLabels: true, readOnly: false, selectedSamples: new Set(), selectedLabels: [], @@ -70,9 +70,9 @@ export const SESSION_DEFAULT: Session = { type SetterKeys = keyof Omit< Session, | "canCreateNewField" - | "canAddSidebarGroup" + | "canModifySidebarGroup" | "canEditCustomColors" - | "canTagSamples" + | "canTagSamplesOrLabels" | "canEditSavedViews" | "canEditWorkspaces" | "readOnly" @@ -184,9 +184,9 @@ export function sessionAtom( if ( options.key === "canCreateNewField" || - options.key === "canAddSidebarGroup" || + options.key === "canModifySidebarGroup" || options.key === "canEditCustomColors" || - options.key === "canTagSamples" || + options.key === "canTagSamplesOrLabels" || options.key === "readOnly" || options.key === "canEditSavedViews" || options.key === "canEditWorkspaces" || From b44b7ed5c20cf9749bad4f0a1d661447a8f64046 Mon Sep 17 00:00:00 2001 From: Lanny W Date: Thu, 9 May 2024 16:15:11 -0500 Subject: [PATCH 035/126] delete sidebar group and rename sidebar group checks canModifySidebarGroup permission flag --- .../core/src/components/Sidebar/Entries/GroupEntries.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx b/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx index f47bc9a2bc..0074faae65 100644 --- a/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx @@ -132,6 +132,9 @@ const GroupEntry = React.memo( const canCommit = useRef(false); const theme = useTheme(); const notify = fos.useNotification(); + const readOnly = useRecoilValue(fos.readOnly); + const canModifySidebarGroup = useRecoilValue(fos.canModifySidebarGroup); + const canModify = !readOnly && canModifySidebarGroup; return (
- {hovering && !editing && setValue && ( + {hovering && !editing && setValue && canModify && ( { @@ -228,7 +231,7 @@ const GroupEntry = React.memo( )} {pills} - {onDelete && !editing && ( + {onDelete && !editing && canModify && ( { From c75b024ce360d159c7e417f094aad6ac2691c773 Mon Sep 17 00:00:00 2001 From: Lanny W Date: Thu, 9 May 2024 16:22:51 -0500 Subject: [PATCH 036/126] fix conditional hook bug --- .../core/src/components/Actions/ActionsRow.tsx | 11 ++++++----- .../src/components/Sidebar/Entries/AddGroupEntry.tsx | 5 +++-- .../core/src/components/Sidebar/Entries/Draggable.tsx | 5 +++-- .../src/components/Sidebar/ViewSelection/index.tsx | 6 +++--- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/packages/core/src/components/Actions/ActionsRow.tsx b/app/packages/core/src/components/Actions/ActionsRow.tsx index 6d0f1d8f88..f34f98fca5 100644 --- a/app/packages/core/src/components/Actions/ActionsRow.tsx +++ b/app/packages/core/src/components/Actions/ActionsRow.tsx @@ -162,8 +162,9 @@ const Tag = ({ const [available, setAvailable] = useState(true); const labels = useRecoilValue(fos.selectedLabelIds); const samples = useRecoilValue(fos.selectedSamples); - const readOnly = - useRecoilValue(fos.readOnly) || !useRecoilValue(fos.canTagSamplesOrLabels); + const canTag = useRecoilValue(fos.canTagSamplesOrLabels); + const readOnly = useRecoilValue(fos.readOnly); + const isReadOnly = readOnly || !canTag; const selected = labels.size > 0 || samples.size > 0; const tagging = useRecoilValue(fos.anyTagging); @@ -180,7 +181,7 @@ const Tag = ({ useEventHandler(lookerRef.current, "pause", () => setAvailable(true)); const baseTitle = `Tag sample${modal ? "" : "s"} or labels`; - const title = readOnly + const title = isReadOnly ? `Can not ${baseTitle.toLowerCase()} in read-only mode.` : baseTitle; @@ -188,7 +189,7 @@ const Tag = ({ : } open={open} - onClick={() => !disabled && available && !readOnly && setOpen(!open)} + onClick={() => !disabled && available && !isReadOnly && setOpen(!open)} highlight={(selected || open) && available} title={title} data-cy="action-tag-sample-labels" diff --git a/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx b/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx index cecba7c126..a843c4114b 100644 --- a/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx @@ -7,8 +7,9 @@ import { InputDiv } from "./utils"; const AddGroup = () => { const [value, setValue] = useState(""); const isFieldVisibilityApplied = useRecoilValue(fos.isFieldVisibilityActive); - const isReadOnly = - useRecoilValue(fos.readOnly) || !useRecoilValue(fos.canModifySidebarGroup); + const readOnly = useRecoilValue(fos.readOnly); + const canModifySidebarGroup = useRecoilValue(fos.canModifySidebarGroup); + const isReadOnly = readOnly || !canModifySidebarGroup; const addGroup = useRecoilCallback( ({ set, snapshot }) => diff --git a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx index e6d706a914..dd0188a49d 100644 --- a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx @@ -19,8 +19,9 @@ const Draggable: React.FC< const theme = useTheme(); const [hovering, setHovering] = useState(false); const [dragging, setDragging] = useState(false); - const isReadOnly = - useRecoilValue(fos.readOnly) || !useRecoilValue(fos.canModifySidebarGroup); + const readOnly = useRecoilValue(fos.readOnly); + const canModifySidebarGroup = useRecoilValue(fos.canModifySidebarGroup); + const isReadOnly = readOnly || !canModifySidebarGroup; const isFieldVisibilityApplied = useRecoilValue(fos.isFieldVisibilityActive); const disableDrag = diff --git a/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx b/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx index 912161cb7c..b574ddbcff 100644 --- a/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx +++ b/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx @@ -43,10 +43,10 @@ export default function ViewSelection() { const setEditView = useSetRecoilState(viewDialogContent); const resetView = useResetRecoilState(fos.view); const [viewSearch, setViewSearch] = useRecoilState(viewSearchTerm); - const isSnapShot = useRecoilValue(fos.readOnly); + const readOnly = useRecoilValue(fos.readOnly); const canEdit = useMemo( - () => canEditSavedViews && !isSnapShot, - [canEditSavedViews, isSnapShot] + () => canEditSavedViews && !readOnly, + [canEditSavedViews, readOnly] ); const [data, refetch] = useRefetchableSavedViews(); From 3f93b81abaa8c1f65c63bd09c0b81be2bf157539 Mon Sep 17 00:00:00 2001 From: Lanny W Date: Mon, 13 May 2024 10:16:11 -0500 Subject: [PATCH 037/126] update session atom schema --- app/packages/state/src/recoil/atoms.ts | 6 +++--- app/packages/state/src/session.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/packages/state/src/recoil/atoms.ts b/app/packages/state/src/recoil/atoms.ts index d3ac4bf685..3ea3dde2ac 100644 --- a/app/packages/state/src/recoil/atoms.ts +++ b/app/packages/state/src/recoil/atoms.ts @@ -320,17 +320,17 @@ export const canEditCustomColors = sessionAtom({ export const canCreateNewField = sessionAtom({ key: "canCreateNewField", - default: true, + default: { enabled: true, message: null }, }); export const canModifySidebarGroup = sessionAtom({ key: "canModifySidebarGroup", - default: true, + default: { enabled: true, message: null }, }); export const canTagSamplesOrLabels = sessionAtom({ key: "canTagSamplesOrLabels", - default: true, + default: { enabled: true, message: null }, }); export const readOnly = sessionAtom({ diff --git a/app/packages/state/src/session.ts b/app/packages/state/src/session.ts index dd4221bda0..6ba4d0306f 100644 --- a/app/packages/state/src/session.ts +++ b/app/packages/state/src/session.ts @@ -29,9 +29,9 @@ export interface Session { canEditCustomColors: boolean; canEditSavedViews: boolean; canEditWorkspaces: boolean; - canCreateNewField: boolean; - canModifySidebarGroup: boolean; - canTagSamplesOrLabels: boolean; + canCreateNewField: { enabled: boolean; message: string | null }; + canModifySidebarGroup: { enabled: boolean; message: string | null }; + canTagSamplesOrLabels: { enabled: boolean; message: string | null }; colorScheme: ColorSchemeInput; readOnly: boolean; selectedSamples: Set; @@ -45,9 +45,9 @@ export const SESSION_DEFAULT: Session = { canEditCustomColors: true, canEditSavedViews: true, canEditWorkspaces: true, - canCreateNewField: true, - canModifySidebarGroup: true, - canTagSamplesOrLabels: true, + canCreateNewField: { enabled: true, message: null }, + canModifySidebarGroup: { enabled: true, message: null }, + canTagSamplesOrLabels: { enabled: true, message: null }, readOnly: false, selectedSamples: new Set(), selectedLabels: [], From f039e3a3280d29101410cf3706ca63a0700f448d Mon Sep 17 00:00:00 2001 From: Lanny W Date: Mon, 13 May 2024 10:30:52 -0500 Subject: [PATCH 038/126] update components to use the new flag and message --- app/packages/core/src/components/Actions/ActionsRow.tsx | 7 ++++--- .../core/src/components/Sidebar/Entries/AddGroupEntry.tsx | 3 +-- .../core/src/components/Sidebar/Entries/Draggable.tsx | 8 +++++--- .../core/src/components/Sidebar/Entries/GroupEntries.tsx | 7 +++---- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/packages/core/src/components/Actions/ActionsRow.tsx b/app/packages/core/src/components/Actions/ActionsRow.tsx index f34f98fca5..26df321f8f 100644 --- a/app/packages/core/src/components/Actions/ActionsRow.tsx +++ b/app/packages/core/src/components/Actions/ActionsRow.tsx @@ -163,8 +163,7 @@ const Tag = ({ const labels = useRecoilValue(fos.selectedLabelIds); const samples = useRecoilValue(fos.selectedSamples); const canTag = useRecoilValue(fos.canTagSamplesOrLabels); - const readOnly = useRecoilValue(fos.readOnly); - const isReadOnly = readOnly || !canTag; + const isReadOnly = canTag.enabled !== true; const selected = labels.size > 0 || samples.size > 0; const tagging = useRecoilValue(fos.anyTagging); @@ -182,7 +181,9 @@ const Tag = ({ const baseTitle = `Tag sample${modal ? "" : "s"} or labels`; const title = isReadOnly - ? `Can not ${baseTitle.toLowerCase()} in read-only mode.` + ? `Can not ${baseTitle.toLowerCase()} in read-only mode` + canTag.message + ? `: ${canTag.message}` + : "" : baseTitle; return ( diff --git a/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx b/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx index a843c4114b..644b3fbc8c 100644 --- a/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx @@ -7,9 +7,8 @@ import { InputDiv } from "./utils"; const AddGroup = () => { const [value, setValue] = useState(""); const isFieldVisibilityApplied = useRecoilValue(fos.isFieldVisibilityActive); - const readOnly = useRecoilValue(fos.readOnly); const canModifySidebarGroup = useRecoilValue(fos.canModifySidebarGroup); - const isReadOnly = readOnly || !canModifySidebarGroup; + const isReadOnly = canModifySidebarGroup.enabled !== true; const addGroup = useRecoilCallback( ({ set, snapshot }) => diff --git a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx index dd0188a49d..1358e12316 100644 --- a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx @@ -19,9 +19,8 @@ const Draggable: React.FC< const theme = useTheme(); const [hovering, setHovering] = useState(false); const [dragging, setDragging] = useState(false); - const readOnly = useRecoilValue(fos.readOnly); const canModifySidebarGroup = useRecoilValue(fos.canModifySidebarGroup); - const isReadOnly = readOnly || !canModifySidebarGroup; + const isReadOnly = canModifySidebarGroup.enabled !== true; const isFieldVisibilityApplied = useRecoilValue(fos.isFieldVisibilityActive); const disableDrag = @@ -91,7 +90,10 @@ const Draggable: React.FC< }} title={ isReadOnly - ? "Can not reorder in read-only mode" + ? "Can not reorder in read-only mode" + + canModifySidebarGroup.message + ? ": " + canModifySidebarGroup.message + : "" : trigger ? "Drag to reorder" : undefined diff --git a/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx b/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx index 0074faae65..14a78e62c5 100644 --- a/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx @@ -132,9 +132,8 @@ const GroupEntry = React.memo( const canCommit = useRef(false); const theme = useTheme(); const notify = fos.useNotification(); - const readOnly = useRecoilValue(fos.readOnly); const canModifySidebarGroup = useRecoilValue(fos.canModifySidebarGroup); - const canModify = !readOnly && canModifySidebarGroup; + const isReadOnly = canModifySidebarGroup.enabled !== true; return (
- {hovering && !editing && setValue && canModify && ( + {hovering && !editing && setValue && isReadOnly && ( { @@ -231,7 +230,7 @@ const GroupEntry = React.memo( )} {pills} - {onDelete && !editing && canModify && ( + {onDelete && !editing && isReadOnly && ( { From 45c11bfc767ca671252f530ceb060d58d3943be5 Mon Sep 17 00:00:00 2001 From: Lanny W Date: Mon, 13 May 2024 10:37:06 -0500 Subject: [PATCH 039/126] improve message --- app/packages/core/src/components/Actions/ActionsRow.tsx | 4 +--- .../core/src/components/Sidebar/Entries/Draggable.tsx | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/packages/core/src/components/Actions/ActionsRow.tsx b/app/packages/core/src/components/Actions/ActionsRow.tsx index 26df321f8f..527827b436 100644 --- a/app/packages/core/src/components/Actions/ActionsRow.tsx +++ b/app/packages/core/src/components/Actions/ActionsRow.tsx @@ -181,9 +181,7 @@ const Tag = ({ const baseTitle = `Tag sample${modal ? "" : "s"} or labels`; const title = isReadOnly - ? `Can not ${baseTitle.toLowerCase()} in read-only mode` + canTag.message - ? `: ${canTag.message}` - : "" + ? canTag.message ?? "Cannot tag in read-only mode" : baseTitle; return ( diff --git a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx index 1358e12316..4a879f8fc3 100644 --- a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx @@ -90,10 +90,8 @@ const Draggable: React.FC< }} title={ isReadOnly - ? "Can not reorder in read-only mode" + - canModifySidebarGroup.message - ? ": " + canModifySidebarGroup.message - : "" + ? canModifySidebarGroup.message ?? + "Can not reorder in read-only mode" : trigger ? "Drag to reorder" : undefined From e3f14a735adc3ed6b6360f3976f1a1c364eb0159 Mon Sep 17 00:00:00 2001 From: Lanny W Date: Mon, 13 May 2024 16:56:00 -0500 Subject: [PATCH 040/126] update canEditSavedViews, canEditWorkspaces and canEditCustomColors to use new schema --- .../src/components/Actions/ActionsRow.tsx | 12 ++++---- .../components/Actions/similar/Similar.tsx | 11 +++---- .../src/components/ColorModal/ColorFooter.tsx | 25 +++++++--------- .../Sidebar/Entries/AddGroupEntry.tsx | 4 +-- .../components/Sidebar/Entries/Draggable.tsx | 18 +++++------ .../Sidebar/ViewSelection/index.tsx | 30 ++++++++----------- .../src/components/Workspaces/Workspace.tsx | 18 ++++++----- .../spaces/src/components/Workspaces/hooks.ts | 20 +------------ .../src/components/Workspaces/index.tsx | 16 +++++----- app/packages/state/src/recoil/atoms.ts | 6 ++-- app/packages/state/src/session.ts | 24 +++++++-------- 11 files changed, 77 insertions(+), 107 deletions(-) diff --git a/app/packages/core/src/components/Actions/ActionsRow.tsx b/app/packages/core/src/components/Actions/ActionsRow.tsx index 527827b436..01e81c83c4 100644 --- a/app/packages/core/src/components/Actions/ActionsRow.tsx +++ b/app/packages/core/src/components/Actions/ActionsRow.tsx @@ -163,13 +163,13 @@ const Tag = ({ const labels = useRecoilValue(fos.selectedLabelIds); const samples = useRecoilValue(fos.selectedSamples); const canTag = useRecoilValue(fos.canTagSamplesOrLabels); - const isReadOnly = canTag.enabled !== true; + const disableTag = canTag.enabled !== true; const selected = labels.size > 0 || samples.size > 0; const tagging = useRecoilValue(fos.anyTagging); const ref = useRef(null); useOutsideClick(ref, () => open && setOpen(false)); - const disabled = tagging; + const disabled = tagging || disableTag; lookerRef && useEventHandler(lookerRef.current, "play", () => { @@ -180,15 +180,15 @@ const Tag = ({ useEventHandler(lookerRef.current, "pause", () => setAvailable(true)); const baseTitle = `Tag sample${modal ? "" : "s"} or labels`; - const title = isReadOnly - ? canTag.message ?? "Cannot tag in read-only mode" + const title = disabled + ? canTag.message ?? "cannot tag in read-only mode" : baseTitle; return ( : } open={open} - onClick={() => !disabled && available && !isReadOnly && setOpen(!open)} + onClick={() => !disabled && available && setOpen(!open)} highlight={(selected || open) && available} title={title} data-cy="action-tag-sample-labels" diff --git a/app/packages/core/src/components/Actions/similar/Similar.tsx b/app/packages/core/src/components/Actions/similar/Similar.tsx index 7deb4b428d..13078dc87c 100644 --- a/app/packages/core/src/components/Actions/similar/Similar.tsx +++ b/app/packages/core/src/components/Actions/similar/Similar.tsx @@ -94,7 +94,8 @@ const SortBySimilarity = ({ ); const isLoading = useRecoilValue(fos.similaritySorting); const canCreateNewField = useRecoilValue(fos.canCreateNewField); - const isReadOnly = useRecoilValue(fos.readOnly); + const disabled = canCreateNewField.enabled !== true; + const disableMsg = canCreateNewField.message; useLayoutEffect(() => { if (!choices.choices.includes(state.brainKey)) { @@ -282,18 +283,14 @@ const SortBySimilarity = ({ Optional: store the distance between each sample and the query in this field !value.startsWith("_")} value={state.distField ?? ""} setter={(value) => updateState({ distField: !value.length ? undefined : value }) } - title={ - isReadOnly - ? "Can not store the distance in a field in read-only mode" - : undefined - } + title={disableMsg} />
)} diff --git a/app/packages/core/src/components/ColorModal/ColorFooter.tsx b/app/packages/core/src/components/ColorModal/ColorFooter.tsx index 11f571a3b0..810913c502 100644 --- a/app/packages/core/src/components/ColorModal/ColorFooter.tsx +++ b/app/packages/core/src/components/ColorModal/ColorFooter.tsx @@ -1,19 +1,18 @@ import { Button } from "@fiftyone/components"; import * as foq from "@fiftyone/relay"; import * as fos from "@fiftyone/state"; -import React, { useEffect, useMemo } from "react"; +import React, { useEffect } from "react"; import { useMutation } from "react-relay"; import { useRecoilState, useRecoilValue } from "recoil"; import { ButtonGroup, ModalActionButtonContainer } from "./ShareStyledDiv"; import { activeColorEntry } from "./state"; const ColorFooter: React.FC = () => { - const isReadOnly = useRecoilValue(fos.readOnly); const canEditCustomColors = useRecoilValue(fos.canEditCustomColors); - const canEdit = useMemo( - () => !isReadOnly && canEditCustomColors, - [canEditCustomColors, isReadOnly] - ); + const disabled = canEditCustomColors.enabled! == true; + const title = disabled + ? canEditCustomColors.message ?? "" + : "Save to dataset app config"; const setColorScheme = fos.useSetSessionColorScheme(); const [activeColorModalField, setActiveColorModalField] = useRecoilState(activeColorEntry); @@ -30,6 +29,7 @@ const ColorFooter: React.FC = () => { () => foq.subscribe(() => setActiveColorModalField(null)), [setActiveColorModalField] ); + if (!activeColorModalField) return null; if (!datasetName) { throw new Error("dataset not defined"); @@ -53,11 +53,7 @@ const ColorFooter: React.FC = () => { Reset - {datasetDefault && ( + {datasetDefault && !disabled && ( diff --git a/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx b/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx index 644b3fbc8c..0f9be3cbe8 100644 --- a/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/AddGroupEntry.tsx @@ -8,7 +8,7 @@ const AddGroup = () => { const [value, setValue] = useState(""); const isFieldVisibilityApplied = useRecoilValue(fos.isFieldVisibilityActive); const canModifySidebarGroup = useRecoilValue(fos.canModifySidebarGroup); - const isReadOnly = canModifySidebarGroup.enabled !== true; + const disabled = canModifySidebarGroup.enabled !== true; const addGroup = useRecoilCallback( ({ set, snapshot }) => @@ -41,7 +41,7 @@ const AddGroup = () => { [] ); - if (isFieldVisibilityApplied || isReadOnly) { + if (isFieldVisibilityApplied || disabled) { return null; } diff --git a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx index 4a879f8fc3..169c433ca8 100644 --- a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx @@ -20,14 +20,14 @@ const Draggable: React.FC< const [hovering, setHovering] = useState(false); const [dragging, setDragging] = useState(false); const canModifySidebarGroup = useRecoilValue(fos.canModifySidebarGroup); - const isReadOnly = canModifySidebarGroup.enabled !== true; + const disabled = canModifySidebarGroup.enabled !== true; const isFieldVisibilityApplied = useRecoilValue(fos.isFieldVisibilityActive); const disableDrag = !entryKey || entryKey.split(",")[1]?.includes("tags") || entryKey.split(",")[1]?.includes("_label_tags") || - isReadOnly || + disabled || isFieldVisibilityApplied; const active = trigger && (dragging || hovering) && !disableDrag; @@ -48,8 +48,8 @@ const Draggable: React.FC< ?.replace("]", ""); const isDraggable = useMemo( - () => !disableDrag && trigger && !isReadOnly, - [disableDrag, trigger, isReadOnly] + () => !disableDrag && trigger && !disabled, + [disableDrag, trigger, disabled] ); return ( @@ -68,10 +68,8 @@ const Draggable: React.FC< } : undefined } - onMouseEnter={ - isReadOnly ? undefined : () => trigger && setHovering(true) - } - onMouseLeave={isReadOnly ? undefined : () => setHovering(false)} + onMouseEnter={disabled ? undefined : () => trigger && setHovering(true)} + onMouseLeave={disabled ? undefined : () => setHovering(false)} style={{ backgroundColor: color, position: "absolute", @@ -86,10 +84,10 @@ const Draggable: React.FC< boxShadow: `0 2px 20px ${theme.custom.shadow}`, overflow: "hidden", ...style, - ...(isReadOnly ? { cursor: "not-allowed" } : {}), + ...(disabled ? { cursor: "not-allowed" } : {}), }} title={ - isReadOnly + disabled ? canModifySidebarGroup.message ?? "Can not reorder in read-only mode" : trigger diff --git a/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx b/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx index b574ddbcff..cdbbc636f7 100644 --- a/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx +++ b/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx @@ -43,11 +43,9 @@ export default function ViewSelection() { const setEditView = useSetRecoilState(viewDialogContent); const resetView = useResetRecoilState(fos.view); const [viewSearch, setViewSearch] = useRecoilState(viewSearchTerm); - const readOnly = useRecoilValue(fos.readOnly); - const canEdit = useMemo( - () => canEditSavedViews && !readOnly, - [canEditSavedViews, readOnly] - ); + + const disabled = canEditSavedViews.enabled !== true; + const disabledMsg = canEditSavedViews.message; const [data, refetch] = useRefetchableSavedViews(); @@ -145,7 +143,7 @@ export default function ViewSelection() { useEffect(() => { const callback = (event: KeyboardEvent) => { - if (!canEdit) { + if (!disabled) { return; } if ((event.metaKey || event.ctrlKey) && event.code === "KeyS") { @@ -160,13 +158,13 @@ export default function ViewSelection() { return () => { document.removeEventListener("keydown", callback); }; - }, [isEmptyView, canEdit]); + }, [isEmptyView, disabled]); return ( { @@ -235,18 +233,14 @@ export default function ViewSelection() { lastFixedOption={ canEdit && !isEmptyView && setIsOpen(true)} - disabled={isEmptyView || !canEdit} - title={ - canEdit - ? undefined - : "Can not save filters as a view in read-only mode" - } + onClick={() => !disabled && !isEmptyView && setIsOpen(true)} + disabled={isEmptyView || disabled} + title={disabledMsg} > - + - + Save current filters as view diff --git a/app/packages/spaces/src/components/Workspaces/Workspace.tsx b/app/packages/spaces/src/components/Workspaces/Workspace.tsx index 47a7be7cca..3c07768fb0 100644 --- a/app/packages/spaces/src/components/Workspaces/Workspace.tsx +++ b/app/packages/spaces/src/components/Workspaces/Workspace.tsx @@ -1,4 +1,5 @@ import { ColoredDot } from "@fiftyone/components"; +import { canEditWorkspaces } from "@fiftyone/state"; import { Edit } from "@mui/icons-material"; import { IconButton, @@ -9,14 +10,15 @@ import { Stack, } from "@mui/material"; import "allotment/dist/style.css"; -import { useSetRecoilState } from "recoil"; +import { useRecoilValue, useSetRecoilState } from "recoil"; import { workspaceEditorStateAtom } from "../../state"; -import { useWorkspacePermission } from "./hooks"; export default function Workspace(props: WorkspacePropsType) { const { name, description, color, onClick, onEdit } = props; const setWorkspaceEditorState = useSetRecoilState(workspaceEditorStateAtom); - const { canEdit, disabledInfo } = useWorkspacePermission(); + const canEdit = useRecoilValue(canEditWorkspaces); + const disabled = canEdit.enabled !== true; + const disabledMsg = canEdit.message; return ( { - if (!canEdit) return; + if (disabled) return; e.preventDefault(); e.stopPropagation(); setWorkspaceEditorState((state) => ({ @@ -66,10 +68,10 @@ export default function Workspace(props: WorkspacePropsType) { })); onEdit(); }} - disabled={!canEdit} + disabled={disabled} > diff --git a/app/packages/spaces/src/components/Workspaces/hooks.ts b/app/packages/spaces/src/components/Workspaces/hooks.ts index 7a12c75194..84640f7db2 100644 --- a/app/packages/spaces/src/components/Workspaces/hooks.ts +++ b/app/packages/spaces/src/components/Workspaces/hooks.ts @@ -1,5 +1,5 @@ import { executeOperator } from "@fiftyone/operators"; -import { canEditWorkspaces, datasetName, readOnly } from "@fiftyone/state"; +import { datasetName } from "@fiftyone/state"; import { toSlug } from "@fiftyone/utilities"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useRecoilState, useRecoilValue, useResetRecoilState } from "recoil"; @@ -61,21 +61,3 @@ export function useWorkspaces() { existingSlugs, }; } - -export function useWorkspacePermission() { - const canEditSavedViews = useRecoilValue(canEditWorkspaces); - const isReadOnly = useRecoilValue(readOnly); - const canEdit = useMemo( - () => canEditSavedViews && !isReadOnly, - [canEditSavedViews, isReadOnly] - ); - const disabledInfo = useMemo(() => { - return !canEditSavedViews - ? "You do not have permission to save a workspace" - : isReadOnly - ? "Can not save workspace in read-only mode" - : undefined; - }, [canEditSavedViews, isReadOnly]); - - return { canEdit, disabledInfo }; -} diff --git a/app/packages/spaces/src/components/Workspaces/index.tsx b/app/packages/spaces/src/components/Workspaces/index.tsx index aabd1b2eb0..84e2ddaeeb 100644 --- a/app/packages/spaces/src/components/Workspaces/index.tsx +++ b/app/packages/spaces/src/components/Workspaces/index.tsx @@ -1,4 +1,5 @@ import { ColoredDot, Popout, scrollable } from "@fiftyone/components"; +import { canEditWorkspaces, sessionSpaces } from "@fiftyone/state"; import { Add, AutoAwesomeMosaicOutlined } from "@mui/icons-material"; import { Box, @@ -18,9 +19,8 @@ import { useRecoilValue, useSetRecoilState } from "recoil"; import { workspaceEditorStateAtom } from "../../state"; import Workspace from "./Workspace"; import WorkspaceEditor from "./WorkspaceEditor"; -import { useWorkspaces, useWorkspacePermission } from "./hooks"; -import { sessionSpaces } from "@fiftyone/state"; import { UNSAVED_WORKSPACE_COLOR } from "./constants"; +import { useWorkspaces } from "./hooks"; export default function Workspaces() { const [open, setOpen] = useState(false); @@ -28,7 +28,9 @@ export default function Workspaces() { const { workspaces, loadWorkspace, initialized, listWorkspace } = useWorkspaces(); const setWorkspaceEditorState = useSetRecoilState(workspaceEditorStateAtom); - const { canEdit, disabledInfo } = useWorkspacePermission(); + const canEditWorkSpace = useRecoilValue(canEditWorkspaces); + const disabled = canEditWorkSpace.enabled !== true; + const disabledMsg = canEditWorkSpace.message; const sessionSpacesState = useRecoilValue(sessionSpaces); const currentWorkspaceName = sessionSpacesState._name; @@ -132,16 +134,16 @@ export default function Workspaces() { "&:hover": { ".MuiStack-root": { visibility: "visible" }, }, - cursor: !canEdit ? "not-allowed" : undefined, + cursor: disabled ? "not-allowed" : undefined, }} - title={disabledInfo} + title={disabledMsg} > { - if (!canEdit) return; + if (disabled) return; setOpen(false); setWorkspaceEditorState((state) => ({ ...state, diff --git a/app/packages/state/src/recoil/atoms.ts b/app/packages/state/src/recoil/atoms.ts index 3ea3dde2ac..4e0a2ab92c 100644 --- a/app/packages/state/src/recoil/atoms.ts +++ b/app/packages/state/src/recoil/atoms.ts @@ -305,17 +305,17 @@ export const theme = atom<"dark" | "light">({ export const canEditSavedViews = sessionAtom({ key: "canEditSavedViews", - default: true, + default: { enabled: true, message: null }, }); export const canEditWorkspaces = sessionAtom({ key: "canEditWorkspaces", - default: true, + default: { enabled: true, message: null }, }); export const canEditCustomColors = sessionAtom({ key: "canEditCustomColors", - default: true, + default: { enabled: true, message: null }, }); export const canCreateNewField = sessionAtom({ diff --git a/app/packages/state/src/session.ts b/app/packages/state/src/session.ts index 6ba4d0306f..2b7755fee3 100644 --- a/app/packages/state/src/session.ts +++ b/app/packages/state/src/session.ts @@ -26,12 +26,12 @@ export const SPACES_DEFAULT = { }; export interface Session { - canEditCustomColors: boolean; - canEditSavedViews: boolean; - canEditWorkspaces: boolean; - canCreateNewField: { enabled: boolean; message: string | null }; - canModifySidebarGroup: { enabled: boolean; message: string | null }; - canTagSamplesOrLabels: { enabled: boolean; message: string | null }; + canEditCustomColors: { enabled: boolean; message?: string }; + canEditSavedViews: { enabled: boolean; message?: string }; + canEditWorkspaces: { enabled: boolean; message?: string }; + canCreateNewField: { enabled: boolean; message?: string }; + canModifySidebarGroup: { enabled: boolean; message?: string }; + canTagSamplesOrLabels: { enabled: boolean; message?: string }; colorScheme: ColorSchemeInput; readOnly: boolean; selectedSamples: Set; @@ -42,12 +42,12 @@ export interface Session { } export const SESSION_DEFAULT: Session = { - canEditCustomColors: true, - canEditSavedViews: true, - canEditWorkspaces: true, - canCreateNewField: { enabled: true, message: null }, - canModifySidebarGroup: { enabled: true, message: null }, - canTagSamplesOrLabels: { enabled: true, message: null }, + canEditCustomColors: { enabled: true, message: undefined }, + canEditSavedViews: { enabled: true, message: undefined }, + canEditWorkspaces: { enabled: true, message: undefined }, + canCreateNewField: { enabled: true, message: undefined }, + canModifySidebarGroup: { enabled: true, message: undefined }, + canTagSamplesOrLabels: { enabled: true, message: undefined }, readOnly: false, selectedSamples: new Set(), selectedLabels: [], From 0d15f30698cfc07a0ec5dbbc88fc147d552b5ee4 Mon Sep 17 00:00:00 2001 From: Lanny W Date: Tue, 14 May 2024 05:01:24 -0500 Subject: [PATCH 041/126] fix message and fix typos --- .../core/src/components/Actions/ActionsRow.tsx | 5 +++-- .../src/components/ColorModal/ColorFooter.tsx | 2 +- .../src/components/Sidebar/Entries/Draggable.tsx | 15 +++++++-------- .../components/Sidebar/Entries/GroupEntries.tsx | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/packages/core/src/components/Actions/ActionsRow.tsx b/app/packages/core/src/components/Actions/ActionsRow.tsx index 01e81c83c4..997d53488c 100644 --- a/app/packages/core/src/components/Actions/ActionsRow.tsx +++ b/app/packages/core/src/components/Actions/ActionsRow.tsx @@ -180,8 +180,9 @@ const Tag = ({ useEventHandler(lookerRef.current, "pause", () => setAvailable(true)); const baseTitle = `Tag sample${modal ? "" : "s"} or labels`; + const title = disabled - ? canTag.message ?? "cannot tag in read-only mode" + ? (canTag.message || "").replace("#action", baseTitle.toLowerCase()) : baseTitle; return ( @@ -196,7 +197,7 @@ const Tag = ({ }} icon={tagging ? : } open={open} - onClick={() => !disabled && available && setOpen(!open)} + onClick={() => !disabled && available && !disableTag && setOpen(!open)} highlight={(selected || open) && available} title={title} data-cy="action-tag-sample-labels" diff --git a/app/packages/core/src/components/ColorModal/ColorFooter.tsx b/app/packages/core/src/components/ColorModal/ColorFooter.tsx index 810913c502..5dd239adb8 100644 --- a/app/packages/core/src/components/ColorModal/ColorFooter.tsx +++ b/app/packages/core/src/components/ColorModal/ColorFooter.tsx @@ -11,7 +11,7 @@ const ColorFooter: React.FC = () => { const canEditCustomColors = useRecoilValue(fos.canEditCustomColors); const disabled = canEditCustomColors.enabled! == true; const title = disabled - ? canEditCustomColors.message ?? "" + ? canEditCustomColors.message : "Save to dataset app config"; const setColorScheme = fos.useSetSessionColorScheme(); const [activeColorModalField, setActiveColorModalField] = diff --git a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx index 169c433ca8..477a0662e5 100644 --- a/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/Draggable.tsx @@ -52,6 +52,12 @@ const Draggable: React.FC< [disableDrag, trigger, disabled] ); + const title = disabled + ? canModifySidebarGroup.message || "Can not reorder in read-only mode" + : trigger + ? "Drag to reorder" + : undefined; + return ( <> {active && } diff --git a/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx b/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx index 14a78e62c5..4d246ee5d6 100644 --- a/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx @@ -133,7 +133,7 @@ const GroupEntry = React.memo( const theme = useTheme(); const notify = fos.useNotification(); const canModifySidebarGroup = useRecoilValue(fos.canModifySidebarGroup); - const isReadOnly = canModifySidebarGroup.enabled !== true; + const disabled = canModifySidebarGroup.enabled !== true; return (
- {hovering && !editing && setValue && isReadOnly && ( + {hovering && !editing && setValue && !disabled && ( { @@ -230,7 +230,7 @@ const GroupEntry = React.memo( )} {pills} - {onDelete && !editing && isReadOnly && ( + {onDelete && !editing && !disabled && ( { From ec3378e91e8cae984e5a0d0d2c8b50c34000c39d Mon Sep 17 00:00:00 2001 From: Lanny W Date: Tue, 21 May 2024 06:34:53 -0500 Subject: [PATCH 042/126] Update app/packages/core/src/components/Sidebar/ViewSelection/index.tsx Co-authored-by: manivoxel51 <109545780+manivoxel51@users.noreply.github.com> --- .../core/src/components/Sidebar/ViewSelection/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx b/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx index cdbbc636f7..834ced047b 100644 --- a/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx +++ b/app/packages/core/src/components/Sidebar/ViewSelection/index.tsx @@ -143,7 +143,7 @@ export default function ViewSelection() { useEffect(() => { const callback = (event: KeyboardEvent) => { - if (!disabled) { + if (disabled) { return; } if ((event.metaKey || event.ctrlKey) && event.code === "KeyS") { From cf13e2ebae01f07bcaa0c059a9323a325d61788b Mon Sep 17 00:00:00 2001 From: Lanny W Date: Tue, 21 May 2024 06:36:23 -0500 Subject: [PATCH 043/126] Update app/packages/core/src/components/Actions/ActionsRow.tsx Co-authored-by: manivoxel51 <109545780+manivoxel51@users.noreply.github.com> --- app/packages/core/src/components/Actions/ActionsRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/packages/core/src/components/Actions/ActionsRow.tsx b/app/packages/core/src/components/Actions/ActionsRow.tsx index 997d53488c..b0c850eed4 100644 --- a/app/packages/core/src/components/Actions/ActionsRow.tsx +++ b/app/packages/core/src/components/Actions/ActionsRow.tsx @@ -163,7 +163,7 @@ const Tag = ({ const labels = useRecoilValue(fos.selectedLabelIds); const samples = useRecoilValue(fos.selectedSamples); const canTag = useRecoilValue(fos.canTagSamplesOrLabels); - const disableTag = canTag.enabled !== true; + const disableTag = !canTag.enabled; const selected = labels.size > 0 || samples.size > 0; const tagging = useRecoilValue(fos.anyTagging); From 99dc2cf6958ee6d05edcd1546eb9e63bac7d7c90 Mon Sep 17 00:00:00 2001 From: Lanny W Date: Tue, 21 May 2024 12:55:02 -0500 Subject: [PATCH 044/126] minor fix --- app/packages/core/src/components/Actions/similar/Similar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/packages/core/src/components/Actions/similar/Similar.tsx b/app/packages/core/src/components/Actions/similar/Similar.tsx index 13078dc87c..d65977f170 100644 --- a/app/packages/core/src/components/Actions/similar/Similar.tsx +++ b/app/packages/core/src/components/Actions/similar/Similar.tsx @@ -94,7 +94,7 @@ const SortBySimilarity = ({ ); const isLoading = useRecoilValue(fos.similaritySorting); const canCreateNewField = useRecoilValue(fos.canCreateNewField); - const disabled = canCreateNewField.enabled !== true; + const disabled = !canCreateNewField.enabled; const disableMsg = canCreateNewField.message; useLayoutEffect(() => { From b8d28b30ab9d75cd42a50b2f18deb323b2c22870 Mon Sep 17 00:00:00 2001 From: imanjra Date: Tue, 21 May 2024 10:16:39 -0400 Subject: [PATCH 045/126] make contained button text color white by default --- .../components/src/components/MuiButton/index.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/packages/components/src/components/MuiButton/index.tsx b/app/packages/components/src/components/MuiButton/index.tsx index 6b824bcc7d..cf92d94cf5 100644 --- a/app/packages/components/src/components/MuiButton/index.tsx +++ b/app/packages/components/src/components/MuiButton/index.tsx @@ -2,7 +2,11 @@ import React from "react"; import { ButtonProps, CircularProgress, Button, Stack } from "@mui/material"; export default function MuiButton(props: ButtonPropsType) { - const { loading, ...otherProps } = props; + const { loading, variant, ...otherProps } = props; + + const containedStyles = + variant === "contained" ? { sx: { color: "white" } } : {}; + return ( -