mod compose;
mod containers;
mod create_dialog;
mod disk;
mod files;
mod images;
mod marks;
mod networks;
pub(in crate::app) mod quick_start;
pub(super) mod run_dialog;
mod volumes;
pub use compose::{pick_compose_file, up_op};
use eframe::egui;
use super::components::icons::Icon;
use super::components::{details, inspect, widgets};
use super::page::{Confirmation, Intent, Page, PageCtx};
use crate::docker::{Action, Resource, Task, plural};
use crate::theme::Palette;
pub fn all() -> Vec<Box<dyn Page>> {
vec![
Box::new(containers::ContainersPage::new()),
Box::new(compose::ComposePage::default()),
Box::new(images::ImagesPage::default()),
Box::new(volumes::VolumesPage::default()),
Box::new(networks::NetworksPage::default()),
Box::new(disk::DiskPage::default()),
Box::new(quick_start::QuickStartPage::default()),
]
}
fn container_links(ui: &mut egui::Ui, ctx: &mut PageCtx, names: &[&str]) {
if let Some(i) = details::link_field(ui, ctx.palette, "Containers", names)
&& let Some(c) = ctx.store.containers.iter().find(|c| c.name == names[i])
{
ctx.request(Intent::Open {
page: "Containers",
id: c.id.clone(),
});
}
}
fn create_dialog(
ui: &mut egui::Ui,
ctx: &mut PageCtx,
form: &mut Option<create_dialog::CreateForm>,
) {
let Some(open) = form else {
return;
};
match create_dialog::show(ui, ctx.palette, open) {
Some(create_dialog::Outcome::Create(change)) => {
ctx.request(Intent::Change(change));
*form = None;
}
Some(create_dialog::Outcome::Cancel) => *form = None,
None => {}
}
}
fn nothing_to_show(
ui: &mut egui::Ui,
ctx: &mut PageCtx,
exists: bool,
empty: (Icon, &str, &str),
action: Option<(&str, Intent)>,
) {
let p = ctx.palette;
if exists {
let hint = "Nothing here matches your search.";
if widgets::empty_state(
ui,
p,
Icon::Search,
"No matches",
hint,
Some("Clear search"),
) {
ctx.request(Intent::ClearSearch);
}
return;
}
let (icon, title, hint) = empty;
let label = action.as_ref().map(|(label, _)| *label);
if widgets::empty_state(ui, p, icon, title, hint, label)
&& let Some((_, intent)) = action
{
ctx.request(intent);
}
}
fn inspect_section(ui: &mut egui::Ui, ctx: &mut PageCtx, resource: Resource, id: &str) {
let p = ctx.palette;
egui::CollapsingHeader::new(egui::RichText::new("INSPECT").size(11.0).color(p.muted))
.id_salt(("inspect", id))
.show(ui, |ui| {
if inspect::needs_loading(ctx.store, id) {
ctx.request(Intent::Inspect(resource, id.to_owned()));
}
inspect::show(ui, p, ctx.store, id);
});
}
fn usage_pill(ui: &mut egui::Ui, p: &Palette, users: usize) {
if users > 0 {
widgets::pill(ui, "In use", p.success);
} else {
widgets::pill(ui, "Unused", p.muted);
}
}
fn remove_task(resource: Resource, id: &str, name: &str) -> Task {
Task {
action: Action::Remove(resource),
id: id.to_owned(),
name: name.to_owned(),
}
}
fn remove_button(
ui: &mut egui::Ui,
ctx: &mut PageCtx,
kind: &str,
task: Task,
blocked: Option<&str>,
) {
let p = ctx.palette;
if let Some(reason) = blocked {
ui.add_enabled(false, egui::Button::new("Remove"))
.on_disabled_hover_text(reason);
} else if widgets::danger_button(ui, p, "Remove").clicked() {
ctx.request(confirm_removal(kind, vec![task]));
}
}
fn remove_unused_button(ui: &mut egui::Ui, ctx: &mut PageCtx, kind: &str, tasks: Vec<Task>) {
let hint = if tasks.is_empty() {
format!("No unused {kind}s")
} else {
format!("Remove {}", plural(tasks.len(), &format!("unused {kind}")))
};
let button = ui
.add_enabled(!tasks.is_empty(), egui::Button::new("Remove unused"))
.on_hover_text(&hint)
.on_disabled_hover_text(&hint);
if button.clicked() {
ctx.request(confirm_removal(kind, tasks));
}
}
fn confirm_removal(kind: &str, tasks: Vec<Task>) -> Intent {
const LISTED: usize = 6;
let (title, message) = match tasks.as_slice() {
[one] => (
format!("Remove {kind} “{}”?", one.name),
format!("The {kind} is deleted permanently. This can't be undone."),
),
many => {
let mut names: Vec<&str> = many.iter().take(LISTED).map(|t| t.name.as_str()).collect();
let rest = many.len().saturating_sub(LISTED);
let more = format!("and {rest} more");
if rest > 0 {
names.push(&more);
}
(
format!("Remove {}?", plural(many.len(), &format!("unused {kind}"))),
format!(
"These are deleted permanently: {}. This can't be undone.",
names.join(", ")
),
)
}
};
Intent::Confirm(Confirmation {
title,
message,
action_label: "Remove".into(),
tasks,
..Default::default()
})
}
#[cfg(test)]
mod tests {
use super::*;
fn tasks(n: usize) -> Vec<Task> {
(0..n)
.map(|i| remove_task(Resource::Volumes, &format!("v{i}"), &format!("v{i}")))
.collect()
}
#[test]
fn single_removal_names_the_item() {
let Intent::Confirm(c) = confirm_removal("volume", tasks(1)) else {
panic!("expected a confirmation");
};
assert_eq!(c.title, "Remove volume “v0”?");
assert_eq!(c.tasks.len(), 1);
}
#[test]
fn bulk_removal_lists_a_few_names() {
let Intent::Confirm(c) = confirm_removal("volume", tasks(8)) else {
panic!("expected a confirmation");
};
assert_eq!(c.title, "Remove 8 unused volumes?");
assert!(c.message.contains("v5, and 2 more"));
assert_eq!(c.tasks.len(), 8);
}
}