yast/yast-storage-ng

View on GitHub
src/lib/y2storage/dialogs/proposal.rb

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) [2016-2022] 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 "ui/installation_dialog"
require "y2storage"
require "y2storage/actions_presenter"
require "y2storage/dump_manager"
require "y2storage/setup_checker"
require "y2storage/setup_errors_presenter"

Yast.import "HTML"

module Y2Storage
  module Dialogs
    # Calculates the storage proposal during installation and provides
    # the user a summary of the storage proposal
    class Proposal < ::UI::InstallationDialog
      attr_reader :proposal
      attr_reader :devicegraph

      # Constructor
      #
      # @param proposal [GuidedProposal]
      # @param devicegraph [Devicegraph]
      # @param excluded_buttons [Array<Symbol>] id of buttons that should not be shown
      def initialize(proposal, devicegraph, excluded_buttons: [])
        log.info "Proposal dialog: start with #{proposal.inspect}"

        super()
        textdomain "storage"

        @proposal = proposal
        @devicegraph = devicegraph
        @excluded_buttons = excluded_buttons

        propose! if proposal && !proposal.proposed?
        @actions_presenter = ActionsPresenter.new(actiongraph)

        DumpManager.dump(@actions_presenter)
      end

      def next_handler
        if devicegraph
          log.info "Proposal dialog: return :next with #{proposal} and #{devicegraph}"
          super
        else
          msg = _("Cannot continue without a valid storage setup.") + "\n"
          msg += _("Please use \"Guided Setup\" or \"Expert Partitioner\".")
          Yast::Report.Error(msg)
        end
      end

      def guided_handler
        finish_dialog(:guided)
      end

      def expert_from_proposal_handler
        finish_dialog(:expert_from_proposal)
      end

      def expert_from_probed_handler
        finish_dialog(:expert_from_probed)
      end

      def handle_event(input)
        return unless @actions_presenter.can_handle?(input)

        @actions_presenter.update_status(input)
        Yast::UI.ChangeWidget(Id(:summary), :Value, actions_html)
      end

      protected

      # @return [GuidedProposal]
      attr_writer :proposal

      # @return [Devicegraph] Desired devicegraph
      attr_writer :devicegraph

      # @return [Array<Symbol>] id of buttons that should not be shown
      attr_reader :excluded_buttons

      # Calculates the desired devicegraph using the storage proposal.
      # Sets the devicegraph to nil if something went wrong
      def propose!
        return if proposal.nil? || proposal.proposed?

        proposal.propose
        self.devicegraph = proposal.devices
      rescue Y2Storage::Error
        log.error("generating proposal failed")
        self.devicegraph = nil
      end

      # HTML-formatted text to display in the dialog
      #
      # If there is a successful proposal, it returns a text representation of
      # the proposal with links to modify the settings.
      #
      # If the devicegraph has been set manually, it shows the actions to
      # perform.
      #
      # If there was an error calculating the proposal, it returns an error
      # message.
      #
      # @return [String]
      def summary
        # TODO: if there is a proposal, use the meaningful description with
        # hyperlinks instead of just delegating the summary to libstorage
        content = devicegraph ? actions_html : failure_html

        RichText(Id(:summary), content)
      end

      # Text for the summary in cases in which a devicegraph was properly
      # calculated
      #
      # @see #summary
      #
      # @return [String] HTML-formatted text
      def actions_html
        actions_source_html +
          boss_html +
          setup_errors_html +
          # Reuse the exact string "Changes to partitioning" from the partitioner
          _("<p>Changes to partitioning:</p>") +
          @actions_presenter.to_html
      end

      def boss_html
        return "" if boss_devices.empty?

        # TRANSLATORS: %s is a linux device name (eg. /dev/sda) (singular),
        #  or a list of comma-separated device names (eg. "/dev/sda, /dev/sdb") (plural)
        n_(
          "<p>The device %s is a Dell BOSS drive.</p>",
          "<p>The following devices are Dell BOSS drives: %s.</p>",
          boss_devices.size
        ) % boss_devices.map(&:name).join(", ")
      end

      def boss_devices
        @boss_devices ||= devicegraph.blk_devices.select(&:boss?)
      end

      # @see #actions_html
      def actions_source_html
        return actions_source_for_partitioner unless proposal
        return actions_source_for_guided_setup unless settings_adjustment
        return actions_source_for_default_settings if settings_adjustment.empty?

        para(_("Initial layout proposed after adjusting the Guided Setup settings:")) +
          list(settings_adjustment.descriptions)
      end

      # @see #actions_source_html
      def actions_source_for_partitioner
        para(_("Layout configured manually using the Expert Partitioner."))
      end

      # @see #actions_source_html
      def actions_source_for_guided_setup
        para(_("Layout proposed by the Guided Setup with the settings provided by the user."))
      end

      # @see #actions_source_html
      def actions_source_for_default_settings
        para(_("Initial layout proposed with the default Guided Setup settings."))
      end

      # Setup errors
      #
      # @return [String] HTML-formatted text
      def setup_errors_html
        setup_checker = Y2Storage::SetupChecker.new(devicegraph)
        return "" if setup_checker.valid?

        Y2Storage::SetupErrorsPresenter.new(setup_checker).to_html
      end

      # Text for the summary in cases in which it was not possible to propose
      # a devicegraph
      #
      # @see #summary
      #
      # @return [String] HTML-formatted text
      def failure_html
        failure_source_html + para(
          _(
            "Please, use \"Guided Setup\" to adjust the proposal settings or " \
            "\"Expert Partitioner\" to create a custom layout."
          )
        )
      end

      # @see #failure_html
      def failure_source_html
        if settings_adjustment
          # Just in case the initial proposal is configured to never adjust any
          # setting automatically
          if settings_adjustment.empty?
            para(
              _(
                "It was not possible to propose an initial partitioning layout " \
                "based on the default Guided Setup settings."
              )
            )
          else
            para(
              _(
                "It was not possible to propose an initial partitioning layout " \
                "even after adjusting the Guided Setup settings:"
              )
            ) + list(settings_adjustment.descriptions)
          end
        else
          para(
            _(
              "The Guided Setup was not able to propose a layout using the " \
              "provided settings."
            )
          )
        end
      end

      def dialog_title
        _("Suggested Partitioning")
      end

      # Button to open the Guided Setup
      #
      # @note This button might not be shown (see {#excluded_buttons}).
      #
      # @return [Yast::UI::Term]
      def guided_setup_button
        return Empty() if excluded_buttons.include?(:guided)

        PushButton(Id(:guided), _("&Guided Setup"))
      end

      # Button to open the Partitioner
      #
      # @note This button might not be shown (see {#excluded_buttons}).
      #
      # @return [Yast::UI::Term]
      def expert_partitioner_button
        items = []

        if !excluded_buttons.include?(:expert_from_proposal) && devicegraph
          items << Item(Id(:expert_from_proposal), _("Start with &Current Proposal"))
        end

        if !excluded_buttons.include?(:expert_from_probed)
          items << Item(Id(:expert_from_probed), _("Start with Existing &Partitions"))
        end

        return Empty() if items.empty?

        MenuButton(_("&Expert Partitioner"), items)
      end

      def dialog_content
        MarginBox(
          2, 1,
          VBox(
            MinHeight(8, summary),
            guided_setup_button,
            expert_partitioner_button
          )
        )
      end

      def help_text
        _(
          "<p>\n" \
          "Your hard disks have been checked. The partition setup\n" \
          "displayed is proposed for your hard drive.</p>"
        )
      end

      def settings_adjustment
        proposal ? proposal.auto_settings_adjustment : nil
      end

      # Shortcut for Yast::HTML.Para
      def para(string)
        Yast::HTML.Para(string)
      end

      # Shortcut for Yast::HTML.List
      def list(items)
        Yast::HTML.List(items)
      end

      # Actions needed to reach the desired devicegraph
      #
      # If a libstorage-ng exception is raised while calculating the
      # actiongraph, it is rescued and a pop-up is presented to the user.
      #
      # @return [Actiongraph, nil] nil if it's not possible to calculate the actions
      def actiongraph
        @devicegraph ? @devicegraph.actiongraph : nil
      rescue ::Storage::Exception => e
        # TODO: the code capturing the exception and displaying the error pop-up
        # should not be directly in this dialog. It should be in some common
        # place ensuring we also catch the exception in other places where we
        # calculate/display the actiongraph (like the Expert Partitioner).
        # That should be part of a bigger effort in reporting invalid devicegraphs.
        # See https://trello.com/c/iMoOGVxg/
        msg = _(
          "An error was found in one of the devices in the system.\n" \
          "The information displayed may not be accurate and the\n" \
          "installation may fail if you continue."
        )
        hint = _("Click below to see more details (English only).")

        Yast2::Popup.show("#{msg}\n\n#{hint}", details: e.what)
        nil
      end
    end
  end
end