diff --git a/pkg/storaged/block-details.jsx b/pkg/storaged/block-details.jsx
index f9deed2b6ddc..b3f166166fc6 100644
--- a/pkg/storaged/block-details.jsx
+++ b/pkg/storaged/block-details.jsx
@@ -20,22 +20,155 @@
import cockpit from "cockpit";
import React from "react";
-import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { Card, CardHeader, CardBody, 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 * as utils from "./utils.js";
import { StdDetailsLayout } from "./details.jsx";
-import * as Content from "./content-views.jsx";
+import { create_tabs } from "./content-views.jsx";
+import { StorageButton } from "./storage-controls.jsx";
const _ = cockpit.gettext;
+export function block_nav_parents(client, block) {
+ // XXX - terrible. The client should build a proper hierachical model.
+
+ const drive = client.drives[block.Drive];
+ const drive_block = drive && client.drives_block[drive.path];
+ if (drive && drive_block) {
+ return [
+ {
+ location: ["drive", utils.block_name(drive_block).replace(/^\/dev\//, "")],
+ title: utils.drive_name(drive)
+ }
+ ];
+ }
+
+ const mdraid = client.mdraids[block.MDRaid];
+ if (mdraid) {
+ return [{ location: ["md", mdraid.UUID], title: "XXX - mdraid" }];
+ }
+
+ const lvol = client.blocks_lvm2[block.path] && client.lvols[client.blocks_lvm2[block.path].LogicalVolume];
+ const pool = lvol && client.lvols[lvol.Pool];
+ const vgroup = lvol && client.vgroups[lvol.VolumeGroup];
+
+ if (lvol && vgroup && pool) {
+ return [{ location: ["vg", vgroup.Name, pool.Name], title: pool.Name },
+ { location: ["vg", vgroup.Name], title: vgroup.Name }
+ ];
+ }
+
+ if (lvol && vgroup) {
+ return [{ location: ["vg", vgroup.Name], title: vgroup.Name }];
+ }
+
+ const stratis_fsys = client.blocks_stratis_fsys[block.path];
+ const stratis_pool = stratis_fsys && client.stratis_pools[stratis_fsys.Pool];
+ if (stratis_fsys && stratis_pool) {
+ return [{ location: ["pool", stratis_pool.Uuid], title: stratis_pool.Name }];
+ }
+
+ return [];
+}
+
+function content_description(client, block) {
+ let is_crypto = (block && block.IdUsage == 'crypto');
+ const content_block = is_crypto ? client.blocks_cleartext[block.path] : block;
+
+ const block_fsys = content_block && client.blocks_fsys[content_block.path];
+ const block_lvm2 = block && client.blocks_lvm2[block.path];
+ const block_pvol = content_block && client.blocks_pvol[content_block.path];
+ const block_swap = content_block && client.blocks_swap[content_block.path];
+
+ const block_stratis_blockdev = block && client.blocks_stratis_blockdev[block.path];
+ const block_stratis_stopped_pool = block && client.blocks_stratis_stopped_pool[block.path];
+
+ const lvol = block_lvm2 && client.lvols[block_lvm2.LogicalVolume];
+
+ const is_filesystem = (content_block && content_block.IdUsage == 'filesystem');
+ const is_stratis = ((content_block && content_block.IdUsage == "raid" && content_block.IdType == "stratis") ||
+ (block_stratis_blockdev && client.stratis_pools[block_stratis_blockdev.Pool]) ||
+ block_stratis_stopped_pool);
+
+ if (is_filesystem) {
+ return _("filesystem");
+ } else if ((content_block && content_block.IdUsage == "raid" && content_block.IdType == "LVM2_member") ||
+ (block_pvol && client.vgroups[block_pvol.VolumeGroup])) {
+ return _("LVM2 physical volume");
+ } else if (is_stratis) {
+ return _("Stratis blockdev");
+ } else if ((content_block && content_block.IdUsage == "raid") ||
+ (content_block && client.mdraids[content_block.MDRaidMember])) {
+ return _("RAID member");
+ } else if (content_block && client.legacy_vdo_overlay.find_by_backing_block(content_block)) {
+ return _("VDO backing device");
+ } else if (block_swap || (content_block && content_block.IdUsage == "other" && content_block.IdType == "swap")) {
+ return _("swap");
+ } else if (content_block) {
+ return _("unrecognized data");
+ } else
+ return "???";
+}
+
+function container_description(client, block) {
+ const drive = client.drives[block.Drive];
+ const drive_block = drive && client.drives_block[drive.path];
+ if (drive && drive_block) {
+ return _("drive");
+ }
+
+ const mdraid = client.mdraids[block.MDRaid];
+ if (mdraid) {
+ return _("MDRAID device");
+ }
+
+ const lvol = client.blocks_lvm2[block.path] && client.lvols[client.blocks_lvm2[block.path].LogicalVolume];
+ const pool = lvol && client.lvols[lvol.Pool];
+ const vgroup = lvol && client.vgroups[lvol.VolumeGroup];
+
+ if (lvol && vgroup && pool) {
+ return _("LVM2 thin provisioning pool");
+ }
+
+ if (lvol && vgroup) {
+ return _("LVM2 volume group");
+ }
+
+ const stratis_fsys = client.blocks_stratis_fsys[block.path];
+ const stratis_pool = stratis_fsys && client.stratis_pools[stratis_fsys.Pool];
+ if (stratis_fsys && stratis_pool) {
+ return _("Stratis pool");
+ }
+
+ return "thing";
+}
+
export class BlockDetails extends React.Component {
render() {
+ const client = this.props.client;
const block = this.props.block;
+ const tabs = create_tabs(this.props.client, block, {});
+
+ 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});
+ });
const header = (
- {_("Block")}
+
+
+ {cockpit.format(_("A $0 on a $1"),
+ content_description(client, block),
+ container_description(client, block))}
+
+
@@ -47,12 +180,11 @@ export class BlockDetails extends React.Component {
{ utils.block_name(block) }
+ { tabs.renderers.map(t =>
) }
);
- const content = ;
-
- return ;
+ return ;
}
}
diff --git a/pkg/storaged/client.js b/pkg/storaged/client.js
index 1562ea38ac99..665892dcb5b7 100644
--- a/pkg/storaged/client.js
+++ b/pkg/storaged/client.js
@@ -544,6 +544,26 @@ function update_indices() {
client.blocks_partitions[path].sort(function (a, b) { return a.Offset - b.Offset });
}
+ client.iscsi_sessions_drives = { };
+ client.drives_iscsi_session = { };
+ for (path in client.drives) {
+ const block = client.drives_block[path];
+ if (!block)
+ continue;
+ for (const session_path in client.iscsi_sessions) {
+ const session = client.iscsi_sessions[session_path];
+ for (i = 0; i < block.Symlinks.length; i++) {
+ console.log("??", block.Symlinks[i], session.data.target_name);
+ if (utils.decode_filename(block.Symlinks[i]).includes(session.data.target_name)) {
+ client.drives_iscsi_session[path] = session;
+ if (!client.iscsi_sessions_drives[session_path])
+ client.iscsi_sessions_drives[session_path] = [];
+ client.iscsi_sessions_drives[session_path].push(client.drives[path]);
+ }
+ }
+ }
+ }
+
client.path_jobs = { };
function enter_job(job) {
if (!job.Objects || !job.Objects.length)
diff --git a/pkg/storaged/content-views.jsx b/pkg/storaged/content-views.jsx
index 2489c12662eb..a2c7526bdb3b 100644
--- a/pkg/storaged/content-views.jsx
+++ b/pkg/storaged/content-views.jsx
@@ -45,10 +45,12 @@ import { FilesystemTab, is_mounted, mounting_dialog, get_fstab_config } from "./
import { CryptoTab } from "./crypto-tab.jsx";
import { get_existing_passphrase, unlock_with_type } from "./crypto-keyslots.jsx";
import { BlockVolTab, PoolVolTab, VDOPoolTab } from "./lvol-tabs.jsx";
+import { PVolTab, MDRaidMemberTab, VDOBackingTab, StratisBlockdevTab } from "./pvol-tabs.jsx";
import { PartitionTab } from "./part-tab.jsx";
import { SwapTab } from "./swap-tab.jsx";
import { UnrecognizedTab } from "./unrecognized-tab.jsx";
import { warnings_icon } from "./warnings.jsx";
+import { vgroup_rename, vgroup_delete } from "./vgroup-details.jsx";
const _ = cockpit.gettext;
@@ -83,7 +85,7 @@ export function pvs_to_spaces(client, pvs) {
});
}
-function create_tabs(client, target, options) {
+export function create_tabs(client, target, options) {
function endsWith(str, suffix) {
return str.indexOf(suffix, str.length - suffix.length) !== -1;
}
@@ -94,6 +96,7 @@ function create_tabs(client, target, options) {
const block_fsys = content_block && client.blocks_fsys[content_block.path];
const block_lvm2 = block && client.blocks_lvm2[block.path];
+ const block_pvol = content_block && client.blocks_pvol[content_block.path];
const block_swap = content_block && client.blocks_swap[content_block.path];
const block_stratis_blockdev = block && client.blocks_stratis_blockdev[block.path];
@@ -108,6 +111,8 @@ function create_tabs(client, target, options) {
(block_stratis_blockdev && client.stratis_pools[block_stratis_blockdev.Pool]) ||
block_stratis_stopped_pool);
+ const target_name = lvol ? utils.lvol_name(lvol) : block ? utils.block_name(block) : null;
+
// Adjust for encryption leaking out of Stratis
if (is_crypto && is_stratis)
is_crypto = false;
@@ -119,24 +124,35 @@ function create_tabs(client, target, options) {
warnings = warnings.concat(client.path_warnings[lvol.path] || []);
const tab_actions = [];
+ const tab_hints = [];
const tab_menu_actions = [];
const tab_menu_danger_actions = [];
- function add_action(title, func) {
- if (tab_actions.length == 0) {
- tab_actions.push({title});
- tab_menu_actions.push({ title, func, only_narrow: true });
+ function add_action(title, func, unified_hint) {
+ if (options.unified) {
+ tab_menu_actions.push({ title, func });
+ if (unified_hint)
+ tab_hints.push(unified_hint);
} else {
- add_menu_action(title, func);
+ if (tab_actions.length == 0) {
+ tab_actions.push({title});
+ tab_menu_actions.push({ title, func, only_narrow: true });
+ } else {
+ add_menu_action(title, func);
+ }
}
}
function add_danger_action(title, func) {
- if (tab_actions.length == 0) {
- tab_actions.push({title});
- tab_menu_danger_actions.push({ title, func, only_narrow: true });
+ if (options.unified) {
+ tab_menu_danger_actions.push({ title, func });
} else {
- add_menu_danger_action(title, func);
+ if (tab_actions.length == 0) {
+ tab_actions.push({title});
+ tab_menu_danger_actions.push({ title, func, only_narrow: true });
+ } else {
+ add_menu_danger_action(title, func);
+ }
}
}
@@ -151,6 +167,11 @@ function create_tabs(client, target, options) {
const tabs = [];
function add_tab(name, renderer, for_content, associated_warnings) {
+ // No tabs on the unified overview
+ // XXX - what about warnings?
+ if (options.unified)
+ return;
+
let tab_warnings = [];
if (associated_warnings)
tab_warnings = warnings.filter(w => associated_warnings.indexOf(w.warning) >= 0);
@@ -175,7 +196,7 @@ function create_tabs(client, target, options) {
return;
dialog_open({
- Title: _("Create thin volume"),
+ Title: cockpit.format(_("Create thin volume in $0/$1"), vgroup.Name, lvol.Name),
Fields: [
TextInput("name", _("Name"),
{
@@ -211,7 +232,7 @@ function create_tabs(client, target, options) {
}
}
- if (options.is_partition) {
+ if (client.blocks_part[block.path]) {
add_tab(_("Partition"), PartitionTab, false, ["unused-space"]);
}
@@ -219,6 +240,16 @@ function create_tabs(client, target, options) {
if (is_filesystem) {
add_tab(_("Filesystem"), FilesystemTab, true, ["mismounted-fsys"]);
+ } else if ((content_block && content_block.IdUsage == "raid" && content_block.IdType == "LVM2_member") ||
+ (block_pvol && client.vgroups[block_pvol.VolumeGroup])) {
+ add_tab(_("LVM2 physical volume"), PVolTab, true);
+ } else if (is_stratis) {
+ add_tab(_("Stratis pool"), StratisBlockdevTab, false);
+ } else if ((content_block && content_block.IdUsage == "raid") ||
+ (content_block && client.mdraids[content_block.MDRaidMember])) {
+ add_tab(_("RAID member"), MDRaidMemberTab, true);
+ } else if (content_block && client.legacy_vdo_overlay.find_by_backing_block(content_block)) {
+ add_tab(_("VDO backing"), VDOBackingTab, true);
} else if (content_block && (content_block.IdUsage == "raid" ||
client.legacy_vdo_overlay.find_by_backing_block(content_block))) {
// no tab for these
@@ -262,7 +293,7 @@ function create_tabs(client, target, options) {
return;
dialog_open({
- Title: _("Unlock"),
+ Title: _("Unlock $0", target_name),
Fields: [
PassInput("passphrase", _("Passphrase"), {})
],
@@ -283,9 +314,9 @@ function create_tabs(client, target, options) {
} else {
const config = client.blocks_crypto[block.path]?.ChildConfiguration.find(c => c[0] == "fstab");
if (config && !content_block)
- add_action(_("Mount"), () => mounting_dialog(client, block, "mount"));
+ add_action(_("Mount"), () => mounting_dialog(client, block, "mount"), _("not mounted"));
else
- add_action(_("Unlock"), unlock);
+ add_action(_("Unlock"), unlock, _("locked"));
}
}
@@ -298,8 +329,12 @@ function create_tabs(client, target, options) {
}
function create_snapshot() {
+ const vgroup = lvol && client.vgroups[lvol.VolumeGroup];
+ if (!vgroup)
+ return;
+
dialog_open({
- Title: _("Create snapshot"),
+ Title: cockpit.format(_("Create snapshot of $0/$1"), vgroup.Name, lvol.Name),
Fields: [
TextInput("name", _("Name"),
{ validate: utils.validate_lvm2_name }),
@@ -374,7 +409,7 @@ function create_tabs(client, target, options) {
if (lvol.Active) {
add_menu_action(_("Deactivate"), deactivate);
} else {
- add_action(_("Activate"), activate);
+ add_action(_("Activate"), activate, _("not active"));
}
}
if (client.lvols[lvol.ThinPool]) {
@@ -406,29 +441,27 @@ function create_tabs(client, target, options) {
if (block)
block_part = client.blocks_part[block.path];
- let name, danger;
+ let danger;
if (lvol) {
- name = utils.lvol_name(lvol);
danger = _("Deleting a logical volume will delete all data in it.");
} else if (block_part) {
- name = utils.block_name(block);
danger = _("Deleting a partition will delete all data in it.");
}
- if (name) {
+ if (target_name) {
const usage = utils.get_active_usage(client, target.path, _("delete"));
if (usage.Blocking) {
dialog_open({
- Title: cockpit.format(_("$0 is in use"), name),
+ Title: cockpit.format(_("$0 is in use"), target_name),
Body: BlockingMessage(usage)
});
return;
}
dialog_open({
- Title: cockpit.format(_("Permanently delete $0?"), name),
+ Title: cockpit.format(_("Permanently delete $0?"), target_name),
Teardown: TeardownMessage(usage),
Action: {
Danger: danger,
@@ -466,12 +499,13 @@ function create_tabs(client, target, options) {
if (is_mounted(client, content_block))
add_menu_action(_("Unmount"), () => mounting_dialog(client, content_block, "unmount"));
else
- add_action(_("Mount"), () => mounting_dialog(client, content_block, "mount"));
+ add_action(_("Mount"), () => mounting_dialog(client, content_block, "mount"), _("not mounted"));
}
return {
renderers: tabs,
actions: tab_actions,
+ hints: tab_hints,
menu_actions: tab_menu_actions,
menu_danger_actions: tab_menu_danger_actions,
warnings
@@ -556,6 +590,9 @@ function block_description(client, block, options) {
if (cleartext && !omit_encrypted_label)
type = cockpit.format(_("$0 (encrypted)"), type);
+ if (options.unified)
+ link = null;
+
return {
type,
used_for,
@@ -601,6 +638,15 @@ function append_row(client, rows, level, key, name, desc, tabs, job_object, opti
if (info)
info = <>{"\n"}{info}>;
+ let location = desc.used_for;
+ if (tabs.hints.length > 0) {
+ const hints = "(" + tabs.hints.join(", ") + ")";
+ if (location)
+ location += " " + hints;
+ else
+ location = hints;
+ }
+
const cols = [
{
title: (
@@ -610,7 +656,7 @@ function append_row(client, rows, level, key, name, desc, tabs, job_object, opti
)
},
{ title: desc.type },
- { title: desc.link ? : desc.used_for },
+ { title: desc.link ? : location },
{
title: desc.size.length
?
@@ -623,7 +669,8 @@ function append_row(client, rows, level, key, name, desc, tabs, job_object, opti
rows.push({
props: { key, className: "content-level-" + level },
columns: cols,
- expandedContent: tabs.renderers.length > 0 ? : null
+ expandedContent: tabs.renderers.length > 0 ? : null,
+ go: options.go
});
}
@@ -631,7 +678,8 @@ function append_non_partitioned_block(client, rows, level, block, options) {
const tabs = create_tabs(client, block, options);
const desc = block_description(client, block, options);
- append_row(client, rows, level, block.path, utils.block_name(block), desc, tabs, block.path, options);
+ append_row(client, rows, level, block.path, utils.block_name(block), desc, tabs, block.path,
+ { ...options, go: () => cockpit.location.go([utils.block_name(block).replace(/^\/dev\//, "")]) });
}
function append_partitions(client, rows, level, block, options) {
@@ -645,20 +693,29 @@ function append_partitions(client, rows, level, block, options) {
format_dialog(client, block.path, start, size, is_dos_partitioned && level <= device_level);
}
- const btn = (
-
- {_("Create partition")}
-
- );
+ let btn, item, menu;
- const item = (
-
- {_("Create partition")}
- );
-
- const menu = ;
+ if (options.unified) {
+ btn = null;
+ item = (
+
+ {_("Create partition")}
+ );
+ menu = ;
+ } else {
+ btn = (
+
+ {_("Create partition")}
+ );
+ item = (
+
+ {_("Create partition")}
+ );
+ menu = ;
+ }
const cols = [
_("Free space"),
@@ -670,7 +727,11 @@ function append_partitions(client, rows, level, block, options) {
rows.push({
columns: cols,
- props: { key: "free-space-" + rows.length.toString(), className: "content-level-" + level }
+ props: {
+ key: "free-space-" + rows.length.toString(),
+ className: "content-level-" + level,
+ },
+ go: options.go,
});
}
@@ -710,7 +771,8 @@ function append_device(client, rows, level, block, options) {
export function block_content_rows(client, block, options) {
const rows = [];
- append_device(client, rows, 0, block, options);
+ append_device(client, rows, options.level || 0, block,
+ { go: () => utils.go_to_block(client, block.path), ...options });
return rows;
}
@@ -771,6 +833,20 @@ function format_disk(client, block) {
});
}
+export function block_menu_items(client, block, options) {
+ function onClick() {
+ if (block.ReadOnly)
+ return Promise.reject(_("Device is read-only"));
+ format_disk(client, block);
+ }
+
+ return [
+
+ {_("Create partition table")}
+
+ ];
+}
+
const BlockContent = ({ client, block, allow_partitions }) => {
if (!block)
return null;
@@ -793,14 +869,28 @@ const BlockContent = ({ client, block, allow_partitions }) => {
else
title = _("Content");
+ 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();
+ }
+
return (
{title}
-
@@ -826,7 +916,8 @@ function append_logical_volume_block(client, rows, level, block, lvol, options)
}
: block_description(client, block, options);
const tabs = create_tabs(client, block, options);
- append_row(client, rows, level, block.path, lvol.Name, desc, tabs, block.path, options);
+ append_row(client, rows, level, block.path, lvol.Name, desc, tabs, block.path,
+ { ...options, go: () => cockpit.location.go([utils.block_name(block).replace(/^\/dev\//, "")]) });
}
function append_logical_volume(client, rows, level, lvol, options) {
@@ -844,9 +935,9 @@ function append_logical_volume(client, rows, level, lvol, options) {
});
} else {
block = client.lvols_block[lvol.path];
- if (block)
+ if (block) {
append_logical_volume_block(client, rows, level, block, lvol, options);
- else {
+ } else {
// If we can't find the block for a active
// volume, Storaged or something below is
// probably misbehaving, and we show it as
@@ -865,12 +956,13 @@ function append_logical_volume(client, rows, level, lvol, options) {
export function vgroup_content_rows(client, vgroup, options) {
const rows = [];
+ const go = () => cockpit.location.go(["vg", vgroup.Name]);
const isVDOPool = lvol => Object.keys(client.vdo_vols).some(v => client.vdo_vols[v].VDOPool == lvol.path);
(client.vgroups_lvols[vgroup.path] || []).forEach(lvol => {
// Don't display VDO pool volumes as separate entities; they are an internal implementation detail and have no actions
if (lvol.ThinPool == "/" && lvol.Origin == "/" && !isVDOPool(lvol))
- append_logical_volume(client, rows, 0, lvol, options);
+ append_logical_volume(client, rows, options.level || 0, lvol, { go, ...options });
});
return rows;
}
@@ -1005,7 +1097,7 @@ function create_logical_volume(client, vgroup) {
}
dialog_open({
- Title: _("Create logical volume"),
+ Title: cockpit.format(_("Create logical volume in $0"), vgroup.Name),
Fields: [
TextInput("name", _("Name"),
{
@@ -1142,6 +1234,26 @@ function create_logical_volume(client, vgroup) {
});
}
+export function vgroup_menu_items(client, vgroup, options) {
+ function onClick() {
+ if (vgroup.FreeSize == 0)
+ return Promise.reject(_("No free space"));
+ create_logical_volume(client, vgroup);
+ }
+
+ return [
+
+ {_("Create logical volume")}
+ ,
+ vgroup_rename(client, vgroup)}>
+ {_("Rename volume group")}
+ ,
+ vgroup_delete(client, vgroup)}>
+ {_("Delete volume group")}
+ ,
+ ];
+}
+
export class VGroup extends React.Component {
constructor () {
super();
@@ -1173,6 +1285,19 @@ export class VGroup extends React.Component {
);
+ 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();
+ }
+
return (
@@ -1181,9 +1306,10 @@ export class VGroup extends React.Component {
+ rows={vgroup_content_rows(client, vgroup, { unified: true })} />
);
diff --git a/pkg/storaged/details.jsx b/pkg/storaged/details.jsx
index a9723443afd9..ece78ba513ac 100644
--- a/pkg/storaged/details.jsx
+++ b/pkg/storaged/details.jsx
@@ -23,11 +23,10 @@ import React from "react";
import { Card } from '@patternfly/react-core/dist/esm/components/Card/index.js';
import { Page, PageBreadcrumb, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
-import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
import * as utils from "./utils.js";
-import { BlockDetails } from "./block-details.jsx";
+import { BlockDetails, block_nav_parents } from "./block-details.jsx";
import { DriveDetails } from "./drive-details.jsx";
import { VGroupDetails } from "./vgroup-details.jsx";
import { MDRaidDetails } from "./mdraid-details.jsx";
@@ -46,37 +45,21 @@ export const StdDetailsLayout = ({ client, alerts, header, content, sidebar }) =
>;
- if (sidebar) {
- return (
- <>
- { top }
-
-
-
- { content }
-
-
-
-
-
- >
- );
- } else {
- return (
- <>
- { top }
-
-
- { content }
-
-
-
- >
- );
- }
+ return (
+ <>
+ { top }
+ { sidebar
+ ? { sidebar }
+ : null
+ }
+
+
+ { content }
+
+
+
+ >
+ );
};
export class Details extends React.Component {
@@ -85,16 +68,21 @@ export class Details extends React.Component {
let body = null;
let name = this.props.name;
- if (this.props.type == "block") {
+ let crumbs = [];
+ if (this.props.type == "drive") {
const block = client.slashdevs_block["/dev/" + this.props.name];
const drive = block && client.drives[block.Drive];
-
if (drive) {
name = utils.drive_name(drive);
body = ;
- } else if (block) {
+ // XXX- crumbs for drives in iscsi sessions
+ }
+ } else if (this.props.type == "block") {
+ const block = client.slashdevs_block["/dev/" + this.props.name];
+ if (block) {
name = utils.block_name(block);
body = ;
+ crumbs = block_nav_parents(client, block);
}
} else if (this.props.type == "vg") {
const vgroup = client.vgnames_vgroup[this.props.name];
@@ -137,6 +125,12 @@ export class Details extends React.Component {
{_("Storage")}
+ { crumbs.map(c => (
+
+ {c.title}
+ ))
+ }
{name}
diff --git a/pkg/storaged/drives-panel.jsx b/pkg/storaged/drives-panel.jsx
index d60a887a739b..4fa0d39f7eb5 100644
--- a/pkg/storaged/drives-panel.jsx
+++ b/pkg/storaged/drives-panel.jsx
@@ -26,7 +26,7 @@ import { fmt_size, drive_name, decode_filename, block_name } from "./utils.js";
const _ = cockpit.gettext;
const C_ = cockpit.gettext;
-export function drive_rows(client) {
+export function drive_rows(client, options) {
function cmp_drive(path_a, path_b) {
return client.drives[path_a].SortKey.localeCompare(client.drives[path_b].SortKey);
}
@@ -87,7 +87,7 @@ export function drive_rows(client) {
size: drive.Size,
type,
detail: desc,
- go: () => cockpit.location.go([dev]),
+ go: () => cockpit.location.go(["drive", dev]),
block: drive && client.drives_block[path],
job_path: path,
key: path
diff --git a/pkg/storaged/iscsi-panel.jsx b/pkg/storaged/iscsi-panel.jsx
index 8a97649a20bb..930baf8ac7fc 100644
--- a/pkg/storaged/iscsi-panel.jsx
+++ b/pkg/storaged/iscsi-panel.jsx
@@ -25,7 +25,7 @@ import { CheckIcon, EditIcon, PlusIcon, TrashIcon } from "@patternfly/react-icon
import { SidePanel } from "./side-panel.jsx";
import { } from "./utils.js";
-import { StorageButton } from "./storage-controls.jsx";
+import { StorageButton, StorageMenuItem } from "./storage-controls.jsx";
import { dialog_open, TextInput, PassInput, SelectRow } from "./dialog.jsx";
const _ = cockpit.gettext;
@@ -186,6 +186,22 @@ function iscsi_change_name(client) {
});
}
+export function iscsi_menu_items(client, options) {
+ if (!client.features.iscsi)
+ return [];
+
+ return [
+ iscsi_change_name(client)}>
+ {_("Change iSCSI initiator name")}
+ ,
+ iscsi_discover(client)}>
+ {_("Add iSCSI portal")}
+ ,
+ ];
+}
+
export function iscsi_rows(client, options) {
function cmp_session(path_a, path_b) {
const a = client.iscsi_sessions[path_a];
@@ -218,8 +234,11 @@ export function iscsi_rows(client, options) {
actions,
kind: "array",
name: session.data.target_name || "",
+ type: _("iSCSI portal"),
key: path,
- detail: session.data.persistent_address + ":" + session.data.persistent_port
+ detail: session.data.persistent_address + ":" + session.data.persistent_port,
+ location: session.data.persistent_address + ":" + session.data.persistent_port,
+ portal: session
};
}
@@ -227,6 +246,18 @@ export function iscsi_rows(client, options) {
.map(make_session);
}
+export function portal_menu_items(client, session, options) {
+ function iscsi_remove() {
+ return session.Logout({ 'node.startup': { t: 's', v: "manual" } });
+ }
+
+ return [
+
+ {_("Disconnect")}
+
+ ];
+}
+
export class IscsiPanel extends React.Component {
constructor() {
super();
diff --git a/pkg/storaged/mdraid-details.jsx b/pkg/storaged/mdraid-details.jsx
index 9d11a27dd0f3..502ae7272687 100644
--- a/pkg/storaged/mdraid-details.jsx
+++ b/pkg/storaged/mdraid-details.jsx
@@ -27,7 +27,7 @@ import * as utils from "./utils.js";
import { StdDetailsLayout } from "./details.jsx";
import { SidePanel } from "./side-panel.jsx";
import { Block } from "./content-views.jsx";
-import { StorageButton } from "./storage-controls.jsx";
+import { StorageButton, StorageMenuItem } from "./storage-controls.jsx";
import {
dialog_open, SelectSpaces, BlockingMessage, TeardownMessage,
init_active_usage_processes
@@ -249,6 +249,26 @@ function mdraid_delete(client, mdraid) {
});
}
+export function mdraid_menu_items(client, mdraid, options) {
+ /* Older versions of Udisks/storaged don't have a Running property */
+ let running = mdraid.Running;
+ if (running === undefined)
+ running = mdraid.ActiveDevices && mdraid.ActiveDevices.length > 0;
+
+ return [
+ (running
+ ? mdraid_stop(client, mdraid)}>
+ {_("Stop RAID device")}
+
+ : mdraid_start(client, mdraid)}>
+ {_("Start RAID device")}
+ ),
+ mdraid_delete(client, mdraid)}>
+ {_("Delete RAID device")}
+ ,
+ ];
+}
+
export class MDRaidDetails extends React.Component {
render() {
const client = this.props.client;
diff --git a/pkg/storaged/mdraids-panel.jsx b/pkg/storaged/mdraids-panel.jsx
index 5b7108a76a63..59695b454568 100644
--- a/pkg/storaged/mdraids-panel.jsx
+++ b/pkg/storaged/mdraids-panel.jsx
@@ -37,9 +37,13 @@ function mdraid_row(client, path) {
name: mdraid_name(mdraid),
devname: block && block_name(block),
detail: fmt_size(mdraid.Size) + " " + _("RAID device"),
+ type: _("RAID device"),
+ size: mdraid.Size,
job_path: path,
key: path,
- go: () => cockpit.location.go(["mdraid", mdraid.UUID])
+ go: () => cockpit.location.go(["mdraid", mdraid.UUID]),
+ block,
+ mdraid
};
}
diff --git a/pkg/storaged/nfs-details.jsx b/pkg/storaged/nfs-details.jsx
index 15d082a6e801..31e3cca61e4b 100644
--- a/pkg/storaged/nfs-details.jsx
+++ b/pkg/storaged/nfs-details.jsx
@@ -28,7 +28,7 @@ import {
} from "./dialog.jsx";
import { StdDetailsLayout } from "./details.jsx";
-import { StorageButton, StorageUsageBar } from "./storage-controls.jsx";
+import { StorageButton, StorageUsageBar, StorageMenuItem } from "./storage-controls.jsx";
import { parse_options, unparse_options, extract_option } from "./utils.js";
const _ = cockpit.gettext;
@@ -273,6 +273,16 @@ function remove(client, entry) {
});
}
+export function nfs_content_menu_items(client, entry) {
+ return [
+ entry.mounted
+ ? unmount(client, entry)}>{_("Unmount")}
+ : mount(client, entry)}>{_("Mount")},
+ edit(client, entry)}>{_("Edit")},
+ remove(client, entry)}>{_("Remove")},
+ ];
+}
+
export class NFSDetails extends React.Component {
render() {
const client = this.props.client;
diff --git a/pkg/storaged/nfs-panel.jsx b/pkg/storaged/nfs-panel.jsx
index ccce2e691a5e..49236123a693 100644
--- a/pkg/storaged/nfs-panel.jsx
+++ b/pkg/storaged/nfs-panel.jsx
@@ -23,24 +23,53 @@ import { SortByDirection } from '@patternfly/react-table';
import { PlusIcon } from '@patternfly/react-icons';
import { ListingTable } from "cockpit-components-table.jsx";
-import { StorageButton, StorageUsageBar } from "./storage-controls.jsx";
-import { nfs_fstab_dialog } from "./nfs-details.jsx";
+import { StorageButton, StorageUsageBar, StorageBarMenu } from "./storage-controls.jsx";
+import { nfs_fstab_dialog, nfs_content_menu_items } from "./nfs-details.jsx";
import { OptionalPanel } from "./optional-panel.jsx";
const _ = cockpit.gettext;
-export class NFSPanel extends React.Component {
- render() {
- const client = this.props.client;
+export function nfs_feature(client) {
+ return {
+ is_enabled: () => client.features.nfs,
+ package: client.get_config("nfs_client_package", false),
+ enable: () => {
+ client.features.nfs = true;
+ client.nfs.start();
+ }
+ };
+}
- function make_nfs_mount(entry) {
- let fsys_size;
- if (entry.mounted)
- fsys_size = client.nfs.get_fsys_size(entry);
+export function nfs_rows(client, options) {
+ function make_nfs_mount(entry) {
+ let fsys_size;
+ if (entry.mounted)
+ fsys_size = client.nfs.get_fsys_size(entry);
- const server = entry.fields[0].split(":")[0];
- const remote_dir = entry.fields[0].split(":")[1];
+ const server = entry.fields[0].split(":")[0];
+ const remote_dir = entry.fields[0].split(":")[1];
+ if (options.unified)
+ return {
+ props: { entry, key: entry.fields[1] },
+ columns: [
+ { title: server + " " + remote_dir },
+ { title: _("NFS mount") },
+ { title: entry.fields[1] },
+ {
+ title: entry.mounted
+ ?
+ : "",
+ props: { className: "pf-v5-u-text-align-right" }
+ },
+ {
+ title: ,
+ props: { className: "pf-v5-c-table__action content-action" }
+ }
+ ],
+ go: () => cockpit.location.go(["nfs", entry.fields[0], entry.fields[1]])
+ };
+ else
return {
props: { entry, key: entry.fields[1] },
columns: [
@@ -53,9 +82,15 @@ export class NFSPanel extends React.Component {
}
]
};
- }
+ }
- const mounts = client.nfs.entries.map(make_nfs_mount);
+ return client.nfs.entries.map(make_nfs_mount);
+}
+
+export class NFSPanel extends React.Component {
+ render() {
+ const client = this.props.client;
+ const mounts = nfs_rows(client, {});
function add() {
nfs_fstab_dialog(client, null);
@@ -67,15 +102,6 @@ export class NFSPanel extends React.Component {
);
- const nfs_feature = {
- is_enabled: () => client.features.nfs,
- package: client.get_config("nfs_client_package", false),
- enable: () => {
- client.features.nfs = true;
- client.nfs.start();
- }
- };
-
function onRowClick(event, row) {
if (!event || event.button !== 0)
return;
@@ -87,7 +113,7 @@ export class NFSPanel extends React.Component {
client={client}
title={_("NFS mounts")}
actions={actions}
- feature={nfs_feature}
+ feature={nfs_feature(client)}
not_installed_text={_("NFS support not installed")}
install_title={_("Install NFS support")}>
.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+
+import { Page, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Card, CardHeader, CardTitle, CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { ListingTable } from "cockpit-components-table.jsx";
+
+import { StorageBarMenu } from "./storage-controls.jsx";
+
+import { thing_menu_items, thing_rows } from "./things-panel.jsx";
+import { drive_rows } from "./drives-panel.jsx";
+import { nfs_rows } from "./nfs-panel.jsx";
+import { iscsi_menu_items, iscsi_rows, portal_menu_items } from "./iscsi-panel.jsx";
+import { other_rows } from "./others-panel.jsx";
+import { block_content_rows, vgroup_content_rows, block_menu_items, vgroup_menu_items } from "./content-views.jsx";
+import { stratis_content_rows, pool_menu_items } from "./stratis-details.jsx";
+import { mdraid_menu_items } from "./mdraid-details.jsx";
+
+import { StoragePlots } from "./plot.jsx";
+import { JobsPanel } from "./jobs-panel.jsx";
+import { StorageLogsPanel } from "./logs-panel.jsx";
+import { fmt_size } from "./utils.js";
+
+const _ = cockpit.gettext;
+
+// XXX - this is terrible code, and is just meant to bring us to a
+// point where we can play with the new UX without having to disturb
+// the existing code too much.
+
+export const OverviewX = ({ client, plot_state }) => {
+ const menu_items = [].concat(
+ thing_menu_items(client, { unified: true }),
+ iscsi_menu_items(client, { unified: true }));
+
+ const actions = ;
+
+ const d_rows = drive_rows(client, { unified: true });
+ const i_rows = iscsi_rows(client, { unified: true });
+
+ // Move iSCSI drives from d_rows to their place in i_rows. Ugh.
+ for (let i = 0; i < i_rows.length; i++) {
+ const session = i_rows[i].portal;
+ for (let j = 0; j < d_rows.length; j++) {
+ if (client.drives_iscsi_session[d_rows[j].block.Drive] == session) {
+ d_rows[j].level = 1;
+ i_rows.splice(i + 1, 0, d_rows[j]);
+ d_rows.splice(j, 1);
+ i += 1;
+ j -= 1;
+ }
+ }
+ }
+
+ const top_rows = [].concat(
+ d_rows,
+ thing_rows(client, { unified: true }),
+ i_rows,
+ other_rows(client, { unified: true }));
+
+ let rows = [];
+ top_rows.forEach(t => {
+ let m = [];
+ if (t.block)
+ m = m.concat(block_menu_items(client, t.block, { unified: true }));
+ if (t.vgroup)
+ m = m.concat(vgroup_menu_items(client, t.vgroup, { unified: true }));
+ if (t.pool)
+ m = m.concat(pool_menu_items(client, t.pool, { unified: true }));
+ if (t.portal)
+ m = m.concat(portal_menu_items(client, t.portal, { unified: true }));
+ if (t.mdraid)
+ m = m.concat(mdraid_menu_items(client, t.mdraid, { unified: true }));
+ const actions = (m.length > 0
+ ?
+ : null);
+ const level = t.level || 0;
+ rows.push({
+ props: {
+ key: t.path,
+ className: "content-level-" + level,
+ },
+ columns: [
+ { title: t.name }, // XXX - use "ID", name is taken.
+ { title: t.type },
+ { title: t.location || t.devname },
+ { title: fmt_size(t.size), props: { className: "pf-v5-u-text-align-right" } },
+ { title: actions, props: { className: "pf-v5-c-table__action content-action" } },
+ ],
+ go: t.go,
+ });
+ if (t.block)
+ rows = rows.concat(block_content_rows(client, t.block, { unified: true, level: level + 1 }));
+ if (t.vgroup)
+ rows = rows.concat(vgroup_content_rows(client, t.vgroup, { unified: true, level: level + 1 }));
+ if (t.pool)
+ rows = rows.concat(stratis_content_rows(client, t.pool, { unified: true, level: level + 1 }));
+ });
+
+ rows = rows.concat(nfs_rows(client, { 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();
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {_("Storage")}
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/pkg/storaged/side-panel.jsx b/pkg/storaged/side-panel.jsx
index 1bdb1684fc81..620afd6610e7 100644
--- a/pkg/storaged/side-panel.jsx
+++ b/pkg/storaged/side-panel.jsx
@@ -20,7 +20,8 @@
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";
@@ -32,146 +33,68 @@ 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..6d99bbd1f45f 100644
--- a/pkg/storaged/storage.scss
+++ b/pkg/storaged/storage.scss
@@ -209,12 +209,28 @@ tr[class*="content-level-"] {
}
}
-@for $i from 1 through 10 {
+#unified tr[class*="content-level-"] {
+ --multiplier: 0;
+ --offset: calc(var(--pf-v5-global--spacer--lg) * var(--multiplier));
+
+ > td:first-child {
+ position: relative;
+ inset-inline-start: var(--offset);
+ }
+}
+
+@for $i from 0 through 10 {
tr.content-level-#{$i} {
--multiplier: #{$i};
}
}
+@for $i from 0 through 10 {
+ #unified tr.content-level-#{$i} {
+ --multiplier: #{$i};
+ }
+}
+
.tab-actions {
float: inline-end;
margin-block-start: -15px;
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 c0eb813bb393..005291e28d4c 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";
@@ -448,8 +449,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)
@@ -484,10 +489,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\//, "")])
};
}
@@ -501,7 +512,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"),
{
@@ -629,6 +640,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] &&
@@ -888,7 +917,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 = (
@@ -904,7 +946,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/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/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,
};
}