yast/yast-storage-ng

View on GitHub
src/lib/y2partitioner/widgets/configurable_blk_devices_table.rb

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) [2017-2021] 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 "cwm/table"

require "y2partitioner/device_graphs"
require "y2partitioner/ui_state"
require "y2partitioner/widgets/blk_devices_table"
require "y2partitioner/widgets/columns"
require "y2partitioner/dialogs/device_description"

module Y2Partitioner
  module Widgets
    # Table widget to represent a given list of devices in one of the
    # the main screens of the partitioner
    class ConfigurableBlkDevicesTable < BlkDevicesTable
      include Yast::I18n

      # Constructor
      #
      # @param entries [Array<DeviceTableEntry>]
      # @param pager [CWM::TreePager]
      # @param buttons_set [DeviceButtonsSet]
      def initialize(entries, pager, buttons_set = nil)
        super()
        textdomain "storage"

        @entries = entries
        @pager = pager
        @buttons_set = buttons_set
      end

      # @macro seeAbstractWidget
      def opt
        [:notify, :immediate]
      end

      def contents
        return @contents if @contents

        # Before calculating the content, ensure consistency of #open_items
        # FIXME: the API to fetch the stored state may change, see the comment in UIState#extra
        self.open_items = UIState.instance.extra&.fetch(widget_id, nil)
        @contents = super
      end

      # @macro seeAbstractWidget
      def init
        return if devices.empty? # do nothing if there is nothing in table

        # Now that the content has been displayed, invalidate its memoization to ensure #open_items
        # is updated in the next UI draw with the information from UIState
        @contents = nil

        self.value = initial_entry.row_id
        handle_selected
      end

      # @macro seeAbstractWidget
      def handle(event)
        case event["EventReason"]
        when "SelectionChanged"
          handle_selected
        when "Activated"
          handle_activated
        end
      end

      # Handles the event generated by the user changing the selected row
      #
      # If a buttons set was provided in the constructor, this updates the set
      # to reflect the currently selected device.
      def handle_selected
        dev = selected_device

        return nil unless dev

        UIState.instance.select_row(dev.sid)
        buttons_set.device = dev if buttons_set

        nil
      end

      # Handles the event generated by the user double clicking on a row (or pressing Enter in ncurses)
      #
      # It jumps to the page associated to the selected device or shows a description popup when the
      # current page is already a page associated to a device.
      def handle_activated
        device = selected_device

        return nil unless device

        if pager.device_page?
          Dialogs::DeviceDescription.new(device).run

          return nil
        end

        jump_to_page(device)
      end

      # Device object selected in the table
      #
      # @return [Y2Storage::Device, nil] nil if anything is selected
      def selected_device
        return nil if items.empty? || !value

        sid = value[/.*:(.*)/, 1].to_i
        device_graph.find_device(sid)
      end

      # Adds new columns to show in the table
      #
      # @note When a column :column_name is added, the methods #column_name_title
      #   and #column_name_value should exist.
      #
      # @param column_names [*Symbol]
      def add_columns(*column_names)
        columns.concat(column_names)
      end

      # Avoids to show some columns in the table
      #
      # @param column_names [*Symbol]
      def remove_columns(*column_names)
        column_names.each { |c| columns.delete(c) }
      end

      # Fixes a set of specific columns to show in the table
      #
      # @param column_names [*Symbol]
      def show_columns(*column_names)
        @columns = column_names
      end

      # @macro seeAbstractWidget
      # @see #columns_help
      def help
        _("<p>The table contains the following columns:</p>") + columns_help
      end

      private

      # @return [DeviceButtonsSet] optional buttons set that must be
      #   updated when the user changes the selection in the table
      attr_reader :buttons_set

      # @return [CWM::TreePager] general pager used to navigate through the Partitioner
      attr_reader :pager

      # @return [Array<DeviceTableEntry>] list of device entries to display
      attr_reader :entries

      DEFAULT_COLUMNS = [
        Columns::Device,
        Columns::Size,
        Columns::Format,
        Columns::Encrypted,
        Columns::Type,
        Columns::FilesystemLabel,
        Columns::MountPoint,
        Columns::RegionStart,
        Columns::RegionEnd
      ].freeze

      def device_graph
        DeviceGraphs.instance.current
      end

      def columns
        @columns ||= default_columns.dup
      end

      def default_columns
        DEFAULT_COLUMNS
      end

      # @see BlkDevicesTable#open_by_default?
      #
      # @param entry [DeviceTableEntry]
      # @return [Boolean]
      def open_by_default?(entry)
        return true unless entry_with_subvols?(entry)

        entry.children.none? { |c| c.device.snapshot? }
      end

      # Whether the children of the given entry are Btrfs subvolumes
      #
      # @return [Boolean]
      def entry_with_subvols?(entry)
        # We never mix subvolumes and other kind of devices in the same level of
        # a branch, so checking the first child is enough
        child = entry.children.first
        return false unless child

        child.device.is?(:btrfs_subvolume)
      end

      # Table entry to select initially when the table is rendered
      #
      # @see #init
      #
      # @return [DeviceTableEntry, nil]
      def initial_entry
        initial_sid = UIState.instance.row_id
        @initial_entry = entry(initial_sid)

        # After adding a new Btrfs, it may happen that such device is not represented in
        # the table as a separate entry, but only through its block device
        @initial_entry ||= fs_blk_device_entry(initial_sid)

        # If we do not have a valid sid, then pick the first available device.
        # Done to allow e.g. chain of delete like described in bsc#1076318,
        # although this is very likely not longer necessary after many changes in the
        # Partitioner and in the libyui tables
        @initial_entry ||= entries.first
      end

      # Given the sid of a single (no multidevice) filesystem, returns the table
      # entry of its block device
      #
      # @param sid [Integer]
      # @return [DeviceTableEntry, nil] nil if the sid does not correspond to a single
      #   filesystem or if the corresponding block device is not in the table
      def fs_blk_device_entry(sid)
        return nil unless sid

        device = device_graph.find_device(sid.to_i)
        return nil unless device&.is?(:blk_filesystem)
        return nil if device.multidevice?

        entry(device.plain_blk_devices.first.sid)
      end

      # Switches to the page of the specified device, if possible
      #
      # If the target page exists, it updates all the corresponding state information
      # before doing the real switch.
      def jump_to_page(device)
        page = device_page(device)

        return nil unless page

        state = UIState.instance

        # First, save the status of the current page
        state.save_extra_info

        # Then, pretend the user visited the new page and then select the device
        state.select_page(page.tree_path)
        state.select_row(device.sid)

        pager.handle("ID" => page.widget_id)
      end

      # Finds a page associated to the given device
      #
      # When there is no page for the given device, it tries to find a page associated to the device of
      # the parent table entries.
      #
      # @param device [Y2Storage::Device]
      # @return [CWM::Page, nil]
      def device_page(device)
        page = pager.device_page(device)
        return page if page

        device = holding_device(device)
        return nil unless device

        device_page(device)
      end

      # Device associated to the parent table entry of the given device
      #
      # @param device [Y2Storage::Device]
      # @return [Y2Storage::Device, nil]
      def holding_device(device)
        parent_entry = parent_entry(entry(device))
        parent_entry&.device
      end

      # Parent table entry of the given one
      #
      # @param entry [DeviceTableEntry]
      # @return [DeviceTableEntry, nil]
      def parent_entry(entry)
        entries.flat_map(&:all_entries).find { |e| e.parent?(entry) }
      end
    end
  end
end