Skip to content

Commit 46de401

Browse files
authored
Add TableStore for table/dataframe entries + basic UI (#9437)
### Related * Part of: #8593 * Will unlock: #7204 ### What This brings a very basic table entry to the Rerun viewer. Tables are essentially dataframes, can contain Rerun-native components, but don't have to adhere to some of the restrictions that we place on regular entities (i.e. we don't require indexation columns). <img width="434" alt="image" src="https://github.com/user-attachments/assets/eab906e9-ad62-4052-ac25-dad811f46840" /> ### Testing The following will create a _very_ basic table in-memory. A follow-up PR will add GRPC-based sending of dataframes. ``` RERUN_EXPERIMENTAL_TABLE=1 pixi run rerun ``` ### Known issues * [x] Deleting all recordings switches to welcome screen although tables still present. (tracked via #9465) * [x] Finding the closest entry after closing is not implemented (will be closed via #9464)
1 parent 0447e94 commit 46de401

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+788
-257
lines changed

Cargo.lock

+3
Original file line numberDiff line numberDiff line change
@@ -8094,6 +8094,7 @@ dependencies = [
80948094
"egui",
80958095
"egui-wgpu",
80968096
"egui_plot",
8097+
"egui_table",
80978098
"ehttp",
80988099
"glam",
80998100
"image",
@@ -8127,6 +8128,7 @@ dependencies = [
81278128
"re_renderer",
81288129
"re_selection_panel",
81298130
"re_smart_channel",
8131+
"re_sorbet",
81308132
"re_time_panel",
81318133
"re_tracing",
81328134
"re_types",
@@ -8205,6 +8207,7 @@ dependencies = [
82058207
"re_query",
82068208
"re_renderer",
82078209
"re_smart_channel",
8210+
"re_sorbet",
82088211
"re_string_interner",
82098212
"re_tracing",
82108213
"re_types",

crates/store/re_log_types/src/lib.rs

+34
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,40 @@ impl std::fmt::Display for ApplicationId {
199199

200200
// ----------------------------------------------------------------------------
201201

202+
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
203+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
204+
pub struct TableId(Arc<String>);
205+
206+
impl TableId {
207+
pub fn new(id: String) -> Self {
208+
Self(Arc::new(id))
209+
}
210+
211+
pub fn as_str(&self) -> &str {
212+
self.0.as_str()
213+
}
214+
}
215+
216+
impl From<&str> for TableId {
217+
fn from(s: &str) -> Self {
218+
Self(Arc::new(s.into()))
219+
}
220+
}
221+
222+
impl From<String> for TableId {
223+
fn from(s: String) -> Self {
224+
Self(Arc::new(s))
225+
}
226+
}
227+
228+
impl std::fmt::Display for TableId {
229+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230+
self.0.fmt(f)
231+
}
232+
}
233+
234+
// ----------------------------------------------------------------------------
235+
202236
/// Command used for activating a blueprint once it has been fully transmitted.
203237
///
204238
/// This command serves two purposes:

crates/viewer/re_blueprint_tree/src/blueprint_tree.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1023,7 +1023,7 @@ impl BlueprintTree {
10231023
focused_item: &Item,
10241024
) -> Option<Item> {
10251025
match focused_item {
1026-
Item::AppId(_) | Item::DataSource(_) | Item::StoreId(_) => None,
1026+
Item::AppId(_) | Item::TableId(_) | Item::DataSource(_) | Item::StoreId(_) => None,
10271027

10281028
Item::Container(container_id) => {
10291029
self.expand_all_contents_until(
@@ -1158,11 +1158,11 @@ fn add_new_view_or_container_menu_button(
11581158

11591159
fn set_blueprint_to_default_menu_buttons(ctx: &ViewerContext<'_>, ui: &mut egui::Ui) {
11601160
let default_blueprint_id = ctx
1161-
.store_context
1161+
.storage_context
11621162
.hub
11631163
.default_blueprint_id_for_app(&ctx.store_context.app_id);
11641164

1165-
let default_blueprint = default_blueprint_id.and_then(|id| ctx.store_context.bundle.get(id));
1165+
let default_blueprint = default_blueprint_id.and_then(|id| ctx.storage_context.bundle.get(id));
11661166

11671167
let disabled_reason = match default_blueprint {
11681168
None => Some("No default blueprint is set for this app"),

crates/viewer/re_context_menu/src/actions/collapse_expand_all.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ impl ContextMenuAction for CollapseExpandAllAction {
3434
// TODO(ab): in an ideal world, we'd check the fully expended/collapsed state of the item to
3535
// avoid showing a command that wouldn't have an effect but that's lots of added complexity.
3636
match item {
37-
Item::AppId(_) | Item::DataSource(_) | Item::StoreId(_) | Item::ComponentPath(_) => {
38-
false
39-
}
37+
Item::AppId(_)
38+
| Item::TableId(_)
39+
| Item::DataSource(_)
40+
| Item::StoreId(_)
41+
| Item::ComponentPath(_) => false,
4042

4143
Item::View(_) | Item::Container(_) | Item::InstancePath(_) => true,
4244

crates/viewer/re_context_menu/src/actions/copy_entity_path.rs

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ impl ContextMenuAction for CopyEntityPathToClipboard {
1212
fn supports_item(&self, _ctx: &ContextMenuContext<'_>, item: &Item) -> bool {
1313
match item {
1414
Item::AppId(_)
15+
| Item::TableId(_)
1516
| Item::DataSource(_)
1617
| Item::StoreId(_)
1718
| Item::Container(_)

crates/viewer/re_context_menu/src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use once_cell::sync::OnceCell;
44

55
use re_entity_db::InstancePath;
6+
use re_log_types::TableId;
67
use re_viewer_context::{
78
ContainerId, Contents, Item, ItemCollection, ItemContext, ViewId, ViewerContext,
89
};
@@ -358,6 +359,7 @@ trait ContextMenuAction {
358359
for (item, _) in ctx.selection.iter() {
359360
match item {
360361
Item::AppId(app_id) => self.process_app_id(ctx, app_id),
362+
Item::TableId(table_id) => self.process_table_id(ctx, table_id),
361363
Item::DataSource(data_source) => self.process_data_source(ctx, data_source),
362364
Item::StoreId(store_id) => self.process_store_id(ctx, store_id),
363365
Item::ComponentPath(component_path) => {
@@ -386,6 +388,9 @@ trait ContextMenuAction {
386388
/// Process a single recording.
387389
fn process_store_id(&self, _ctx: &ContextMenuContext<'_>, _store_id: &re_log_types::StoreId) {}
388390

391+
/// Process a table.
392+
fn process_table_id(&self, _ctx: &ContextMenuContext<'_>, _store_id: &TableId) {}
393+
389394
/// Process a single container.
390395
fn process_container(&self, _ctx: &ContextMenuContext<'_>, _container_id: &ContainerId) {}
391396

crates/viewer/re_data_ui/src/app_id.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ impl crate::DataUi for ApplicationId {
3737

3838
// Find all recordings with this app id
3939
let recordings: Vec<&EntityDb> = ctx
40-
.store_context
40+
.storage_context
4141
.bundle
4242
.recordings()
4343
.filter(|db| db.app_id() == Some(self))

crates/viewer/re_data_ui/src/data_source.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ impl crate::DataUi for re_smart_channel::SmartChannelSource {
2727
let mut blueprints = vec![];
2828

2929
for other in ctx
30-
.store_context
30+
.storage_context
3131
.bundle
3232
.entity_dbs()
3333
.filter(|db| db.data_source.as_ref() == Some(self))

crates/viewer/re_data_ui/src/entity_db.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ impl crate::DataUi for EntityDb {
156156
}
157157
});
158158

159-
let hub = ctx.store_context.hub;
159+
let hub = ctx.storage_context.hub;
160160
let store_id = Some(self.store_id());
161161

162162
match self.store_kind() {

crates/viewer/re_data_ui/src/item_ui.rs

+70-5
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
use re_entity_db::{EntityTree, InstancePath};
66
use re_format::format_uint;
77
use re_log_types::{
8-
ApplicationId, ComponentPath, EntityPath, TimeInt, TimeType, Timeline, TimelineName,
8+
ApplicationId, ComponentPath, EntityPath, TableId, TimeInt, TimeType, Timeline, TimelineName,
99
};
1010
use re_types::components::{Name, Timestamp};
1111
use re_ui::{icons, list_item, SyntaxHighlighting as _, UiExt as _};
12-
use re_viewer_context::{HoverHighlight, Item, UiLayout, ViewId, ViewerContext};
12+
use re_viewer_context::{
13+
HoverHighlight, Item, SystemCommand, SystemCommandSender as _, UiLayout, ViewId, ViewerContext,
14+
};
1315

1416
use super::DataUi as _;
1517

@@ -722,7 +724,7 @@ pub fn store_id_button_ui(
722724
store_id: &re_log_types::StoreId,
723725
ui_layout: UiLayout,
724726
) {
725-
if let Some(entity_db) = ctx.store_context.bundle.get(store_id) {
727+
if let Some(entity_db) = ctx.storage_context.bundle.get(store_id) {
726728
entity_db_button_ui(ctx, ui, entity_db, ui_layout, true);
727729
} else {
728730
ui_layout.label(ui, store_id.to_string());
@@ -801,7 +803,7 @@ pub fn entity_db_button_ui(
801803
});
802804
if resp.clicked() {
803805
ctx.command_sender()
804-
.send_system(SystemCommand::CloseStore(store_id.clone()));
806+
.send_system(SystemCommand::CloseEntry(store_id.clone().into()));
805807
}
806808
resp
807809
});
@@ -843,10 +845,73 @@ pub fn entity_db_button_ui(
843845
// for the blueprint.
844846
if store_id.kind == re_log_types::StoreKind::Recording {
845847
ctx.command_sender()
846-
.send_system(SystemCommand::ActivateRecording(store_id.clone()));
848+
.send_system(SystemCommand::ActivateEntry(store_id.clone().into()));
847849
}
848850

849851
ctx.command_sender()
850852
.send_system(SystemCommand::SetSelection(item));
851853
}
852854
}
855+
856+
pub fn table_id_button_ui(
857+
ctx: &ViewerContext<'_>,
858+
ui: &mut egui::Ui,
859+
table_id: &TableId,
860+
ui_layout: UiLayout,
861+
) {
862+
let item = re_viewer_context::Item::TableId(table_id.clone());
863+
864+
let icon = &icons::VIEW_DATAFRAME;
865+
866+
let mut item_content =
867+
list_item::LabelContent::new(table_id.as_str()).with_icon_fn(|ui, rect, visuals| {
868+
// Color icon based on whether this is the active table or not:
869+
let color = if ctx.active_table.as_ref() == Some(table_id) {
870+
visuals.fg_stroke.color
871+
} else {
872+
ui.visuals().widgets.noninteractive.fg_stroke.color
873+
};
874+
icon.as_image().tint(color).paint_at(ui, rect);
875+
});
876+
877+
if ui_layout.is_selection_panel() {
878+
item_content = item_content.with_buttons(|ui| {
879+
// Close-button:
880+
let resp = ui
881+
.small_icon_button(&icons::REMOVE)
882+
.on_hover_text("Close this table (all data will be lost)");
883+
if resp.clicked() {
884+
ctx.command_sender()
885+
.send_system(SystemCommand::CloseEntry(table_id.clone().into()));
886+
}
887+
resp
888+
});
889+
}
890+
891+
let mut list_item = ui
892+
.list_item()
893+
.selected(ctx.selection().contains_item(&item));
894+
895+
if ctx.hovered().contains_item(&item) {
896+
list_item = list_item.force_hovered(true);
897+
}
898+
899+
let response = list_item::list_item_scope(ui, "entity db button", |ui| {
900+
list_item
901+
.show_hierarchical(ui, item_content)
902+
.on_hover_ui(|ui| {
903+
ui.label(format!("Table: {table_id}"));
904+
})
905+
});
906+
907+
if response.hovered() {
908+
ctx.selection_state().set_hovered(item.clone());
909+
}
910+
911+
if response.clicked() {
912+
ctx.command_sender()
913+
.send_system(SystemCommand::ActivateEntry(table_id.clone().into()));
914+
ctx.command_sender()
915+
.send_system(SystemCommand::SetSelection(item));
916+
}
917+
}

crates/viewer/re_data_ui/src/store_id.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ impl crate::DataUi for re_log_types::StoreId {
99
query: &re_chunk_store::LatestAtQuery,
1010
db: &re_entity_db::EntityDb,
1111
) {
12-
if let Some(entity_db) = ctx.store_context.bundle.get(self) {
12+
if let Some(entity_db) = ctx.storage_context.bundle.get(self) {
1313
entity_db.data_ui(ctx, ui, ui_layout, query, db);
1414
} else {
1515
ui.label(format!("{} ID {} (not found)", self.kind, self.id));

crates/viewer/re_selection_panel/src/item_heading_no_breadcrumbs.rs

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ fn item_heading_no_breadcrumbs(
4444
Item::AppId(_)
4545
| Item::DataSource(_)
4646
| Item::StoreId(_)
47+
| Item::TableId(_)
4748
| Item::Container(_)
4849
| Item::View(_) => {
4950
let ItemTitle {

crates/viewer/re_selection_panel/src/item_heading_with_breadcrumbs.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ fn item_bread_crumbs_ui(
7373
item: &Item,
7474
) {
7575
match item {
76-
Item::AppId(_) | Item::DataSource(_) | Item::StoreId(_) => {
76+
Item::AppId(_) | Item::DataSource(_) | Item::StoreId(_) | Item::TableId(_) => {
7777
// These have no bread crumbs, at least not currently.
7878
// I guess one could argue that the `StoreId` should have the `AppId` as its ancestor?
7979
}
@@ -192,6 +192,7 @@ fn last_part_of_item_heading(
192192
| Item::DataSource { .. }
193193
| Item::Container { .. }
194194
| Item::View { .. }
195+
| Item::TableId { .. }
195196
| Item::StoreId { .. } => true,
196197

197198
Item::InstancePath { .. } | Item::DataResult { .. } | Item::ComponentPath { .. } => false,

crates/viewer/re_selection_panel/src/item_title.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use egui::WidgetText;
33
use re_chunk::EntityPath;
44
use re_data_ui::item_ui::{guess_instance_path_icon, guess_query_and_db_for_selected_entity};
55
use re_entity_db::InstancePath;
6-
use re_log_types::ComponentPath;
6+
use re_log_types::{ComponentPath, TableId};
77
use re_types::components::Timestamp;
88
use re_ui::{
99
icons,
@@ -47,6 +47,7 @@ impl ItemTitle {
4747
}
4848

4949
Item::StoreId(store_id) => Self::from_store_id(ctx, store_id),
50+
Item::TableId(table_id) => Self::from_table_id(ctx, table_id),
5051

5152
Item::InstancePath(instance_path) => {
5253
Self::from_instance_path(ctx, style, instance_path)
@@ -73,10 +74,14 @@ impl ItemTitle {
7374
}
7475
}
7576

77+
pub fn from_table_id(_ctx: &ViewerContext<'_>, table_id: &TableId) -> Self {
78+
Self::new(table_id.as_str(), &icons::ENTITY_RESERVED).with_tooltip(table_id.as_str())
79+
}
80+
7681
pub fn from_store_id(ctx: &ViewerContext<'_>, store_id: &re_log_types::StoreId) -> Self {
7782
let id_str = format!("{} ID: {}", store_id.kind, store_id);
7883

79-
let title = if let Some(entity_db) = ctx.store_context.bundle.get(store_id) {
84+
let title = if let Some(entity_db) = ctx.storage_context.bundle.get(store_id) {
8085
match (
8186
entity_db.app_id(),
8287
entity_db.recording_property::<Timestamp>(),

crates/viewer/re_selection_panel/src/selection_panel.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ fn data_section_ui(item: &Item) -> Option<Box<dyn DataUi>> {
644644
Some(Box::new(instance_path.clone()))
645645
}
646646
// Skip data ui since we don't know yet what to show for these.
647-
Item::View(_) | Item::Container(_) => None,
647+
Item::TableId(_) | Item::View(_) | Item::Container(_) => None,
648648
}
649649
}
650650

crates/viewer/re_view_spatial/src/ui_3d.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -561,9 +561,11 @@ impl SpatialView3D {
561561
// Track focused entity if any.
562562
if let Some(focused_item) = ctx.focused_item {
563563
let focused_entity = match focused_item {
564-
Item::AppId(_) | Item::DataSource(_) | Item::StoreId(_) | Item::Container(_) => {
565-
None
566-
}
564+
Item::AppId(_)
565+
| Item::DataSource(_)
566+
| Item::StoreId(_)
567+
| Item::Container(_)
568+
| Item::TableId(_) => None,
567569

568570
Item::View(view_id) => {
569571
if view_id == &query.view_id {

crates/viewer/re_viewer/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ re_viewport.workspace = true
9595
re_analytics = { workspace = true, optional = true }
9696
re_view_map = { workspace = true, optional = true }
9797

98+
# TODO(grtlr): The following deps are for the table. They should go into a separate crate.
99+
re_sorbet.workspace = true
100+
egui_table.workspace = true
101+
98102

99103
# External
100104
ahash.workspace = true

0 commit comments

Comments
 (0)