diff --git a/src/Volumes.jsx b/src/Volumes.jsx
new file mode 100644
index 000000000..b552e0f97
--- /dev/null
+++ b/src/Volumes.jsx
@@ -0,0 +1,149 @@
+import React from 'react';
+
+import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card";
+import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection";
+import { Text, TextVariants } from "@patternfly/react-core/dist/esm/components/Text";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex";
+import { cellWidth, SortByDirection } from '@patternfly/react-table';
+
+import cockpit from 'cockpit';
+import { ListingTable } from "cockpit-components-table.jsx";
+
+import * as utils from './util.js';
+
+const _ = cockpit.gettext;
+
+const initContainerVolumeMap = containers => {
+ const containerVolumes = {};
+ if (containers === null)
+ return containerVolumes;
+
+ Object.keys(containers).forEach(id => {
+ const container = containers[id];
+ for (const mount of container.Mounts) {
+ if (mount.Type === "volume") {
+ const volume_key = mount.Name + container.isSystem.toString();
+ if (volume_key in containerVolumes) {
+ containerVolumes[volume_key] += 1;
+ } else {
+ containerVolumes[volume_key] = 1;
+ }
+ }
+ }
+ });
+
+ return containerVolumes;
+};
+
+const Volumes = ({ user, volumes, containers }) => {
+ const [isExpanded, setIsExpanded] = React.useState(false);
+ const containerVolumes = initContainerVolumeMap(containers);
+
+ const getUsedByText = volume => {
+ const containers = containerVolumes[volume.Name + volume.isSystem.toString()];
+ if (containers !== undefined) {
+ const title = cockpit.format(cockpit.ngettext("$0 container", "$0 containers", containers), containers);
+ return { title, count: containers.length };
+ } else {
+ return { title: _("unused"), count: 0 };
+ }
+ };
+
+ const renderRow = volume => {
+ const { title: usedByText, count: usedByCount } = getUsedByText(volume);
+
+ const columns = [
+ { title: volume.Name, header: true, props: { modifier: "breakWord" } },
+ {
+ title: volume.isSystem ? _("system") :
{_("user:")} {user}
,
+ props: { className: "ignore-pixels", modifier: "nowrap" },
+ sortKey: volume.isSystem.toString(),
+ },
+ { title: volume.Mountpoint, header: true, props: { modifier: "breakWord" } },
+ { title: volume.Driver, header: true, props: { modifier: "breakWord" } },
+ { title: , props: { className: "ignore-pixels" } },
+ {
+ title: {usedByText},
+ props: { className: "ignore-pixels", modifier: "nowrap" },
+ },
+ ];
+
+ return {
+ columns,
+ props: {
+ key: volume.Name + volume.isSystem.toString(),
+ "data-row-id": volume.Name + volume.isSystem.toString(),
+ },
+ };
+ };
+
+ const sortRows = (rows, direction, idx) => {
+ const isNumeric = idx == 4;
+ const sortedRows = rows.sort((a, b) => {
+ const aitem = a.columns[idx].sortKey ?? a.columns[idx].title;
+ const bitem = b.columns[idx].sortKey ?? b.columns[idx].title;
+ if (isNumeric) {
+ return bitem - aitem;
+ } else {
+ return aitem.localeCompare(bitem);
+ }
+ });
+ return direction === SortByDirection.asc ? sortedRows : sortedRows.reverse();
+ };
+
+ const columnTitles = [
+ { title: _("Name"), transforms: [cellWidth(20)], sortable: true },
+ { title: _("Owner"), sortable: true },
+ { title: _("Mount point"), sortable: true },
+ { title: _("Driver"), sortable: true },
+ { title: _("Created"), sortable: true },
+ { title: _("Used by"), sortable: true },
+ ];
+
+ const rows = Object.keys(volumes || {}).map(name => renderRow(volumes[name]));
+ const volumesTotal = Object.keys(volumes || {}).length;
+
+ const cardBody = (
+
+ );
+
+ const volumesTitleStats = (
+
+ {cockpit.format(cockpit.ngettext("$0 volume total", "$0 volumes total", volumesTotal), volumesTotal)}
+
+ );
+
+ return (
+
+
+
+
+
+
+ {_("Volumes")}
+
+ {volumesTitleStats}
+
+
+
+
+
+ {volumes && Object.keys(volumes).length
+ ? setIsExpanded(!isExpanded)}
+ isExpanded={isExpanded}>
+ {cardBody}
+
+ : cardBody}
+
+
+ );
+};
+
+export default Volumes;
diff --git a/src/app.jsx b/src/app.jsx
index 2f3733f03..beff085e4 100644
--- a/src/app.jsx
+++ b/src/app.jsx
@@ -34,6 +34,8 @@ import { superuser } from "superuser";
import ContainerHeader from './ContainerHeader.jsx';
import Containers from './Containers.jsx';
import Images from './Images.jsx';
+import { Volume } from './Volume.jsx';
+import Volumes from './Volumes.jsx';
import * as client from './client.js';
import { WithPodmanInfo } from './util.js';
@@ -52,10 +54,13 @@ class Application extends React.Component {
containers: null,
containersFilter: "all",
containersStats: {},
+ volumes: null,
userContainersLoaded: null,
systemContainersLoaded: null,
userPodsLoaded: null,
systemPodsLoaded: null,
+ userVolumesLoaded: null,
+ systemVolumesLoaded: null,
userServiceExists: false,
textFilter: "",
ownerFilter: "all",
@@ -203,6 +208,31 @@ class Application extends React.Component {
.catch(console.log);
}
+ initVolumes(system) {
+ return client.getVolumes(system)
+ .then(volumesList => {
+ this.setState(prevState => {
+ const copyVolumes = {};
+ Object.entries(prevState.volumes || {}).forEach(([id, volume]) => {
+ if (volume.isSystem !== system) {
+ copyVolumes[id] = volume;
+ }
+ });
+
+ for (const volume of volumesList) {
+ volume.isSystem = system;
+ copyVolumes[volume.Name + system.toString()] = volume;
+ }
+
+ return {
+ volumes: copyVolumes,
+ [system ? "systemVolumesLoaded" : "userVolumesLoaded"]: true,
+ };
+ });
+ })
+ .catch(console.log);
+ }
+
updateImages(system) {
client.getImages(system)
.then(reply => {
@@ -424,6 +454,23 @@ class Application extends React.Component {
}
}
+ handleVolumeEvent(event, system) {
+ switch (event.Action) {
+ case 'create':
+ this.initVolumes(system);
+ break;
+ case 'remove':
+ this.setState(prevState => {
+ const volumes = { ...prevState.volumes };
+ delete volumes[event.Actor.Attributes.name + system.toString()];
+ return { volumes };
+ });
+ break;
+ default:
+ console.warn('Unhandled event type ', event.Type, event.Action);
+ }
+ }
+
handleEvent(event, system) {
switch (event.Type) {
case 'container':
@@ -435,6 +482,9 @@ class Application extends React.Component {
case 'pod':
this.handlePodEvent(event, system);
break;
+ case 'volume':
+ this.handleVolumeEvent(event, system);
+ break;
default:
console.warn('Unhandled event type ', event.Type);
}
@@ -465,6 +515,7 @@ class Application extends React.Component {
});
this.updateImages(system);
this.initContainers(system);
+ this.initVolumes(system);
this.updatePods(system);
client.streamEvents(system,
message => this.handleEvent(message, system))
@@ -736,6 +787,15 @@ class Application extends React.Component {
/>
);
+ const volumeList = (
+
+ );
+
const notificationList = (
{this.state.notifications.map((notification, index) => {
@@ -780,6 +840,7 @@ class Application extends React.Component {
{ this.state.showStartService ? startService : null }
{imageList}
+ {volumeList}
{containerList}
diff --git a/src/client.js b/src/client.js
index a6455429c..c7031bca1 100644
--- a/src/client.js
+++ b/src/client.js
@@ -181,3 +181,5 @@ export const imageHistory = (system, id) => podmanJson(`libpod/images/${id}/hist
export const imageExists = (system, id) => podmanCall("libpod/images/" + id + "/exists", "GET", {}, system);
export const containerExists = (system, id) => podmanCall("libpod/containers/" + id + "/exists", "GET", {}, system);
+
+export const getVolumes = system => podmanJson("libpod/volumes/json", "GET", {}, system);
diff --git a/src/podman.scss b/src/podman.scss
index fdc05c821..ee72e3612 100644
--- a/src/podman.scss
+++ b/src/podman.scss
@@ -6,7 +6,7 @@
// For pf-u-disabled-color-100
@import "@patternfly/patternfly/utilities/Text/text.css";
-#app .pf-v5-c-card.containers-containers, #app .pf-v5-c-card.containers-images {
+#app .pf-v5-c-card.containers-containers, #app .pf-v5-c-card.containers-images, #app .pf-v5-c-card.containers-volumes {
@extend .ct-card;
}
@@ -14,7 +14,7 @@
white-space: break-spaces;
}
-#containers-images, #containers-containers {
+#containers-images, #containers-containers, #containers-volumes {
// Decrease padding for the image/container toggle button list
.pf-v5-c-table.pf-m-compact .pf-v5-c-table__toggle {
padding-inline-start: 0;