Panics · P34 Dashboard: handlers in new; gone tiles move nothing, unmatched tiles and cards stay out
This commit is contained in:
1 parent
05e846b62a
commit
45cecebea5
6 files changed
+248
-241
No files matched your search
@@ -82,50 +82,51 @@ pub fn grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
picked: service,
|
||||
},
|
||||
];
|
||||
let grid = DashboardGrid::new("dashboard-grid", 12, tiles)
|
||||
.card(
|
||||
"requests",
|
||||
DashboardCard::new("Requests").subtitle("per minute").body(
|
||||
Statistic::new("dashboard-requests", "This hour", 12_400.0)
|
||||
.trend(TrendIndicator::new(0.082)),
|
||||
),
|
||||
)
|
||||
.card(
|
||||
"errors",
|
||||
DashboardCard::new("Errors").subtitle("5xx share").body(
|
||||
Statistic::new("dashboard-errors", "This hour", 0.42)
|
||||
.decimals(2)
|
||||
.suffix("%")
|
||||
.trend(TrendIndicator::new(-0.12)),
|
||||
),
|
||||
)
|
||||
.card(
|
||||
"cpu",
|
||||
DashboardCard::new("CPU").body(Gauge::new(
|
||||
"dashboard-cpu",
|
||||
"percent busy",
|
||||
62.0,
|
||||
0.0,
|
||||
100.0,
|
||||
)),
|
||||
)
|
||||
.card(
|
||||
"latency",
|
||||
DashboardCard::new("Latency")
|
||||
.subtitle("p95, milliseconds")
|
||||
.body(LineChart::new("dashboard-latency", hours).series(latency)),
|
||||
)
|
||||
.card(
|
||||
"saturation",
|
||||
DashboardCard::new("Saturation").body(Gauge::new(
|
||||
"dashboard-memory",
|
||||
"memory in use",
|
||||
81.0,
|
||||
0.0,
|
||||
100.0,
|
||||
)),
|
||||
)
|
||||
.on_change(move |next, _, cx| change(&moved, cx, |wall| wall.tiles = next));
|
||||
let grid = DashboardGrid::new("dashboard-grid", 12, tiles, move |next, _, cx| {
|
||||
change(&moved, cx, |wall| wall.tiles = next)
|
||||
})
|
||||
.card(
|
||||
"requests",
|
||||
DashboardCard::new("Requests").subtitle("per minute").body(
|
||||
Statistic::new("dashboard-requests", "This hour", 12_400.0)
|
||||
.trend(TrendIndicator::new(0.082)),
|
||||
),
|
||||
)
|
||||
.card(
|
||||
"errors",
|
||||
DashboardCard::new("Errors").subtitle("5xx share").body(
|
||||
Statistic::new("dashboard-errors", "This hour", 0.42)
|
||||
.decimals(2)
|
||||
.suffix("%")
|
||||
.trend(TrendIndicator::new(-0.12)),
|
||||
),
|
||||
)
|
||||
.card(
|
||||
"cpu",
|
||||
DashboardCard::new("CPU").body(Gauge::new(
|
||||
"dashboard-cpu",
|
||||
"percent busy",
|
||||
62.0,
|
||||
0.0,
|
||||
100.0,
|
||||
)),
|
||||
)
|
||||
.card(
|
||||
"latency",
|
||||
DashboardCard::new("Latency")
|
||||
.subtitle("p95, milliseconds")
|
||||
.body(LineChart::new("dashboard-latency", hours).series(latency)),
|
||||
)
|
||||
.card(
|
||||
"saturation",
|
||||
DashboardCard::new("Saturation").body(Gauge::new(
|
||||
"dashboard-memory",
|
||||
"memory in use",
|
||||
81.0,
|
||||
0.0,
|
||||
100.0,
|
||||
)),
|
||||
);
|
||||
section(
|
||||
"Dashboard Grid · Widget / DashboardCard · RefreshIntervalSelector · DashboardFilterBar · StatCard → data_display::Statistic · MetricsChart → charts::LineChart · ResourceGauge → data_display::Gauge",
|
||||
"Widgets on a grid of twelve columns: a tile's top strip drags it and its corner resizes it, a cell at a time, while the others make room and rise to fill gaps; a focused tile steps with the arrows. Over it, the window of time and the filters, and how often it refreshes.",
|
||||
@@ -143,9 +144,7 @@ pub fn grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
.justify_between()
|
||||
.gap_3()
|
||||
.child(
|
||||
DashboardFilterBar::new("dashboard-filters", time, filters)
|
||||
.on_window(move |next, _, cx| change(&windowed, cx, |wall| wall.window = next))
|
||||
.on_filter(move |key, value, _, cx| {
|
||||
DashboardFilterBar::new("dashboard-filters", time, filters, move |next, _, cx| change(&windowed, cx, |wall| wall.window = next), move |key, value, _, cx| {
|
||||
change(&filtered, cx, |wall| match key.as_ref() {
|
||||
"environment" => wall.environment = value,
|
||||
_ => wall.service = value,
|
||||
@@ -153,9 +152,7 @@ pub fn grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
RefreshIntervalSelector::new("dashboard-refresh", every, clock - jiff::SignedDuration::from_secs(since), clock)
|
||||
.on_change(move |next, _, cx| change(&timed, cx, |wall| wall.every = next))
|
||||
.on_refresh(move |_, cx| change(&refreshed, cx, |wall| wall.since = 0)),
|
||||
RefreshIntervalSelector::new("dashboard-refresh", every, clock - jiff::SignedDuration::from_secs(since), clock, move |next, _, cx| change(&timed, cx, |wall| wall.every = next), move |_, cx| change(&refreshed, cx, |wall| wall.since = 0)),
|
||||
),
|
||||
)
|
||||
.child(div().w(px(900.)).child(grid)),
|
||||
|
||||
+16
-50
@@ -34,8 +34,8 @@ pub struct RefreshIntervalSelector {
|
||||
every: Option<u32>,
|
||||
last: Timestamp,
|
||||
now: Timestamp,
|
||||
on_change: Option<OnInterval>,
|
||||
on_refresh: Option<Run>,
|
||||
on_change: OnInterval,
|
||||
on_refresh: Run,
|
||||
}
|
||||
|
||||
impl RefreshIntervalSelector {
|
||||
@@ -45,6 +45,8 @@ impl RefreshIntervalSelector {
|
||||
every: Option<u32>,
|
||||
last: Timestamp,
|
||||
now: Timestamp,
|
||||
on_change: impl Fn(Option<u32>, &mut Window, &mut App) + 'static,
|
||||
on_refresh: impl Fn(&mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
if let Some(every) = every {
|
||||
assert!(
|
||||
@@ -59,23 +61,10 @@ impl RefreshIntervalSelector {
|
||||
every,
|
||||
last,
|
||||
now,
|
||||
on_change: None,
|
||||
on_refresh: None,
|
||||
on_change: Rc::new(on_change),
|
||||
on_refresh: Rc::new(on_refresh),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_change(
|
||||
mut self,
|
||||
handler: impl Fn(Option<u32>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_change = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_refresh(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_refresh = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The seconds a choice of the selector names; its 0 is Off.
|
||||
@@ -88,12 +77,8 @@ impl RenderOnce for RefreshIntervalSelector {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let id = self.id;
|
||||
let on_change = self
|
||||
.on_change
|
||||
.unwrap_or_else(|| panic!("refresh interval selector {id:?} has no on_change"));
|
||||
let on_refresh = self
|
||||
.on_refresh
|
||||
.unwrap_or_else(|| panic!("refresh interval selector {id:?} has no on_refresh"));
|
||||
let on_change = self.on_change;
|
||||
let on_refresh = self.on_refresh;
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
@@ -195,8 +180,8 @@ pub struct DashboardFilterBar {
|
||||
id: ElementId,
|
||||
window: TimeWindow,
|
||||
filters: Vec<DashboardFilter>,
|
||||
on_window: Option<OnWindow>,
|
||||
on_filter: Option<OnFilter>,
|
||||
on_window: OnWindow,
|
||||
on_filter: OnFilter,
|
||||
}
|
||||
|
||||
impl DashboardFilterBar {
|
||||
@@ -204,44 +189,25 @@ impl DashboardFilterBar {
|
||||
id: impl Into<ElementId>,
|
||||
window: TimeWindow,
|
||||
filters: impl IntoIterator<Item = DashboardFilter>,
|
||||
on_window: impl Fn(TimeWindow, &mut Window, &mut App) + 'static,
|
||||
on_filter: impl Fn(&SharedString, Option<SharedString>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
window,
|
||||
filters: filters.into_iter().collect(),
|
||||
on_window: None,
|
||||
on_filter: None,
|
||||
on_window: Rc::new(on_window),
|
||||
on_filter: Rc::new(on_filter),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_window(
|
||||
mut self,
|
||||
handler: impl Fn(TimeWindow, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_window = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Gets a filter's key and what is picked in it now; Clear sends none for each one picked.
|
||||
pub fn on_filter(
|
||||
mut self,
|
||||
handler: impl Fn(&SharedString, Option<SharedString>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_filter = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for DashboardFilterBar {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let id = self.id;
|
||||
let theme = cx.theme();
|
||||
let on_window = self
|
||||
.on_window
|
||||
.unwrap_or_else(|| panic!("dashboard filter bar {id:?} has no on_window"));
|
||||
let on_filter = self
|
||||
.on_filter
|
||||
.unwrap_or_else(|| panic!("dashboard filter bar {id:?} has no on_filter"));
|
||||
let on_window = self.on_window;
|
||||
let on_filter = self.on_filter;
|
||||
let picked: Vec<SharedString> = self
|
||||
.filters
|
||||
.iter()
|
||||
|
||||
+124
-119
@@ -41,7 +41,7 @@ pub struct DashboardGrid {
|
||||
columns: u32,
|
||||
tiles: Vec<Tile>,
|
||||
cards: Vec<(SharedString, AnyElement)>,
|
||||
on_change: Option<OnTiles>,
|
||||
on_change: OnTiles,
|
||||
}
|
||||
|
||||
impl DashboardGrid {
|
||||
@@ -49,6 +49,7 @@ impl DashboardGrid {
|
||||
id: impl Into<ElementId>,
|
||||
columns: u32,
|
||||
tiles: impl IntoIterator<Item = Tile>,
|
||||
on_change: impl Fn(Vec<Tile>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
assert!(columns > 0, "a grid of no columns");
|
||||
Self {
|
||||
@@ -56,7 +57,7 @@ impl DashboardGrid {
|
||||
columns,
|
||||
tiles: tiles.into_iter().collect(),
|
||||
cards: Vec::new(),
|
||||
on_change: None,
|
||||
on_change: Rc::new(on_change),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,14 +66,6 @@ impl DashboardGrid {
|
||||
self.cards.push((key.into(), card.into_any_element()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_change(
|
||||
mut self,
|
||||
handler: impl Fn(Vec<Tile>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_change = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The tile a drag of `by` pixels makes of `start`, a cell at a time.
|
||||
@@ -124,24 +117,28 @@ fn stacked(
|
||||
tiles.sort_by_key(|tile| (tile.y, tile.x));
|
||||
let column: Vec<_> = tiles
|
||||
.into_iter()
|
||||
.map(|tile| {
|
||||
let at = cards
|
||||
.iter()
|
||||
.position(|(key, _)| *key == tile.key)
|
||||
.unwrap_or_else(|| panic!("dashboard grid {id:?}: no card for tile {}", tile.key));
|
||||
.filter_map(|tile| {
|
||||
let Some(at) = cards.iter().position(|(key, _)| *key == tile.key) else {
|
||||
log::error!(
|
||||
"dashboard grid {id:?}: no card for tile {}; left out",
|
||||
tile.key
|
||||
);
|
||||
return None;
|
||||
};
|
||||
let card = cards.remove(at).1;
|
||||
let key = tile.key.clone();
|
||||
div()
|
||||
.debug_selector(move || format!("stacked-{key}"))
|
||||
.h((sizes.row + sizes.gap) * tile.h as f32 - sizes.gap)
|
||||
.child(card)
|
||||
Some(
|
||||
div()
|
||||
.debug_selector(move || format!("stacked-{key}"))
|
||||
.h((sizes.row + sizes.gap) * tile.h as f32 - sizes.gap)
|
||||
.child(card),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert!(
|
||||
cards.is_empty(),
|
||||
"dashboard grid: cards with no tile: {:?}",
|
||||
cards.iter().map(|(key, _)| key).collect::<Vec<_>>()
|
||||
);
|
||||
if !cards.is_empty() {
|
||||
let keys: Vec<_> = cards.iter().map(|(key, _)| key).collect();
|
||||
log::error!("dashboard grid: cards with no tile {keys:?}; left out");
|
||||
}
|
||||
div()
|
||||
.id(id)
|
||||
.relative()
|
||||
@@ -172,26 +169,27 @@ impl RenderOnce for DashboardGrid {
|
||||
let cell = ((width + gap) / columns as f32, row + gap);
|
||||
let landing = board.read(cx).landing.clone();
|
||||
let shown = match &landing {
|
||||
Some(tile) => arranged(&self.tiles, tile.clone(), columns),
|
||||
Some(tile) => {
|
||||
arranged(&self.tiles, tile.clone(), columns).unwrap_or_else(|| self.tiles.clone())
|
||||
}
|
||||
None => self.tiles.clone(),
|
||||
};
|
||||
let height = shown.iter().map(|tile| tile.y + tile.h).max().unwrap_or(0) as f32 * cell.1;
|
||||
let tiles = Rc::new(self.tiles);
|
||||
let on_change = self
|
||||
.on_change
|
||||
.unwrap_or_else(|| panic!("dashboard grid {id:?} has no on_change"));
|
||||
let on_change = self.on_change;
|
||||
let (colors, radius) = (theme.colors.clone(), theme.radius(Radius::Lg));
|
||||
let grip = theme.control_height(crate::theme::ControlSize::Sm);
|
||||
let mut cards = self.cards;
|
||||
let placed: Vec<AnyElement> = shown
|
||||
.iter()
|
||||
.map(|tile| {
|
||||
let at = cards
|
||||
.iter()
|
||||
.position(|(key, _)| *key == tile.key)
|
||||
.unwrap_or_else(|| {
|
||||
panic!("dashboard grid {id:?}: no card for tile {}", tile.key)
|
||||
});
|
||||
.filter_map(|tile| {
|
||||
let Some(at) = cards.iter().position(|(key, _)| *key == tile.key) else {
|
||||
log::error!(
|
||||
"dashboard grid {id:?}: no card for tile {}; left out",
|
||||
tile.key
|
||||
);
|
||||
return None;
|
||||
};
|
||||
let card = cards.remove(at).1;
|
||||
let focus = tab_stop(
|
||||
(id.clone(), format!("tile-{}", tile.key)).into(),
|
||||
@@ -217,92 +215,97 @@ impl RenderOnce for DashboardGrid {
|
||||
};
|
||||
let (keys, keyed_tiles, keyed_change, own) =
|
||||
(key.clone(), tiles.clone(), on_change.clone(), focus.clone());
|
||||
div()
|
||||
.id((id.clone(), format!("tile-{}", tile.key)))
|
||||
.track_focus(&focus)
|
||||
.absolute()
|
||||
.left(Pixels::from(tile.x as f32 * cell.0))
|
||||
.top(Pixels::from(tile.y as f32 * cell.1))
|
||||
.w(Pixels::from(tile.w as f32 * cell.0 - gap))
|
||||
.h(Pixels::from(tile.h as f32 * cell.1 - gap))
|
||||
.rounded(radius)
|
||||
.border_1()
|
||||
.border_color(if focused {
|
||||
colors.focus
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
})
|
||||
.on_key_down(move |event, window, cx| {
|
||||
if !own.is_focused(window) {
|
||||
return;
|
||||
}
|
||||
let grow = event.keystroke.modifiers.shift;
|
||||
let key = event.keystroke.key.as_str();
|
||||
let (dx, dy): (i64, i64) = match key {
|
||||
"left" => (-1, 0),
|
||||
"right" => (1, 0),
|
||||
"up" => (0, -1),
|
||||
"down" => (0, 1),
|
||||
_ => return,
|
||||
};
|
||||
cx.stop_propagation();
|
||||
let tile = keyed_tiles
|
||||
.iter()
|
||||
.find(|tile| tile.key == keys)
|
||||
.expect("a tile of the grid");
|
||||
let to = match (grow, dy) {
|
||||
(false, 0) => pulled(tile, Grip::Move, (dx as f32, 0.0), (1.0, 1.0)),
|
||||
(false, _) => match stepped(&keyed_tiles, &keys, dy > 0) {
|
||||
Some(to) => to,
|
||||
None => return,
|
||||
},
|
||||
(true, _) => {
|
||||
pulled(tile, Grip::Size, (dx as f32, dy as f32), (1.0, 1.0))
|
||||
Some(
|
||||
div()
|
||||
.id((id.clone(), format!("tile-{}", tile.key)))
|
||||
.track_focus(&focus)
|
||||
.absolute()
|
||||
.left(Pixels::from(tile.x as f32 * cell.0))
|
||||
.top(Pixels::from(tile.y as f32 * cell.1))
|
||||
.w(Pixels::from(tile.w as f32 * cell.0 - gap))
|
||||
.h(Pixels::from(tile.h as f32 * cell.1 - gap))
|
||||
.rounded(radius)
|
||||
.border_1()
|
||||
.border_color(if focused {
|
||||
colors.focus
|
||||
} else {
|
||||
gpui::transparent_black()
|
||||
})
|
||||
.on_key_down(move |event, window, cx| {
|
||||
if !own.is_focused(window) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
log::info!("dashboard grid: {} to {to:?}", keys);
|
||||
keyed_change(arranged(&keyed_tiles, to, columns), window, cx);
|
||||
})
|
||||
.child(div().size_full().child(card))
|
||||
.child(
|
||||
div()
|
||||
.id((id.clone(), format!("move-{}", tile.key)))
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.right(grip)
|
||||
.h(grip)
|
||||
.cursor_grab()
|
||||
.on_mouse_down(MouseButton::Left, start(Grip::Move))
|
||||
.on_drag(Pull { owner }, |_, _, _, cx| cx.new(|_| EmptyView)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id((id.clone(), format!("size-{}", tile.key)))
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.right_0()
|
||||
.size(grip)
|
||||
.flex()
|
||||
.items_end()
|
||||
.justify_end()
|
||||
.cursor(gpui::CursorStyle::ResizeUpLeftDownRight)
|
||||
.on_mouse_down(MouseButton::Left, start(Grip::Size))
|
||||
.on_drag(Pull { owner }, |_, _, _, cx| cx.new(|_| EmptyView))
|
||||
.child(
|
||||
Icon::new(IconName::GripHorizontal)
|
||||
.size(IconSize::Xs)
|
||||
.color(colors.fg_subtle),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
let grow = event.keystroke.modifiers.shift;
|
||||
let key = event.keystroke.key.as_str();
|
||||
let (dx, dy): (i64, i64) = match key {
|
||||
"left" => (-1, 0),
|
||||
"right" => (1, 0),
|
||||
"up" => (0, -1),
|
||||
"down" => (0, 1),
|
||||
_ => return,
|
||||
};
|
||||
cx.stop_propagation();
|
||||
let tile = keyed_tiles
|
||||
.iter()
|
||||
.find(|tile| tile.key == keys)
|
||||
.expect("a tile of the grid");
|
||||
let to = match (grow, dy) {
|
||||
(false, 0) => {
|
||||
pulled(tile, Grip::Move, (dx as f32, 0.0), (1.0, 1.0))
|
||||
}
|
||||
(false, _) => match stepped(&keyed_tiles, &keys, dy > 0) {
|
||||
Some(to) => to,
|
||||
None => return,
|
||||
},
|
||||
(true, _) => {
|
||||
pulled(tile, Grip::Size, (dx as f32, dy as f32), (1.0, 1.0))
|
||||
}
|
||||
};
|
||||
log::info!("dashboard grid: {} to {to:?}", keys);
|
||||
if let Some(tiles) = arranged(&keyed_tiles, to, columns) {
|
||||
keyed_change(tiles, window, cx);
|
||||
}
|
||||
})
|
||||
.child(div().size_full().child(card))
|
||||
.child(
|
||||
div()
|
||||
.id((id.clone(), format!("move-{}", tile.key)))
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.right(grip)
|
||||
.h(grip)
|
||||
.cursor_grab()
|
||||
.on_mouse_down(MouseButton::Left, start(Grip::Move))
|
||||
.on_drag(Pull { owner }, |_, _, _, cx| cx.new(|_| EmptyView)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id((id.clone(), format!("size-{}", tile.key)))
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.right_0()
|
||||
.size(grip)
|
||||
.flex()
|
||||
.items_end()
|
||||
.justify_end()
|
||||
.cursor(gpui::CursorStyle::ResizeUpLeftDownRight)
|
||||
.on_mouse_down(MouseButton::Left, start(Grip::Size))
|
||||
.on_drag(Pull { owner }, |_, _, _, cx| cx.new(|_| EmptyView))
|
||||
.child(
|
||||
Icon::new(IconName::GripHorizontal)
|
||||
.size(IconSize::Xs)
|
||||
.color(colors.fg_subtle),
|
||||
),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert!(
|
||||
cards.is_empty(),
|
||||
"dashboard grid: cards with no tile: {:?}",
|
||||
cards.iter().map(|(key, _)| key).collect::<Vec<_>>()
|
||||
);
|
||||
if !cards.is_empty() {
|
||||
let keys: Vec<_> = cards.iter().map(|(key, _)| key).collect();
|
||||
log::error!("dashboard grid: cards with no tile {keys:?}; left out");
|
||||
}
|
||||
let ghost = landing.as_ref().map(|tile| {
|
||||
div()
|
||||
.absolute()
|
||||
@@ -334,7 +337,9 @@ impl RenderOnce for DashboardGrid {
|
||||
});
|
||||
if let Some(to) = landing {
|
||||
log::info!("dashboard grid: {} lands at {to:?}", to.key);
|
||||
on_change(arranged(tiles, to, columns), window, cx);
|
||||
if let Some(tiles) = arranged(tiles, to, columns) {
|
||||
on_change(tiles, window, cx);
|
||||
}
|
||||
}
|
||||
};
|
||||
let end = Rc::new(end);
|
||||
|
||||
+28
-3
@@ -27,7 +27,7 @@ impl Render for Wall {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let owner = cx.entity();
|
||||
let grid = self.tiles.iter().fold(
|
||||
DashboardGrid::new("grid", 12, self.tiles.clone()).on_change(move |tiles, _, cx| {
|
||||
DashboardGrid::new("grid", 12, self.tiles.clone(), move |tiles, _, cx| {
|
||||
owner.update(cx, |wall, cx| {
|
||||
wall.tiles = tiles.clone();
|
||||
wall.heard.push(tiles);
|
||||
@@ -269,9 +269,9 @@ fn filters(_: &Board, owner: Entity<Board>) -> AnyElement {
|
||||
"filters",
|
||||
TimeWindow::ALL[0],
|
||||
[filter("region"), filter("env")],
|
||||
|_, _, _| {},
|
||||
move |key, value, _, cx| say(&owner, format!("{key} {value:?}"), cx),
|
||||
)
|
||||
.on_window(|_, _, _| {})
|
||||
.on_filter(move |key, value, _, cx| say(&owner, format!("{key} {value:?}"), cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -309,3 +309,28 @@ fn a_narrow_grid_stacks_its_tiles_in_reading_order(cx: &mut TestAppContext) {
|
||||
);
|
||||
assert_eq!(a.size.width, px(300.0), "each fills the width");
|
||||
}
|
||||
|
||||
/// A grid at `width` whose tile b has no card and whose card c has no tile.
|
||||
struct Mismatched(f32);
|
||||
|
||||
impl Render for Mismatched {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
let tiles = [
|
||||
Tile::new("a", (0, 0), (6, 2)),
|
||||
Tile::new("b", (6, 0), (6, 2)),
|
||||
];
|
||||
let grid = DashboardGrid::new("grid", 12, tiles, |_, _, _| {})
|
||||
.card("a", DashboardCard::new("A"))
|
||||
.card("c", DashboardCard::new("C"));
|
||||
div().w(px(self.0)).child(grid)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn tiles_and_cards_that_do_not_match_are_left_out(cx: &mut TestAppContext) {
|
||||
cx.update(Theme::init);
|
||||
for width in [960.0, 320.0] {
|
||||
let (_, cx) = cx.add_window_view(move |_, _| Mismatched(width));
|
||||
settle(cx);
|
||||
}
|
||||
}
|
||||
+31
-18
@@ -58,14 +58,16 @@ fn compacted(mut tiles: Vec<Tile>) -> Vec<Tile> {
|
||||
out
|
||||
}
|
||||
|
||||
/// The tiles once `to` takes the place of the tile of its key: it goes where it was put, the tiles it lands on move down below it, then every tile, it too, rises as far as the ones above allow.
|
||||
pub fn arranged(tiles: &[Tile], to: Tile, columns: u32) -> Vec<Tile> {
|
||||
/// The tiles once `to` takes the place of the tile of its key: it goes where it was put, the tiles it lands on move down below it, then every tile, it too, rises as far as the ones above allow. None, logged, when the grid lost that tile.
|
||||
pub fn arranged(tiles: &[Tile], to: Tile, columns: u32) -> Option<Vec<Tile>> {
|
||||
assert!(columns > 0, "a grid of no columns");
|
||||
assert!(
|
||||
tiles.iter().any(|tile| tile.key == to.key),
|
||||
"no tile {} on the grid",
|
||||
to.key
|
||||
);
|
||||
if !tiles.iter().any(|tile| tile.key == to.key) {
|
||||
log::error!(
|
||||
"dashboard grid: no tile {} on the grid; nothing moves",
|
||||
to.key
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let mut placed = vec![to.clone().fitted(columns)];
|
||||
let mut rest: Vec<Tile> = tiles
|
||||
.iter()
|
||||
@@ -79,15 +81,15 @@ pub fn arranged(tiles: &[Tile], to: Tile, columns: u32) -> Vec<Tile> {
|
||||
}
|
||||
placed.push(tile);
|
||||
}
|
||||
compacted(placed)
|
||||
Some(compacted(placed))
|
||||
}
|
||||
|
||||
/// Where a tile goes on a step down or up: below the next tile under it, or into the place of the next one above; none at the edge. Both overlap its columns.
|
||||
pub fn stepped(tiles: &[Tile], key: &str, down: bool) -> Option<Tile> {
|
||||
let tile = tiles
|
||||
.iter()
|
||||
.find(|tile| tile.key == key)
|
||||
.unwrap_or_else(|| panic!("no tile {key} on the grid"));
|
||||
let Some(tile) = tiles.iter().find(|tile| tile.key == key) else {
|
||||
log::error!("dashboard grid: no tile {key} on the grid");
|
||||
return None;
|
||||
};
|
||||
let beside = |other: &&Tile| {
|
||||
other.key != tile.key && other.x < tile.x + tile.w && tile.x < other.x + other.w
|
||||
};
|
||||
@@ -131,9 +133,17 @@ mod tests {
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tile_gone_from_the_grid_moves_nothing() {
|
||||
let gone = Tile::new("gone", (0, 0), (6, 2));
|
||||
assert_eq!(arranged(&grid(), gone, 12), None);
|
||||
assert_eq!(stepped(&grid(), "gone", true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tile_dropped_on_another_pushes_it_below() {
|
||||
let moved = arranged(&grid(), Tile::new("b", (0, 0), (6, 2)), 12);
|
||||
let moved =
|
||||
arranged(&grid(), Tile::new("b", (0, 0), (6, 2)), 12).expect("a tile of the grid");
|
||||
assert_eq!(at(&moved, "b"), (0, 0, 6, 2), "the dropped tile stays");
|
||||
assert_eq!(at(&moved, "a"), (0, 2, 6, 2), "a goes below b");
|
||||
assert_eq!(at(&moved, "c"), (0, 4, 12, 3), "and c below a");
|
||||
@@ -141,20 +151,23 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn tiles_rise_into_the_gap_a_tile_leaves() {
|
||||
let moved = arranged(&grid(), Tile::new("a", (0, 9), (6, 2)), 12);
|
||||
let moved =
|
||||
arranged(&grid(), Tile::new("a", (0, 9), (6, 2)), 12).expect("a tile of the grid");
|
||||
assert_eq!(at(&moved, "c"), (0, 2, 12, 3), "c keeps below b");
|
||||
assert_eq!(at(&moved, "a"), (0, 5, 6, 2), "a rises to rest on c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tile_stays_inside_the_grid() {
|
||||
let moved = arranged(&grid(), Tile::new("a", (10, 0), (6, 2)), 12);
|
||||
let moved =
|
||||
arranged(&grid(), Tile::new("a", (10, 0), (6, 2)), 12).expect("a tile of the grid");
|
||||
assert_eq!(
|
||||
at(&moved, "a"),
|
||||
(6, 0, 6, 2),
|
||||
"pulled back from the right edge"
|
||||
);
|
||||
let wide = arranged(&grid(), Tile::new("b", (0, 0), (20, 1)), 12);
|
||||
let wide =
|
||||
arranged(&grid(), Tile::new("b", (0, 0), (20, 1)), 12).expect("a tile of the grid");
|
||||
assert_eq!(at(&wide, "b"), (0, 0, 12, 1), "no wider than the grid");
|
||||
}
|
||||
|
||||
@@ -162,12 +175,12 @@ mod tests {
|
||||
fn a_step_down_passes_the_tile_under_and_a_step_up_takes_its_place() {
|
||||
let down = stepped(&grid(), "a", true).expect("c lies under a");
|
||||
assert_eq!((down.x, down.y), (0, 5), "a steps under the bottom of c");
|
||||
let moved = arranged(&grid(), down, 12);
|
||||
let moved = arranged(&grid(), down, 12).expect("a tile of the grid");
|
||||
assert_eq!(at(&moved, "c"), (0, 2, 12, 3), "c rests under b");
|
||||
assert_eq!(at(&moved, "a"), (0, 5, 6, 2), "a rests under c");
|
||||
let up = stepped(&moved, "a", false).expect("c lies over a");
|
||||
assert_eq!(
|
||||
at(&arranged(&moved, up, 12), "a"),
|
||||
at(&arranged(&moved, up, 12).expect("a tile of the grid"), "a"),
|
||||
(0, 0, 6, 2),
|
||||
"back on top"
|
||||
);
|
||||
|
||||
+2
-1
@@ -48,7 +48,7 @@ Each item reads every site in its folders, fixes what the rules name, updates ca
|
||||
- [x] P31 Project — `src/project`
|
||||
- [x] P32 Canvas & Design — `src/canvas`
|
||||
- [x] P33 DB & Dev Tools — `src/devtools`
|
||||
- [ ] P34 Dashboard — `src/dashboard`
|
||||
- [x] P34 Dashboard — `src/dashboard`
|
||||
- [ ] P35 Settings — `src/settings`
|
||||
- [ ] P36 Account — `src/account`
|
||||
- [ ] P37 Onboarding — `src/onboarding`
|
||||
@@ -99,3 +99,4 @@ One line per item: what changed, what stayed and why, review rounds.
|
||||
- P31: a gone assignee logs and shows nobody; a milestone past its count fills, the text real; a roadmap past jiff's range ends inclusively on its last day; the database timeline leaves out and counts tasks due before they start and counts months in `i32`, and its calendar keeps to days `calendar::within` holds; the board's drag and drop take only columns, cards and places this frame holds. Kept: one list's uniqueness, an initiative's and a milestone's own order, shares, month counts, pomodoro lengths, issue keys, lookups over the components' own tables. Review: 3 rounds.
|
||||
- P32: the eight design panels take their handlers in `new` (AGENTS.md says so); edges, links, wires, links in progress, resizes, restacks, topics and whiteboard edits that name a node, port, shape, layer or topic gone from this frame log and change nothing; a wire's release fits its source as it now stands; `Topic::adding`, `removing` and `renaming` return `Option`; a mind map's selection, a palette's tool and a history position no longer held mark none. Kept: one-value contracts (alignment counts, frames, zoom, polygon sides, brush hues and type weights among the theme's tables), lookups over the panels' own option tables, button-gated expects. Review: 2 rounds.
|
||||
- P33: six devtools take their handlers in `new` (contrast checker, environment selector, GraphQL explorer, WebSocket console, structure editor, index manager); `collection::moved` returns `Option` for a gone key or target or a drop into a request; a gone environment shows none chosen; an ER table without a place stays out. Kept: readings, costs, lookups over the components' own tables, button-gated expects, JSON that read writes back. Review: 1 round.
|
||||
- P34: the refresh interval selector, filter bar and dashboard grid take their handlers in `new`; `tiles::arranged` and `stepped` return `Option` for a tile gone from the grid, so a landing or step after the owner removed it moves nothing; tiles without cards and cards without tiles stay out. Kept: columns, cells, intervals offered, services and uptimes, lookups from one render. Review: 1 round.
|
||||
Reference in new issue
Block a user