Repository: rerun-io/rerun
Pull request #9478: Support `#datapath` fragment on redap URL
Author: login: emilk
Labels: name: enhancement
description: New feature or request
name: exclude from changelog
description: PRs with this won't show up in CHANGELOG.md
name: dataplatform
description: Rerun Data Platform integration
Created: 2025-04-03T13:51:45Z | Merged: 2025-04-03T17:37:00Z

This adds support for appending a data path (e.g. `/some/entity[#42]`) to a redap URL, as a `#fragment`.

When clicking a redap URI with such a fragment, we will focus on that entity.
This is untested, however, and depends on us keeping the focus until the entity is loaded.

A `DataPath` is currently defined as

```rs
pub struct DataPath {
    pub entity_path: EntityPath,
    pub instance: Option<Instance>,
    pub component_name: Option<ComponentName>,
}
```

We will wanna extend this in the future to support tagging, and maybe even a timepoint.

### Extending it with time point

It's not clear how to specify _both_ time _and_ a data path in the fragment 🤔  I guess we need to use a special character, like comma:

`#/entity/path,log_tick=42` maybe

…and then comes the trouble of escaping


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 88581d9752c4..7f30de26190a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7800,6 +7800,7 @@ dependencies = [
 name = "re_uri"
 version = "0.23.0-alpha.1+dev"
 dependencies = [
+ "re_log",
  "re_log_types",
  "re_tuid",
  "serde",
diff --git a/crates/utils/re_uri/Cargo.toml b/crates/utils/re_uri/Cargo.toml
index d32ae9283da5..c86ec397b149 100644
--- a/crates/utils/re_uri/Cargo.toml
+++ b/crates/utils/re_uri/Cargo.toml
@@ -15,6 +15,7 @@ version.workspace = true
 workspace = true
 
 [dependencies]
+re_log.workspace = true
 re_log_types = { workspace = true, features = ["serde"] }
 re_tuid.workspace = true
 
diff --git a/crates/utils/re_uri/src/endpoints/dataset.rs b/crates/utils/re_uri/src/endpoints/dataset.rs
index 68cec5116856..27a6c35a7bcc 100644
--- a/crates/utils/re_uri/src/endpoints/dataset.rs
+++ b/crates/utils/re_uri/src/endpoints/dataset.rs
@@ -1,4 +1,4 @@
-use crate::{Origin, RedapUri, TimeRange};
+use crate::{Error, Fragment, Origin, RedapUri, TimeRange};
 
 //TODO(ab): add `DatasetTableEndpoint`, the URI pointing at the "table view" of the dataset (aka. its partition table).
 
@@ -7,15 +7,20 @@ use crate::{Origin, RedapUri, TimeRange};
 /// Currently, the following format is supported:
 /// `<origin>/dataset/$DATASET_ID/data?partition_id=$PARTITION_ID&time_range=$TIME_RANGE`
 ///
-/// `partition_id` is mandatory, and `time_range` is optional. In the future, it will be extended to
-/// richer queries.
+/// `partition_id` is currently mandatory, and `time_range` is optional.
+/// In the future we will add richer queries.
 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
 pub struct DatasetDataEndpoint {
     pub origin: Origin,
     pub dataset_id: re_tuid::Tuid,
 
+    // Query parameters: these affect what data is returned.
+    /// Currently mandatory.
     pub partition_id: String,
     pub time_range: Option<TimeRange>,
+
+    // Fragment parameters: these affect what the viewer focuses on:
+    pub fragment: Fragment,
 }
 
 impl std::fmt::Display for DatasetDataEndpoint {
@@ -25,62 +30,91 @@ impl std::fmt::Display for DatasetDataEndpoint {
             dataset_id,
             partition_id,
             time_range,
+            fragment,
         } = self;
 
         write!(f, "{origin}/dataset/{dataset_id}")?;
 
-        // query (for now, partition_id is the only supported one and is mandatory)
+        // ?query:
         {
             write!(f, "?partition_id={partition_id}")?;
         }
-
         if let Some(time_range) = time_range {
             write!(f, "&time_range={time_range}")?;
         }
 
+        // #fragment:
+        let fragment = fragment.to_string();
+        if !fragment.is_empty() {
+            write!(f, "#{fragment}")?;
+        }
+
         Ok(())
     }
 }
 
 impl DatasetDataEndpoint {
-    pub fn new(
-        origin: Origin,
-        dataset_id: re_tuid::Tuid,
-        partition_id: String,
-        time_range: Option<TimeRange>,
-    ) -> Self {
-        Self {
+    pub fn new(origin: Origin, dataset_id: re_tuid::Tuid, url: &url::Url) -> Result<Self, Error> {
+        let mut partition_id = None;
+        let mut time_range = None;
+
+        for (key, value) in url.query_pairs() {
+            match key.as_ref() {
+                "partition_id" => {
+                    partition_id = Some(value.to_string());
+                }
+                "time_range" => {
+                    time_range = Some(value.parse::<TimeRange>()?);
+                }
+                _ => {
+                    re_log::warn_once!("Unknown query parameter: {key}={value}");
+                }
+            }
+        }
+
+        let Some(partition_id) = partition_id else {
+            return Err(Error::MissingPartitionId);
+        };
+
+        let mut fragment = Fragment::default();
+        if let Some(string) = url.fragment() {
+            fragment = Fragment::parse_forgiving(string);
+        }
+
+        Ok(Self {
             origin,
             dataset_id,
             partition_id,
             time_range,
-        }
+            fragment,
+        })
     }
 
-    /// Returns a [`DatasetDataEndpoint`] without the optional query part.
-    pub fn without_query(&self) -> std::borrow::Cow<'_, Self> {
-        let mut cow = std::borrow::Cow::Borrowed(self);
+    /// Returns a [`DatasetDataEndpoint`] without any (optional) `?query` or `#fragment`.
+    pub fn without_query_and_fragment(mut self) -> Self {
+        let Self {
+            origin: _,       // Mandatory
+            dataset_id: _,   // Mandatory
+            partition_id: _, // Mandatory
+            time_range,
+            fragment,
+        } = &mut self;
 
-        if self.time_range.is_some() {
-            cow.to_mut().time_range = None;
-        }
+        *time_range = None;
+        *fragment = Default::default();
 
-        cow
+        self
     }
 }
 
 impl std::str::FromStr for DatasetDataEndpoint {
-    type Err = crate::Error;
+    type Err = Error;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         match RedapUri::from_str(s)? {
             RedapUri::DatasetData(endpoint) => Ok(endpoint),
-            RedapUri::Catalog(endpoint) => {
-                Err(crate::Error::UnexpectedEndpoint(format!("/{endpoint}")))
-            }
-            RedapUri::Proxy(endpoint) => {
-                Err(crate::Error::UnexpectedEndpoint(format!("/{endpoint}")))
-            }
+            RedapUri::Catalog(endpoint) => Err(Error::UnexpectedEndpoint(format!("/{endpoint}"))),
+            RedapUri::Proxy(endpoint) => Err(Error::UnexpectedEndpoint(format!("/{endpoint}"))),
         }
     }
 }
diff --git a/crates/utils/re_uri/src/fragment.rs b/crates/utils/re_uri/src/fragment.rs
new file mode 100644
index 000000000000..fa5e5f5ccce4
--- /dev/null
+++ b/crates/utils/re_uri/src/fragment.rs
@@ -0,0 +1,39 @@
+use re_log_types::DataPath;
+
+/// We use the `#fragment` of the URI to point to a specific entity.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
+pub struct Fragment {
+    pub data_path: Option<DataPath>,
+}
+
+impl std::fmt::Display for Fragment {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let Self { data_path } = self;
+
+        if let Some(data_path) = data_path {
+            write!(f, "{data_path}")?;
+        }
+
+        Ok(())
+    }
+}
+
+impl Fragment {
+    /// Parse fragment, excluding hash
+    pub fn parse_forgiving(fragment: &str) -> Self {
+        let mut data_path = None;
+
+        match fragment.parse::<DataPath>() {
+            Ok(path) => {
+                data_path = Some(path);
+            }
+            Err(err) => {
+                re_log::warn_once!(
+                    "Failed to parse URL fragment '#{fragment}`: {err} (expected a data path)"
+                );
+            }
+        }
+
+        Self { data_path }
+    }
+}
diff --git a/crates/utils/re_uri/src/lib.rs b/crates/utils/re_uri/src/lib.rs
index 5d93992ccda8..e4e949362107 100644
--- a/crates/utils/re_uri/src/lib.rs
+++ b/crates/utils/re_uri/src/lib.rs
@@ -35,6 +35,7 @@
 
 mod endpoints;
 mod error;
+mod fragment;
 mod origin;
 mod redap_uri;
 mod scheme;
@@ -43,6 +44,7 @@ mod time_range;
 pub use self::{
     endpoints::{catalog::CatalogEndpoint, dataset::DatasetDataEndpoint, proxy::ProxyEndpoint},
     error::Error,
+    fragment::Fragment,
     origin::Origin,
     redap_uri::RedapUri,
     scheme::Scheme,
diff --git a/crates/utils/re_uri/src/redap_uri.rs b/crates/utils/re_uri/src/redap_uri.rs
index 633ac8e18485..2d1b84676fef 100644
--- a/crates/utils/re_uri/src/redap_uri.rs
+++ b/crates/utils/re_uri/src/redap_uri.rs
@@ -1,4 +1,4 @@
-use crate::{CatalogEndpoint, DatasetDataEndpoint, Error, Origin, ProxyEndpoint, TimeRange};
+use crate::{CatalogEndpoint, DatasetDataEndpoint, Error, Fragment, Origin, ProxyEndpoint};
 
 /// Parsed from `rerun://addr:port/recording/12345` or `rerun://addr:port/catalog`
 #[derive(Debug, PartialEq, Eq, Clone, Hash)]
@@ -11,6 +11,16 @@ pub enum RedapUri {
     Proxy(ProxyEndpoint),
 }
 
+impl RedapUri {
+    /// Return the parsed `#fragment` of the URI, if any.
+    pub fn fragment(&self) -> Option<&Fragment> {
+        match self {
+            Self::Catalog(_) | Self::Proxy(_) => None,
+            Self::DatasetData(dataset_data_endpoint) => Some(&dataset_data_endpoint.fragment),
+        }
+    }
+}
+
 impl std::fmt::Display for RedapUri {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
@@ -36,11 +46,6 @@ impl std::str::FromStr for RedapUri {
             .filter(|s| !s.is_empty()) // handle trailing slashes
             .collect::<Vec<_>>();
 
-        let time_range = http_url
-            .query_pairs()
-            .find(|(key, _)| key == TimeRange::QUERY_KEY)
-            .map(|(_, value)| TimeRange::from_str(value.as_ref()));
-
         match segments.as_slice() {
             ["proxy"] => Ok(Self::Proxy(ProxyEndpoint::new(origin))),
 
@@ -49,19 +54,7 @@ impl std::str::FromStr for RedapUri {
             ["dataset", dataset_id] => {
                 let dataset_id = re_tuid::Tuid::from_str(dataset_id).map_err(Error::InvalidTuid)?;
 
-                let partition_id = http_url
-                    .query_pairs()
-                    .find(|(key, _)| key == "partition_id")
-                    .ok_or(Error::MissingPartitionId)?
-                    .1
-                    .into_owned();
-
-                Ok(Self::DatasetData(DatasetDataEndpoint::new(
-                    origin,
-                    dataset_id,
-                    partition_id,
-                    time_range.transpose()?,
-                )))
+                DatasetDataEndpoint::new(origin, dataset_id, &http_url).map(Self::DatasetData)
             }
             [unknown, ..] => Err(Error::UnexpectedEndpoint(format!("{unknown}/"))),
         }
@@ -96,7 +89,9 @@ impl<'de> serde::Deserialize<'de> for RedapUri {
 #[cfg(test)]
 mod tests {
 
-    use crate::Scheme;
+    use re_log_types::DataPath;
+
+    use crate::{Fragment, Scheme, TimeRange};
 
     use super::*;
     use core::net::Ipv4Addr;
@@ -143,6 +138,36 @@ mod tests {
             dataset_id,
             partition_id,
             time_range,
+            fragment,
+        }) = address
+        else {
+            panic!("Expected recording");
+        };
+
+        assert_eq!(origin.scheme, Scheme::Rerun);
+        assert_eq!(origin.host, url::Host::<String>::Ipv4(Ipv4Addr::LOCALHOST));
+        assert_eq!(origin.port, 1234);
+        assert_eq!(
+            dataset_id,
+            "1830B33B45B963E7774455beb91701ae".parse().unwrap(),
+        );
+        assert_eq!(partition_id, "pid");
+        assert_eq!(time_range, None);
+        assert_eq!(fragment, Default::default());
+    }
+
+    #[test]
+    fn test_dataset_data_url_with_fragment() {
+        let url =
+            "rerun://127.0.0.1:1234/dataset/1830B33B45B963E7774455beb91701ae/data?partition_id=pid#/some/entity[#42]";
+        let address: RedapUri = url.parse().unwrap();
+
+        let RedapUri::DatasetData(DatasetDataEndpoint {
+            origin,
+            dataset_id,
+            partition_id,
+            time_range,
+            fragment,
         }) = address
         else {
             panic!("Expected recording");
@@ -157,6 +182,16 @@ mod tests {
         );
         assert_eq!(partition_id, "pid");
         assert_eq!(time_range, None);
+        assert_eq!(
+            fragment,
+            Fragment {
+                data_path: Some(DataPath {
+                    entity_path: "/some/entity".into(),
+                    instance: Some(42.into()),
+                    component_name: None,
+                })
+            }
+        );
     }
 
     #[test]
@@ -169,6 +204,7 @@ mod tests {
             dataset_id,
             partition_id,
             time_range,
+            fragment,
         }) = address
         else {
             panic!("Expected recording");
@@ -192,6 +228,7 @@ mod tests {
                 )
             })
         );
+        assert_eq!(fragment, Default::default());
     }
 
     #[test]
@@ -204,6 +241,7 @@ mod tests {
             dataset_id,
             partition_id,
             time_range,
+            fragment,
         }) = address
         else {
             panic!("Expected recording");
@@ -231,6 +269,7 @@ mod tests {
                 )
             })
         );
+        assert_eq!(fragment, Default::default());
     }
 
     #[test]
@@ -246,6 +285,7 @@ mod tests {
                 dataset_id,
                 partition_id,
                 time_range,
+                fragment,
             }) = address
             else {
                 panic!("Expected recording");
@@ -269,6 +309,7 @@ mod tests {
                     )
                 })
             );
+            assert_eq!(fragment, Default::default());
         }
     }
 
diff --git a/crates/utils/re_uri/src/time_range.rs b/crates/utils/re_uri/src/time_range.rs
index 8aed940fb430..24315a91500a 100644
--- a/crates/utils/re_uri/src/time_range.rs
+++ b/crates/utils/re_uri/src/time_range.rs
@@ -8,10 +8,6 @@ pub struct TimeRange {
     pub range: re_log_types::ResolvedTimeRangeF,
 }
 
-impl TimeRange {
-    pub(crate) const QUERY_KEY: &'static str = "time_range";
-}
-
 impl std::fmt::Display for TimeRange {
     /// Used for formatting time ranges in URLs
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
diff --git a/crates/viewer/re_viewer/src/app.rs b/crates/viewer/re_viewer/src/app.rs
index 635861604a1a..830d7417a734 100644
--- a/crates/viewer/re_viewer/src/app.rs
+++ b/crates/viewer/re_viewer/src/app.rs
@@ -565,7 +565,8 @@ impl App {
                 self.state.display_mode = display_mode;
             }
             SystemCommand::AddRedapServer { endpoint } => {
-                self.state.redap_servers.add_server(endpoint.origin);
+                let re_uri::CatalogEndpoint { origin } = endpoint;
+                self.state.redap_servers.add_server(origin);
                 self.command_sender.send_ui(UICommand::ExpandBlueprintPanel);
             }
 
diff --git a/crates/viewer/re_viewer/src/app_state.rs b/crates/viewer/re_viewer/src/app_state.rs
index 7b3f6b1e0bb5..74c1d74567ce 100644
--- a/crates/viewer/re_viewer/src/app_state.rs
+++ b/crates/viewer/re_viewer/src/app_state.rs
@@ -3,7 +3,7 @@ use egui::{text_selection::LabelSelectionState, NumExt as _};
 
 use re_chunk::TimelineName;
 use re_chunk_store::LatestAtQuery;
-use re_entity_db::EntityDb;
+use re_entity_db::{EntityDb, InstancePath};
 use re_log_types::{LogMsg, ResolvedTimeRangeF, StoreId};
 use re_redap_browser::RedapServers;
 use re_smart_channel::ReceiveSet;
@@ -11,9 +11,10 @@ use re_types::blueprint::components::PanelState;
 use re_ui::{ContextExt as _, DesignTokens};
 use re_viewer_context::{
     AppOptions, ApplicationSelectionState, BlueprintUndoState, CommandSender, ComponentUiRegistry,
-    DisplayMode, DragAndDropManager, GlobalContext, PlayState, RecordingConfig, SelectionChange,
-    StorageContext, StoreContext, StoreHub, SystemCommand, SystemCommandSender as _, TableContext,
-    ViewClassExt as _, ViewClassRegistry, ViewStates, ViewerContext,
+    DisplayMode, DragAndDropManager, GlobalContext, Item, PlayState, RecordingConfig,
+    SelectionChange, StorageContext, StoreContext, StoreHub, SystemCommand,
+    SystemCommandSender as _, TableContext, ViewClassExt as _, ViewClassRegistry, ViewStates,
+    ViewerContext,
 };
 use re_viewport::ViewportUi;
 use re_viewport_blueprint::ui::add_view_or_container_modal_ui;
@@ -80,7 +81,7 @@ pub struct AppState {
     /// The focused item is cleared every frame, but views may react with side-effects
     /// that last several frames.
     #[serde(skip)]
-    pub(crate) focused_item: Option<re_viewer_context::Item>,
+    pub(crate) focused_item: Option<Item>,
 }
 
 impl Default for AppState {
@@ -210,7 +211,7 @@ impl AppState {
 
         let selection_change = selection_state.on_frame_start(
             |item| {
-                if let re_viewer_context::Item::StoreId(store_id) = item {
+                if let Item::StoreId(store_id) = item {
                     if store_id.is_empty_recording() {
                         return false;
                     }
@@ -218,9 +219,7 @@ impl AppState {
 
                 viewport_ui.blueprint.is_item_valid(storage_context, item)
             },
-            Some(re_viewer_context::Item::StoreId(
-                store_context.recording.store_id().clone(),
-            )),
+            Some(Item::StoreId(store_context.recording.store_id().clone())),
         );
 
         if let SelectionChange::SelectionChanged(selection) = selection_change {
@@ -230,9 +229,8 @@ impl AppState {
         }
 
         // The root container cannot be dragged.
-        let drag_and_drop_manager = DragAndDropManager::new(re_viewer_context::Item::Container(
-            viewport_ui.blueprint.root_container,
-        ));
+        let drag_and_drop_manager =
+            DragAndDropManager::new(Item::Container(viewport_ui.blueprint.root_container));
 
         let recording = store_context.recording;
 
@@ -881,6 +879,7 @@ fn check_for_clicked_hyperlinks(ctx: &ViewerContext<'_>) {
     let recording_scheme = "recording://";
 
     let mut recording_path = None;
+    let mut fragment = None;
 
     ctx.egui_ctx().output_mut(|o| {
         o.commands.retain_mut(|command| {
@@ -893,6 +892,10 @@ fn check_for_clicked_hyperlinks(ctx: &ViewerContext<'_>) {
                         open_url.url.clone(),
                     );
 
+                    if let re_data_source::DataSource::RerunGrpcStream(redap_uri) = &data_source {
+                        fragment = redap_uri.fragment().cloned();
+                    }
+
                     let command_sender = ctx.command_sender().clone();
                     let on_cmd = Box::new(move |cmd| match cmd {
                         re_data_source::DataSourceCommand::SetLoopSelection {
@@ -940,7 +943,7 @@ fn check_for_clicked_hyperlinks(ctx: &ViewerContext<'_>) {
     });
 
     if let Some(path) = recording_path {
-        match path.parse::<re_viewer_context::Item>() {
+        match path.parse::<Item>() {
             Ok(item) => {
                 ctx.selection_state.set_selection(item);
             }
@@ -949,6 +952,30 @@ fn check_for_clicked_hyperlinks(ctx: &ViewerContext<'_>) {
             }
         }
     }
+
+    // Focus on a specific thing:
+    let re_uri::Fragment { data_path } = fragment.unwrap_or_default();
+    if let Some(data_path) = data_path {
+        let re_log_types::DataPath {
+            entity_path,
+            instance,
+            component_name,
+        } = data_path;
+
+        let item = if let Some(component_name) = component_name {
+            Item::from(re_log_types::ComponentPath::new(
+                entity_path,
+                component_name,
+            ))
+        } else if let Some(instance) = instance {
+            Item::from(InstancePath::instance(entity_path, instance))
+        } else {
+            Item::from(entity_path)
+        };
+
+        ctx.command_sender()
+            .send_system(SystemCommand::SetFocus(item));
+    }
 }
 
 pub fn default_blueprint_panel_width(screen_width: f32) -> f32 {
diff --git a/crates/viewer/re_viewer/src/ui/top_panel.rs b/crates/viewer/re_viewer/src/ui/top_panel.rs
index 1ba21598e2f9..146d4dfaaf60 100644
--- a/crates/viewer/re_viewer/src/ui/top_panel.rs
+++ b/crates/viewer/re_viewer/src/ui/top_panel.rs
@@ -226,7 +226,10 @@ fn connection_status_ui(ui: &mut egui::Ui, rx: &ReceiveSet<re_log_types::LogMsg>
                 format!("Waiting for data on {url}…")
             }
             re_smart_channel::SmartChannelSource::RedapGrpcStream(endpoint) => {
-                format!("Waiting for data on {}…", endpoint.without_query())
+                format!(
+                    "Waiting for data on {}…",
+                    endpoint.clone().without_query_and_fragment()
+                )
             }
             re_smart_channel::SmartChannelSource::RrdWebEventListener
             | re_smart_channel::SmartChannelSource::JsChannel { .. } => {
diff --git a/scripts/lint.py b/scripts/lint.py
index a6ccc3d465b1..ba48ddc1a841 100755
--- a/scripts/lint.py
+++ b/scripts/lint.py
@@ -249,11 +249,6 @@ def lint_line(
             if not app_id.startswith("rerun_example_") and not app_id == "<your_app_name>":
                 return f"All examples should have an app_id starting with 'rerun_example_'. Found '{app_id}'"
 
-    # Methods that return Self should usually be marked #[inline] or #[inline(always)] since they indicate a builder.
-    if re.search(r"\(mut self.*-> Self", line):
-        if prev_line_stripped != "#[inline]" and prev_line_stripped != "#[inline(always)]":
-            return "Builder methods impls should be marked #[inline]"
-
     # Deref impls should be marked #[inline] or #[inline(always)].
     if "fn deref(&self)" in line or "fn deref_mut(&mut self)" in line:
         if prev_line_stripped != "#[inline]" and prev_line_stripped != "#[inline(always)]":
@@ -406,7 +401,6 @@ def test_lint_line() -> None:
         'rr.script_setup(args, "missing_prefix")',
         'rr.script_setup(args, "")',
         "I accidentally wrote the same same word twice",
-        "fn foo(mut self) -> Self {",
         "fn deref(&self) -> Self::Target {",
         "fn deref_mut(&mut self) -> &mut Self::Target",
         "fn borrow(&self) -> &Self",


Reviews:
user: login: Wumpf
body: >  depends on us keeping the focus until the entity is loaded.

that's a bit of a problem 🤔
We need to find a way to delay that. Iirc we kill of invalid selections, so the selection will be lost immediately if the target in question isn't there yet

Would have been nice to always support that fragment, even when dealing with non datasetdata urls (i.e. with the "classic" url parameter on the web viewer). But I'm too much out of the loop there to understand how hard that would be.

looks very promising otherwise, let's ship it and iterate as needed
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-03T16:57:09Z
user: login: emilk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T17:36:18Z

Review comments:
diff_hunk: @@ -7,15 +7,20 @@ use crate::{Origin, RedapUri, TimeRange};
 /// Currently, the following format is supported:
 /// `<origin>/dataset/$DATASET_ID/data?partition_id=$PARTITION_ID&time_range=$TIME_RANGE`
 ///
-/// `partition_id` is mandatory, and `time_range` is optional. In the future, it will be extended to
-/// richer queries.
+/// `partition_id` is currnelty mandatory, and `time_range` is optional.
path: crates/utils/re_uri/src/endpoints/dataset.rs
user: login: Wumpf
body: ```suggestion
/// `partition_id` is currently mandatory, and `time_range` is optional.
```
created_at: 2025-04-03T16:49:00Z
author_association: MEMBER
diff_hunk: @@ -249,11 +249,6 @@ def lint_line(
             if not app_id.startswith("rerun_example_") and not app_id == "<your_app_name>":
                 return f"All examples should have an app_id starting with 'rerun_example_'. Found '{app_id}'"
 
-    # Methods that return Self should usually be marked #[inline] or #[inline(always)] since they indicate a builder.
-    if re.search(r"\(mut self.*-> Self", line):
-        if prev_line_stripped != "#[inline]" and prev_line_stripped != "#[inline(always)]":
-            return "Builder methods impls should be marked #[inline]"
path: scripts/lint.py
user: login: Wumpf
body: that wasn't a bad lint though, no? could just do `NOLINT` on `without_query_and_fragment` 🤷 
created_at: 2025-04-03T16:54:57Z
author_association: MEMBER
diff_hunk: @@ -249,11 +249,6 @@ def lint_line(
             if not app_id.startswith("rerun_example_") and not app_id == "<your_app_name>":
                 return f"All examples should have an app_id starting with 'rerun_example_'. Found '{app_id}'"
 
-    # Methods that return Self should usually be marked #[inline] or #[inline(always)] since they indicate a builder.
-    if re.search(r"\(mut self.*-> Self", line):
-        if prev_line_stripped != "#[inline]" and prev_line_stripped != "#[inline(always)]":
-            return "Builder methods impls should be marked #[inline]"
path: scripts/lint.py
user: login: emilk
body: I added it back before rustc did auto-inlining of trivial functions. It _should_ not be needed anymore
created_at: 2025-04-03T17:36:18Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-03T13:52:12Z
body: <!-- add-pr-comment:web-viewer-build-status -->

Web viewer built successfully. If applicable, you should also test it:

- [ ] I have tested the web viewer

| Result | Commit  | Link | Manifest |
| ------ | ------- | ---- | -------- |
| ✅ | 78c5506 | https://rerun.io/viewer/pr/9478 | [`+nightly`](https://rerun.io/viewer/pr/9478?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json) [`+main`](https://rerun.io/viewer/pr/9478?manifest_url=https://app.rerun.io/version/main/examples_manifest.json) |

<sup>Note: This comment is updated whenever you push a commit.</sup>
author_association: CONTRIBUTOR
user: login: emilk
created_at: 2025-04-07T07:43:29Z
body: > > depends on us keeping the focus until the entity is loaded.
> 
> that's a bit of a problem 🤔 We need to find a way to delay that. Iirc we kill of invalid selections, so the selection will be lost immediately if the target in question isn't there yet

This is a somewhat recurring problem, where state changes arrives out-of-order with the data they pertain to (e.g. opening a view with no contents, switching to a non-existing blueprint, opening a non-existing recording, etc). I think we need to take a robust approach to this, where out-of-orderness is fine, and e.g. "focus on this entity" will happen on the first frame we have that entity.

> Would have been nice to always support that fragment, even when dealing with non datasetdata urls (i.e. with the "classic" url parameter on the web viewer).

I agree, but I only got so far in this PR
author_association: MEMBER
user: login: emilk
created_at: 2025-04-07T20:16:18Z
body: * This is improved in https://github.com/rerun-io/rerun/pull/9535
author_association: MEMBER