Skip to content

Commit 6c651ab

Browse files
authored
Mark valid data ranges in timeline when loading data via range-limited URL (#11340)
1 parent 865e2fe commit 6c651ab

File tree

27 files changed

+640
-138
lines changed

27 files changed

+640
-138
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11541,6 +11541,7 @@ version = "1.12.1"
1154111541
source = "registry+https://github.com/rust-lang/crates.io-index"
1154211542
checksum = "eab68b56840f69efb0fefbe3ab6661499217ffdc58e2eef7c3f6f69835386322"
1154311543
dependencies = [
11544+
"serde",
1154411545
"smallvec",
1154511546
]
1154611547

crates/store/re_log_types/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,12 @@ pub struct StoreInfo {
583583
// NOTE: The version comes directly from the decoded RRD stream's header, duplicating it here
584584
// would probably only lead to more issues down the line.
585585
pub store_version: Option<CrateVersion>,
586+
587+
/// If true, the Viewer downloaded only a subset of an existing recording.
588+
///
589+
/// This happens when opening URLs with a time range.
590+
/// If we don't know for sure whether the recording is partial, we set this to `false`.
591+
pub is_partial: bool,
586592
}
587593

588594
impl StoreInfo {
@@ -593,6 +599,7 @@ impl StoreInfo {
593599
cloned_from: None,
594600
store_source,
595601
store_version: Some(CrateVersion::LOCAL),
602+
is_partial: false,
596603
}
597604
}
598605

@@ -603,6 +610,7 @@ impl StoreInfo {
603610
cloned_from: None,
604611
store_source,
605612
store_version: None,
613+
is_partial: false,
606614
}
607615
}
608616

@@ -949,6 +957,7 @@ impl SizeBytes for StoreInfo {
949957
cloned_from: _,
950958
store_source,
951959
store_version,
960+
is_partial: _,
952961
} = self;
953962

954963
store_id.heap_size_bytes()

crates/store/re_protos/src/v1alpha1/rerun.log_msg.v1alpha1.ext.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ impl TryFrom<crate::log_msg::v1alpha1::StoreInfo> for re_log_types::StoreInfo {
253253
cloned_from: None,
254254
store_source,
255255
store_version,
256+
is_partial: false,
256257
})
257258
}
258259
}

crates/store/re_redap_client/src/grpc.rs

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use re_auth::client::AuthDecorator;
2-
use re_chunk::Chunk;
2+
use re_chunk::{Chunk, TimelineName};
33
use re_log_types::{
4-
BlueprintActivationCommand, EntryId, LogMsg, SetStoreInfo, StoreId, StoreInfo, StoreKind,
5-
StoreSource,
4+
AbsoluteTimeRange, BlueprintActivationCommand, EntryId, LogMsg, SetStoreInfo, StoreId,
5+
StoreInfo, StoreKind, StoreSource,
66
};
77
use re_protos::cloud::v1alpha1::GetChunksRequest;
88
use re_protos::cloud::v1alpha1::ext::{Query, QueryLatestAt, QueryRange};
@@ -21,13 +21,16 @@ use crate::{
2121
///
2222
/// If you're not in a ui context you can safely ignore these.
2323
pub enum UiCommand {
24-
SetLoopSelection {
25-
recording_id: re_log_types::StoreId,
26-
timeline: re_log_types::Timeline,
27-
time_range: re_log_types::AbsoluteTimeRangeF,
24+
AddValidTimeRange {
25+
store_id: StoreId,
26+
27+
/// If `None`, signals that all timelines are entirely valid.
28+
timeline: Option<TimelineName>,
29+
time_range: AbsoluteTimeRange,
2830
},
31+
2932
SetUrlFragment {
30-
recording_id: re_log_types::StoreId,
33+
store_id: StoreId,
3134
fragment: re_uri::Fragment,
3235
},
3336
}
@@ -428,6 +431,7 @@ pub async fn stream_blueprint_and_partition_from_server(
428431
cloned_from: None,
429432
store_source: StoreSource::Unknown,
430433
store_version: None,
434+
is_partial: false,
431435
};
432436

433437
stream_partition_from_server(
@@ -460,13 +464,6 @@ pub async fn stream_blueprint_and_partition_from_server(
460464
re_log::debug!("No blueprint dataset found for {uri}");
461465
}
462466

463-
let store_info = StoreInfo {
464-
store_id: recording_store_id,
465-
cloned_from: None,
466-
store_source: StoreSource::Unknown,
467-
store_version: None,
468-
};
469-
470467
let re_uri::DatasetPartitionUri {
471468
origin: _,
472469
dataset_id,
@@ -475,6 +472,14 @@ pub async fn stream_blueprint_and_partition_from_server(
475472
fragment,
476473
} = uri;
477474

475+
let store_info = StoreInfo {
476+
store_id: recording_store_id,
477+
cloned_from: None,
478+
store_source: StoreSource::Unknown,
479+
store_version: None,
480+
is_partial: time_range.is_some(),
481+
};
482+
478483
stream_partition_from_server(
479484
&mut client,
480485
store_info,
@@ -563,17 +568,27 @@ async fn stream_partition_from_server(
563568

564569
let store_id = store_info.store_id.clone();
565570

566-
if let Some(on_ui_cmd) = on_ui_cmd {
571+
// Send UI commands for recording (as opposed to blueprint) stores.
572+
if let Some(on_ui_cmd) = on_ui_cmd
573+
&& store_info.store_id.is_recording()
574+
{
567575
if let Some(time_range) = time_range {
568-
on_ui_cmd(UiCommand::SetLoopSelection {
569-
recording_id: store_id.clone(),
570-
timeline: time_range.timeline,
576+
on_ui_cmd(UiCommand::AddValidTimeRange {
577+
store_id: store_id.clone(),
578+
timeline: Some(*time_range.timeline.name()),
571579
time_range: time_range.into(),
572580
});
581+
} else {
582+
on_ui_cmd(UiCommand::AddValidTimeRange {
583+
store_id: store_id.clone(),
584+
timeline: None,
585+
time_range: AbsoluteTimeRange::EVERYTHING,
586+
});
573587
}
588+
574589
if !fragment.is_empty() {
575590
on_ui_cmd(UiCommand::SetUrlFragment {
576-
recording_id: store_id.clone(),
591+
store_id: store_id.clone(),
577592
fragment,
578593
});
579594
}

crates/viewer/re_component_ui/src/variant_uis/redap_uri_button.rs

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,18 @@ pub fn redap_uri_button(
3636

3737
let uri = RedapUri::from_str(url_str)?;
3838

39-
let loaded_recording_id = ctx.storage_context.bundle.recordings().find_map(|db| {
39+
let loaded_recording_info = ctx.storage_context.bundle.recordings().find_map(|db| {
4040
if db
4141
.data_source
4242
.as_ref()
4343
.is_some_and(|source| source.stripped_redap_uri().as_ref() == Some(&uri))
4444
{
45-
Some(db.store_id().clone())
45+
db.store_info()
4646
} else {
4747
None
4848
}
4949
});
50-
let is_loading = loaded_recording_id.is_none()
50+
let is_loading = loaded_recording_info.is_none()
5151
&& ctx
5252
.connected_receivers
5353
.sources()
@@ -96,20 +96,31 @@ pub fn redap_uri_button(
9696
.0
9797
};
9898

99-
if let Some(loaded_recording_id) = loaded_recording_id {
100-
let response = link_with_copy(ui, Link::new("Switch to")).on_hover_ui(|ui| {
101-
ui.label("This recording is already loaded. Click to switch to it.");
102-
});
99+
ui.horizontal(|ui| {
100+
if let Some(loaded_recording_info) = loaded_recording_info {
101+
if loaded_recording_info.is_partial {
102+
let response = ui.add(Link::new("Open full")).on_hover_text(
103+
"Part of this recording is already loaded. Click to download the rest.",
104+
);
105+
handle_open_full_recording_link(ui, uri, &response);
106+
}
103107

104-
if response.clicked() {
105-
// Show it:
106-
ctx.command_sender()
107-
.send_system(SystemCommand::SetSelection(
108-
re_viewer_context::Item::StoreId(loaded_recording_id).into(),
109-
));
110-
}
111-
} else if is_loading {
112-
ui.horizontal(|ui| {
108+
let response = link_with_copy(ui, Link::new("Switch to")).on_hover_text(
109+
if loaded_recording_info.is_partial {
110+
"Part of this recording is already loaded. Click to switch to it."
111+
} else {
112+
"This recording is already loaded. Click to switch to it."
113+
},
114+
);
115+
if response.clicked() {
116+
// Show it:
117+
ctx.command_sender()
118+
.send_system(SystemCommand::SetSelection(
119+
re_viewer_context::Item::StoreId(loaded_recording_info.store_id.clone())
120+
.into(),
121+
));
122+
}
123+
} else if is_loading {
113124
ui.spinner();
114125

115126
if ui
@@ -119,18 +130,22 @@ pub fn redap_uri_button(
119130
{
120131
ctx.connected_receivers.remove_by_uri(&uri.to_string());
121132
}
122-
});
123-
} else {
124-
let response = link_with_copy(ui, Link::new("Open")).on_hover_ui(|ui| {
125-
ui.label(uri.to_string());
126-
});
133+
} else {
134+
let response = link_with_copy(ui, Link::new("Open")).on_hover_ui(|ui| {
135+
ui.label(uri.to_string());
136+
});
127137

128-
if response.clicked_with_open_in_background() {
129-
ui.ctx().open_url(egui::OpenUrl::new_tab(uri));
130-
} else if response.clicked() {
131-
ui.ctx().open_url(egui::OpenUrl::same_tab(uri));
138+
handle_open_full_recording_link(ui, uri, &response);
132139
}
133-
}
140+
});
134141

135142
Ok(())
136143
}
144+
145+
fn handle_open_full_recording_link(ui: &Ui, uri: RedapUri, response: &egui::Response) {
146+
if response.clicked_with_open_in_background() {
147+
ui.ctx().open_url(egui::OpenUrl::new_tab(uri));
148+
} else if response.clicked() {
149+
ui.ctx().open_url(egui::OpenUrl::same_tab(uri));
150+
}
151+
}

crates/viewer/re_data_ui/src/entity_db.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ impl crate::DataUi for EntityDb {
5353
cloned_from,
5454
store_source,
5555
store_version,
56+
is_partial,
5657
} = store_info;
5758

5859
if let Some(cloned_from) = cloned_from {
@@ -79,6 +80,15 @@ impl crate::DataUi for EntityDb {
7980
);
8081
}
8182

83+
// TODO(#11315): In the future we will want to reduce this to a mere highlighting feature and no longer need this at the store level
84+
// as all data will be pulled on-demand from a server.
85+
ui.grid_left_hand_label("Partial store");
86+
ui.monospace(format!("{is_partial:?}"))
87+
.on_hover_text(
88+
"If true, only a subset of the recording is presented in the viewer.",
89+
);
90+
ui.end_row();
91+
8292
ui.grid_left_hand_label("Kind");
8393
ui.label(store_id.kind().to_string());
8494
ui.end_row();

crates/viewer/re_data_ui/src/item_ui.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -718,8 +718,17 @@ pub fn entity_db_button_ui(
718718
}
719719
.unwrap_or("<unknown>".to_owned());
720720

721+
let partial_postfix = if entity_db
722+
.store_info()
723+
.is_some_and(|store_info| store_info.is_partial)
724+
{
725+
" (partial)"
726+
} else {
727+
""
728+
};
729+
721730
let size = re_format::format_bytes(entity_db.total_size_bytes() as _);
722-
let title = format!("{app_id_prefix}{recording_name} - {size}");
731+
let title = format!("{app_id_prefix}{recording_name}{partial_postfix} - {size}");
723732

724733
let store_id = entity_db.store_id().clone();
725734
let item = re_viewer_context::Item::StoreId(store_id.clone());

crates/viewer/re_global_context/src/command_sender.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use re_chunk::{EntityPath, Timeline};
22
use re_chunk_store::external::re_chunk::Chunk;
33
use re_data_source::LogDataSource;
4-
use re_log_types::{AbsoluteTimeRangeF, StoreId};
4+
use re_log_types::{AbsoluteTimeRange, AbsoluteTimeRangeF, StoreId};
55
use re_ui::{UICommand, UICommandSender};
66

77
use crate::RecordingOrTable;
@@ -126,6 +126,18 @@ pub enum SystemCommand {
126126
time_range: AbsoluteTimeRangeF,
127127
},
128128

129+
/// Mark a time range as valid.
130+
///
131+
/// Everything outside can still be navigated to, but will be considered potentially lacking some data and therefore "invalid".
132+
/// Visually, it is outside of the normal time range and shown greyed out.
133+
///
134+
/// If timeline is `None`, this signals that all timelines are considered to be valid entirely.
135+
AddValidTimeRange {
136+
store_id: StoreId,
137+
timeline: Option<re_chunk::TimelineName>,
138+
time_range: AbsoluteTimeRange,
139+
},
140+
129141
/// Sets the focus to the given item.
130142
///
131143
/// The focused item is cleared out every frame.
Lines changed: 2 additions & 2 deletions
Loading

crates/viewer/re_test_context/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,19 @@ impl TestContext {
635635
}
636636
}
637637

638+
SystemCommand::AddValidTimeRange {
639+
store_id: rec_id,
640+
timeline,
641+
time_range,
642+
} => {
643+
assert_eq!(
644+
&rec_id,
645+
self.store_hub.lock().active_recording().unwrap().store_id()
646+
);
647+
let mut time_ctrl = self.recording_config.time_ctrl.write();
648+
time_ctrl.mark_time_range_valid(timeline, time_range);
649+
}
650+
638651
// not implemented
639652
SystemCommand::ActivateApp(_)
640653
| SystemCommand::ActivateRecordingOrTable(_)

0 commit comments

Comments
 (0)