Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/VolumeDeleteModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
import React, { useState } from 'react';

import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import {
Modal, ModalBody, ModalFooter, ModalHeader
} from '@patternfly/react-core/dist/esm/components/Modal';
import { useDialogs } from "dialogs.jsx";

import cockpit from 'cockpit';

import * as client from './client.js';

const _ = cockpit.gettext;

export const VolumeDeleteModal = ({ con, volume }) => {
const [force, setForce] = useState(false);
const [reason, setReason] = useState(null);
const Dialogs = useDialogs();

const handleRemoveVolume = async () => {
setReason(null);
try {
await client.deleteVolume(con, volume.Name, force);
Dialogs.close();
} catch (exc) {
setReason(exc.message);
setForce(true);
}
};

return (
<Modal isOpen
position="top" variant="medium"
onClose={Dialogs.close}
>
<ModalHeader title={cockpit.format(_("Delete $0 volume?"), volume.Name)}
titleIconVariant="warning"
/>
<ModalBody>
{reason}
</ModalBody>
<ModalFooter>
<Button id="btn-volume-delete" variant="danger"
onClick={() => handleRemoveVolume()}>
{force ? _("Force delete volume") : _("Delete volume")}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Force is never used atm, state is never modified

</Button>
<Button variant="link" onClick={Dialogs.close}>{_("Cancel")}</Button>
</ModalFooter>
</Modal>
);
};
188 changes: 188 additions & 0 deletions src/Volumes.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
import React from 'react';

import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card";
import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection";
import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex";
import { cellWidth, SortByDirection } from '@patternfly/react-table';
import { KebabDropdown } from "cockpit-components-dropdown.jsx";
import { ListingTable } from "cockpit-components-table.jsx";
import { useDialogs } from 'dialogs.jsx';

import cockpit from 'cockpit';

import { VolumeDeleteModal } from './VolumeDeleteModal.jsx';
import * as utils from './util.js';

const _ = cockpit.gettext;

const Volumes = ({ users, volumes, ownerFilter, textFilter, volumeContainerMap }) => {
const [isExpanded, setIsExpanded] = React.useState(false);

const getUsedByText = (volume) => {
if (volumeContainerMap === null) {
return { title: _("unused"), count: 0 };
}
const containers = volumeContainerMap[volume.key];
if (containers !== undefined) {
const title = cockpit.format(cockpit.ngettext("$0 container", "$0 containers", containers.length), containers.length);
return { title, count: containers.length };
} else {
return { title: _("unused"), count: 0 };
}
};

const renderRow = volume => {
const { title: usedByText, count: usedByCount } = getUsedByText(volume);
const user = users.find(user => user.uid === volume.uid);
cockpit.assert(user, `User not found for volume uid ${volume.uid}`);

const columns = [
{ title: volume.Name, header: true, props: { modifier: "breakWord" } },
{
title: (volume.uid === 0) ? _("system") : <div><span className="ct-grey-text">{_("user:")} </span>{user.name}</div>,
props: { modifier: "nowrap" },
sortKey: volume.key,
},
{ title: volume.Mountpoint, header: true, props: { modifier: "breakWord" } },
{ title: volume.Driver, header: true, props: { modifier: "breakWord" } },
{ title: <utils.RelativeTime time={volume.CreatedAt} />, props: { className: "ignore-pixels" } },
{ title: <span className={usedByCount === 0 ? "ct-grey-text" : ""}>{usedByText}</span>, props: { className: "ignore-pixels", modifier: "nowrap" }, sortKey: usedByCount },
{
title: <VolumeActions con={user.con} volume={volume} />,
props: { className: 'pf-v6-c-table__action content-action' }
},
];

return {
columns,
props: {
key: volume.key,
"data-row-id": volume.key,
"data-row-name": `${volume.uid === null ? 'user' : volume.uid}-${volume.Name}`
},
};
};

const sortRows = (rows, direction, idx) => {
// Name / Owner / Mount Point / Driver / Created / Used by
const isNumeric = idx == 4 || idx == 5;
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 },
];

let emptyCaption = _("No volumes");
if (volumes === null) {
emptyCaption = _("Loading...");
} else if (textFilter.length > 0) {
emptyCaption = _("No volumes that match the current filter");
}

const volumeKeys = Object.keys(volumes || {});
const volumesTotal = volumeKeys.length;

let filtered = [...volumeKeys];
if (volumes !== null) {
if (ownerFilter !== "all") {
filtered = filtered.filter(id => {
if (ownerFilter === "user")
return volumes[id].uid === null;
return volumes[id].uid === ownerFilter;
});
}

if (textFilter.length > 0) {
filtered = filtered.filter(id => {
const name = volumes[id].Name;
return name.toLowerCase().includes(textFilter);
});
}
}

const rows = filtered.map(name => renderRow(volumes[name]));

const cardBody = (
<ListingTable variant='compact'
aria-label={_("Volumes")}
emptyCaption={emptyCaption}
columns={columnTitles}
rows={rows}
sortMethod={sortRows}
/>
);

const volumesTitleStats = (
<h5>
{cockpit.format(cockpit.ngettext("$0 volume total", "$0 volumes total", volumesTotal), volumesTotal)}
</h5>
);

return (
<Card id="containers-volumes" className="containers-volumes">
<CardHeader>
<Flex flexWrap={{ default: 'nowrap' }} className="pf-v6-u-w-100">
<FlexItem grow={{ default: 'grow' }}>
<Flex>
<CardTitle>
<h2 className="containers-volumes-title">{_("Volumes")}</h2>
</CardTitle>
<Flex className="ignore-pixels" style={{ rowGap: "var(--pf-v6-global--spacer--xs)" }}>{volumesTitleStats}</Flex>
</Flex>
</FlexItem>
</Flex>
</CardHeader>
<CardBody>
{volumes && Object.keys(volumes).length
? <ExpandableSection toggleText={isExpanded ? _("Hide volumes") : _("Show volumes")}
onToggle={() => setIsExpanded(!isExpanded)}
isExpanded={isExpanded}>
{cardBody}
</ExpandableSection>
: cardBody}
</CardBody>
</Card>
);
};

const VolumeActions = ({ con, volume }) => {
const Dialogs = useDialogs();

const removeVolume = () => {
Dialogs.show(<VolumeDeleteModal
con={con}
volume={volume}

/>);
};

const dropdownActions = [
<DropdownItem key={volume.Name + "delete"}
component="button"
className="pf-m-danger btn-delete"
onClick={removeVolume}>
{_("Delete")}
</DropdownItem>
];

return <KebabDropdown position="right" dropdownItems={dropdownActions} />;
};

export default Volumes;
Loading
Loading