- {_("Name")}
+ {_("Partition label")}
{block_part.Name || "-"}
{ !unused_space &&
@@ -67,12 +67,12 @@ export const PartitionTab = ({ client, block, warnings }) => {
}
- {_("UUID")}
+ {_("Partition UUID")}
{block_part.UUID}
- {_("Type")}
+ {_("Partition type")}
{block_part.Type}
diff --git a/pkg/storaged/side-panel.jsx b/pkg/storaged/side-panel.jsx
index 1bdb1684fc81..8c592e027660 100644
--- a/pkg/storaged/side-panel.jsx
+++ b/pkg/storaged/side-panel.jsx
@@ -20,158 +20,77 @@
import cockpit from "cockpit";
import React from "react";
-import { OptionalPanel } from "./optional-panel.jsx";
+import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { ListingTable } from "cockpit-components-table.jsx";
import { get_block_link_parts, block_name } from "./utils.js";
-import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
-import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
-import { EmptyState, EmptyStateBody, EmptyStateVariant } from "@patternfly/react-core/dist/esm/components/EmptyState/index.js";
-import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
-import { warnings_icon } from "./warnings.jsx";
-
const _ = cockpit.gettext;
export class SidePanel extends React.Component {
- constructor() {
- super();
- this.state = { collapsed: true };
- this.current_rows_keys = [];
- this.new_keys = [];
- }
-
render() {
- let show_all_button = null;
- let rows = this.props.rows.filter(row => !!row);
+ const rows = this.props.rows.filter(row => !!row);
// Find new items for animations
- const current_keys = rows.map(row => row.key);
-
- if (JSON.stringify(this.current_rows_keys) !== JSON.stringify(current_keys)) {
- if (this.current_rows_keys.length !== 0) {
- const new_keys = current_keys.filter(key => this.current_rows_keys.indexOf(key) === -1);
- if (new_keys.length)
- this.new_keys.push(...new_keys);
+ const children = rows.map(row => {
+ if (row.block) {
+ const client = row.client;
+ const parts = get_block_link_parts(client, row.block.path);
+ const backing = client.blocks[row.block.CryptoBackingDevice];
+ row.name = cockpit.format(parts.format, parts.link);
+ row.devname = block_name(backing || row.block);
+ row.go = () => { cockpit.location.go([row.devname.replace(/^\/dev\//, "")]) };
}
- this.current_rows_keys = current_keys;
- }
-
- // Collapse items by default if more than 20
- if (this.state.collapsed && rows.length > 20) {
- show_all_button = (
-
-
- );
- rows = rows.slice(0, 20);
- }
- rows.forEach(row => {
- if (row.key && this.new_keys.indexOf(row.key) !== -1)
- row.className = (row.className || "") + " ct-new-item";
+ const eat_event = (event) => {
+ // Stop events from disabled actions. Otherwise they would
+ // reach the
element and cause spurious navigation.
+ event.stopPropagation();
+ };
+
+ return {
+ props: { },
+ columns: [
+ row.name,
+ row.devname,
+ row.detail,
+ {
+ title:
+ {row.actions}
+
+ }
+ ],
+ go: row.go
+ };
});
- const children = rows.map(row => row.block ? : );
-
- return (
-
- { this.props.rows.length > 0
- ?
- { children }
- { show_all_button }
-
- :
-
- {this.props.empty_text}
-
-
- }
-
- );
- }
-}
-
-class SidePanelRow extends React.Component {
- render() {
- const { client, job_path } = this.props;
-
- const go = (event) => {
- if (!event)
- return;
-
- // only consider primary mouse button for clicks
- if (event.type === 'click' && event.button !== 0)
+ function onRowClick(event, row) {
+ if (!event || event.button !== 0)
return;
- // only consider enter button for keyboard events
- if (event.type === 'KeyDown' && event.key !== "Enter")
+ // StorageBarMenu sets this to tell us not to navigate when
+ // the kebabs are opened.
+ if (event.defaultPrevented)
return;
- return this.props.go();
- };
-
- const eat_event = (event) => {
- // Stop events from disabled actions. Otherwise they would
- // reach the
element and cause spurious navigation.
- event.stopPropagation();
- };
-
- let decoration = null;
- if (this.props.actions)
- decoration = (
-
- {this.props.actions}
-
);
- else if (client.path_jobs[job_path])
- decoration = ;
- else if (client.path_warnings[job_path])
- decoration = warnings_icon(client.path_warnings[job_path]);
+ if (row.go)
+ row.go();
+ }
return (
-
-
- {this.props.name}
- {decoration}
-
-
- {this.props.detail}
- {this.props.devname}
-
-
+
+
+ {this.props.title}
+
+
+
+
+
);
}
}
-
-class SidePanelBlockRow extends React.Component {
- render() {
- const { client, block, detail, actions } = this.props;
-
- const parts = get_block_link_parts(client, block.path);
- const name = cockpit.format(parts.format, parts.link);
- const backing = client.blocks[block.CryptoBackingDevice];
-
- return { cockpit.location.go(parts.location) }}
- actions={actions}
- className={this.props.className}
- />;
- }
-}
diff --git a/pkg/storaged/storage-controls.jsx b/pkg/storaged/storage-controls.jsx
index 771971fba534..5671b294dd87 100644
--- a/pkg/storaged/storage-controls.jsx
+++ b/pkg/storaged/storage-controls.jsx
@@ -241,7 +241,10 @@ export const StorageBarMenu = ({ label, isKebab, onlyNarrow, menuItems }) => {
return null;
function onToggle(event, isOpen) {
- event.stopPropagation();
+ // Tell Overview that we handled this event. We can't use
+ // stopPrevention() since the Toggles depend on seeing other
+ // Togglers events at the top level to close themselves.
+ event.preventDefault();
setIsOpen(isOpen);
}
diff --git a/pkg/storaged/storage.scss b/pkg/storaged/storage.scss
index 39de034e56e8..ceb4b63427a5 100644
--- a/pkg/storaged/storage.scss
+++ b/pkg/storaged/storage.scss
@@ -197,19 +197,13 @@ tr[class*="content-level-"] {
--multiplier: 0;
--offset: calc(var(--pf-v5-global--spacer--lg) * var(--multiplier));
- // Move the button over
- > .pf-v5-c-table__toggle > button {
+ > td:first-child {
position: relative;
inset-inline-start: var(--offset);
}
-
- // Add space for the button and offset
- > .pf-v5-c-table__toggle + td {
- padding-inline-start: calc(var(--offset) + var(--pf-v5-c-table--cell--PaddingLeft));
- }
}
-@for $i from 1 through 10 {
+@for $i from 0 through 10 {
tr.content-level-#{$i} {
--multiplier: #{$i};
}
diff --git a/pkg/storaged/storaged.jsx b/pkg/storaged/storaged.jsx
index 4a04b8e381e9..67f8554cb385 100644
--- a/pkg/storaged/storaged.jsx
+++ b/pkg/storaged/storaged.jsx
@@ -30,7 +30,7 @@ import { PlotState } from "plot.js";
import client from "./client";
import { MultipathAlert } from "./multipath.jsx";
-import { Overview } from "./overview.jsx";
+import { OverviewX } from "./overviewx.jsx";
import { Details } from "./details.jsx";
import { update_plot_state } from "./plot.jsx";
@@ -88,7 +88,7 @@ class StoragePage extends React.Component {
return (
<>
- {detail || }
+ {detail || }
>
);
}
diff --git a/pkg/storaged/stratis-details.jsx b/pkg/storaged/stratis-details.jsx
index 2fde5b647dc7..70d00c77697f 100644
--- a/pkg/storaged/stratis-details.jsx
+++ b/pkg/storaged/stratis-details.jsx
@@ -47,7 +47,8 @@ import {
encode_filename, decode_filename,
get_active_usage, teardown_active_usage,
get_available_spaces, prepare_available_spaces,
- reload_systemd, for_each_async
+ reload_systemd, for_each_async,
+ block_name
} from "./utils.js";
import { fmt_to_fragments } from "utils.jsx";
import { mount_explanation } from "./format-dialog.jsx";
@@ -447,8 +448,12 @@ export function stratis_content_rows(client, pool, options) {
const menuitems = [];
if (!fs_is_mounted) {
- actions.push({_("Mount")});
- menuitems.push({_("Mount")});
+ if (options.unified) {
+ menuitems.push({_("Mount")});
+ } else {
+ actions.push({_("Mount")});
+ menuitems.push({_("Mount")});
+ }
}
if (fs_is_mounted)
@@ -483,10 +488,16 @@ export function stratis_content_rows(client, pool, options) {
}
];
+ if (options.unified) {
+ // insert "Type" column
+ cols.splice(1, 0, { title: _("Stratis filesystem") });
+ }
+
return {
- props: { key: fsys.Name },
+ props: { key: fsys.Name, className: "content-level-" + (options.level || 0) },
columns: cols,
- expandedContent:
+ expandedContent: options.unified ? null : ,
+ go: () => cockpit.location.go([block_name(block).replace(/^\/dev\//, "")])
};
}
@@ -500,7 +511,7 @@ function create_fs(client, pool) {
const managed_fsys_sizes = client.features.stratis_managed_fsys_sizes && !pool.Overprovisioning;
dialog_open({
- Title: _("Create filesystem"),
+ Title: cockpit.format(_("Create filesystem in $0"), pool.Name),
Fields: [
TextInput("name", _("Name"),
{
@@ -627,6 +638,24 @@ function rename_pool(client, pool) {
});
}
+export function pool_menu_items(client, pool, options) {
+ return [
+ create_fs(client, pool)}>
+ {_("Create filesystem")}
+ ,
+ rename_pool(client, pool)}>
+ {_("Rename pool")}
+ ,
+ delete_pool(client, pool)}>
+ {_("Delete pool")}
+ ,
+ ];
+}
+
export const StratisPoolDetails = ({ client, pool }) => {
const key_desc = (pool.Encrypted &&
pool.KeyDescription[0] &&
@@ -886,7 +915,20 @@ export const StratisPoolDetails = ({ client, pool }) => {
);
const sidebar = ;
- const rows = stratis_content_rows(client, pool, {});
+ const rows = stratis_content_rows(client, pool, { unified: true });
+
+ function onRowClick(event, row) {
+ if (!event || event.button !== 0)
+ return;
+
+ // StorageBarMenu sets this to tell us not to navigate when
+ // the kebabs are opened.
+ if (event.defaultPrevented)
+ return;
+
+ if (row.go)
+ row.go();
+ }
const content = (
@@ -902,7 +944,13 @@ export const StratisPoolDetails = ({ client, pool }) => {
!!row)} />
diff --git a/pkg/storaged/stratis-panel.jsx b/pkg/storaged/stratis-panel.jsx
index 9306ee7c3789..78cd6e1e5a33 100644
--- a/pkg/storaged/stratis-panel.jsx
+++ b/pkg/storaged/stratis-panel.jsx
@@ -59,8 +59,11 @@ function stratis_pool_row(client, path) {
key: path,
devname: "/dev/stratis/" + pool.Name + "/",
detail: cockpit.format(_("$0 Stratis pool"), fmt_size(pool.TotalPhysicalSize)),
+ size: pool.TotalPhysicalSize,
+ type: _("Stratis pool"),
go: () => cockpit.location.go(["pool", pool.Uuid]),
- job_path: path
+ job_path: path,
+ pool,
};
}
diff --git a/pkg/storaged/things-panel.jsx b/pkg/storaged/things-panel.jsx
index 043f7528fc45..941166ffa744 100644
--- a/pkg/storaged/things-panel.jsx
+++ b/pkg/storaged/things-panel.jsx
@@ -27,6 +27,8 @@ import { create_vgroup, vgroup_rows } from "./vgroups-panel.jsx";
import { vdo_rows } from "./vdos-panel.jsx";
import { StorageBarMenu, StorageMenuItem } from "./storage-controls.jsx";
import { stratis_feature, create_stratis_pool, stratis_rows } from "./stratis-panel.jsx";
+import { nfs_feature } from "./nfs-panel.jsx";
+import { nfs_fstab_dialog } from "./nfs-details.jsx";
import { dialog_open } from "./dialog.jsx";
const _ = cockpit.gettext;
@@ -73,7 +75,8 @@ export function thing_menu_items(client, options) {
const menu_items = [
menu_item(null, _("Create RAID device"), () => create_mdraid(client)),
menu_item(lvm2_feature, _("Create LVM2 volume group"), () => create_vgroup(client)),
- menu_item(stratis_feature(client), _("Create Stratis pool"), () => create_stratis_pool(client))
+ menu_item(stratis_feature(client), _("Create Stratis pool"), () => create_stratis_pool(client)),
+ options.unified ? menu_item(nfs_feature(client), _("New NFS mount"), () => nfs_fstab_dialog(client, null)) : null,
].filter(item => item !== null);
return menu_items;
diff --git a/pkg/storaged/utils.js b/pkg/storaged/utils.js
index 20971698b330..a66702d27264 100644
--- a/pkg/storaged/utils.js
+++ b/pkg/storaged/utils.js
@@ -339,11 +339,14 @@ export function get_block_link_parts(client, path) {
location = ["vdo", vdo.name];
link = cockpit.format(_("VDO device $0"), vdo.name);
} else {
- location = [block_name(block).replace(/^\/dev\//, "")];
- if (client.drives[block.Drive])
- link = drive_name(client.drives[block.Drive]);
- else
+ if (client.drives[block.Drive]) {
+ const drive = client.drives[block.Drive];
+ location = ["drive", block_name(client.drives_block[drive.path]).replace(/^\/dev\//, "")];
+ link = drive_name(drive);
+ } else {
+ location = [block_name(block).replace(/^\/dev\//, "")];
link = block_name(block);
+ }
}
}
diff --git a/pkg/storaged/vdos-panel.jsx b/pkg/storaged/vdos-panel.jsx
index d2cbff10fe4d..6ccef65ff563 100644
--- a/pkg/storaged/vdos-panel.jsx
+++ b/pkg/storaged/vdos-panel.jsx
@@ -34,6 +34,8 @@ function vdo_row(client, vdo) {
name: vdo.name,
devname: vdo.dev,
detail: fmt_size(vdo.logical_size) + " " + _("VDO device"),
+ size: vdo.logical_size,
+ type: _("VDO device"),
go: () => cockpit.location.go(["vdo", vdo.name]),
job_path: block && block.path
};
diff --git a/pkg/storaged/vgroup-details.jsx b/pkg/storaged/vgroup-details.jsx
index 83b5ab872c6c..dd3a9aede7b3 100644
--- a/pkg/storaged/vgroup-details.jsx
+++ b/pkg/storaged/vgroup-details.jsx
@@ -24,18 +24,20 @@ import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js
import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
import { PlusIcon, MinusIcon } from "@patternfly/react-icons";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import * as utils from "./utils.js";
import { fmt_to_fragments } from "utils.jsx";
import { StdDetailsLayout } from "./details.jsx";
import { SidePanel } from "./side-panel.jsx";
-import { VGroup } from "./content-views.jsx";
+import { VGroup, create_tabs, ThinPoolContent } from "./content-views.jsx";
import { StorageButton } from "./storage-controls.jsx";
import {
dialog_open, TextInput, SelectSpaces,
BlockingMessage, TeardownMessage,
init_active_usage_processes
} from "./dialog.jsx";
+import { BlockDetails } from "./block-details.jsx";
const _ = cockpit.gettext;
@@ -323,3 +325,117 @@ export class VGroupDetails extends React.Component {
content={ content } />;
}
}
+
+const ThinPoolDetails = ({ client, lvol }) => {
+ // XXX - mostly a copy of BlockDetails
+ const tabs = create_tabs(client, lvol, {});
+
+ const actions = tabs.actions;
+ tabs.menu_actions.forEach(a => {
+ if (!a.only_narrow)
+ actions.push({a.title});
+ });
+ tabs.menu_danger_actions.forEach(a => {
+ if (!a.only_narrow)
+ actions.push({a.title});
+ });
+
+ function is_container(r) {
+ return r.name == _("Logical volume") || r.name == _("Partition");
+ }
+
+ const content_renderers = tabs.renderers.filter(r => !is_container(r));
+ const vgroup = client.vgroups[lvol.VolumeGroup];
+
+ const header = (
+
+
+
+ {_("Pool for thinly provisioned logical volumes")}
+
+
+
+
+
+ {_("Stored on")}
+
+
+
+
+
+ { content_renderers.map(t =>
) }
+
+
+ );
+
+ const content = ;
+
+ return ;
+};
+
+const InactiveVolumeDetails = ({ client, lvol }) => {
+ // XXX - mostly a copy of BlockDetails
+ const tabs = create_tabs(client, lvol, {});
+
+ const actions = tabs.actions;
+ tabs.menu_actions.forEach(a => {
+ if (!a.only_narrow)
+ actions.push({a.title});
+ });
+ tabs.menu_danger_actions.forEach(a => {
+ if (!a.only_narrow)
+ actions.push({a.title});
+ });
+
+ function is_container(r) {
+ return r.name == _("Logical volume") || r.name == _("Partition");
+ }
+
+ const content_renderers = tabs.renderers.filter(r => !is_container(r));
+ const vgroup = client.vgroups[lvol.VolumeGroup];
+
+ const header = (
+
+
+
+ {_("Inactive (or unsupported) logical volume")}
+
+
+
+
+
+ {_("Stored on")}
+
+
+
+
+
+ { content_renderers.map(t =>
) }
+
+
+ );
+
+ return ;
+};
+
+export const LVolDetails = ({ client, lvol }) => {
+ const block = client.lvols_block[lvol.path];
+
+ if (lvol.Type == "pool") {
+ return ;
+ } else if (block) {
+ return ;
+ } else {
+ return ;
+ }
+};
diff --git a/pkg/storaged/vgroups-panel.jsx b/pkg/storaged/vgroups-panel.jsx
index 4392baee92a8..d4baf11f3b7e 100644
--- a/pkg/storaged/vgroups-panel.jsx
+++ b/pkg/storaged/vgroups-panel.jsx
@@ -38,7 +38,10 @@ function vgroup_row(client, path) {
job_path: path,
devname: "/dev/" + vgroup.Name + "/",
detail: fmt_size(vgroup.Size) + " " + _("LVM2 volume group"),
- go: () => cockpit.location.go(["vg", vgroup.Name])
+ size: vgroup.Size,
+ type: _("LVM2 volume group"),
+ go: () => cockpit.location.go(["vg", vgroup.Name]),
+ vgroup,
};
}