use eframe::egui;
use super::create_dialog::{CreateForm, Kind};
use super::files::{self, FilesTab, Target};
use super::{
confirm_removal, inspect_section, remove_button, remove_task, remove_unused_button, usage_pill,
};
use crate::app::components::icons::Icon;
use crate::app::components::{details, table, widgets};
use crate::app::page::{Command, Intent, Page, PageCtx, toggle_selection};
use crate::app::store::Store;
use crate::docker::{FileOp, Resource, Task, VOLUME_ROOT, VolumeInfo, plural, time_ago};
#[derive(Default)]
pub struct VolumesPage {
selected: Option<String>,
create: Option<CreateForm>,
files: FilesTab,
}
fn end_browse(ctx: &mut PageCtx, volume: &str) {
ctx.request(Intent::Files(FileOp::EndBrowse {
volume: volume.to_owned(),
}));
}
impl VolumesPage {
fn browse(&mut self, ui: &mut egui::Ui, ctx: &mut PageCtx, volume: &str) {
let p = ctx.palette;
if widgets::button(ui, Icon::Close, "Done browsing").clicked() {
end_browse(ctx, volume);
return;
}
ui.add_space(10.0);
let helper = ctx
.store
.volume_browser
.as_ref()
.and_then(|(_, helper)| helper.clone());
match helper {
None => {
ui.horizontal(|ui| {
ui.spinner();
ui.label(egui::RichText::new("Opening the volumeā¦").color(p.muted));
});
}
Some(Err(e)) => {
ui.label(
egui::RichText::new(format!("Couldn't open the volume: {e}")).color(p.danger),
);
}
Some(Ok(container)) => {
let target = Target {
container: &container,
root: VOLUME_ROOT,
writable: false,
note: Some("Read-only: nothing here can be changed from this view."),
};
files::show(ui, ctx, &mut self.files, &target);
}
}
}
fn selected<'a>(&self, store: &'a Store) -> Option<&'a VolumeInfo> {
let name = self.selected.as_deref()?;
store.volumes.iter().find(|v| v.name == name)
}
}
impl Page for VolumesPage {
fn title(&self) -> &'static str {
"Volumes"
}
fn icon(&self) -> Icon {
Icon::Database
}
fn resource(&self) -> Resource {
Resource::Volumes
}
fn badge(&self, store: &Store) -> String {
store.volumes.len().to_string()
}
fn summary(&self, store: &Store) -> String {
let in_use = store
.volumes
.iter()
.filter(|v| !store.containers_using_volume(&v.name).is_empty())
.count();
format!(
"{} Ā· {in_use} in use",
plural(store.volumes.len(), "volume")
)
}
fn search_hint(&self) -> &'static str {
"Search name or driver"
}
fn toolbar(&mut self, ui: &mut egui::Ui, ctx: &mut PageCtx) {
let unused = unused(ctx.store);
remove_unused_button(ui, ctx, "volume", unused);
if widgets::button(ui, Icon::Database, "New volume").clicked() {
self.create = Some(CreateForm::new(Kind::Volume));
}
}
fn has_overlay(&self) -> bool {
self.create.is_some()
}
fn show(&mut self, ui: &mut egui::Ui, ctx: &mut PageCtx) {
super::create_dialog(ui, ctx, &mut self.create);
let p = ctx.palette;
let store = ctx.store;
let visible: Vec<&VolumeInfo> = store
.volumes
.iter()
.filter(|v| ctx.matches_search(&[&v.name, &v.driver]))
.collect();
if visible.is_empty() {
super::nothing_to_show(
ui,
ctx,
!store.volumes.is_empty(),
(
Icon::Database,
"No volumes",
"Volumes appear when containers create them, or with `docker volume create`.",
),
None,
);
return;
}
let columns = vec![
table::Col::fill("NAME"),
table::Col::fixed("USAGE", 110.0),
table::Col::fixed("CREATED", 130.0),
];
let selected = self.selected.clone();
let clicked = table::show(
ui,
p,
"volumes",
columns,
visible.len(),
|i| selected.as_deref() == Some(visible[i].name.as_str()),
|i, row| {
let volume = visible[i];
let users = store.containers_using_volume(&volume.name).len();
row.col(|ui| {
table::with_icon(
ui,
p,
Icon::Database,
p.accent,
&volume.name,
&volume.driver,
)
});
row.col(|ui| usage_pill(ui, p, users));
row.col(|ui| {
table::muted(ui, p, &volume.created.map(time_ago).unwrap_or_default())
});
},
);
if let Some(i) = clicked {
toggle_selection(&mut self.selected, visible[i].name.clone());
}
}
fn has_selection(&self, store: &Store) -> bool {
self.selected(store).is_some()
}
fn show_details(&mut self, ui: &mut egui::Ui, ctx: &mut PageCtx) {
let p = ctx.palette;
let Some(volume) = self.selected(ctx.store) else {
return;
};
let users = ctx.store.containers_using_volume(&volume.name);
let browsing = match &ctx.store.volume_browser {
Some((browsed, _)) if *browsed != volume.name => {
ctx.request(Intent::Files(FileOp::EndBrowse {
volume: browsed.clone(),
}));
false
}
other => other.is_some(),
};
if details::header(ui, p, &volume.name) {
self.selected = None;
if browsing {
end_browse(ctx, &volume.name);
}
}
usage_pill(ui, p, users.len());
ui.add_space(10.0);
if browsing {
self.browse(ui, ctx, &volume.name);
return;
}
let blocked = (!users.is_empty()).then_some("Mounted by a container");
let task = remove_task(Resource::Volumes, &volume.name, &volume.name);
ui.horizontal(|ui| {
if widgets::button(ui, Icon::Folder, "Browse files").clicked() {
ctx.request(Intent::Files(FileOp::BrowseVolume {
volume: volume.name.clone(),
}));
}
if !ctx.store.pending.contains_key(&volume.name) {
remove_button(ui, ctx, "volume", task, blocked);
} else {
ui.spinner();
}
});
ui.add_space(16.0);
egui::ScrollArea::vertical()
.auto_shrink(false)
.show(ui, |ui| {
details::section(ui, p, "Overview", |ui| {
details::field(ui, p, "Name", &volume.name);
details::field(ui, p, "Driver", &volume.driver);
details::field(ui, p, "Scope", &volume.scope);
details::field(
ui,
p,
"Created",
&volume.created.map(time_ago).unwrap_or_default(),
);
});
details::section(ui, p, "Location", |ui| {
details::field(ui, p, "Mountpoint", &volume.mountpoint);
});
details::section(ui, p, "Used by", |ui| {
super::container_links(ui, ctx, &users);
});
inspect_section(ui, ctx, Resource::Volumes, &volume.name);
});
}
fn clear_selection(&mut self) {
self.selected = None;
}
fn select(&mut self, id: &str) {
self.selected = Some(id.to_owned());
}
fn commands(&self, store: &Store) -> Vec<Command> {
let mut list: Vec<Command> = store
.volumes
.iter()
.map(|v| Command::select(&v.name, "Volume", &v.name))
.collect();
let unused = unused(store);
if !unused.is_empty() {
list.push(Command::intent(
"Remove unused volumes",
plural(unused.len(), "volume"),
confirm_removal("volume", unused),
));
}
list
}
}
fn unused(store: &Store) -> Vec<Task> {
store
.volumes
.iter()
.filter(|v| store.containers_using_volume(&v.name).is_empty())
.map(|v| remove_task(Resource::Volumes, &v.name, &v.name))
.collect()
}