Skip to main content

button

Function button 

Source
pub fn button<M>(text: impl Into<String>) -> Button<M>
Expand description

A push button labeled text.

Examples found in repository?
examples/ask.rs (line 37)
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 707)
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}