src/lib/y2partitioner/widgets/overview.rb
# Copyright (c) [2017-2023] SUSE LLC
#
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of version 2 of the GNU General Public License as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, contact SUSE LLC.
#
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.
require "yast"
require "yast2/popup"
require "cwm/widget"
require "cwm/tree"
require "y2partitioner/icons"
require "y2partitioner/ui_state"
require "y2partitioner/widgets/pages"
require "y2storage/setup_errors_presenter"
require "y2storage/setup_checker"
require "y2storage/package_handler"
require "y2storage/bcache"
Yast.import "UI"
Yast.import "Mode"
module Y2Partitioner
module Widgets
# A tree that is told what its items are.
# We need a tree whose items include Pages that point to the OverviewTreePager.
class OverviewTree < CWM::Tree
def initialize(items)
super()
textdomain "storage"
@items = items
end
# @macro seeAbstractWidget
def label
""
end
attr_reader :items
end
# Widget representing partitioner overview pager with tree on left side and rest on right side.
#
# It has replace point where it displays more details about selected element in partitioning.
class OverviewTreePager < CWM::TreePager
# Constructor
#
# @param [String] hostname of the system
def initialize(hostname)
textdomain "storage"
@hostname = hostname
super(OverviewTree.new(items))
end
# Ensures that UIState clears obsolete statuses and it is aware of current page
#
# That's especially needed when initial page is a candidate of a no longer existing one
#
# @see #initial_page
def init
super
UIState.instance.prune(keep: @pages.map(&:id))
UIState.instance.select_page(@current_page.tree_path)
end
# Ensures the tree is properly initialized according to {UIState} after
# a redraw.
#
# @see #find_initial_page
def initial_page
find_initial_page || super
end
# @see http://www.rubydoc.info/github/yast/yast-yast2/CWM%2FTree:items
def items
[
system_section,
disks_section,
raids_section,
lvm_section,
bcache_section,
# TODO: Bring this back to life - disabled for now (bsc#1078849)
# crypt_files_items,
# device_mapper_items,
btrfs_section,
tmpfs_section,
# TODO: Bring this back to life - disabled for now (bsc#1078849)
# unused_items
nfs_section
].compact
end
# Overrides default behavior of TreePager to register with {UIState} the status
# of the current page and the new destination, before jumping to the tree node
def switch_page(page)
state = UIState.instance
state.save_extra_info
state.select_page(page.tree_path)
super
end
# Status of open/expanded items in the UI
#
# @see UIState#open_items
#
# @return [Hash{String => Boolean}]
def open_items
items_with_children = with_children(tree.items)
open_items = Yast::UI.QueryWidget(Id(tree.widget_id), :OpenItems).keys
items_with_children.map { |i| [i.to_s, open_items.include?(i)] }.to_h
end
# Checks whether the current page is associated to a specific device
#
# @return [Boolean]
def device_page?
current_page.respond_to?(:device)
end
# Obtains the page associated to a specific device
#
# @param device [Y2Storage::Device]
# @return [CWM::Page, nil]
def device_page(device)
@pages.find { |p| p.respond_to?(:device) && p.device.sid == device.sid }
end
# @macro seeAbstractWidget
#
# @return [Boolean]
def validate
valid_setup? && packages_installed?
end
# @macro seeAbstractWidget
# @return [String] localized help text
def help
_(
# TRANSLATORS: html text of the Partitioner Help. Please make sure the menu
# names actually match the ones in the menubar widget
"<p>Below the menu bar, the main element of the interface is the table\n" \
"that represents the available devices, with some buttons to provide quick\n" \
"access to the most common actions. Additionally, the <b>Add</b> and \n" \
"<b>Device</b> menus can be used to perform any action on the device\n" \
"selected in the table.</p>\n" \
"<p>The left tree can be used to navigate through the list of devices,\n" \
"focusing on a particular device or type of devices.</p>"
)
end
private
attr_reader :tree
# Select the initial page
#
# @return [Page, nil]
def find_initial_page
candidate = UIState.instance.find_page(@pages.map(&:id))
return nil unless candidate
@pages.find { |page| page.id == candidate }
end
# Checks whether the current setup is valid, that is, it contains necessary
# devices for booting (e.g., /boot/efi) and for the system runs properly (e.g., /).
#
# @see Y2Storage::SetupChecker
#
# @return [Boolean]
def valid_setup?
setup_checker = Y2Storage::SetupChecker.new(device_graph)
return true if setup_checker.valid?
errors = Y2Storage::SetupErrorsPresenter.new(setup_checker).to_html
if setup_checker.errors.empty? # so only warnings there
# FIXME: improve Yast2::Popup to allow some text before the buttons
errors += _("Do you want to continue?")
result = Yast2::Popup.show(errors,
headline: :warning, richtext: true, buttons: :yes_no, focus: :no)
result == :yes
else
Yast2::Popup.show(errors,
headline: :error, richtext: true, buttons: :ok)
false
end
end
# Checks whether the needed packages are installed
#
# As a side effect, it will ask the user to install missing packages.
#
# @see Y2Storage::StorageFeature
#
# @return [Boolean]
def packages_installed?
return true if Yast::Mode.installation
features = device_graph.actiongraph.used_features
features.concat(device_graph.yast_commit_features)
pkgs = features.pkg_list
Y2Storage::PackageHandler.new(pkgs).install
end
# @return [String]
attr_reader :hostname
def device_graph
DeviceGraphs.instance.current
end
# @return [CWM::PagerTreeItem]
def system_section
page = Pages::System.new(hostname, self)
section_item(page, Icons::ALL)
end
# @return [CWM::PagerTreeItem]
def disks_section
devices = device_graph.disk_devices + device_graph.stray_blk_devices
page = Pages::Disks.new(devices, self)
children = devices.map do |dev|
dev.is?(:stray_blk_device) ? stray_blk_device_item(dev) : disk_items(dev)
end
section_item(page, Icons::HD, children: children)
end
# @return [CWM::PagerTreeItem]
def disk_items(disk, page_class = Pages::Disk)
page = page_class.new(disk, self)
device_item(page)
end
# @return [CWM::PagerTreeItem]
def stray_blk_device_item(device)
page = Pages::StrayBlkDevice.new(device, self)
device_item(page)
end
# @return [CWM::PagerTreeItem]
def raids_section
devices = device_graph.software_raids
page = Pages::MdRaids.new(self)
children = devices.map { |m| raid_items(m) }
section_item(page, Icons::RAID, children: children)
end
# @return [CWM::PagerTreeItem]
def raid_items(md)
page = Pages::MdRaid.new(md, self)
device_item(page)
end
# @return [CWM::PagerTreeItem]
def bcache_section
return nil unless Y2Storage::Bcache.supported?
devices = device_graph.bcaches
page = Pages::Bcaches.new(self)
children = devices.map { |v| disk_items(v, Pages::Bcache) }
section_item(page, Icons::BCACHE, children: children)
end
# @return [CWM::PagerTreeItem]
def lvm_section
devices = device_graph.lvm_vgs
page = Pages::Lvm.new(self)
children = devices.map { |v| lvm_vg_items(v) }
section_item(page, Icons::LVM, children: children)
end
# @return [CWM::PagerTreeItem]
def lvm_vg_items(vg)
page = Pages::LvmVg.new(vg, self)
device_item(page)
end
# @return [CWM::PagerTreeItem]
def btrfs_section
filesystems = device_graph.btrfs_filesystems.sort_by(&:blk_device_basename)
page = Pages::BtrfsFilesystems.new(filesystems, self)
children = filesystems.map { |f| btrfs_item(f) }
section_item(page, Icons::BTRFS, children: children)
end
# @return [CWM::PagerTreeItem]
def btrfs_item(filesystem)
page = Pages::Btrfs.new(filesystem, self)
device_item(page)
end
# @return [CWM::PagerTreeItem]
def tmpfs_section
page = Pages::TmpfsFilesystems.new(self)
children = device_graph.tmp_filesystems.map { |f| tmpfs_item(f) }
section_item(page, Icons::TMPFS, children: children)
end
# @param filesystem [Y2Storage::Filesystems::Tmpfs]
# @return [CWM::PagerTreeItem]
def tmpfs_item(filesystem)
page = Pages::Tmpfs.new(filesystem, self)
device_item(page)
end
# @return [CWM::PagerTreeItem]
def nfs_section
page = Pages::NfsMounts.new(self)
children = device_graph.nfs_mounts.map { |f| nfs_item(f) }
section_item(page, Icons::NFS, children: children)
end
# @param nfs [Y2Storage::Filesystems::Nfs]
# @return [CWM::PagerTreeItem]
def nfs_item(nfs)
page = Pages::Nfs.new(nfs, self)
device_item(page)
end
# Generates a `section` tree item for given page
#
# The OverviewTreePager has two kinds of items: section or device. Sections always has icon
# and starts expanded; devices has not icon and starts collapsed. See also {device_item}.
#
# @param page [CWM::Page]
# @param icon [Icons]
# @param children [Array<CWM::PagerTreeItem>]
#
# @return [CWM::PagerTreeItem]
def section_item(page, icon, children: [])
CWM::PagerTreeItem.new(page, children: children, icon: icon, open: item_open?(page, true))
end
# Generates a `device` tree item for given page
#
# @see #section_item
#
# @param page [CWM::Page]
# @param children [Array<CWM::PagerTreeItem>]
#
# @return [CWM::PagerTreeItem]
def device_item(page, children: [])
CWM::PagerTreeItem.new(page, children: children, open: item_open?(page, false))
end
# For a list of tree entries, returns the ids of those that have children,
# including nested tree entries (i.e. recursively)
#
# @param items [Array<CWM::PagerTreeItem>]
# @return [Array<String, Symbol>]
def with_children(items)
items = items.select { |i| i.children.any? }
items.map(&:id) + items.flat_map { |i| with_children(i.children.values) }
end
# Whether the tree item for given page should be open (expanded) or closed (collapsed)
#
# When open items are not initialized, the default value will be used. For better
# understanding, see the note about the {OverviewTreePager} redrawing in
# {Dialogs::Main#contents}
#
# @param page [CWM::Page]
# @param default [Boolean] value when open items are not initialized yet
#
# @return [Boolean] true when item must be expanded; false if must be collapsed
def item_open?(page, default)
UIState.instance.open_items.fetch(page.widget_id.to_s, default)
end
end
end
end