+
+
+ >}
+ />
+ );
+ }
+}
diff --git a/src/VolumeDetails.jsx b/src/VolumeDetails.jsx
new file mode 100644
index 000000000..152088d97
--- /dev/null
+++ b/src/VolumeDetails.jsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import cockpit from 'cockpit';
+
+import { DescriptionList, DescriptionListTerm, DescriptionListDescription, DescriptionListGroup, List, ListItem } from "@patternfly/react-core";
+
+import VolumeUsedBy from './VolumeUsedBy.jsx';
+const _ = cockpit.gettext;
+
+const render_map = (labels) => {
+ if (!labels)
+ return null;
+
+ const result = Object.keys(labels).map((key, value) => {
+ return (
+
+ { key }: { value }
+
+ );
+ });
+
+ return {result}
;
+};
+
+const VolumeDetails = ({ volume, containers, showAll }) => {
+ const labels = volume.Labels && render_map(volume.Labels);
+ const options = volume.Options && render_map(volume.Options);
+
+ return (
+
+ {volume.Labels.length &&
+
+ {_("Labels")}
+ {labels}
+
+ }
+ {volume.Scope &&
+
+ {_("Scope")}
+ {volume.Scope}
+
+ }
+ {volume.Options.length &&
+
+ {_("Options")}
+ {options}
+
+ }
+ {containers &&
+
+ {_("Used by")}
+
+
+ }
+
+ );
+};
+
+export default VolumeDetails;
diff --git a/src/VolumeUsedBy.jsx b/src/VolumeUsedBy.jsx
new file mode 100644
index 000000000..28e28512a
--- /dev/null
+++ b/src/VolumeUsedBy.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import cockpit from 'cockpit';
+import { Button, Badge, Flex, List, ListItem } from "@patternfly/react-core";
+
+const _ = cockpit.gettext;
+
+const VolumeUsedBy = ({ containers, showAll }) => {
+ if (containers === null)
+ return _("Loading...");
+ if (containers === undefined)
+ return _("No containers are using this volume");
+
+ return (
+
+ {containers.map(c => {
+ const container = c.container;
+ const isRunning = container.State == "running";
+ return (
+
+
+
+ {isRunning && {_("Running")}}
+
+
+ );
+ })}
+
+ );
+};
+
+export default VolumeUsedBy;
diff --git a/src/Volumes.css b/src/Volumes.css
new file mode 100644
index 000000000..6f5d8c6d1
--- /dev/null
+++ b/src/Volumes.css
@@ -0,0 +1,18 @@
+#containers-volumes div.download-in-progress {
+ color: grey;
+ font-weight: bold;
+}
+
+/* Danger dropdown items should be red */
+.pf-c-dropdown__menu-item.pf-m-danger {
+ color: var(--pf-global--danger-color--200);
+}
+
+#containers-volumes .pf-c-table.pf-m-compact .pf-c-table__action {
+ --pf-c-table__action--PaddingTop: 0.5rem;
+ --pf-c-table__action--PaddingBottom: 0.5rem;
+}
+
+.containers-volumes .pf-c-expandable-section__content {
+ margin-top: 0px;
+}
diff --git a/src/Volumes.jsx b/src/Volumes.jsx
new file mode 100644
index 000000000..55c12f2dd
--- /dev/null
+++ b/src/Volumes.jsx
@@ -0,0 +1,307 @@
+import React, { useState } from 'react';
+import {
+ Card, CardBody, CardHeader,
+ Dropdown, DropdownItem,
+ Flex, FlexItem,
+ ExpandableSection,
+ KebabToggle,
+ Text, TextVariants,
+ ToolbarItem, Button
+} from '@patternfly/react-core';
+import { cellWidth } from '@patternfly/react-table';
+
+import cockpit from 'cockpit';
+import { ListingTable } from "cockpit-components-table.jsx";
+import { ListingPanel } from 'cockpit-components-listing-panel.jsx';
+import VolumeDetails from './VolumeDetails.jsx';
+import { VolumeDeleteModal } from './VolumeDeleteModal.jsx';
+import PruneUnusedVolumesModal from './PruneUnusedVolumesModal.jsx';
+import ForceRemoveModal from './ForceRemoveModal.jsx';
+import { VolumeCreateModal } from './VolumeCreateModal.jsx';
+import * as client from './client.js';
+import * as utils from './util.js';
+
+import './Volumes.css';
+import '@patternfly/react-styles/css/utilities/Sizing/sizing.css';
+
+const _ = cockpit.gettext;
+
+class Volumes extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isExpanded: false,
+ showVolumeCreateModal: false,
+ };
+
+ this.renderRow = this.renderRow.bind(this);
+ }
+
+ onOpenPruneUnusedVolumesDialog = () => {
+ this.setState({ showPruneUnusedVolumesModal: true });
+ }
+
+ calculateStats = () => {
+ const { volumes, volumeContainerList } = this.props;
+ const unusedVolumes = [];
+ const volumeStats = {
+ volumesTotal: 0,
+ unusedTotal: 0,
+ };
+
+ if (volumeContainerList === null) {
+ return { volumeStats, unusedVolumes };
+ }
+
+ if (volumes !== null) {
+ Object.keys(volumes).forEach(name => {
+ const volume = volumes[name];
+ volumeStats.volumesTotal += 1;
+
+ const usedBy = volumeContainerList[volume.Name + volume.isSystem.toString()];
+ if (usedBy === undefined) {
+ volumeStats.unusedTotal += 1;
+ unusedVolumes.push(volume);
+ }
+ });
+ }
+
+ return { volumeStats, unusedVolumes };
+ }
+
+ renderRow(volume) {
+ const tabs = [];
+
+ const columns = [
+ { title: volume.Name },
+ { title: { volume.Mountpoint } },
+ { title: volume.isSystem ? _("system") : {_("user:")} {this.props.user}
, props: { modifier: "nowrap" } },
+ utils.localize_date(volume.CreatedAt),
+ { title: volume.Driver, props: { modifier: "nowrap" } },
+ {
+ title: ,
+ props: { className: 'pf-c-table__action content-action' }
+ },
+ ];
+
+ tabs.push({
+ name: _("Details"),
+ renderer: VolumeDetails,
+ data: {
+ volume: volume,
+ containers: this.props.volumeContainerList !== null ? this.props.volumeContainerList[volume.Name + volume.isSystem.toString()] : null,
+ showAll: this.props.showAll,
+ }
+ });
+ return {
+ expandedContent: ,
+ columns: columns,
+ props: {
+ key :volume.Name + volume.isSystem.toString(),
+ "data-row-id": volume.Name + volume.isSystem.toString(),
+ },
+ };
+ }
+
+ render() {
+ const columnTitles = [
+ _("Name"),
+ { title: _("Mountpoint"), transforms: [cellWidth(20)] },
+ _("Owner"),
+ _("Created"),
+ _("Driver"),
+ ];
+ let emptyCaption = _("No volumes");
+ if (this.props.volumes === null)
+ emptyCaption = "Loading...";
+ else if (this.props.textFilter.length > 0)
+ emptyCaption = _("No volumes that match the current filter");
+
+ let filtered = [];
+ if (this.props.volumes !== null) {
+ filtered = Object.keys(this.props.volumes).filter(id => {
+ if (this.props.userServiceAvailable && this.props.systemServiceAvailable && this.props.ownerFilter !== "all") {
+ if (this.props.ownerFilter === "system" && !this.props.volumes[id].isSystem)
+ return false;
+ if (this.props.ownerFilter !== "system" && this.props.volumes[id].isSystem)
+ return false;
+ }
+ return true;
+ });
+ }
+
+ filtered.sort((a, b) => {
+ // User volumes are in front of system ones
+ if (this.props.volumes[a].isSystem !== this.props.volumes[b].isSystem)
+ return this.props.volumes[a].isSystem ? 1 : -1;
+ const name_a = this.props.volumes[a].name;
+ const name_b = this.props.volumes[b].name;
+ if (name_a === "")
+ return 1;
+ if (name_b === "")
+ return -1;
+ return name_a > name_b ? 1 : -1;
+ });
+
+ const volumeRows = filtered.map(id => this.renderRow(this.props.volumes[id]));
+
+ const cardBody = (
+ <>
+
+ >
+ );
+
+ const { volumeStats, unusedVolumes } = this.calculateStats();
+ const volumeTitleStats = (
+ <>
+
+ {cockpit.format(cockpit.ngettext("$0 volume total", "$0 volumes total", volumeStats.volumesTotal), volumeStats.volumesTotal)}
+
+ >
+ );
+
+ return (
+
+
+
+
+
+ {_("Volumes")}
+ {volumeTitleStats}
+
+
+
+
+
+ {this.state.showVolumeCreateModal &&
+ this.setState({ showVolumeCreateModal: false })}
+ selinuxAvailable={this.props.selinuxAvailable}
+ podmanRestartAvailable={this.props.podmanRestartAvailable}
+ systemServiceAvailable={this.props.systemServiceAvailable}
+ userServiceAvailable={this.props.userServiceAvailable}
+ onAddNotification={this.props.onAddNotification}
+ /> }
+
+
+
+
+
+
+ {filtered.length
+ ? this.setState({ isExpanded: !this.state.isExpanded })}
+ isExpanded={this.state.isExpanded}>
+ {cardBody}
+
+ : cardBody}
+
+ {this.state.showPruneUnusedVolumesModal &&
+ this.setState({ showPruneUnusedVolumesModal: false })}
+ unusedVolumes={unusedVolumes}
+ onAddNotification={this.props.onAddNotification}
+ userServiceAvailable={this.props.userServiceAvailable}
+ systemServiceAvailable={this.props.systemServiceAvailable} /> }
+
+ );
+ }
+}
+
+const VolumeOverActions = ({ handlePruneUsedVolumes, unusedVolumes }) => {
+ const [isActionsKebabOpen, setIsActionsKebabOpen] = useState(false);
+
+ return (
+ setIsActionsKebabOpen(!isActionsKebabOpen)} id="volume-actions-dropdown" />}
+ isOpen={isActionsKebabOpen}
+ isPlain
+ position="right"
+ dropdownItems={[
+
+ {_("Prune unused volumes")}
+ ,
+ ]} />
+ );
+};
+
+const VolumeActions = ({ volume, onAddNotification }) => {
+ const [showVolumeDeleteModal, setShowVolumeDeleteModal] = useState(false);
+ const [showVolumeDeleteErrorModal, setShowVolumeDeleteErrorModal] = useState(false);
+ const [volumeDeleteErrorMsg, setVolumeDeleteErrorMsg] = useState();
+ const [isActionsKebabOpen, setIsActionsKebabOpen] = useState(false);
+
+ const handleRemoveVolume = () => {
+ setShowVolumeDeleteModal(false);
+ client.delVolume(volume.isSystem, volume.Name, false)
+ .catch(ex => {
+ setVolumeDeleteErrorMsg(ex.message);
+ setShowVolumeDeleteErrorModal(true);
+ });
+ };
+
+ const handleForceRemoveVolume = () => {
+ return client.delVolume(volume.isSystem, volume.Name, true)
+ .then(reply => setShowVolumeDeleteErrorModal(false))
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to force remove volume $0"), volume.Name);
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ throw ex;
+ });
+ };
+
+ const extraActions = (
+ setIsActionsKebabOpen(!isActionsKebabOpen)} />}
+ isOpen={isActionsKebabOpen}
+ isPlain
+ position="right"
+ dropdownItems={[
+ setShowVolumeDeleteModal(true)}>
+ {_("Delete")}
+
+ ]} />
+ );
+
+ return (
+ <>
+ {extraActions}
+ {showVolumeDeleteErrorModal &&
+ setShowVolumeDeleteErrorModal(false)}
+ handleForceRemove={handleForceRemoveVolume}
+ reason={volumeDeleteErrorMsg} /> }
+ {showVolumeDeleteModal &&
+ setShowVolumeDeleteModal(false)}
+ handleRemoveVolume={handleRemoveVolume} /> }
+ >
+ );
+};
+
+export default Volumes;
diff --git a/src/app.jsx b/src/app.jsx
index 26183f1e1..8b86d2df2 100644
--- a/src/app.jsx
+++ b/src/app.jsx
@@ -32,6 +32,7 @@ import { superuser } from "superuser";
import ContainerHeader from './ContainerHeader.jsx';
import Containers from './Containers.jsx';
import Images from './Images.jsx';
+import Volumes from './Volumes.jsx';
import * as client from './client.js';
const _ = cockpit.gettext;
@@ -50,6 +51,9 @@ class Application extends React.Component {
containersFilter: "running",
containersStats: {},
containersDetails: {},
+ volumes: null,
+ userVolumesLoaded: false,
+ systemVolumesLoaded: false,
userContainersLoaded: null,
systemContainersLoaded: null,
userPodsLoaded: null,
@@ -72,11 +76,13 @@ class Application extends React.Component {
this.onFilterChanged = this.onFilterChanged.bind(this);
this.onOwnerChanged = this.onOwnerChanged.bind(this);
this.updateImagesAfterEvent = this.updateImagesAfterEvent.bind(this);
+ this.updateVolumesAfterEvent = this.updateVolumesAfterEvent.bind(this);
this.updateContainerAfterEvent = this.updateContainerAfterEvent.bind(this);
this.updateContainerStats = this.updateContainerStats.bind(this);
this.startService = this.startService.bind(this);
this.goToServicePage = this.goToServicePage.bind(this);
this.handleImageEvent = this.handleImageEvent.bind(this);
+ this.handleVolumeEvent = this.handleVolumeEvent.bind(this);
this.handleContainerEvent = this.handleContainerEvent.bind(this);
this.checkUserService = this.checkUserService.bind(this);
}
@@ -190,11 +196,9 @@ class Application extends React.Component {
[system ? "systemContainersLoaded" : "userContainersLoaded"]: true,
};
});
- if (init) {
- this.updateContainerStats(system);
- for (const container of reply || []) {
- this.inspectContainerDetail(container.Id, system);
- }
+ this.updateContainerStats(system);
+ for (const container of reply || []) {
+ this.inspectContainerDetail(container.Id, system);
}
})
.catch(console.log);
@@ -227,6 +231,33 @@ class Application extends React.Component {
});
}
+ updateVolumesAfterEvent(system) {
+ client.getVolumes(system)
+ .then(reply => {
+ this.setState(prevState => {
+ // Copy only volumes that could not be deleted with this event
+ // So when event from system come, only copy user volumes and vice versa
+ const copyVolumes = {};
+ Object.entries(prevState.volumes || {}).forEach(([Id, volume]) => {
+ if (volume.isSystem !== system)
+ copyVolumes[Id] = volume;
+ });
+ Object.entries(reply).forEach(([Id, volume]) => {
+ volume.isSystem = system;
+ copyVolumes[volume.Name + system.toString()] = volume;
+ });
+
+ return {
+ volumes: copyVolumes,
+ [system ? "systemVolumesLoaded" : "userVolumesLoaded"]: true
+ };
+ });
+ })
+ .catch(ex => {
+ console.warn("Failed to do Update Volumes:", JSON.stringify(ex));
+ });
+ }
+
updatePodsAfterEvent(system) {
client.getPods(system)
.then(reply => {
@@ -334,6 +365,18 @@ class Application extends React.Component {
}
}
+ handleVolumeEvent(event, system) {
+ switch (event.Action) {
+ case 'remove':
+ case 'prune':
+ case 'create':
+ this.updateVolumesAfterEvent(system);
+ break;
+ default:
+ console.warn('Unhandled event type ', event.Type, event.Action);
+ }
+ }
+
handleContainerEvent(event, system) {
switch (event.Action) {
/* The following events do not need to trigger any state updates */
@@ -408,6 +451,9 @@ class Application extends React.Component {
case 'image':
this.handleImageEvent(event, system);
break;
+ case 'volume':
+ this.handleVolumeEvent(event, system);
+ break;
case 'pod':
this.handlePodEvent(event, system);
break;
@@ -417,7 +463,7 @@ class Application extends React.Component {
}
cleanupAfterService(system, key) {
- ["images", "containers", "pods"].forEach(t => {
+ ["images", "volumes", "containers", "pods"].forEach(t => {
if (this.state[t])
this.setState(prevState => {
const copy = {};
@@ -440,6 +486,7 @@ class Application extends React.Component {
cgroupVersion: reply.host.cgroupVersion,
});
this.updateImagesAfterEvent(system);
+ this.updateVolumesAfterEvent(system);
this.updateContainersAfterEvent(system, true);
this.updatePodsAfterEvent(system);
client.streamEvents(system,
@@ -468,6 +515,7 @@ class Application extends React.Component {
[system ? "systemServiceAvailable" : "userServiceAvailable"]: false,
[system ? "systemContainersLoaded" : "userContainersLoaded"]: true,
[system ? "systemImagesLoaded" : "userImagesLoaded"]: true,
+ [system ? "systemVolumesLoaded" : "userVolumesLoaded"]: true,
[system ? "systemPodsLoaded" : "userPodsLoaded"]: true
});
});
@@ -485,6 +533,7 @@ class Application extends React.Component {
} else {
this.setState({
userImagesLoaded: true,
+ userVolumesLoaded: true,
userContainersLoaded: true,
userPodsLoaded: true,
userServiceExists: false
@@ -542,7 +591,8 @@ class Application extends React.Component {
this.setState({
systemServiceAvailable: false,
systemContainersLoaded: true,
- systemImagesLoaded: true
+ systemImagesLoaded: true,
+ systemVolumesLoaded: true
});
console.warn("Failed to start system podman.socket:", JSON.stringify(err));
});
@@ -559,7 +609,8 @@ class Application extends React.Component {
userServiceAvailable: false,
userContainersLoaded: true,
userPodsLoaded: true,
- userImagesLoaded: true
+ userImagesLoaded: true,
+ userVolumesLoaded: true
});
console.warn("Failed to start user podman.socket:", JSON.stringify(err));
});
@@ -620,6 +671,23 @@ class Application extends React.Component {
} else
imageContainerList = null;
+ let volumeContainerList = {};
+ if (this.state.volumes !== null) {
+ Object.keys(this.state.volumes).forEach(c => {
+ const volume = this.state.volumes[c];
+ if (volumeContainerList[volume]) {
+ volumeContainerList[volume].push({
+ stats: this.state.volumes[volume.Name + volume.isSystem.toString()],
+ });
+ } else {
+ volumeContainerList[volume] = [{
+ stats: this.state.volumes[volume.Name + volume.isSystem.toString()]
+ }];
+ }
+ });
+ } else
+ volumeContainerList = null;
+
let startService = "";
const action = (<>
{_("Start")}
@@ -652,11 +720,28 @@ class Application extends React.Component {
selinuxAvailable={this.state.selinuxAvailable}
podmanRestartAvailable={this.state.podmanRestartAvailable}
/>;
+ const volumeList =
+ this.setState({ containersFilter: "all" }) }
+ user={this.state.currentUser}
+ userServiceAvailable={this.state.userServiceAvailable}
+ systemServiceAvailable={this.state.systemServiceAvailable}
+ registries={this.state.registries}
+ selinuxAvailable={this.state.selinuxAvailable}
+ podmanRestartAvailable={this.state.podmanRestartAvailable}
+ />;
const containerList =
{ this.state.showStartService ? startService : null }
- {imageList}
{containerList}
+ {volumeList}
+ {imageList}
diff --git a/src/client.js b/src/client.js
index 4494b2327..d234e70dc 100644
--- a/src/client.js
+++ b/src/client.js
@@ -1,7 +1,7 @@
import rest from './rest.js';
const PODMAN_SYSTEM_ADDRESS = "/run/podman/podman.sock";
-export const VERSION = "/v1.12/";
+export const VERSION = "/v4.0/";
export function getAddress(system) {
if (system)
@@ -285,3 +285,54 @@ export function pruneUnusedImages(system) {
export function imageExists(system, id) {
return podmanCall("libpod/images/" + id + "/exists", "GET", {}, system);
}
+
+export function inspectVolume(system, name) {
+ return new Promise((resolve, reject) => {
+ const options = {};
+ podmanCall("libpod/volumes/" + name + "/json", "GET", options, system)
+ .then(reply => resolve(JSON.parse(reply)))
+ .catch(reject);
+ });
+}
+
+export function getVolumes(system, name) {
+ return new Promise((resolve, reject) => {
+ const options = {};
+ if (name)
+ options.filters = JSON.stringify({ name: [name] });
+ podmanCall("libpod/volumes/json", "GET", options, system)
+ .then(reply => resolve(JSON.parse(reply)))
+ .catch(reject);
+ });
+}
+
+export function delVolume(system, name, force) {
+ return new Promise((resolve, reject) => {
+ const options = {
+ force: force,
+ };
+ podmanCall("libpod/volumes/" + name, "DELETE", options, system)
+ .then(reply => reply)
+ .catch(reject);
+ });
+}
+
+export function pruneUnusedVolumes(system) {
+ return new Promise((resolve, reject) => {
+ podmanCall("libpod/volumes/prune?all=true", "POST", {}, system).then(resolve)
+ .then(reply => resolve(JSON.parse(reply)))
+ .catch(reject);
+ });
+}
+
+export function volumeExists(system, name) {
+ return podmanCall("libpod/volumes/" + name + "/exists", "GET", {}, system);
+}
+
+export function createVolume(system, config) {
+ return new Promise((resolve, reject) => {
+ podmanCall("libpod/volumes/create", "POST", {}, system, JSON.stringify(config))
+ .then(reply => resolve(JSON.parse(reply)))
+ .catch(reject);
+ });
+}
diff --git a/src/podman.scss b/src/podman.scss
index 626d7eac5..6f79150d8 100644
--- a/src/podman.scss
+++ b/src/podman.scss
@@ -15,6 +15,13 @@
}
}
+#containers-volumes, #containers-containers {
+ // Decrease padding for the volume/container toggle button list
+ .pf-c-table.pf-m-compact .pf-c-table__toggle {
+ padding-left: 0px;
+ }
+}
+
@media screen and (max-width: 768px) {
// Badges should not stretch in mobile mode
.pf-c-table [data-label] > .pf-c-badge {
@@ -61,13 +68,19 @@
display: none;
}
+// Hide the header nav from the expandable rows - this should be better done with JS but the current cockpit-listing-panel implementation does not support this variant
+#containers-volumes .ct-listing-panel-head {
+ display: none;
+}
+
.ct-grey-text {
color: var(--pf-global--Color--200);
}
// Add borders to no pod containers list and images list
.container-section.pf-m-plain tbody,
-.containers-images tbody {
+.containers-images tbody,
+.containers-volumes tbody {
border: var(--pf-c-card--m-flat--BorderWidth) solid var(--pf-c-card--m-flat--BorderColor);
}
@@ -76,7 +89,7 @@
white-space: nowrap !important;
}
-@media ( max-width: $pf-global--breakpoint--md - 1 ) {
+@media ( max-width: $pf-global--breakpoint--md ) {
.show-only-when-wide {
display: none;
}
diff --git a/src/util.js b/src/util.js
index ab83b399c..43bd0ce9c 100644
--- a/src/util.js
+++ b/src/util.js
@@ -24,6 +24,11 @@ export function localize_time(unix_timestamp) {
return formatRelative(unix_timestamp * 1000, Date.now(), { locale });
}
+export function localize_date(timestamp_string) {
+ const date = new Date(timestamp_string);
+ return date.toLocaleString(cockpit.language);
+}
+
export function format_memory_and_limit(usage, limit) {
if (usage === undefined || isNaN(usage))
return "";
From 40080ac7a9258271c28084f63c3e6eb86dcf2034 Mon Sep 17 00:00:00 2001
From: Pavel Strzinek
Date: Tue, 12 Apr 2022 19:44:46 +0200
Subject: [PATCH 2/8] Fix volume layout and UI deficiencies
---
src/VolumeCreateModal.jsx | 39 +++++++++++++++++----------------------
src/VolumeDeleteModal.jsx | 2 +-
src/Volumes.css | 5 -----
src/Volumes.jsx | 9 ++++-----
src/app.jsx | 4 ++--
5 files changed, 24 insertions(+), 35 deletions(-)
diff --git a/src/VolumeCreateModal.jsx b/src/VolumeCreateModal.jsx
index 45529199a..cf84343b1 100644
--- a/src/VolumeCreateModal.jsx
+++ b/src/VolumeCreateModal.jsx
@@ -6,7 +6,7 @@ import {
Form, FormGroup, FormFieldGroup, FormFieldGroupHeader,
HelperText, HelperTextItem,
Modal, Radio,
- TextInput, Tabs, Tab, TabTitleText
+ TextInput
} from '@patternfly/react-core';
import * as dockerNames from 'docker-names';
@@ -189,7 +189,7 @@ export class VolumeCreateModal extends React.Component {
render() {
const dialogValues = this.state;
- const { activeTabKey, owner } = this.state;
+ const { owner } = this.state;
const defaultBody = (
);
return (
@@ -228,7 +223,7 @@ export class VolumeCreateModal extends React.Component {
onEscapePress={() => {
this.props.close();
}}
- title={_("Create container")}
+ title={_("Create volume")}
footer={<>
{this.state.dialogError && }