Skip to main content

Button

Struct Button 

Source
pub struct Button<M = ()>(/* private fields */);
Expand description

button: a push button. A click sends action with actions.click ("click" by default); it never takes focus.

Implementations§

Source§

impl<M> Button<M>

Source

pub fn key(self, key: impl Into<NodeKey>) -> Self

Identity among siblings: keeps the node’s id (and Tern’s view state) across reorders. A string, or an integer.

Examples found in repository?
examples/ask.rs (line 38)
28fn view() -> View<()> {
29	let sizes = SIZES.iter().map(|(value, label)| choice(value, label));
30	View::new().main([html::form().key("ask").child(
31		ui::card()
32			.key("card")
33			.head("Which size?")
34			.max(Bound::w(Extent::Ch(48.0)))
35			.child(ui::row().key("sizes").gap("lg").children(sizes))
36			.child(
37				ui::button("Create")
38					.key("go")
39					.variant("primary")
40					.actions(Actions::new().click("submit")),
41			),
42	)])
43}
More examples
Hide additional examples
examples/dashboard.rs (line 708)
680	fn head(&self) -> ui::Row<Msg> {
681		let title = ui::text(spans![
682			ui::span("Fleet").style("title"),
683			" ",
684			ui::span(format!(
685				"production · {} services · up {}",
686				self.services.len(),
687				ago(UPTIME + u64::from(self.ticks))
688			))
689			.style("muted"),
690		])
691		.key("title");
692		let worst = self
693			.services
694			.iter()
695			.map(|s| s.health)
696			.max_by_key(|h| match h {
697				Health::Healthy => 0,
698				Health::Degraded => 1,
699				Health::Down => 2,
700			})
701			.unwrap_or(Health::Healthy);
702		let status = match worst {
703			Health::Healthy => ui::badge("Operational").tone(Tone::Success),
704			Health::Degraded => ui::badge("Degraded").tone(Tone::Warning),
705			Health::Down => ui::badge("Partial outage").tone(Tone::Error),
706		};
707		let refresh = ui::button("Refresh")
708			.key("refresh")
709			.icon("repeat")
710			.keys(["r"])
711			.on_click(Msg::Refresh);
712		ui::row()
713			.key("head")
714			.justify("between")
715			.align("center")
716			.child(title)
717			.child(
718				ui::row()
719					.key("end")
720					.gap("sm")
721					.align("center")
722					.children(nodes![status.key("status"), refresh]),
723			)
724	}
725
726	/// The stats strip: at most four tiles, directly on the page.
727	fn stats(&self) -> ui::Stats<Msg> {
728		let total = self.services.len();
729		let healthy = self
730			.services
731			.iter()
732			.filter(|s| s.health == Health::Healthy)
733			.count();
734		let deploys = |state| self.deploys.iter().filter(|d| d.state == state).count();
735		let open = self
736			.incidents
737			.iter()
738			.filter(|i| i.phase != Phase::Resolved)
739			.count();
740		let unacked = self
741			.incidents
742			.iter()
743			.filter(|i| i.phase == Phase::Open)
744			.count();
745		let rps: f64 = self.services.iter().filter_map(|s| s.rps.last()).sum();
746		let mut services = StatItem::new("Healthy", format!("{healthy}/{total}")).sub("services");
747		if healthy < total {
748			services = services.tone(Tone::Warning);
749		}
750		let mut incidents =
751			StatItem::new("Open incidents", open.to_string()).sub(format!("{unacked} unacknowledged"));
752		if unacked > 0 {
753			incidents = incidents.tone(Tone::Error);
754		}
755		ui::stats([
756			services,
757			StatItem::new("Rolling out", deploys(State::Running).to_string())
758				.sub(format!("{} need approval", deploys(State::Waiting))),
759			incidents,
760			StatItem::new("Traffic", format!("{rps:.0}")).sub("req/s"),
761		])
762	}
763
764	/// The tabs: one per view, with muted counts.
765	fn nav(&self) -> ui::Tabs<Msg> {
766		let count = |n: usize| ui::span(n.to_string()).style("muted");
767		let open = self
768			.incidents
769			.iter()
770			.filter(|i| i.phase != Phase::Resolved)
771			.count();
772		ui::tabs()
773			.key("nav")
774			.active(self.page.id())
775			.tab("services", spans!["Services ", count(self.services.len())])
776			.tab("deploys", spans!["Deploys ", count(self.deploys.len())])
777			.tab("incidents", spans!["Incidents ", count(open)])
778			.on_select(|id| Msg::Page(id.to_owned()))
779	}
780
781	/// The active view: its table beside the selected row's detail card
782	/// (under it when `plain`).
783	fn body(&self, plain: bool) -> Node<Msg> {
784		let page = self.page;
785		let selected = self.selected[page.index()];
786		let (list, detail) = match page {
787			Page::Services => (
788				nodes![self.services_table()],
789				selected
790					.and_then(|id| self.services.iter().find(|s| s.id == id))
791					.map(service_card),
792			),
793			Page::Deploys => (
794				nodes![self.deploys_bar(), self.deploys_table()],
795				selected
796					.and_then(|id| self.deploys.iter().find(|d| d.id == id))
797					.map(deploy_card),
798			),
799			Page::Incidents => (
800				nodes![self.incidents_table()],
801				selected
802					.and_then(|id| self.incidents.iter().find(|i| i.id == id))
803					.map(incident_card),
804			),
805		};
806		let mut parts: Vec<Node<Msg>> = nodes![
807			ui::col()
808				.key("list")
809				.gap("sm")
810				.grow(1.0)
811				.basis(0.0)
812				.children(list)
813		];
814		if let Some(card) = detail {
815			parts.push(
816				card
817					.key("detail")
818					.min(Bound::w(Extent::Ch(44.0)))
819					.max(Bound::w(Extent::Ch(56.0)))
820					.into(),
821			);
822		}
823		if plain {
824			ui::col().key(page.id()).gap("md").children(parts).into()
825		} else {
826			ui::row()
827				.key(page.id())
828				.align("stretch")
829				.gap("lg")
830				.children(parts)
831				.into()
832		}
833	}
834
835	/// A selectable table of `page` with its handlers and selection.
836	fn table(&self, page: Page, empty: &str) -> ui::Table<Msg> {
837		let mut table = ui::table()
838			.key("table")
839			.selectable(true)
840			.empty(empty)
841			.on_select(move |id| Msg::Select(page, id.to_owned()))
842			.on_activate(move |id| Msg::Open(page, id.to_owned()));
843		if let Some(id) = self.selected[page.index()] {
844			table = table.selected(id);
845		}
846		table
847	}
848
849	/// The services table.
850	fn services_table(&self) -> ui::Table<Msg> {
851		let mut table = self
852			.table(Page::Services, "No services yet")
853			.col(Column::new("name", "Service").grow(1.0).priority(6.0))
854			.col(Column::new("region", "Region").priority(3.0))
855			.col(Column::new("version", "Version").mono(true).priority(2.0))
856			.col(Column::new("traffic", "Traffic").priority(1.0))
857			.col(
858				Column::new("rps", "req/s")
859					.align(TextAlign::End)
860					.mono(true)
861					.priority(4.0),
862			)
863			.col(
864				Column::new("p95", "p95")
865					.align(TextAlign::End)
866					.mono(true)
867					.priority(4.0),
868			)
869			.col(
870				Column::new("errors", "Errors")
871					.align(TextAlign::End)
872					.mono(true)
873					.priority(4.0),
874			)
875			.col(Column::new("health", "Health").priority(5.0));
876		for s in &self.services {
877			let (label, tone) = s.health.look();
878			let mut errors = TextCell::new(format!("{:.1}%", s.errors));
879			if s.errors >= 1.0 {
880				errors = errors.tone(Tone::Error);
881			}
882			table = table.row(
883				TableRow::new(s.id)
884					.cell("name", TextCell::new(s.id).dot(tone.clone()))
885					.cell("region", s.region)
886					.cell("version", s.version)
887					.cell("traffic", SparkCell::new(s.rps.clone()))
888					.cell("rps", format!("{:.0}", s.rps.last().copied().unwrap_or_default()))
889					.cell("p95", format!("{:.0} ms", s.p95))
890					.cell("errors", errors)
891					.cell("health", BadgeSpec::new(label).tone(tone)),
892			);
893		}
894		table
895	}
896
897	/// The deploys toolbar: a muted description and the filter.
898	fn deploys_bar(&self) -> ui::Row<Msg> {
899		let attention = self
900			.deploys
901			.iter()
902			.filter(|d| matches!(d.state, State::Waiting | State::Failed))
903			.count();
904		ui::row()
905			.key("bar")
906			.justify("between")
907			.align("center")
908			.child(
909				ui::text(ui::span("Rollouts to production, newest first").style("muted")).key("about"),
910			)
911			.child(
912				ui::tabs()
913					.key("filter")
914					.variant("segmented")
915					.active(self.filter.clone())
916					.tab("all", "All")
917					.tab("attention", spans![
918						"Needs you ",
919						ui::span(attention.to_string()).style("muted")
920					])
921					.on_select(|id| Msg::Filter(id.to_owned())),
922			)
923	}
924
925	/// The deploys table.
926	fn deploys_table(&self) -> ui::Table<Msg> {
927		let mut table = self
928			.table(Page::Deploys, "Nothing needs you right now")
929			.col(Column::new("service", "Service").grow(1.0).priority(6.0))
930			.col(Column::new("commit", "Commit").mono(true).priority(3.0))
931			.col(Column::new("region", "Region").priority(2.0))
932			.col(Column::new("traffic", "Traffic").priority(1.0))
933			.col(Column::new("rollout", "Rollout").priority(1.0))
934			.col(Column::new("state", "State").priority(5.0));
935		for d in self.shown_deploys() {
936			let (label, tone) = d.state.look();
937			table = table.row(
938				TableRow::new(d.id)
939					.cell("service", TextCell::new(d.service).dot(tone.clone()))
940					.cell("commit", d.commit)
941					.cell("region", d.region)
942					.cell("traffic", SparkCell::new(d.rps.clone()))
943					.cell("rollout", MeterCell {
944						value: Some(d.done),
945						title: Some(format!("{:.0}%", d.done * 100.0)),
946						..MeterCell::default()
947					})
948					.cell("state", BadgeSpec::new(label).tone(tone)),
949			);
950		}
951		table
952	}
953
954	/// The incidents table.
955	fn incidents_table(&self) -> ui::Table<Msg> {
956		let mut table = self
957			.table(Page::Incidents, "No incidents. All quiet.")
958			.col(Column::new("title", "Incident").grow(1.0).priority(6.0))
959			.col(Column::new("id", "Id").mono(true).priority(2.0))
960			.col(Column::new("service", "Service").priority(3.0))
961			.col(Column::new("severity", "Severity").priority(4.0))
962			.col(
963				Column::new("age", "Open for")
964					.align(TextAlign::End)
965					.mono(true)
966					.priority(4.0),
967			)
968			.col(Column::new("phase", "State").priority(5.0));
969		for i in &self.incidents {
970			let (sev, sev_tone) = i.severity.look();
971			let (phase, phase_tone) = i.phase.look();
972			table = table.row(
973				TableRow::new(i.id)
974					.cell("title", TextCell::new(i.title).dot(sev_tone.clone()))
975					.cell("id", i.id)
976					.cell("service", i.service)
977					.cell("severity", BadgeSpec::new(sev).tone(sev_tone))
978					.cell("age", ago(i.age))
979					.cell("phase", BadgeSpec::new(phase).tone(phase_tone)),
980			);
981		}
982		table
983	}
984
985	/// The key legend.
986	fn legend(&self) -> ui::Row<Msg> {
987		let key = |k: &str| ui::span(k).style("key");
988		let muted = |t: &str| ui::span(t).style("muted");
989		let keys = if self.confirming.is_some() {
990			spans![key("enter"), muted(" confirm  "), key("esc"), muted(" back")]
991		} else {
992			spans![
993				key("tab"),
994				muted(" views  "),
995				key("j"),
996				key("k"),
997				muted(" move  "),
998				key("enter"),
999				muted(" open  "),
1000				key("a"),
1001				muted(" action  "),
1002				key("r"),
1003				muted(" refresh  "),
1004				key("q"),
1005				muted(" quit"),
1006			]
1007		};
1008		let note = if self.note.is_empty() {
1009			"Up to date"
1010		} else {
1011			self.note.as_str()
1012		};
1013		ui::row()
1014			.key("legend")
1015			.justify("between")
1016			.align("center")
1017			.child(ui::text(keys).key("keys"))
1018			.child(ui::text(muted(note)).key("note"))
1019	}
1020}
1021
1022/// A detail card: the head with the primary action at its end, the `kv` and
1023/// the `danger` trigger.
1024fn detail(
1025	page: Page,
1026	id: &'static str,
1027	meta: impl Into<Text>,
1028	kv: ui::Kv<Msg>,
1029	action: Option<&'static str>,
1030	risk: &Risk,
1031) -> ui::Card<Msg> {
1032	let mut card = ui::card().head(id).meta(meta);
1033	if let Some(label) = action {
1034		card = card.end(
1035			ui::button(label)
1036				.key("act")
1037				.variant("primary")
1038				.keys(["a"])
1039				.on_click(Msg::Act(page, id.into())),
1040		);
1041	}
1042	let mut trigger = ui::button(risk.button)
1043		.key("risk")
1044		.variant("danger")
1045		.on_click(Msg::Ask(page, id.into()));
1046	if let Some(why) = risk.blocked {
1047		trigger = trigger.disabled(why);
1048	}
1049	card
1050		.child(kv.key("kv"))
1051		.child(ui::row().key("bar").child(trigger))
1052}
1053
1054/// The selected service's detail card.
1055fn service_card(s: &Service) -> ui::Card<Msg> {
1056	let (label, _) = s.health.look();
1057	let rps = s.rps.last().copied().unwrap_or_default();
1058	let kv = ui::kv()
1059		.item("Health", ui::span(label).style("strong"))
1060		.item("Version", mono(s.version))
1061		.item("Replicas", mono(s.replicas.to_string()))
1062		.item("Traffic", mono(format!("{rps:.0} req/s")))
1063		.item("p95", mono(format!("{:.0} ms", s.p95)))
1064		.item("Errors", mono(format!("{:.1}%", s.errors)));
1065	detail(Page::Services, s.id, s.region, kv, Some(s.action()), &s.risk())
1066}
1067
1068/// The selected deploy's detail card.
1069fn deploy_card(d: &Deploy) -> ui::Card<Msg> {
1070	let (label, _) = d.state.look();
1071	let rps = d.rps.last().copied().unwrap_or_default();
1072	let kv = ui::kv()
1073		.item("State", ui::span(label).style("strong"))
1074		.item("Service", d.service)
1075		.item("Commit", mono(d.commit))
1076		.item("Rollout", mono(format!("{:.0}%", d.done * 100.0)))
1077		.item("Traffic", mono(format!("{rps:.0} req/s")));
1078	detail(Page::Deploys, d.id, d.region, kv, d.action(), &d.risk())
1079}
1080
1081/// The selected incident's detail card.
1082fn incident_card(i: &Incident) -> ui::Card<Msg> {
1083	let (phase, _) = i.phase.look();
1084	let (sev, _) = i.severity.look();
1085	let kv = ui::kv()
1086		.item("State", ui::span(phase).style("strong"))
1087		.item("What", i.title)
1088		.item("Service", i.service)
1089		.item("Severity", sev)
1090		.item("Open for", mono(ago(i.age)));
1091	detail(Page::Incidents, i.id, i.service, kv, i.action(), &i.risk())
1092}
1093
1094/// The confirm strip before a destructive action: `error` with a `danger`
1095/// confirm; its buttons sit in the head's end.
1096fn confirm(risk: &Risk) -> ui::Card<Msg> {
1097	let buttons = ui::row()
1098		.gap("sm")
1099		.child(
1100			ui::button("Keep")
1101				.key("keep")
1102				.variant("quiet")
1103				.keys(["esc"])
1104				.on_click(Msg::Keep),
1105		)
1106		.child(
1107			ui::button(risk.button)
1108				.key("go")
1109				.variant("danger")
1110				.keys(["enter"])
1111				.on_click(Msg::Confirm),
1112		);
1113	ui::card()
1114		.key("confirm")
1115		.tone(Tone::Error)
1116		.head(risk.ask.clone())
1117		.end(buttons)
1118		.child(ui::text(risk.then).key("t"))
1119}
Source

pub fn role(self, role: impl Into<String>) -> Self

A name of yours, exposed to CSS as data-role.

Source

pub fn tone(self, tone: impl Into<Tone>) -> Self

The semantic color of the node’s chrome.

Source

pub fn hidden(self, hidden: bool) -> Self

Keeps the node mounted but not laid out.

Source

pub fn mark(self, mark: impl Into<Mark>) -> Self

A transient selection mark drawn over the node.

Source

pub fn aria(self, aria: impl Into<String>) -> Self

The accessible name of a node that shows no text.

Source

pub fn hovercard<N>(self, card: impl Into<Node<N>>) -> Self

A floating card of card’s tree shown while the pointer rests on the node, in place of title. Sent id-less (ids, keys and handlers inside are ignored); an older Tern ignores it.

Source

pub fn href(self, href: impl Into<String>) -> Self

A link: ⌘-click opens it; copy and open use it.

Source

pub fn grow(self, grow: f64) -> Self

Flex grow inside a row or col.

Source

pub fn shrink(self, shrink: f64) -> Self

Flex shrink inside a row or col.

Source

pub fn basis(self, basis: impl Into<Basis>) -> Self

Flex basis: a fraction of the parent, or the content’s size.

Source

pub fn prop(self, name: &str, value: impl Serialize) -> Self

Sets any prop untyped; null removes it.

Source

pub fn min(self, min: Bound) -> Self

Lower size bounds.

Source

pub fn title(self, title: impl Into<String>) -> Self

A tooltip.

Source

pub fn max(self, max: Bound) -> Self

Upper size bounds (a max.h also clips the node).

Source

pub fn actions(self, actions: Actions) -> Self

What a pointer does, by action name.

Examples found in repository?
examples/ask.rs (line 40)
28fn view() -> View<()> {
29	let sizes = SIZES.iter().map(|(value, label)| choice(value, label));
30	View::new().main([html::form().key("ask").child(
31		ui::card()
32			.key("card")
33			.head("Which size?")
34			.max(Bound::w(Extent::Ch(48.0)))
35			.child(ui::row().key("sizes").gap("lg").children(sizes))
36			.child(
37				ui::button("Create")
38					.key("go")
39					.variant("primary")
40					.actions(Actions::new().click("submit")),
41			),
42	)])
43}
Source

pub fn on_click(self, msg: M) -> Self
where M: Clone + Send + Sync + 'static,

Sends msg on a click (sets actions.click to "click" unless the node names its click action itself).

Examples found in repository?
examples/dashboard.rs (line 711)
680	fn head(&self) -> ui::Row<Msg> {
681		let title = ui::text(spans![
682			ui::span("Fleet").style("title"),
683			" ",
684			ui::span(format!(
685				"production · {} services · up {}",
686				self.services.len(),
687				ago(UPTIME + u64::from(self.ticks))
688			))
689			.style("muted"),
690		])
691		.key("title");
692		let worst = self
693			.services
694			.iter()
695			.map(|s| s.health)
696			.max_by_key(|h| match h {
697				Health::Healthy => 0,
698				Health::Degraded => 1,
699				Health::Down => 2,
700			})
701			.unwrap_or(Health::Healthy);
702		let status = match worst {
703			Health::Healthy => ui::badge("Operational").tone(Tone::Success),
704			Health::Degraded => ui::badge("Degraded").tone(Tone::Warning),
705			Health::Down => ui::badge("Partial outage").tone(Tone::Error),
706		};
707		let refresh = ui::button("Refresh")
708			.key("refresh")
709			.icon("repeat")
710			.keys(["r"])
711			.on_click(Msg::Refresh);
712		ui::row()
713			.key("head")
714			.justify("between")
715			.align("center")
716			.child(title)
717			.child(
718				ui::row()
719					.key("end")
720					.gap("sm")
721					.align("center")
722					.children(nodes![status.key("status"), refresh]),
723			)
724	}
725
726	/// The stats strip: at most four tiles, directly on the page.
727	fn stats(&self) -> ui::Stats<Msg> {
728		let total = self.services.len();
729		let healthy = self
730			.services
731			.iter()
732			.filter(|s| s.health == Health::Healthy)
733			.count();
734		let deploys = |state| self.deploys.iter().filter(|d| d.state == state).count();
735		let open = self
736			.incidents
737			.iter()
738			.filter(|i| i.phase != Phase::Resolved)
739			.count();
740		let unacked = self
741			.incidents
742			.iter()
743			.filter(|i| i.phase == Phase::Open)
744			.count();
745		let rps: f64 = self.services.iter().filter_map(|s| s.rps.last()).sum();
746		let mut services = StatItem::new("Healthy", format!("{healthy}/{total}")).sub("services");
747		if healthy < total {
748			services = services.tone(Tone::Warning);
749		}
750		let mut incidents =
751			StatItem::new("Open incidents", open.to_string()).sub(format!("{unacked} unacknowledged"));
752		if unacked > 0 {
753			incidents = incidents.tone(Tone::Error);
754		}
755		ui::stats([
756			services,
757			StatItem::new("Rolling out", deploys(State::Running).to_string())
758				.sub(format!("{} need approval", deploys(State::Waiting))),
759			incidents,
760			StatItem::new("Traffic", format!("{rps:.0}")).sub("req/s"),
761		])
762	}
763
764	/// The tabs: one per view, with muted counts.
765	fn nav(&self) -> ui::Tabs<Msg> {
766		let count = |n: usize| ui::span(n.to_string()).style("muted");
767		let open = self
768			.incidents
769			.iter()
770			.filter(|i| i.phase != Phase::Resolved)
771			.count();
772		ui::tabs()
773			.key("nav")
774			.active(self.page.id())
775			.tab("services", spans!["Services ", count(self.services.len())])
776			.tab("deploys", spans!["Deploys ", count(self.deploys.len())])
777			.tab("incidents", spans!["Incidents ", count(open)])
778			.on_select(|id| Msg::Page(id.to_owned()))
779	}
780
781	/// The active view: its table beside the selected row's detail card
782	/// (under it when `plain`).
783	fn body(&self, plain: bool) -> Node<Msg> {
784		let page = self.page;
785		let selected = self.selected[page.index()];
786		let (list, detail) = match page {
787			Page::Services => (
788				nodes![self.services_table()],
789				selected
790					.and_then(|id| self.services.iter().find(|s| s.id == id))
791					.map(service_card),
792			),
793			Page::Deploys => (
794				nodes![self.deploys_bar(), self.deploys_table()],
795				selected
796					.and_then(|id| self.deploys.iter().find(|d| d.id == id))
797					.map(deploy_card),
798			),
799			Page::Incidents => (
800				nodes![self.incidents_table()],
801				selected
802					.and_then(|id| self.incidents.iter().find(|i| i.id == id))
803					.map(incident_card),
804			),
805		};
806		let mut parts: Vec<Node<Msg>> = nodes![
807			ui::col()
808				.key("list")
809				.gap("sm")
810				.grow(1.0)
811				.basis(0.0)
812				.children(list)
813		];
814		if let Some(card) = detail {
815			parts.push(
816				card
817					.key("detail")
818					.min(Bound::w(Extent::Ch(44.0)))
819					.max(Bound::w(Extent::Ch(56.0)))
820					.into(),
821			);
822		}
823		if plain {
824			ui::col().key(page.id()).gap("md").children(parts).into()
825		} else {
826			ui::row()
827				.key(page.id())
828				.align("stretch")
829				.gap("lg")
830				.children(parts)
831				.into()
832		}
833	}
834
835	/// A selectable table of `page` with its handlers and selection.
836	fn table(&self, page: Page, empty: &str) -> ui::Table<Msg> {
837		let mut table = ui::table()
838			.key("table")
839			.selectable(true)
840			.empty(empty)
841			.on_select(move |id| Msg::Select(page, id.to_owned()))
842			.on_activate(move |id| Msg::Open(page, id.to_owned()));
843		if let Some(id) = self.selected[page.index()] {
844			table = table.selected(id);
845		}
846		table
847	}
848
849	/// The services table.
850	fn services_table(&self) -> ui::Table<Msg> {
851		let mut table = self
852			.table(Page::Services, "No services yet")
853			.col(Column::new("name", "Service").grow(1.0).priority(6.0))
854			.col(Column::new("region", "Region").priority(3.0))
855			.col(Column::new("version", "Version").mono(true).priority(2.0))
856			.col(Column::new("traffic", "Traffic").priority(1.0))
857			.col(
858				Column::new("rps", "req/s")
859					.align(TextAlign::End)
860					.mono(true)
861					.priority(4.0),
862			)
863			.col(
864				Column::new("p95", "p95")
865					.align(TextAlign::End)
866					.mono(true)
867					.priority(4.0),
868			)
869			.col(
870				Column::new("errors", "Errors")
871					.align(TextAlign::End)
872					.mono(true)
873					.priority(4.0),
874			)
875			.col(Column::new("health", "Health").priority(5.0));
876		for s in &self.services {
877			let (label, tone) = s.health.look();
878			let mut errors = TextCell::new(format!("{:.1}%", s.errors));
879			if s.errors >= 1.0 {
880				errors = errors.tone(Tone::Error);
881			}
882			table = table.row(
883				TableRow::new(s.id)
884					.cell("name", TextCell::new(s.id).dot(tone.clone()))
885					.cell("region", s.region)
886					.cell("version", s.version)
887					.cell("traffic", SparkCell::new(s.rps.clone()))
888					.cell("rps", format!("{:.0}", s.rps.last().copied().unwrap_or_default()))
889					.cell("p95", format!("{:.0} ms", s.p95))
890					.cell("errors", errors)
891					.cell("health", BadgeSpec::new(label).tone(tone)),
892			);
893		}
894		table
895	}
896
897	/// The deploys toolbar: a muted description and the filter.
898	fn deploys_bar(&self) -> ui::Row<Msg> {
899		let attention = self
900			.deploys
901			.iter()
902			.filter(|d| matches!(d.state, State::Waiting | State::Failed))
903			.count();
904		ui::row()
905			.key("bar")
906			.justify("between")
907			.align("center")
908			.child(
909				ui::text(ui::span("Rollouts to production, newest first").style("muted")).key("about"),
910			)
911			.child(
912				ui::tabs()
913					.key("filter")
914					.variant("segmented")
915					.active(self.filter.clone())
916					.tab("all", "All")
917					.tab("attention", spans![
918						"Needs you ",
919						ui::span(attention.to_string()).style("muted")
920					])
921					.on_select(|id| Msg::Filter(id.to_owned())),
922			)
923	}
924
925	/// The deploys table.
926	fn deploys_table(&self) -> ui::Table<Msg> {
927		let mut table = self
928			.table(Page::Deploys, "Nothing needs you right now")
929			.col(Column::new("service", "Service").grow(1.0).priority(6.0))
930			.col(Column::new("commit", "Commit").mono(true).priority(3.0))
931			.col(Column::new("region", "Region").priority(2.0))
932			.col(Column::new("traffic", "Traffic").priority(1.0))
933			.col(Column::new("rollout", "Rollout").priority(1.0))
934			.col(Column::new("state", "State").priority(5.0));
935		for d in self.shown_deploys() {
936			let (label, tone) = d.state.look();
937			table = table.row(
938				TableRow::new(d.id)
939					.cell("service", TextCell::new(d.service).dot(tone.clone()))
940					.cell("commit", d.commit)
941					.cell("region", d.region)
942					.cell("traffic", SparkCell::new(d.rps.clone()))
943					.cell("rollout", MeterCell {
944						value: Some(d.done),
945						title: Some(format!("{:.0}%", d.done * 100.0)),
946						..MeterCell::default()
947					})
948					.cell("state", BadgeSpec::new(label).tone(tone)),
949			);
950		}
951		table
952	}
953
954	/// The incidents table.
955	fn incidents_table(&self) -> ui::Table<Msg> {
956		let mut table = self
957			.table(Page::Incidents, "No incidents. All quiet.")
958			.col(Column::new("title", "Incident").grow(1.0).priority(6.0))
959			.col(Column::new("id", "Id").mono(true).priority(2.0))
960			.col(Column::new("service", "Service").priority(3.0))
961			.col(Column::new("severity", "Severity").priority(4.0))
962			.col(
963				Column::new("age", "Open for")
964					.align(TextAlign::End)
965					.mono(true)
966					.priority(4.0),
967			)
968			.col(Column::new("phase", "State").priority(5.0));
969		for i in &self.incidents {
970			let (sev, sev_tone) = i.severity.look();
971			let (phase, phase_tone) = i.phase.look();
972			table = table.row(
973				TableRow::new(i.id)
974					.cell("title", TextCell::new(i.title).dot(sev_tone.clone()))
975					.cell("id", i.id)
976					.cell("service", i.service)
977					.cell("severity", BadgeSpec::new(sev).tone(sev_tone))
978					.cell("age", ago(i.age))
979					.cell("phase", BadgeSpec::new(phase).tone(phase_tone)),
980			);
981		}
982		table
983	}
984
985	/// The key legend.
986	fn legend(&self) -> ui::Row<Msg> {
987		let key = |k: &str| ui::span(k).style("key");
988		let muted = |t: &str| ui::span(t).style("muted");
989		let keys = if self.confirming.is_some() {
990			spans![key("enter"), muted(" confirm  "), key("esc"), muted(" back")]
991		} else {
992			spans![
993				key("tab"),
994				muted(" views  "),
995				key("j"),
996				key("k"),
997				muted(" move  "),
998				key("enter"),
999				muted(" open  "),
1000				key("a"),
1001				muted(" action  "),
1002				key("r"),
1003				muted(" refresh  "),
1004				key("q"),
1005				muted(" quit"),
1006			]
1007		};
1008		let note = if self.note.is_empty() {
1009			"Up to date"
1010		} else {
1011			self.note.as_str()
1012		};
1013		ui::row()
1014			.key("legend")
1015			.justify("between")
1016			.align("center")
1017			.child(ui::text(keys).key("keys"))
1018			.child(ui::text(muted(note)).key("note"))
1019	}
1020}
1021
1022/// A detail card: the head with the primary action at its end, the `kv` and
1023/// the `danger` trigger.
1024fn detail(
1025	page: Page,
1026	id: &'static str,
1027	meta: impl Into<Text>,
1028	kv: ui::Kv<Msg>,
1029	action: Option<&'static str>,
1030	risk: &Risk,
1031) -> ui::Card<Msg> {
1032	let mut card = ui::card().head(id).meta(meta);
1033	if let Some(label) = action {
1034		card = card.end(
1035			ui::button(label)
1036				.key("act")
1037				.variant("primary")
1038				.keys(["a"])
1039				.on_click(Msg::Act(page, id.into())),
1040		);
1041	}
1042	let mut trigger = ui::button(risk.button)
1043		.key("risk")
1044		.variant("danger")
1045		.on_click(Msg::Ask(page, id.into()));
1046	if let Some(why) = risk.blocked {
1047		trigger = trigger.disabled(why);
1048	}
1049	card
1050		.child(kv.key("kv"))
1051		.child(ui::row().key("bar").child(trigger))
1052}
1053
1054/// The selected service's detail card.
1055fn service_card(s: &Service) -> ui::Card<Msg> {
1056	let (label, _) = s.health.look();
1057	let rps = s.rps.last().copied().unwrap_or_default();
1058	let kv = ui::kv()
1059		.item("Health", ui::span(label).style("strong"))
1060		.item("Version", mono(s.version))
1061		.item("Replicas", mono(s.replicas.to_string()))
1062		.item("Traffic", mono(format!("{rps:.0} req/s")))
1063		.item("p95", mono(format!("{:.0} ms", s.p95)))
1064		.item("Errors", mono(format!("{:.1}%", s.errors)));
1065	detail(Page::Services, s.id, s.region, kv, Some(s.action()), &s.risk())
1066}
1067
1068/// The selected deploy's detail card.
1069fn deploy_card(d: &Deploy) -> ui::Card<Msg> {
1070	let (label, _) = d.state.look();
1071	let rps = d.rps.last().copied().unwrap_or_default();
1072	let kv = ui::kv()
1073		.item("State", ui::span(label).style("strong"))
1074		.item("Service", d.service)
1075		.item("Commit", mono(d.commit))
1076		.item("Rollout", mono(format!("{:.0}%", d.done * 100.0)))
1077		.item("Traffic", mono(format!("{rps:.0} req/s")));
1078	detail(Page::Deploys, d.id, d.region, kv, d.action(), &d.risk())
1079}
1080
1081/// The selected incident's detail card.
1082fn incident_card(i: &Incident) -> ui::Card<Msg> {
1083	let (phase, _) = i.phase.look();
1084	let (sev, _) = i.severity.look();
1085	let kv = ui::kv()
1086		.item("State", ui::span(phase).style("strong"))
1087		.item("What", i.title)
1088		.item("Service", i.service)
1089		.item("Severity", sev)
1090		.item("Open for", mono(ago(i.age)));
1091	detail(Page::Incidents, i.id, i.service, kv, i.action(), &i.risk())
1092}
1093
1094/// The confirm strip before a destructive action: `error` with a `danger`
1095/// confirm; its buttons sit in the head's end.
1096fn confirm(risk: &Risk) -> ui::Card<Msg> {
1097	let buttons = ui::row()
1098		.gap("sm")
1099		.child(
1100			ui::button("Keep")
1101				.key("keep")
1102				.variant("quiet")
1103				.keys(["esc"])
1104				.on_click(Msg::Keep),
1105		)
1106		.child(
1107			ui::button(risk.button)
1108				.key("go")
1109				.variant("danger")
1110				.keys(["enter"])
1111				.on_click(Msg::Confirm),
1112		);
1113	ui::card()
1114		.key("confirm")
1115		.tone(Tone::Error)
1116		.head(risk.ask.clone())
1117		.end(buttons)
1118		.child(ui::text(risk.then).key("t"))
1119}
Source

pub fn on_dblclick(self, msg: M) -> Self
where M: Clone + Send + Sync + 'static,

Sends msg on a double click (sets actions.dblclick to "dblclick" unless the node names it).

Source

pub fn on_menu(self, act: impl Into<String>, msg: M) -> Self
where M: Clone + Send + Sync + 'static,

Adds act to the context menu and sends msg when it is picked.

Source

pub fn on_action(self, act: impl Into<String>, msg: M) -> Self
where M: Clone + Send + Sync + 'static,

Sends msg on action events whose action is act (name or name=value).

Source

pub fn on_action_with( self, act: impl Into<String>, f: impl Fn(&Action) -> M + Send + Sync + 'static, ) -> Self

Maps action events whose action is act to a message.

Source

pub fn on_toggle(self, f: impl Fn(bool) -> M + Send + Sync + 'static) -> Self

Maps toggle events (the new collapsed; a menu, select or trace line that opened is not collapsed) to a message.

Source

pub fn on_select(self, f: impl Fn(&str) -> M + Send + Sync + 'static) -> Self

Maps select events (the item) to a message. On a list’s item the item’s own handler runs first.

Source

pub fn on_activate(self, f: impl Fn(&str) -> M + Send + Sync + 'static) -> Self

Maps activate events (the item) to a message.

Source

pub fn on_change(self, f: impl Fn(&Change) -> M + Send + Sync + 'static) -> Self

Maps change events to a message.

Source

pub fn on_focus(self, msg: M) -> Self
where M: Clone + Send + Sync + 'static,

Sends msg on focus events (a click asked for the keys).

Source

pub fn on_edit(self, f: impl Fn(&Edit) -> M + Send + Sync + 'static) -> Self

Maps edit events (native editing) to a message.

Source

pub fn on_undo(self, msg: M) -> Self
where M: Clone + Send + Sync + 'static,

Sends msg on undo events.

Source

pub fn on_send(self, f: impl Fn(&str) -> M + Send + Sync + 'static) -> Self

Maps send events (the text to submit) to a message.

Source§

impl<M> Button<M>

Source

pub fn text(self, value: impl Into<String>) -> Self

The label.

Source

pub fn icon(self, value: impl Into<Icon>) -> Self

A leading icon.

Examples found in repository?
examples/dashboard.rs (line 709)
680	fn head(&self) -> ui::Row<Msg> {
681		let title = ui::text(spans![
682			ui::span("Fleet").style("title"),
683			" ",
684			ui::span(format!(
685				"production · {} services · up {}",
686				self.services.len(),
687				ago(UPTIME + u64::from(self.ticks))
688			))
689			.style("muted"),
690		])
691		.key("title");
692		let worst = self
693			.services
694			.iter()
695			.map(|s| s.health)
696			.max_by_key(|h| match h {
697				Health::Healthy => 0,
698				Health::Degraded => 1,
699				Health::Down => 2,
700			})
701			.unwrap_or(Health::Healthy);
702		let status = match worst {
703			Health::Healthy => ui::badge("Operational").tone(Tone::Success),
704			Health::Degraded => ui::badge("Degraded").tone(Tone::Warning),
705			Health::Down => ui::badge("Partial outage").tone(Tone::Error),
706		};
707		let refresh = ui::button("Refresh")
708			.key("refresh")
709			.icon("repeat")
710			.keys(["r"])
711			.on_click(Msg::Refresh);
712		ui::row()
713			.key("head")
714			.justify("between")
715			.align("center")
716			.child(title)
717			.child(
718				ui::row()
719					.key("end")
720					.gap("sm")
721					.align("center")
722					.children(nodes![status.key("status"), refresh]),
723			)
724	}
Source

pub fn variant(self, value: impl Into<ButtonVariant>) -> Self

The chrome.

Examples found in repository?
examples/ask.rs (line 39)
28fn view() -> View<()> {
29	let sizes = SIZES.iter().map(|(value, label)| choice(value, label));
30	View::new().main([html::form().key("ask").child(
31		ui::card()
32			.key("card")
33			.head("Which size?")
34			.max(Bound::w(Extent::Ch(48.0)))
35			.child(ui::row().key("sizes").gap("lg").children(sizes))
36			.child(
37				ui::button("Create")
38					.key("go")
39					.variant("primary")
40					.actions(Actions::new().click("submit")),
41			),
42	)])
43}
More examples
Hide additional examples
examples/dashboard.rs (line 1037)
1024fn detail(
1025	page: Page,
1026	id: &'static str,
1027	meta: impl Into<Text>,
1028	kv: ui::Kv<Msg>,
1029	action: Option<&'static str>,
1030	risk: &Risk,
1031) -> ui::Card<Msg> {
1032	let mut card = ui::card().head(id).meta(meta);
1033	if let Some(label) = action {
1034		card = card.end(
1035			ui::button(label)
1036				.key("act")
1037				.variant("primary")
1038				.keys(["a"])
1039				.on_click(Msg::Act(page, id.into())),
1040		);
1041	}
1042	let mut trigger = ui::button(risk.button)
1043		.key("risk")
1044		.variant("danger")
1045		.on_click(Msg::Ask(page, id.into()));
1046	if let Some(why) = risk.blocked {
1047		trigger = trigger.disabled(why);
1048	}
1049	card
1050		.child(kv.key("kv"))
1051		.child(ui::row().key("bar").child(trigger))
1052}
1053
1054/// The selected service's detail card.
1055fn service_card(s: &Service) -> ui::Card<Msg> {
1056	let (label, _) = s.health.look();
1057	let rps = s.rps.last().copied().unwrap_or_default();
1058	let kv = ui::kv()
1059		.item("Health", ui::span(label).style("strong"))
1060		.item("Version", mono(s.version))
1061		.item("Replicas", mono(s.replicas.to_string()))
1062		.item("Traffic", mono(format!("{rps:.0} req/s")))
1063		.item("p95", mono(format!("{:.0} ms", s.p95)))
1064		.item("Errors", mono(format!("{:.1}%", s.errors)));
1065	detail(Page::Services, s.id, s.region, kv, Some(s.action()), &s.risk())
1066}
1067
1068/// The selected deploy's detail card.
1069fn deploy_card(d: &Deploy) -> ui::Card<Msg> {
1070	let (label, _) = d.state.look();
1071	let rps = d.rps.last().copied().unwrap_or_default();
1072	let kv = ui::kv()
1073		.item("State", ui::span(label).style("strong"))
1074		.item("Service", d.service)
1075		.item("Commit", mono(d.commit))
1076		.item("Rollout", mono(format!("{:.0}%", d.done * 100.0)))
1077		.item("Traffic", mono(format!("{rps:.0} req/s")));
1078	detail(Page::Deploys, d.id, d.region, kv, d.action(), &d.risk())
1079}
1080
1081/// The selected incident's detail card.
1082fn incident_card(i: &Incident) -> ui::Card<Msg> {
1083	let (phase, _) = i.phase.look();
1084	let (sev, _) = i.severity.look();
1085	let kv = ui::kv()
1086		.item("State", ui::span(phase).style("strong"))
1087		.item("What", i.title)
1088		.item("Service", i.service)
1089		.item("Severity", sev)
1090		.item("Open for", mono(ago(i.age)));
1091	detail(Page::Incidents, i.id, i.service, kv, i.action(), &i.risk())
1092}
1093
1094/// The confirm strip before a destructive action: `error` with a `danger`
1095/// confirm; its buttons sit in the head's end.
1096fn confirm(risk: &Risk) -> ui::Card<Msg> {
1097	let buttons = ui::row()
1098		.gap("sm")
1099		.child(
1100			ui::button("Keep")
1101				.key("keep")
1102				.variant("quiet")
1103				.keys(["esc"])
1104				.on_click(Msg::Keep),
1105		)
1106		.child(
1107			ui::button(risk.button)
1108				.key("go")
1109				.variant("danger")
1110				.keys(["enter"])
1111				.on_click(Msg::Confirm),
1112		);
1113	ui::card()
1114		.key("confirm")
1115		.tone(Tone::Error)
1116		.head(risk.ask.clone())
1117		.end(buttons)
1118		.child(ui::text(risk.then).key("t"))
1119}
Source

pub fn size(self, value: impl Into<ButtonSize>) -> Self

The height.

Source

pub fn on(self, value: impl Into<bool>) -> Self

Drawn pressed (a toggle that is on).

Source

pub fn disabled(self, value: impl Into<Disabled>) -> Self

Dimmed and silent; a reason string is the tooltip.

Examples found in repository?
examples/dashboard.rs (line 1047)
1024fn detail(
1025	page: Page,
1026	id: &'static str,
1027	meta: impl Into<Text>,
1028	kv: ui::Kv<Msg>,
1029	action: Option<&'static str>,
1030	risk: &Risk,
1031) -> ui::Card<Msg> {
1032	let mut card = ui::card().head(id).meta(meta);
1033	if let Some(label) = action {
1034		card = card.end(
1035			ui::button(label)
1036				.key("act")
1037				.variant("primary")
1038				.keys(["a"])
1039				.on_click(Msg::Act(page, id.into())),
1040		);
1041	}
1042	let mut trigger = ui::button(risk.button)
1043		.key("risk")
1044		.variant("danger")
1045		.on_click(Msg::Ask(page, id.into()));
1046	if let Some(why) = risk.blocked {
1047		trigger = trigger.disabled(why);
1048	}
1049	card
1050		.child(kv.key("kv"))
1051		.child(ui::row().key("bar").child(trigger))
1052}
Source

pub fn busy(self, value: impl Into<bool>) -> Self

A spinner leads and it sends nothing.

Source

pub fn keys<K: Into<String>>(self, keys: impl IntoIterator<Item = K>) -> Self

Keycaps drawn after the label (display only).

Examples found in repository?
examples/dashboard.rs (line 710)
680	fn head(&self) -> ui::Row<Msg> {
681		let title = ui::text(spans![
682			ui::span("Fleet").style("title"),
683			" ",
684			ui::span(format!(
685				"production · {} services · up {}",
686				self.services.len(),
687				ago(UPTIME + u64::from(self.ticks))
688			))
689			.style("muted"),
690		])
691		.key("title");
692		let worst = self
693			.services
694			.iter()
695			.map(|s| s.health)
696			.max_by_key(|h| match h {
697				Health::Healthy => 0,
698				Health::Degraded => 1,
699				Health::Down => 2,
700			})
701			.unwrap_or(Health::Healthy);
702		let status = match worst {
703			Health::Healthy => ui::badge("Operational").tone(Tone::Success),
704			Health::Degraded => ui::badge("Degraded").tone(Tone::Warning),
705			Health::Down => ui::badge("Partial outage").tone(Tone::Error),
706		};
707		let refresh = ui::button("Refresh")
708			.key("refresh")
709			.icon("repeat")
710			.keys(["r"])
711			.on_click(Msg::Refresh);
712		ui::row()
713			.key("head")
714			.justify("between")
715			.align("center")
716			.child(title)
717			.child(
718				ui::row()
719					.key("end")
720					.gap("sm")
721					.align("center")
722					.children(nodes![status.key("status"), refresh]),
723			)
724	}
725
726	/// The stats strip: at most four tiles, directly on the page.
727	fn stats(&self) -> ui::Stats<Msg> {
728		let total = self.services.len();
729		let healthy = self
730			.services
731			.iter()
732			.filter(|s| s.health == Health::Healthy)
733			.count();
734		let deploys = |state| self.deploys.iter().filter(|d| d.state == state).count();
735		let open = self
736			.incidents
737			.iter()
738			.filter(|i| i.phase != Phase::Resolved)
739			.count();
740		let unacked = self
741			.incidents
742			.iter()
743			.filter(|i| i.phase == Phase::Open)
744			.count();
745		let rps: f64 = self.services.iter().filter_map(|s| s.rps.last()).sum();
746		let mut services = StatItem::new("Healthy", format!("{healthy}/{total}")).sub("services");
747		if healthy < total {
748			services = services.tone(Tone::Warning);
749		}
750		let mut incidents =
751			StatItem::new("Open incidents", open.to_string()).sub(format!("{unacked} unacknowledged"));
752		if unacked > 0 {
753			incidents = incidents.tone(Tone::Error);
754		}
755		ui::stats([
756			services,
757			StatItem::new("Rolling out", deploys(State::Running).to_string())
758				.sub(format!("{} need approval", deploys(State::Waiting))),
759			incidents,
760			StatItem::new("Traffic", format!("{rps:.0}")).sub("req/s"),
761		])
762	}
763
764	/// The tabs: one per view, with muted counts.
765	fn nav(&self) -> ui::Tabs<Msg> {
766		let count = |n: usize| ui::span(n.to_string()).style("muted");
767		let open = self
768			.incidents
769			.iter()
770			.filter(|i| i.phase != Phase::Resolved)
771			.count();
772		ui::tabs()
773			.key("nav")
774			.active(self.page.id())
775			.tab("services", spans!["Services ", count(self.services.len())])
776			.tab("deploys", spans!["Deploys ", count(self.deploys.len())])
777			.tab("incidents", spans!["Incidents ", count(open)])
778			.on_select(|id| Msg::Page(id.to_owned()))
779	}
780
781	/// The active view: its table beside the selected row's detail card
782	/// (under it when `plain`).
783	fn body(&self, plain: bool) -> Node<Msg> {
784		let page = self.page;
785		let selected = self.selected[page.index()];
786		let (list, detail) = match page {
787			Page::Services => (
788				nodes![self.services_table()],
789				selected
790					.and_then(|id| self.services.iter().find(|s| s.id == id))
791					.map(service_card),
792			),
793			Page::Deploys => (
794				nodes![self.deploys_bar(), self.deploys_table()],
795				selected
796					.and_then(|id| self.deploys.iter().find(|d| d.id == id))
797					.map(deploy_card),
798			),
799			Page::Incidents => (
800				nodes![self.incidents_table()],
801				selected
802					.and_then(|id| self.incidents.iter().find(|i| i.id == id))
803					.map(incident_card),
804			),
805		};
806		let mut parts: Vec<Node<Msg>> = nodes![
807			ui::col()
808				.key("list")
809				.gap("sm")
810				.grow(1.0)
811				.basis(0.0)
812				.children(list)
813		];
814		if let Some(card) = detail {
815			parts.push(
816				card
817					.key("detail")
818					.min(Bound::w(Extent::Ch(44.0)))
819					.max(Bound::w(Extent::Ch(56.0)))
820					.into(),
821			);
822		}
823		if plain {
824			ui::col().key(page.id()).gap("md").children(parts).into()
825		} else {
826			ui::row()
827				.key(page.id())
828				.align("stretch")
829				.gap("lg")
830				.children(parts)
831				.into()
832		}
833	}
834
835	/// A selectable table of `page` with its handlers and selection.
836	fn table(&self, page: Page, empty: &str) -> ui::Table<Msg> {
837		let mut table = ui::table()
838			.key("table")
839			.selectable(true)
840			.empty(empty)
841			.on_select(move |id| Msg::Select(page, id.to_owned()))
842			.on_activate(move |id| Msg::Open(page, id.to_owned()));
843		if let Some(id) = self.selected[page.index()] {
844			table = table.selected(id);
845		}
846		table
847	}
848
849	/// The services table.
850	fn services_table(&self) -> ui::Table<Msg> {
851		let mut table = self
852			.table(Page::Services, "No services yet")
853			.col(Column::new("name", "Service").grow(1.0).priority(6.0))
854			.col(Column::new("region", "Region").priority(3.0))
855			.col(Column::new("version", "Version").mono(true).priority(2.0))
856			.col(Column::new("traffic", "Traffic").priority(1.0))
857			.col(
858				Column::new("rps", "req/s")
859					.align(TextAlign::End)
860					.mono(true)
861					.priority(4.0),
862			)
863			.col(
864				Column::new("p95", "p95")
865					.align(TextAlign::End)
866					.mono(true)
867					.priority(4.0),
868			)
869			.col(
870				Column::new("errors", "Errors")
871					.align(TextAlign::End)
872					.mono(true)
873					.priority(4.0),
874			)
875			.col(Column::new("health", "Health").priority(5.0));
876		for s in &self.services {
877			let (label, tone) = s.health.look();
878			let mut errors = TextCell::new(format!("{:.1}%", s.errors));
879			if s.errors >= 1.0 {
880				errors = errors.tone(Tone::Error);
881			}
882			table = table.row(
883				TableRow::new(s.id)
884					.cell("name", TextCell::new(s.id).dot(tone.clone()))
885					.cell("region", s.region)
886					.cell("version", s.version)
887					.cell("traffic", SparkCell::new(s.rps.clone()))
888					.cell("rps", format!("{:.0}", s.rps.last().copied().unwrap_or_default()))
889					.cell("p95", format!("{:.0} ms", s.p95))
890					.cell("errors", errors)
891					.cell("health", BadgeSpec::new(label).tone(tone)),
892			);
893		}
894		table
895	}
896
897	/// The deploys toolbar: a muted description and the filter.
898	fn deploys_bar(&self) -> ui::Row<Msg> {
899		let attention = self
900			.deploys
901			.iter()
902			.filter(|d| matches!(d.state, State::Waiting | State::Failed))
903			.count();
904		ui::row()
905			.key("bar")
906			.justify("between")
907			.align("center")
908			.child(
909				ui::text(ui::span("Rollouts to production, newest first").style("muted")).key("about"),
910			)
911			.child(
912				ui::tabs()
913					.key("filter")
914					.variant("segmented")
915					.active(self.filter.clone())
916					.tab("all", "All")
917					.tab("attention", spans![
918						"Needs you ",
919						ui::span(attention.to_string()).style("muted")
920					])
921					.on_select(|id| Msg::Filter(id.to_owned())),
922			)
923	}
924
925	/// The deploys table.
926	fn deploys_table(&self) -> ui::Table<Msg> {
927		let mut table = self
928			.table(Page::Deploys, "Nothing needs you right now")
929			.col(Column::new("service", "Service").grow(1.0).priority(6.0))
930			.col(Column::new("commit", "Commit").mono(true).priority(3.0))
931			.col(Column::new("region", "Region").priority(2.0))
932			.col(Column::new("traffic", "Traffic").priority(1.0))
933			.col(Column::new("rollout", "Rollout").priority(1.0))
934			.col(Column::new("state", "State").priority(5.0));
935		for d in self.shown_deploys() {
936			let (label, tone) = d.state.look();
937			table = table.row(
938				TableRow::new(d.id)
939					.cell("service", TextCell::new(d.service).dot(tone.clone()))
940					.cell("commit", d.commit)
941					.cell("region", d.region)
942					.cell("traffic", SparkCell::new(d.rps.clone()))
943					.cell("rollout", MeterCell {
944						value: Some(d.done),
945						title: Some(format!("{:.0}%", d.done * 100.0)),
946						..MeterCell::default()
947					})
948					.cell("state", BadgeSpec::new(label).tone(tone)),
949			);
950		}
951		table
952	}
953
954	/// The incidents table.
955	fn incidents_table(&self) -> ui::Table<Msg> {
956		let mut table = self
957			.table(Page::Incidents, "No incidents. All quiet.")
958			.col(Column::new("title", "Incident").grow(1.0).priority(6.0))
959			.col(Column::new("id", "Id").mono(true).priority(2.0))
960			.col(Column::new("service", "Service").priority(3.0))
961			.col(Column::new("severity", "Severity").priority(4.0))
962			.col(
963				Column::new("age", "Open for")
964					.align(TextAlign::End)
965					.mono(true)
966					.priority(4.0),
967			)
968			.col(Column::new("phase", "State").priority(5.0));
969		for i in &self.incidents {
970			let (sev, sev_tone) = i.severity.look();
971			let (phase, phase_tone) = i.phase.look();
972			table = table.row(
973				TableRow::new(i.id)
974					.cell("title", TextCell::new(i.title).dot(sev_tone.clone()))
975					.cell("id", i.id)
976					.cell("service", i.service)
977					.cell("severity", BadgeSpec::new(sev).tone(sev_tone))
978					.cell("age", ago(i.age))
979					.cell("phase", BadgeSpec::new(phase).tone(phase_tone)),
980			);
981		}
982		table
983	}
984
985	/// The key legend.
986	fn legend(&self) -> ui::Row<Msg> {
987		let key = |k: &str| ui::span(k).style("key");
988		let muted = |t: &str| ui::span(t).style("muted");
989		let keys = if self.confirming.is_some() {
990			spans![key("enter"), muted(" confirm  "), key("esc"), muted(" back")]
991		} else {
992			spans![
993				key("tab"),
994				muted(" views  "),
995				key("j"),
996				key("k"),
997				muted(" move  "),
998				key("enter"),
999				muted(" open  "),
1000				key("a"),
1001				muted(" action  "),
1002				key("r"),
1003				muted(" refresh  "),
1004				key("q"),
1005				muted(" quit"),
1006			]
1007		};
1008		let note = if self.note.is_empty() {
1009			"Up to date"
1010		} else {
1011			self.note.as_str()
1012		};
1013		ui::row()
1014			.key("legend")
1015			.justify("between")
1016			.align("center")
1017			.child(ui::text(keys).key("keys"))
1018			.child(ui::text(muted(note)).key("note"))
1019	}
1020}
1021
1022/// A detail card: the head with the primary action at its end, the `kv` and
1023/// the `danger` trigger.
1024fn detail(
1025	page: Page,
1026	id: &'static str,
1027	meta: impl Into<Text>,
1028	kv: ui::Kv<Msg>,
1029	action: Option<&'static str>,
1030	risk: &Risk,
1031) -> ui::Card<Msg> {
1032	let mut card = ui::card().head(id).meta(meta);
1033	if let Some(label) = action {
1034		card = card.end(
1035			ui::button(label)
1036				.key("act")
1037				.variant("primary")
1038				.keys(["a"])
1039				.on_click(Msg::Act(page, id.into())),
1040		);
1041	}
1042	let mut trigger = ui::button(risk.button)
1043		.key("risk")
1044		.variant("danger")
1045		.on_click(Msg::Ask(page, id.into()));
1046	if let Some(why) = risk.blocked {
1047		trigger = trigger.disabled(why);
1048	}
1049	card
1050		.child(kv.key("kv"))
1051		.child(ui::row().key("bar").child(trigger))
1052}
1053
1054/// The selected service's detail card.
1055fn service_card(s: &Service) -> ui::Card<Msg> {
1056	let (label, _) = s.health.look();
1057	let rps = s.rps.last().copied().unwrap_or_default();
1058	let kv = ui::kv()
1059		.item("Health", ui::span(label).style("strong"))
1060		.item("Version", mono(s.version))
1061		.item("Replicas", mono(s.replicas.to_string()))
1062		.item("Traffic", mono(format!("{rps:.0} req/s")))
1063		.item("p95", mono(format!("{:.0} ms", s.p95)))
1064		.item("Errors", mono(format!("{:.1}%", s.errors)));
1065	detail(Page::Services, s.id, s.region, kv, Some(s.action()), &s.risk())
1066}
1067
1068/// The selected deploy's detail card.
1069fn deploy_card(d: &Deploy) -> ui::Card<Msg> {
1070	let (label, _) = d.state.look();
1071	let rps = d.rps.last().copied().unwrap_or_default();
1072	let kv = ui::kv()
1073		.item("State", ui::span(label).style("strong"))
1074		.item("Service", d.service)
1075		.item("Commit", mono(d.commit))
1076		.item("Rollout", mono(format!("{:.0}%", d.done * 100.0)))
1077		.item("Traffic", mono(format!("{rps:.0} req/s")));
1078	detail(Page::Deploys, d.id, d.region, kv, d.action(), &d.risk())
1079}
1080
1081/// The selected incident's detail card.
1082fn incident_card(i: &Incident) -> ui::Card<Msg> {
1083	let (phase, _) = i.phase.look();
1084	let (sev, _) = i.severity.look();
1085	let kv = ui::kv()
1086		.item("State", ui::span(phase).style("strong"))
1087		.item("What", i.title)
1088		.item("Service", i.service)
1089		.item("Severity", sev)
1090		.item("Open for", mono(ago(i.age)));
1091	detail(Page::Incidents, i.id, i.service, kv, i.action(), &i.risk())
1092}
1093
1094/// The confirm strip before a destructive action: `error` with a `danger`
1095/// confirm; its buttons sit in the head's end.
1096fn confirm(risk: &Risk) -> ui::Card<Msg> {
1097	let buttons = ui::row()
1098		.gap("sm")
1099		.child(
1100			ui::button("Keep")
1101				.key("keep")
1102				.variant("quiet")
1103				.keys(["esc"])
1104				.on_click(Msg::Keep),
1105		)
1106		.child(
1107			ui::button(risk.button)
1108				.key("go")
1109				.variant("danger")
1110				.keys(["enter"])
1111				.on_click(Msg::Confirm),
1112		);
1113	ui::card()
1114		.key("confirm")
1115		.tone(Tone::Error)
1116		.head(risk.ask.clone())
1117		.end(buttons)
1118		.child(ui::text(risk.then).key("t"))
1119}

Trait Implementations§

Source§

impl<M> Clone for Button<M>

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl<M> Debug for Button<M>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<M> From<Button<M>> for Slot<M>

Source§

fn from(n: Button<M>) -> Self

Converts to this type from the input type.
Source§

impl<M> From<Button<M>> for Node<M>

Source§

fn from(n: Button<M>) -> Self

Converts to this type from the input type.
Source§

impl<M> From<Button<M>> for View<M>

A bare node is main’s one child.

Source§

fn from(n: Button<M>) -> Self

Converts to this type from the input type.
Source§

impl<M> From<Node<M>> for Button<M>

Source§

fn from(node: Node<M>) -> Self

Converts to this type from the input type.

Auto Trait Implementations§

§

impl<M = ()> !RefUnwindSafe for Button<M>

§

impl<M = ()> !UnwindSafe for Button<M>

§

impl<M> Freeze for Button<M>
where Node<M>: Freeze,

§

impl<M> Send for Button<M>
where Node<M>: Send,

§

impl<M> Sync for Button<M>
where Node<M>: Sync,

§

impl<M> Unpin for Button<M>
where Node<M>: Unpin,

§

impl<M> UnsafeUnpin for Button<M>
where Node<M>: UnsafeUnpin,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more