Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(1->0) Add RecordingSession data structure and integrate with Labels #1260

Open
wants to merge 24 commits into
base: liezl/add-camera-group
Choose a base branch
from

Conversation

roomrys
Copy link
Collaborator

@roomrys roomrys commented Apr 5, 2023

Description

  • add ability to group sets of videos that are synchronized -- dropdown column in videos table? something else?
    • add RecordingSession data structure (and a few changes to existing Camcorder and CameraCluster data structures)

Types of changes

  • Bugfix
  • New feature
  • Refactor / Code style update (no logical changes)
  • Build / CI changes
  • Documentation Update
  • Other (explain)

Does this address any currently open issues?

[list open issues here]

Outside contributors checklist

  • Review the guidelines for contributing to this repository
  • Read and sign the CLA and add yourself to the authors list
  • Make sure you are making a pull request against the develop branch (not main). Also you should start your branch off develop
  • Add tests that prove your fix is effective or that your feature works
  • Add necessary documentation (if appropriate)

Thank you for contributing to SLEAP!

❤️

Summary by CodeRabbit

  • New Feature: Added a new menu item "Add Recording Session..." to the File menu, allowing users to add recording sessions to their projects.
  • New Feature: Introduced the RecordingSession class in sleap.io.cameras, enabling management of videos and cameras within a session.
  • Enhancement: Updated the Camcorder and CameraCluster classes in sleap.io.cameras for better interaction with RecordingSession.
  • New Feature: Added support for serialization and deserialization of RecordingSession objects in sleap.io.dataset and sleap.io.format.labels_json.
  • Test: Expanded test coverage for the new features and enhancements in tests/gui/test_commands.py, tests/io/test_cameras.py, and tests/io/test_dataset.py.

@codecov
Copy link

codecov bot commented Apr 5, 2023

Codecov Report

❗ No coverage uploaded for pull request base (liezl/add-camera-group@0230a97). Click here to learn what that means.
The diff coverage is n/a.

@@                    Coverage Diff                    @@
##             liezl/add-camera-group    #1260   +/-   ##
=========================================================
  Coverage                          ?   73.57%           
=========================================================
  Files                             ?      135           
  Lines                             ?    24318           
  Branches                          ?        0           
=========================================================
  Hits                              ?    17893           
  Misses                            ?     6425           
  Partials                          ?        0           

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@roomrys roomrys marked this pull request as ready for review April 7, 2023 16:09
* Add GUI/API for loading `RecordingSession` into `Labels`

* Add structure/unstructure method for `CameraCluster`

* Add test for (un)structuring `RecordingSession`

* Add `RecordingSession.make_cattr` method

* Add `RecordingSession` (un)structure method to `Labels` reader/writer
@roomrys roomrys changed the title Add RecordingSession data structure (enhance Camcorder and CameraCluster) Add RecordingSession data structure and integrate with Labels Apr 15, 2023
@roomrys roomrys requested a review from talmo April 18, 2023 22:23
@roomrys roomrys added the MultiView Stack This PR is part of the MultView stacked PRs. label Apr 20, 2023
@roomrys roomrys changed the title Add RecordingSession data structure and integrate with Labels (1 -> 0) Add RecordingSession data structure and integrate with Labels Jul 6, 2023
@roomrys roomrys changed the title (1 -> 0) Add RecordingSession data structure and integrate with Labels (1->0) Add RecordingSession data structure and integrate with Labels Jul 6, 2023
@coderabbitai
Copy link

coderabbitai bot commented Sep 26, 2023

Walkthrough

This pull request introduces significant enhancements to the SLEAP application, primarily focusing on adding support for recording sessions. It includes changes to the GUI, command structure, and data handling classes, along with corresponding updates to the test suite.

Changes

File(s) Summary
sleap/gui/app.py,
sleap/gui/commands.py
Added a new menu item "Add Recording Session..." to the GUI and implemented the associated command.
sleap/io/cameras.py Introduced the RecordingSession class and enhanced Camcorder and CameraCluster classes with session management capabilities.
sleap/io/dataset.py,
sleap/io/format/labels_json.py
Extended the Labels class to include recording sessions and updated serialization/deserialization processes accordingly.
sleap/util.py Added deep iterable conversion functionality.
tests/fixtures/cameras.py,
tests/gui/test_commands.py,
tests/io/test_cameras.py,
tests/io/test_dataset.py
Updated and added tests to verify the correct functioning of the new features and modifications.

🐇💻

Code hopping through the night,

Adding sessions, pure delight.

With each hop, a feature grows,

In the moonlight, our codebase glows. 🌙✨


Tips

Chat with CodeRabbit Bot (@coderabbitai)

  • Mention @coderabbitai in any review comment for bot assistance.
  • Note: Review comments are made on code diffs or files, not on the PR overview.

Pause Incremental Reviews

  • Insert @coderabbitai: ignore in the PR description to halt the bot's ongoing reviews. Remove the line to resume.
  • Additionally, enter @coderabbitai pause to halt the bot's ongoing reviews as any issue or review comment. To
    resume reviews, enter @coderabbitai resume as any issue or review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 8

Commits Files that changed from the base of the PR and between 587331f and 348a656.
Files selected for processing (10)
  • sleap/gui/app.py (1 hunks)
  • sleap/gui/commands.py (4 hunks)
  • sleap/io/cameras.py (4 hunks)
  • sleap/io/dataset.py (5 hunks)
  • sleap/io/format/labels_json.py (6 hunks)
  • sleap/util.py (2 hunks)
  • tests/fixtures/cameras.py (1 hunks)
  • tests/gui/test_commands.py (2 hunks)
  • tests/io/test_cameras.py (2 hunks)
  • tests/io/test_dataset.py (2 hunks)
Files skipped from review due to trivial changes (1)
  • tests/fixtures/cameras.py
Additional comments (Suppressed): 27
sleap/gui/app.py (1)
  • 467-476: The new menu item "Add Recording Session..." has been added to the File menu. This triggers the addSession command which allows users to load a RecordingSession from a file and add it to the project. Ensure that the addSession command is implemented correctly and handles all possible edge cases, such as invalid or non-existent files.
sleap/gui/commands.py (4)
  • 54-57: The import of RecordingSession from sleap.io.cameras is new and seems to be necessary for the added functionality of handling recording sessions. Ensure that this module and class are available in the project.

  • 434-439: A new method addSession has been added which triggers the AddSession command. This command is responsible for adding a new recording session to the project. The implementation looks correct.

  • 1939-1954: The AddSession class has been introduced to handle the addition of a new recording session. It uses the RecordingSession.load method to load a session from a file and then adds it to the labels context. If no session is currently loaded, it sets the newly loaded session as the current session. This logic seems sound, but ensure that the RecordingSession.load method handles errors appropriately, such as when the specified file does not exist or is not a valid session file.

  • 1956-1968: The ask method shows a GUI for adding a video to the project. It uses a FileDialog to allow the user to select a camera calibration file. The selected filename is stored in the params dictionary under the key "camera_calibration". This method returns True if a filename was selected and False otherwise. This seems like a reasonable approach, but make sure that the rest of your code can handle the case where ask returns False.

sleap/io/dataset.py (4)
  • 65-65: The import of RecordingSession from sleap.io.cameras is new. Ensure that the RecordingSession class is correctly implemented and tested in sleap.io.cameras.

  • 420-426: A new attribute sessions has been added to store RecordingSession objects. This change seems appropriate given the context provided.

  • 1929-1946: The serialization process now includes RecordingSession instances. Ensure that the make_cattr method in the RecordingSession class is correctly implemented and tested.

  • 1952-1958: The serialized data now includes sessions. This change aligns with the addition of the sessions attribute and its inclusion in the serialization process.

sleap/util.py (2)
  • 36-65: The _DeepIterableConverter class has been added. This class is a callable that performs deep conversion of an iterable. It takes two arguments: member_converter, which is a converter to apply to iterable members, and iterable_converter, which is an optional converter to apply to the iterable itself. The __call__ method applies the member_converter to each member of the iterable and then applies the iterable_converter to the entire iterable if it is not None. The __repr__ method returns a string representation of the converter.

  • 69-82: The deep_iterable_converter function has been added. This function creates a _DeepIterableConverter object with the given member_converter and iterable_converter. If member_converter is a list or tuple, it combines them using the and_ function from attrs.validators.

sleap/io/format/labels_json.py (5)
  • 7-13: The import of logging is new in this hunk. Ensure that it's used appropriately throughout the code and doesn't introduce unnecessary overhead or verbosity in the application logs.

  • 18-24: The import of RecordingSession from sleap.io.cameras is new. Make sure that the RecordingSession class is properly defined and tested, and that its usage in this file aligns with its intended purpose.

  • 503-514: Error handling has been added for loading RecordingSessions. This is a good practice as it prevents the application from crashing due to exceptions during the loading process. However, ensure that these exceptions are handled appropriately and not just logged and ignored, as this could lead to silent failures.

  • 542-547: A structure hook has been registered for RecordingSession. This is necessary for the correct deserialization of RecordingSession objects. Ensure that the lambda function correctly handles the conversion from dictionary to RecordingSession object.

  • 556-562: The sessions attribute has been added to the Labels object creation. Ensure that the Labels class has been updated to include this new attribute and handle it correctly in all relevant methods.

tests/gui/test_commands.py (1)
  • 932-958: The new test function test_AddSession is well-written and covers two important scenarios: when no session is selected and when a session is already selected. It checks that the AddSession command correctly adds a session to the labels and updates the current session in the context state. This test will help ensure that the new functionality for adding recording sessions works as expected.
tests/io/test_dataset.py (3)
  • 971-998: The test test_save_labels_with_sessions checks if the labels with sessions attribute can be saved and loaded correctly. It verifies that the loaded session is an instance of RecordingSession, not the same object in memory, and has the same number of cameras and videos as the original session. The test also checks if all cameras in the loaded session are equal to the corresponding cameras in the original session. This test seems to cover all necessary aspects of saving and loading labels with sessions.

  • 1000-1007: The test test_add_session checks if a RecordingSession can be added to a Labels object. It verifies that after adding the session, the sessions attribute of the labels contains the added session. This test is simple and straightforward.

  • 1010-1012: Ensure that the changes made in this PR do not affect the functionality tested by test_labels_hdf5. Run this test after making the changes to verify that it still passes.

tests/io/test_cameras.py (4)
  • 11-21: The test function test_camcorder has been updated to include two new parameters: min_session_session and centered_pair_vid. Ensure that these fixtures are correctly defined and provide the expected types of objects (RecordingSession and Video, respectively). The Camcorder object is now retrieved from the RecordingSession object instead of directly from a CameraCluster object. This change reflects the introduction of the RecordingSession class and its integration with the existing classes.

  • 36-52: New tests have been added to check the videos and sessions properties of the Camcorder class, as well as the __getitem__ method. These tests reflect the new functionality introduced in the Camcorder class to support the RecordingSession structure. Ensure that these methods behave as expected when interacting with Video and RecordingSession objects.

  • 54-123: The test_camera_cluster function has been significantly expanded to test new functionality related to the RecordingSession class. New tests include checking the sessions property and the add_session method, as well as the videos property and the __getitem__ method with different types of keys (Video, Camcorder, and RecordingSession). These tests validate the changes made to the CameraCluster class to support the RecordingSession structure. Ensure that these methods behave as expected when interacting with Video and RecordingSession objects.

  • 125-244: A new test function test_recording_session has been added to test the RecordingSession data structure. This function includes comprehensive tests for various methods and properties of the RecordingSession class, including loading from a calibration file, adding and removing videos, accessing linked and unlinked cameras, and serialization/deserialization. These tests validate the functionality of the RecordingSession class and its integration with the existing classes.

sleap/io/cameras.py (3)
  • 156-166: The cameras attribute of CameraCluster is defined with a validator and converter that ensure its elements are instances of Camcorder. This is good practice as it ensures data integrity for the cameras list.

  • 241-243: The get_videos_from_camcorder method raises a ValueError if the camcorder is not found in self.cameras. This is a good practice as it makes error handling more explicit.

  • 284-314: The __getitem__ method of CameraCluster is well-implemented with clear handling for different types of keys. It correctly raises a KeyError when the key is not found.

sleap/io/dataset.py Outdated Show resolved Hide resolved
tests/gui/test_commands.py Outdated Show resolved Hide resolved
sleap/io/cameras.py Show resolved Hide resolved
sleap/io/cameras.py Show resolved Hide resolved
sleap/io/cameras.py Outdated Show resolved Hide resolved
sleap/io/cameras.py Show resolved Hide resolved
sleap/io/cameras.py Show resolved Hide resolved
sleap/io/cameras.py Show resolved Hide resolved
@roomrys roomrys removed the request for review from talmo September 26, 2023 14:45
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 4

Commits Files that changed from the base of the PR and between 348a656 and 9c21530.
Files selected for processing (3)
  • sleap/io/cameras.py (4 hunks)
  • sleap/io/dataset.py (5 hunks)
  • tests/gui/test_commands.py (3 hunks)
Additional comments (Suppressed): 13
sleap/io/dataset.py (6)
  • 65-65: The import of RecordingSession from sleap.io.cameras is new. Ensure that the RecordingSession class has been properly implemented and tested in sleap.io.cameras.

  • 423-423: A new attribute sessions of type List[RecordingSession] has been added to the Labels class. This attribute will hold a list of recording sessions associated with the labels.

  • 1589-1600: A new method add_session has been added to the Labels class. This method takes a RecordingSession instance as an argument and adds it to the sessions list if it's not already present. The method checks if the input is an instance of RecordingSession, raising a TypeError if it's not. This is a good practice for ensuring data integrity.

  • 1937-1938: A new unstructure hook has been registered for RecordingSession instances. This hook converts a RecordingSession instance to its index in the sessions list. Make sure this conversion is appropriate for your serialization needs.

  • 1949-1950: A converter for RecordingSession instances is created using the make_cattr method of the RecordingSession class, passing the videos list as an argument. Ensure that the make_cattr method of RecordingSession handles this correctly.

  • 1959-1959: The sessions attribute is now included in the serialized output. This change ensures that RecordingSession instances associated with the Labels object are preserved when the object is serialized.

tests/gui/test_commands.py (3)
  • 11-14: The import statement has been updated to include the AddSession command from sleap.gui.commands. This is in line with the summary of the pull request which mentions the addition of a new command AddSession.

  • 23-23: The RecordingSession class from sleap.io.cameras is now being imported. This aligns with the PR summary that mentions the introduction of a new RecordingSession data structure.

  • 929-955: A new test function test_AddSession has been added. This test checks the functionality of adding a recording session to the labels. It verifies that the session is correctly added and selected in the context state, and that multiple sessions can be added. The test seems comprehensive and covers different scenarios (no session selected, session selected).

+def test_AddSession(
+    min_tracks_2node_labels: Labels,
+    min_session_calibration_toml_path: str,
+):
+    """Test that adding a session works."""
+    labels = min_tracks_2node_labels
+    camera_calibration = min_session_calibration_toml_path
+
+    # Set-up CommandContext
+    context: CommandContext = CommandContext.from_labels(labels)
+
+    # Case 1: No session selected
+    assert context.state["session"] is None
+    assert labels.sessions == []
+
+    params = {"camera_calibration": camera_calibration}
+    AddSession.do_action(context, params)
+    assert len(labels.sessions) == 1
+    session = labels.sessions[0]
+    assert context.state["session"] is session
+
+    # Case 2: Session selected
+    params = {"camera_calibration": camera_calibration}
+    AddSession.do_action(context, params)
+    assert len(labels.sessions) == 2
+    assert context.state["session"] is session
+    assert labels.sessions[1] is not session
sleap/io/cameras.py (4)
  • 57-58: The __eq__ method is incomplete, it only checks if the other object is an instance of Camcorder but does not compare any attributes. You should complete this method by comparing relevant attributes of the two objects.
- def __eq__(self, other):
-     if not isinstance(other, Camcorder):
+ def __eq__(self, other: Any) -> bool:
+     if not isinstance(other, Camcorder):
+         return False
+     return self.camera == other.camera and self.videos == other.videos
  • 141-148: The from_camera method raises a TypeError if the input is not a Camera instance. However, the method signature suggests that it can also accept a FisheyeCamera. You should update the check to include FisheyeCamera.
- if not isinstance(cam, Camera):
+ if not isinstance(cam, (Camera, FisheyeCamera)):
  • 202-207: The get_videos_from_session method returns None if the session is not found in self.sessions. This could potentially lead to issues if the return value is used without checking for None. Consider raising an exception instead to make error handling more explicit.
- return None
+ raise ValueError(f"RecordingSession {session} not found in {self}.")
  • 219-222: Similar to the previous comment, the get_session_from_video method returns None if the video is not found in self.videos. Consider raising an exception instead.
- return None
+ raise ValueError(f"Video {video} not found in any RecordingSession in {self}.")

Comment on lines 54 to 55
def __attrs_post_init__(self):
self.camera_cluster = CameraCluster()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The __attrs_post_init__ method in the Camcorder class is initializing the camera_cluster attribute with a new instance of CameraCluster. This could potentially overwrite any existing CameraCluster that was previously assigned to the Camcorder. If this is the intended behavior, then it's fine. However, if you want to preserve any existing CameraCluster, you should check if camera_cluster is None before assigning a new CameraCluster.

def __attrs_post_init__(self):
-    self.camera_cluster = CameraCluster()
+    if self.camera_cluster is None:
+        self.camera_cluster = CameraCluster()

sleap/io/cameras.py Show resolved Hide resolved
sleap/io/cameras.py Show resolved Hide resolved
sleap/io/cameras.py Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 1

Commits Files that changed from the base of the PR and between 9c21530 and 99d3228.
Files selected for processing (3)
  • sleap/io/cameras.py (4 hunks)
  • tests/gui/test_commands.py (3 hunks)
  • tests/io/test_cameras.py (2 hunks)
Files skipped from review due to trivial changes (1)
  • tests/gui/test_commands.py
Additional comments (Suppressed): 9
sleap/io/cameras.py (5)
  • 111-112: The __repr__ method is referencing name and size attributes which are not defined in the Camcorder class. This will raise an AttributeError when __repr__ is called. You should update this to reference the actual attributes of the Camcorder class.
-     return f"{self.__class__.__name__}(name={self.name}, size={self.size})"
+     return f"{self.__class__.__name__}(camera={self.camera}, videos={len(self.videos)})"
  • 143-150: The from_camera method raises a TypeError if the input is not a Camera instance. However, the method signature suggests that it can also accept a FisheyeCamera. You should update the check to include FisheyeCamera.
-        if not isinstance(cam, Camera):
+        if not isinstance(cam, (Camera, FisheyeCamera)):
  • 204-209: The get_videos_from_session method returns None if the session is not found in self.sessions. This could potentially lead to issues if the return value is used without checking for None. Consider raising an exception instead to make error handling more explicit.
-            return None
+            raise ValueError(f"RecordingSession {session} not found in {self}.")
  • 221-224: Similar to the previous comment, the get_session_from_video method returns None if the video is not found in self.videos. Consider raising an exception instead.
-            return None
+            raise ValueError(f"Video {video} not found in any RecordingSession in {self}.")
  • 261-265: The __attrs_post_init__ method in the CameraCluster class is initializing the cameras attribute with self.cameras. This seems redundant as self.cameras is already being passed to the superclass's __init__ method. If there's no additional processing needed for self.cameras, you can remove this line.
def __attrs_post_init__(self):
    """Initialize `CameraCluster` object."""
-   super().__init__(cameras=self.cameras, metadata=self.metadata)
+   super().__init__(metadata=self.metadata)
    for cam in self.cameras:
        cam.camera_cluster = self
tests/io/test_cameras.py (4)
  • 10-20: The test_camcorder function has been updated to include testing of the new RecordingSession class. The test now includes a RecordingSession and Video object as parameters, which are used to test the Camcorder data structure's interaction with these new objects. This is a good addition as it ensures that the Camcorder class works correctly with the new RecordingSession class.

  • 38-50: The tests for the videos and sessions properties, as well as the __getitem__ method, have been added to the test_camcorder function. These tests ensure that videos can be added to a Camcorder object through a RecordingSession, and that the Camcorder object correctly maintains references to its associated sessions and videos. The __getitem__ method is tested with both valid and invalid keys, ensuring that it behaves as expected in both scenarios.

  • 53-122: The test_camera_cluster function has been significantly expanded to test the new functionality related to the RecordingSession class. It now tests the sessions property, the add_session method, the videos property, and the __getitem__ method with various types of keys. It also tests the to_calibration_dict and from_calibration_dict methods, ensuring that they correctly serialize and deserialize the state of the CameraCluster. All these additions are important for verifying the correct behavior of the CameraCluster class when interacting with RecordingSession objects.

  • 124-243: A new test function, test_recording_session, has been added to test the RecordingSession data structure. This function tests all aspects of the RecordingSession class, including loading from a calibration file, accessing metadata, adding videos, removing videos, and serialization/deserialization. This is a comprehensive test that ensures the RecordingSession class behaves as expected in various scenarios.

sleap/io/cameras.py Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 0

Commits Files that changed from the base of the PR and between 99d3228 and 48d415d.
Files selected for processing (1)
  • sleap/io/cameras.py (4 hunks)
Additional comments (Suppressed): 8
sleap/io/cameras.py (8)
  • 54-58: The __attrs_post_init__ method in the Camcorder class is still initializing the camera_cluster attribute with a new instance of CameraCluster if it's not already an instance of CameraCluster. This could potentially overwrite any existing value of camera_cluster. Consider using a lazy initialization approach as suggested in previous comments.

  • 60-60: The __eq__ method is still incomplete. It only checks if the other object is an instance of Camcorder but does not compare any attributes. You should complete this method by comparing relevant attributes of the two objects.

  • 108-109: The __hash__ method is currently only considering the camera attribute for generating the hash. If two Camcorder instances have the same camera but different videos, they will still have the same hash. However, based on the previous discussion, if the Camera attribute is the primary identifier for a Camcorder, then it makes sense to only include that in the hash computation.

  • 111-112: The __repr__ method is referencing name and size attributes which are not defined in the Camcorder class. This will raise an AttributeError when __repr__ is called. You should update this to reference the actual attributes of the Camcorder class. Based on the previous discussion, if name and size are attributes that a Camcorder should have, I would recommend adding them explicitly to the Camcorder class. If they are attributes of specific types of camera objects, then consider checking for these attributes in the __repr__ method and handle cases when they are not present.

  • 143-150: The from_camera method raises a TypeError if the input is not a Camera instance. However, the method signature suggests that it can also accept a FisheyeCamera. You should update the check to include FisheyeCamera. Based on the previous discussion, since FisheyeCamera is a subclass of Camera, the isinstance(cam, Camera) check will return True for both Camera and FisheyeCamera instances. Therefore, the current implementation is correct and doesn't need to be changed.

  • 204-209: The get_videos_from_session method returns None if the session is not found in self.sessions. This could potentially lead to issues if the return value is used without checking for None. Consider raising an exception instead to make error handling more explicit.

  • 221-224: Similar to the previous comment, the get_session_from_video method returns None if the video is not found in self.videos. Consider raising an exception instead.

  • 261-265: The __attrs_post_init__ method in the CameraCluster class is initializing the cameras attribute with self.cameras. This seems redundant as self.cameras is already being passed to the superclass's __init__ method. If there's no additional processing needed for self.cameras, you can remove this line.

@coderabbitai
Copy link

coderabbitai bot commented Oct 19, 2023

CodeRabbit review skipped

By default, CodeRabbit only reviews PRs on the default branch. If you wish to have PRs reviewed on additional branches, you can configure that under the repository settings in the UI.

@roomrys roomrys self-assigned this Jan 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
MultiView Stack This PR is part of the MultView stacked PRs.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant