From 0306f4ba298b28746187966f279bf4f329f2b3c2 Mon Sep 17 00:00:00 2001 From: Marius Vollmer Date: Mon, 2 Oct 2023 15:35:17 +0300 Subject: [PATCH] WIP - storage redesign --- pkg/storaged/block-details.jsx | 107 ++++++++++++- pkg/storaged/client.js | 20 +++ pkg/storaged/content-views.jsx | 239 +++++++++++++++++++++++------- pkg/storaged/details.jsx | 66 ++++----- pkg/storaged/drives-panel.jsx | 4 +- pkg/storaged/fsys-tab.jsx | 2 +- pkg/storaged/iscsi-panel.jsx | 35 ++++- pkg/storaged/lvol-tabs.jsx | 8 +- pkg/storaged/mdraid-details.jsx | 22 ++- pkg/storaged/mdraids-panel.jsx | 6 +- pkg/storaged/nfs-details.jsx | 12 +- pkg/storaged/nfs-panel.jsx | 72 ++++++--- pkg/storaged/overviewx.jsx | 171 +++++++++++++++++++++ pkg/storaged/part-tab.jsx | 6 +- pkg/storaged/side-panel.jsx | 181 +++++++--------------- pkg/storaged/storage-controls.jsx | 5 +- pkg/storaged/storage.scss | 18 ++- pkg/storaged/storaged.jsx | 4 +- pkg/storaged/stratis-details.jsx | 64 +++++++- pkg/storaged/stratis-panel.jsx | 5 +- pkg/storaged/things-panel.jsx | 5 +- pkg/storaged/utils.js | 11 +- pkg/storaged/vdos-panel.jsx | 2 + pkg/storaged/vgroups-panel.jsx | 5 +- 24 files changed, 789 insertions(+), 281 deletions(-) create mode 100644 pkg/storaged/overviewx.jsx diff --git a/pkg/storaged/block-details.jsx b/pkg/storaged/block-details.jsx index f9deed2b6ddc..c75ba3a507b5 100644 --- a/pkg/storaged/block-details.jsx +++ b/pkg/storaged/block-details.jsx @@ -20,24 +20,110 @@ 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 { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; +import { fmt_to_fragments } from "utils.jsx"; import * as utils from "./utils.js"; import { StdDetailsLayout } from "./details.jsx"; -import * as Content from "./content-views.jsx"; +import { block_description, create_tabs } from "./content-views.jsx"; +import { StorageButton } from "./storage-controls.jsx"; +import { StructureDescription } from "./lvol-tabs.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 []; +} + 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 block_lvm2 = client.blocks_lvm2[block.path]; + + const lvol = block_lvm2 && client.lvols[block_lvm2.LogicalVolume]; + + 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 cparts = utils.get_block_link_parts(client, block.path); + + function is_container(r) { + return r.name == _("Logical volume") || r.name == _("Partition"); + } + + const container_renderers = tabs.renderers.filter(is_container); + const content_renderers = tabs.renderers.filter(r => !is_container(r)); const header = ( - {_("Block")} + + + {block_description(client, block, {}).type} + + + + {_("Stored on")} + + {fmt_to_fragments(cparts.format, + )} + + {_("storage", "Capacity")} { utils.fmt_size_long(block.Size) } @@ -47,11 +133,24 @@ export class BlockDetails extends React.Component { { utils.block_name(block) } + { content_renderers.map(t =>
) }
); - const content = ; + const content = container_renderers.map(t => { + return ( + + + + {t.name} + + + + + + ); + }); 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..9222462876be 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); @@ -165,6 +186,7 @@ function create_tabs(client, target, options) { block: for_content ? content_block : block, lvol, warnings: tab_warnings, + options } }); } @@ -175,7 +197,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"), { @@ -204,14 +226,14 @@ function create_tabs(client, target, options) { add_tab(_("Pool"), PoolVolTab); add_action(_("Create thin volume"), create_thin); } else { - add_tab(_("Volume"), BlockVolTab, false, ["unused-space", "partial-lvol"]); + add_tab(_("Logical volume"), BlockVolTab, false, ["unused-space", "partial-lvol"]); if (client.vdo_vols[lvol.path]) add_tab(_("VDO pool"), VDOPoolTab); } } - if (options.is_partition) { + if (client.blocks_part[block.path]) { add_tab(_("Partition"), PartitionTab, false, ["unused-space"]); } @@ -219,6 +241,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 +294,7 @@ function create_tabs(client, target, options) { return; dialog_open({ - Title: _("Unlock"), + Title: _("Unlock $0", target_name), Fields: [ PassInput("passphrase", _("Passphrase"), {}) ], @@ -283,9 +315,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 +330,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 +410,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 +442,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,19 +500,20 @@ 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 }; } -function block_description(client, block, options) { +export function block_description(client, block, options) { let type, used_for, link, size, critical_size; const block_stratis_blockdev = client.blocks_stratis_blockdev[block.path]; const block_stratis_stopped_pool = client.blocks_stratis_stopped_pool[block.path]; @@ -498,7 +533,7 @@ function block_description(client, block, options) { type = C_("storage-id-desc", "Filesystem (encrypted)"); used_for = mount_point; } else if (block_stratis_stopped_pool) { - type = _("Stratis member"); + type = _("Stratis block device"); used_for = block_stratis_stopped_pool; link = ["pool", used_for]; omit_encrypted_label = true; @@ -513,7 +548,7 @@ function block_description(client, block, options) { } else if (block.IdUsage == "raid") { if (block_pvol && client.vgroups[block_pvol.VolumeGroup]) { const vgroup = client.vgroups[block_pvol.VolumeGroup]; - type = _("LVM2 member"); + type = _("LVM2 physical volume"); used_for = vgroup.Name; link = ["vg", used_for]; size = [block_pvol.Size - block_pvol.FreeSize, block_pvol.Size]; @@ -525,14 +560,14 @@ function block_description(client, block, options) { link = ["mdraid", mdraid.UUID]; } else if (block_stratis_blockdev && client.stratis_pools[block_stratis_blockdev.Pool]) { const pool = client.stratis_pools[block_stratis_blockdev.Pool]; - type = _("Stratis member"); + type = _("Stratis block device"); used_for = pool.Name; link = ["pool", pool.Uuid]; omit_encrypted_label = true; } else if (block.IdType == "LVM2_member") { - type = _("LVM2 member"); + type = _("LVM2 physical volume"); } else if (block.IdType == "stratis") { - type = _("Stratis member"); + type = _("Stratis block device"); omit_encrypted_label = true; } else { type = _("RAID member"); @@ -556,6 +591,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 +639,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 +657,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 +670,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 +679,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 +694,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 +728,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 +772,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 +834,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 +870,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 +917,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 +936,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 +957,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 +1098,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 +1235,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 +1286,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 +1307,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 } - -
-
- - { sidebar } - -
- - ); - } 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/fsys-tab.jsx b/pkg/storaged/fsys-tab.jsx index a6465500b488..2b4c5b8a2a46 100644 --- a/pkg/storaged/fsys-tab.jsx +++ b/pkg/storaged/fsys-tab.jsx @@ -717,7 +717,7 @@ export class FilesystemTab extends React.Component { { !stratis_fsys && - {_("Name")} + {_("Filesystem label")} {this.props.block.IdLabel || "-"} 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/lvol-tabs.jsx b/pkg/storaged/lvol-tabs.jsx index 0c97644fb53f..d9ee6bd31a1f 100644 --- a/pkg/storaged/lvol-tabs.jsx +++ b/pkg/storaged/lvol-tabs.jsx @@ -57,7 +57,7 @@ function lvol_rename(lvol) { }); } -const StructureDescription = ({ client, lvol }) => { +export const StructureDescription = ({ client, lvol }) => { const struct = lvol.Structure; if (!struct) @@ -218,7 +218,7 @@ export class BlockVolTab extends React.Component {
- {_("Name")} + {_("Logical volume name")} {this.props.lvol.Name} @@ -290,7 +290,7 @@ export class PoolVolTab extends React.Component { return ( - {_("Name")} + {_("Logical volume name")} {this.props.lvol.Name} @@ -351,7 +351,7 @@ export const VDOPoolTab = ({ client, lvol }) => { return ( - {_("Name")} + {_("VDO name")} {vdo_pool_vol.Name} 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/part-tab.jsx b/pkg/storaged/part-tab.jsx index 9440e2a28127..5dc9388ec408 100644 --- a/pkg/storaged/part-tab.jsx +++ b/pkg/storaged/part-tab.jsx @@ -51,7 +51,7 @@ export const PartitionTab = ({ client, block, warnings }) => {
- {_("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..81ed6032aa9f 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,70 @@ 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 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..916525b30a3c 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/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, }; }