From fc0a32485176f4cbcc192c43b054ea4e54f89679 Mon Sep 17 00:00:00 2001 From: Ben Armston Date: Mon, 4 Mar 2024 18:40:21 +0000 Subject: [PATCH 1/6] Better cluster type selection page * Use grid to display large number of cards sensibly. * Format card similarly to that used on Flight Warehouse. * Remove "Check for cluster type updates" link. This should happen automatically; no need to confuse user with its presence. --- .../components/_cluster_types_view.scss | 90 +++++++++++++++---- app/controllers/cluster_types_controller.rb | 1 + app/jobs/sync_all_cluster_types_job.rb | 2 + app/jobs/sync_individual_cluster_type_job.rb | 2 + app/models/cluster_type.rb | 6 +- app/views/cluster_types/_actions.html.erb | 7 -- app/views/cluster_types/_card.html.erb | 36 ++++---- .../cluster_types/_card_content.html.erb | 7 ++ app/views/cluster_types/index.html.erb | 5 +- ..._add_order_and_logo_url_to_cluster_type.rb | 6 ++ db/schema.rb | 4 +- .../sync_individual_cluster_type_job_spec.rb | 2 + .../sync_latest_cluster_types_job_spec.rb | 8 +- 13 files changed, 125 insertions(+), 51 deletions(-) delete mode 100644 app/views/cluster_types/_actions.html.erb create mode 100644 app/views/cluster_types/_card_content.html.erb create mode 100644 db/migrate/20240304155139_add_order_and_logo_url_to_cluster_type.rb diff --git a/app/assets/stylesheets/components/_cluster_types_view.scss b/app/assets/stylesheets/components/_cluster_types_view.scss index 1ea4b874b..37be68037 100644 --- a/app/assets/stylesheets/components/_cluster_types_view.scss +++ b/app/assets/stylesheets/components/_cluster_types_view.scss @@ -1,17 +1,77 @@ +.cluster-types-card-deck { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(28em, 1fr)); + grid-gap: 1rem; + justify-content: space-between; + justify-items: center; + padding: 1.25rem 2.5rem; +} + + +.cluster-type-card { + border: solid #3b5168 1px; + border-radius: 2px; + padding: 1.5rem; + font-weight: 400; + transition: background 125ms; +} + .cluster-type-card { - width: 19rem; - border: darkgray 1px solid; - padding: 0.5em; - margin-left: 0.5rem; - margin-right: 0.5rem; - - .card-divider { - border-bottom: darkgray 1px solid; - } - - .disabled-cluster-link { - color: #63666A; - cursor: not-allowed; - font-weight: bold; - } + display: grid; + grid-template-rows: [row1-start] auto [row1-end row2-start] auto [row2-end row3-start] auto [row3-end]; + grid-template-columns: 1fr 1fr; + grid-template-areas: "header header" "icon stats" ". button"; + + column-gap: 1.2rem; + row-gap: 0.3rem; + + align-items: center; + margin: 0 !important; + width: 100%; + min-height: 14.2rem; +} + +.cluster-type-card-title { + grid-area: header; + text-align: center; + margin: 0; + line-height: 1.8; + font-weight: 400; +} + +.cluster-type-card-icon { + grid-area: icon; + margin: 10px 0 10px 10px; + justify-self: end; + max-height: 10rem; + max-width: calc(100% - 0.5rem) !important; +} + +.cluster-type-card-description { + font-size: larger; + color: #3b5168; +} + +.cluster-type-select-link { + grid-area: button; + justify-self: end; + + .disabled-cluster-link { + color: #63666A; + cursor: not-allowed; + font-weight: bold; + } +} + +.cluster-type-card:hover { + background: $lightest-blue; + .cluster-type-card-title { } + .cluster-type-card-description { } +} + +.cluster-type-card--disabled { + background: $lighter-grey; + &:hover { + background: $lighter-grey; + } } diff --git a/app/controllers/cluster_types_controller.rb b/app/controllers/cluster_types_controller.rb index 3aa9808b5..091cd0523 100644 --- a/app/controllers/cluster_types_controller.rb +++ b/app/controllers/cluster_types_controller.rb @@ -8,5 +8,6 @@ def index result = SyncAllClusterTypesJob.perform_now(@cloud_service_config, use_cache) flash.now.alert = result.error_message unless result.success? end + @cluster_types = @cluster_types.reorder(:order, :id) end end diff --git a/app/jobs/sync_all_cluster_types_job.rb b/app/jobs/sync_all_cluster_types_job.rb index 548956e78..c2f456afb 100644 --- a/app/jobs/sync_all_cluster_types_job.rb +++ b/app/jobs/sync_all_cluster_types_job.rb @@ -84,6 +84,8 @@ def sync_data(types) type.fields = ordered_fields(type_details["parameters"]) type.field_groups = type_details["parameter_groups"] type.version = type_details["last_modified"] + type.order = type_details["order"] + type.logo_url = type_details["logo_url"] unless type.save errors << "Unable to #{type.persisted? ? "update" : "create"} type '#{type.descriptive_name}': #{type.errors.full_messages.join("; ")}" end diff --git a/app/jobs/sync_individual_cluster_type_job.rb b/app/jobs/sync_individual_cluster_type_job.rb index 2921835fc..c925ebd25 100644 --- a/app/jobs/sync_individual_cluster_type_job.rb +++ b/app/jobs/sync_individual_cluster_type_job.rb @@ -37,6 +37,8 @@ def sync_data(type_details) @cluster_type.fields = ordered_fields(type_details["parameters"]) @cluster_type.field_groups = type_details["parameter_groups"] @cluster_type.version = type_details["last_modified"] + @cluster_type.order = type_details["order"] + @cluster_type.logo_url = type_details["logo_url"] unless @cluster_type.save ["Unable to update type '#{@cluster_type.descriptive_name}': #{@cluster_type.errors.full_messages.join("; ")}"] else diff --git a/app/models/cluster_type.rb b/app/models/cluster_type.rb index 7e7ef17ac..5d8be6504 100644 --- a/app/models/cluster_type.rb +++ b/app/models/cluster_type.rb @@ -23,7 +23,11 @@ class ClusterType < ApplicationRecord # fields will require some sophisticated validation validates :fields, - presence: true + presence: true + + validates :order, + presence: true, + numericality: true validate :valid_fields_structure? diff --git a/app/views/cluster_types/_actions.html.erb b/app/views/cluster_types/_actions.html.erb deleted file mode 100644 index 7261c5e86..000000000 --- a/app/views/cluster_types/_actions.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -

Actions

- -<%= - render_lhm_actions("Cluster type actions") do |actions| - actions.add title: "Check for latest cluster types", path: cluster_types_path(use_cache: false) - end -%> diff --git a/app/views/cluster_types/_card.html.erb b/app/views/cluster_types/_card.html.erb index 56992bb8e..71f4f427a 100644 --- a/app/views/cluster_types/_card.html.erb +++ b/app/views/cluster_types/_card.html.erb @@ -1,23 +1,17 @@ <% cluster_type = card %> -
-
-

<%= cluster_type.name %>

-
-
-

<%= cluster_type.description %>

-
-
- <% if current_user.can?(:create, Cluster) %> - <%= link_to "Select", new_cluster_type_cluster_path(cluster_type) %> - <% else %> - <% title = if current_ability.enough_credits_to_create_cluster? - "You do not have permission to create clusters" - else - "Insufficient credits. You must have at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster" - end - %> - Select - <% end %> -
-
+<% if current_user.can?(:create, Cluster) %> + <%= link_to(new_cluster_type_cluster_path(cluster_type), class: "cluster-type-card") do %> + <%= render partial: 'card_content', object: cluster_type %> + <% end %> +<% else %> + <% title = if current_ability.enough_credits_to_create_cluster? + "You do not have permission to create clusters" + else + "Insufficient credits. You must have at least #{Rails.application.config.cluster_credit_requirement} credits to create a cluster" + end + %> + + <%= render partial: 'card_content', object: cluster_type %> + +<% end %> diff --git a/app/views/cluster_types/_card_content.html.erb b/app/views/cluster_types/_card_content.html.erb new file mode 100644 index 000000000..1d1426e12 --- /dev/null +++ b/app/views/cluster_types/_card_content.html.erb @@ -0,0 +1,7 @@ +<% cluster_type = card_content %> + +

<%= cluster_type.name %>

+<% if cluster_type.logo_url.present? %> + <%= image_tag cluster_type.logo_url, class: "cluster-type-card-icon" %> +<% end %> +<%= cluster_type.description %> diff --git a/app/views/cluster_types/index.html.erb b/app/views/cluster_types/index.html.erb index 4766bd9cc..9d8495e2b 100644 --- a/app/views/cluster_types/index.html.erb +++ b/app/views/cluster_types/index.html.erb @@ -1,7 +1,4 @@ <% set_title "Select cluster type" %> -<% content_for(:side_content) do %> - <%= render 'actions' %> -<% end %> <% if @cloud_service_config.nil? %>

@@ -15,7 +12,7 @@ <% elsif @cluster_types.empty? %>

There are no cluster types available at present.

<% else %> -
+
<% @cluster_types.each do |cluster_type| %> <%= render partial: 'card', object: cluster_type %> <% end %> diff --git a/db/migrate/20240304155139_add_order_and_logo_url_to_cluster_type.rb b/db/migrate/20240304155139_add_order_and_logo_url_to_cluster_type.rb new file mode 100644 index 000000000..84ac8d832 --- /dev/null +++ b/db/migrate/20240304155139_add_order_and_logo_url_to_cluster_type.rb @@ -0,0 +1,6 @@ +class AddOrderAndLogoUrlToClusterType < ActiveRecord::Migration[7.1] + def change + add_column :cluster_types, :order, :integer, null: false, default: 0 + add_column :cluster_types, :logo_url, :string, limit: 255 + end +end diff --git a/db/schema.rb b/db/schema.rb index b0ec9c942..84a237fe8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_02_20_150125) do +ActiveRecord::Schema[7.1].define(version: 2024_03_04_155139) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -56,6 +56,8 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.jsonb "field_groups", default: [], null: false + t.integer "order", default: 0, null: false + t.string "logo_url", limit: 255 end create_table "data_source_maps", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| diff --git a/spec/jobs/sync_individual_cluster_type_job_spec.rb b/spec/jobs/sync_individual_cluster_type_job_spec.rb index 61b19d1bb..c531094a8 100644 --- a/spec/jobs/sync_individual_cluster_type_job_spec.rb +++ b/spec/jobs/sync_individual_cluster_type_job_spec.rb @@ -49,6 +49,8 @@ class << subject }, parameter_groups: [], title: "Fault-Tolerant Web Hosting", + order: 100, + logo_url: '/images/cluster-types/testing-cluster-one.svg', } end let!(:cluster_type) do diff --git a/spec/jobs/sync_latest_cluster_types_job_spec.rb b/spec/jobs/sync_latest_cluster_types_job_spec.rb index ae470466f..0ec0a5f1e 100644 --- a/spec/jobs/sync_latest_cluster_types_job_spec.rb +++ b/spec/jobs/sync_latest_cluster_types_job_spec.rb @@ -48,7 +48,9 @@ class << subject }, }, parameter_groups: {}, - title: "Fault-Tolerant Web Hosting" + title: "Fault-Tolerant Web Hosting", + order: 100, + logo_url: '/images/cluster-types/testing-cluster-one.svg', } end let(:cluster_two_details) do @@ -96,7 +98,9 @@ class << subject }, }, parameter_groups: {}, - title: "My testing cluster type" + title: "My testing cluster type", + order: 100, + logo_url: '/images/cluster-types/testing-cluster-two.svg', } end From 3145c3b620f3f00dee1bad1d4893c36a4f02136a Mon Sep 17 00:00:00 2001 From: Ben Armston Date: Mon, 4 Mar 2024 18:42:23 +0000 Subject: [PATCH 2/6] Improvements to cluster launch page * Remove extraneous cluster name input where we can. * Hidden parameters are rendered as `type="password"` inputs. * Replace side-menu actions. * Remove "Reselect cluster type", the user can just click on "Launch cluster" again. * Remove "Check for updates", this should happen automatically. * Add "Manage key-pairs" link, the user may not have created a key pair yet, let's make it easy for them to find how to do that. --- app/cells/cluster_form_name_cell.rb | 13 ++++++++++++- app/controllers/clusters_controller.rb | 6 +++++- app/models/cluster.rb | 4 ++++ app/presenters/cluster/field_presenter.rb | 8 +++++++- app/views/clusters/_actions.html.erb | 3 +-- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/app/cells/cluster_form_name_cell.rb b/app/cells/cluster_form_name_cell.rb index 2dcfc34ac..bf0fd3f68 100644 --- a/app/cells/cluster_form_name_cell.rb +++ b/app/cells/cluster_form_name_cell.rb @@ -4,11 +4,22 @@ def show(cluster, form) @form = form @errors = @record.errors @attribute = :name - render + if has_clustername_parameter? + # We don't render anything here. Instead the value provided for the + # 'clustername' parameter will be used. + else + render + end end private + def has_clustername_parameter? + @record.fields.any? do |field| + field.id == Cluster::NAME_FIELD + end + end + def label_text 'Cluster name' end diff --git a/app/controllers/clusters_controller.rb b/app/controllers/clusters_controller.rb index 16c8aa3ee..38de864df 100644 --- a/app/controllers/clusters_controller.rb +++ b/app/controllers/clusters_controller.rb @@ -75,7 +75,11 @@ def permitted_params valid_selections = @cluster_type.field_groups .select { |group| group["optional"].present? } .map { |group| group["optional"]["name"] } - params.require(:cluster).permit(:name, cluster_params: @cluster_type.fields.keys, selections: valid_selections) + params.require(:cluster).permit(:name, cluster_params: @cluster_type.fields.keys, selections: valid_selections).tap do |h| + if !h.key?(:name) && h[:cluster_params].key?(Cluster::NAME_FIELD.to_sym) + h[:name] = h[:cluster_params][Cluster::NAME_FIELD.to_sym] + end + end end def set_cloud_assets diff --git a/app/models/cluster.rb b/app/models/cluster.rb index 1602549a7..40ce522b1 100644 --- a/app/models/cluster.rb +++ b/app/models/cluster.rb @@ -1,6 +1,10 @@ class Cluster include ActiveModel::API + # The field from which the cluster name will be copied if such a field + # exists. + NAME_FIELD = 'clustername'.freeze + #################################### # # Properties diff --git a/app/presenters/cluster/field_presenter.rb b/app/presenters/cluster/field_presenter.rb index c272f6ac6..a83c6d389 100644 --- a/app/presenters/cluster/field_presenter.rb +++ b/app/presenters/cluster/field_presenter.rb @@ -30,7 +30,13 @@ def initialize(object, view_context, cloud_assets) end def form_field_type - select_box? ? 'select' : "#{MAPPED_FIELD_TYPES[type]}" + if select_box? + 'select' + elsif type == 'string' && hidden + 'password_field' + else + MAPPED_FIELD_TYPES[type] + end end def select_box? diff --git a/app/views/clusters/_actions.html.erb b/app/views/clusters/_actions.html.erb index 5744d6aae..c2513ec87 100644 --- a/app/views/clusters/_actions.html.erb +++ b/app/views/clusters/_actions.html.erb @@ -2,7 +2,6 @@ <%= render_lhm_actions("Cluster actions") do |actions| - actions.add title: "Re-select cluster type", path: cluster_types_path - actions.add title: "Check for cluster type updates", path: new_cluster_type_cluster_path(@cluster_type, use_cache: false) + actions.add title: "Manage key-pairs", path: key_pairs_path end %> From 9991046e587c12e475836ed835d55fce59bf6902 Mon Sep 17 00:00:00 2001 From: Ben Armston Date: Tue, 5 Mar 2024 12:27:11 +0000 Subject: [PATCH 3/6] Add cluster type logos --- public/images/cluster-types/cfd-jumpstart.svg | 72 +++++++++++++++ .../container-cruncher-large.svg | 90 +++++++++++++++++++ .../container-cruncher-medium.svg | 89 ++++++++++++++++++ .../container-cruncher-small.svg | 89 ++++++++++++++++++ .../cluster-types/jupyterlab-jumpstart.svg | 76 ++++++++++++++++ .../images/cluster-types/slurm-standalone.svg | 90 +++++++++++++++++++ .../cluster-types/slurm-team-edition.svg | 90 +++++++++++++++++++ 7 files changed, 596 insertions(+) create mode 100644 public/images/cluster-types/cfd-jumpstart.svg create mode 100644 public/images/cluster-types/container-cruncher-large.svg create mode 100644 public/images/cluster-types/container-cruncher-medium.svg create mode 100644 public/images/cluster-types/container-cruncher-small.svg create mode 100644 public/images/cluster-types/jupyterlab-jumpstart.svg create mode 100644 public/images/cluster-types/slurm-standalone.svg create mode 100644 public/images/cluster-types/slurm-team-edition.svg diff --git a/public/images/cluster-types/cfd-jumpstart.svg b/public/images/cluster-types/cfd-jumpstart.svg new file mode 100644 index 000000000..d0be82af9 --- /dev/null +++ b/public/images/cluster-types/cfd-jumpstart.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + diff --git a/public/images/cluster-types/container-cruncher-large.svg b/public/images/cluster-types/container-cruncher-large.svg new file mode 100644 index 000000000..f660a4e6e --- /dev/null +++ b/public/images/cluster-types/container-cruncher-large.svg @@ -0,0 +1,90 @@ + + + + diff --git a/public/images/cluster-types/container-cruncher-medium.svg b/public/images/cluster-types/container-cruncher-medium.svg new file mode 100644 index 000000000..0e3d94a08 --- /dev/null +++ b/public/images/cluster-types/container-cruncher-medium.svg @@ -0,0 +1,89 @@ + + + + diff --git a/public/images/cluster-types/container-cruncher-small.svg b/public/images/cluster-types/container-cruncher-small.svg new file mode 100644 index 000000000..243b336e2 --- /dev/null +++ b/public/images/cluster-types/container-cruncher-small.svg @@ -0,0 +1,89 @@ + + + + diff --git a/public/images/cluster-types/jupyterlab-jumpstart.svg b/public/images/cluster-types/jupyterlab-jumpstart.svg new file mode 100644 index 000000000..50c7eb8e0 --- /dev/null +++ b/public/images/cluster-types/jupyterlab-jumpstart.svg @@ -0,0 +1,76 @@ + + + + diff --git a/public/images/cluster-types/slurm-standalone.svg b/public/images/cluster-types/slurm-standalone.svg new file mode 100644 index 000000000..749373cbf --- /dev/null +++ b/public/images/cluster-types/slurm-standalone.svg @@ -0,0 +1,90 @@ + + + + diff --git a/public/images/cluster-types/slurm-team-edition.svg b/public/images/cluster-types/slurm-team-edition.svg new file mode 100644 index 000000000..749373cbf --- /dev/null +++ b/public/images/cluster-types/slurm-team-edition.svg @@ -0,0 +1,90 @@ + + + + From a4fbb59bf4c6769d7722634de1e92f7534954310 Mon Sep 17 00:00:00 2001 From: Ben Armston Date: Tue, 5 Mar 2024 15:58:20 +0000 Subject: [PATCH 4/6] Better cluster selection and launch pages --- .../components/_cluster_types_view.scss | 25 ++++++++++++++++++- app/presenters/cluster/field_presenter.rb | 2 ++ app/views/cluster_types/index.html.erb | 7 ++++++ app/views/clusters/new.html.erb | 12 ++++++--- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/components/_cluster_types_view.scss b/app/assets/stylesheets/components/_cluster_types_view.scss index 37be68037..e1c547ffa 100644 --- a/app/assets/stylesheets/components/_cluster_types_view.scss +++ b/app/assets/stylesheets/components/_cluster_types_view.scss @@ -20,7 +20,7 @@ display: grid; grid-template-rows: [row1-start] auto [row1-end row2-start] auto [row2-end row3-start] auto [row3-end]; grid-template-columns: 1fr 1fr; - grid-template-areas: "header header" "icon stats" ". button"; + grid-template-areas: "header header" "icon description" ". button"; column-gap: 1.2rem; row-gap: 0.3rem; @@ -75,3 +75,26 @@ background: $lighter-grey; } } + + +.cluster-type-header { + display: grid; + grid-template-rows: [row1-start] auto [row1-end row2-start] auto [row2-end]; + grid-template-columns: auto auto; + grid-template-areas: "icon header" "icon description"; + column-gap: 1.2rem; + row-gap: 0.3rem; + justify-content: start; + + .cluster-type-card-title { + justify-self: left; + align-self: end; + max-width: 28em; + } + + .cluster-type-card-description { + justify-self: left; + align-self: start; + max-width: 28em; + } +} diff --git a/app/presenters/cluster/field_presenter.rb b/app/presenters/cluster/field_presenter.rb index a83c6d389..24063acf0 100644 --- a/app/presenters/cluster/field_presenter.rb +++ b/app/presenters/cluster/field_presenter.rb @@ -74,6 +74,8 @@ def form_input(form) @cloud_assets[cloud_asset].map { |a| [a["name"], a["id"]] } end form.send(form_field_type, :value, values, {prompt: true}, form_options) + elsif form_field_type == 'password_field' + form.send(form_field_type, :value, form_options.merge(autocomplete: 'off')) else form.send(form_field_type, :value, form_options) end diff --git a/app/views/cluster_types/index.html.erb b/app/views/cluster_types/index.html.erb index 9d8495e2b..da62a346b 100644 --- a/app/views/cluster_types/index.html.erb +++ b/app/views/cluster_types/index.html.erb @@ -1,5 +1,12 @@ <% set_title "Select cluster type" %> +
+

+ Choose from our selection of cluster types to start building a cluster using + your private cloud resources. +

+
+ <% if @cloud_service_config.nil? %>

New clusters cannot be launched as the Cloud Environment has not yet been configured. diff --git a/app/views/clusters/new.html.erb b/app/views/clusters/new.html.erb index 3d2a0e729..c54b666ee 100644 --- a/app/views/clusters/new.html.erb +++ b/app/views/clusters/new.html.erb @@ -7,8 +7,14 @@ <%= render 'actions' %> <% end %> -

Configure details for cluster <%= @cluster_type.name %>

-

<%= @cluster_type.description %>

+
+ <%= render partial: 'cluster_types/card_content', object: @cluster_type %> +
+ +

+Complete the details below and click "Launch". +

+ <%= form_for(@cluster, url: cluster_type_clusters_path(@cluster_type)) do |f| %> <%= cell('cluster_form_errors').(:show, @cluster) %> @@ -46,5 +52,5 @@
<% end %> - <%= f.submit 'Create' %> + <%= f.submit 'Launch' %> <% end %> From 6d536d08956462510458348d5c6559133ab66cdf Mon Sep 17 00:00:00 2001 From: Ben Armston Date: Tue, 5 Mar 2024 17:22:31 +0000 Subject: [PATCH 5/6] Cluster launch and rack pages contain instructions The cluster launch page displays the launch instructions for the cluster type if any. For all non-launch instructions, the rack page contains a tab for those instructions. Effectively, this results in a new "Usage instructions" tab and possibly a new "Admin instructions" tab. Displaying the correct instructions on the rack page requires visualiser to be able to map from rack to cluster type. We achive this by having each stack's ouput contain a `concertim_cluster_type` value. Visualiser uses that to locate the cluster type that was used to launch the cluster and pull the admin and usage instructions from it. A better approach would be to have Visualiser create a Cluster model when the launch request is made. Bookkeeping such as the cluster type used could then be added directly to that cluster model. However, given the discussion around concertim.com, I suspect that it is best to wait until that development is underway. Also, if the stack ouputs contain a `web_access` output, we use that to link to the cluster's web access. Presumably, this is Flight Web Suite. --- Gemfile | 1 + Gemfile.lock | 2 ++ app/assets/stylesheets/application.scss | 1 + .../components/_new_cluster_form.scss | 7 ++++++ .../components/_rack_show_page.scss | 14 +++++++++++ app/controllers/racks_controller.rb | 4 ++++ app/jobs/sync_all_cluster_types_job.rb | 1 + app/jobs/sync_individual_cluster_type_job.rb | 1 + app/lib/markdown_renderer.rb | 13 ++++++++++ app/presenters/cluster_type_presenter.rb | 10 ++++++++ app/presenters/rack_presenter.rb | 24 +++++++++++++++++++ app/views/clusters/new.html.erb | 7 ++++++ app/views/racks/_tabbar.html.erb | 5 ++++ app/views/racks/instructions.html.erb | 10 ++++++++ app/views/racks/show.html.erb | 4 +--- config/routes.rb | 1 + ..._add_launch_instruction_to_cluster_type.rb | 5 ++++ db/schema.rb | 3 ++- 18 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 app/assets/stylesheets/components/_rack_show_page.scss create mode 100644 app/lib/markdown_renderer.rb create mode 100644 app/presenters/cluster_type_presenter.rb create mode 100644 app/views/racks/instructions.html.erb create mode 100644 db/migrate/20240305173627_add_launch_instruction_to_cluster_type.rb diff --git a/Gemfile b/Gemfile index 926f8ea45..07f45a543 100644 --- a/Gemfile +++ b/Gemfile @@ -82,6 +82,7 @@ gem "cells-erb", "~> 0.1.0" gem "rabl", "~> 0.16.1" gem "pagy", "~> 6.4" gem "simple_form", "~> 5.3" +gem "commonmarker", "~> 1.0" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] # gem "bcrypt", "~> 3.1.7" diff --git a/Gemfile.lock b/Gemfile.lock index 308d027ef..503846138 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -105,6 +105,7 @@ GEM cells-rails (0.1.5) actionpack (>= 5.0) cells (>= 4.1.6, < 5.0.0) + commonmarker (1.0.4-x86_64-linux) concurrent-ruby (1.2.3) connection_pool (2.4.1) crack (0.4.5) @@ -351,6 +352,7 @@ DEPENDENCIES capybara cells-erb (~> 0.1.0) cells-rails (~> 0.1.5) + commonmarker (~> 1.0) crack (~> 0.4.5) dartsass-rails (= 0.4.1) debug diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 705c30eea..90f0723b3 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -57,6 +57,7 @@ @import "components/simple_form"; @import "components/tabs"; @import "components/ct_widget"; +@import "components/rack_show_page"; /* Components/Widgets diff --git a/app/assets/stylesheets/components/_new_cluster_form.scss b/app/assets/stylesheets/components/_new_cluster_form.scss index 53f79dd1f..b2c42759c 100644 --- a/app/assets/stylesheets/components/_new_cluster_form.scss +++ b/app/assets/stylesheets/components/_new_cluster_form.scss @@ -1,4 +1,5 @@ #new_cluster { + border: none; .formItem { margin-bottom: 1rem; } @@ -37,3 +38,9 @@ margin-left: 1rem; } } + +.cluster-launch-instructions { + max-width: 30rem; + margin-top: -1rem; + margin-bottom: 1rem; +} diff --git a/app/assets/stylesheets/components/_rack_show_page.scss b/app/assets/stylesheets/components/_rack_show_page.scss new file mode 100644 index 000000000..5a33354de --- /dev/null +++ b/app/assets/stylesheets/components/_rack_show_page.scss @@ -0,0 +1,14 @@ +.usage-instructions { + margin-bottom: 2em; + + &:last-child { + margin-bottom: 0; + } + + pre { + background: #fff !important; + code { + background: #2b303b; + } + } +} diff --git a/app/controllers/racks_controller.rb b/app/controllers/racks_controller.rb index becf3d6ad..16fb88195 100644 --- a/app/controllers/racks_controller.rb +++ b/app/controllers/racks_controller.rb @@ -8,4 +8,8 @@ def show def devices @devices = resource_table_collection(@rack.devices, model: Device) end + + def instructions + @instruction_id = params[:instruction_id] + end end diff --git a/app/jobs/sync_all_cluster_types_job.rb b/app/jobs/sync_all_cluster_types_job.rb index c2f456afb..ca91dc84f 100644 --- a/app/jobs/sync_all_cluster_types_job.rb +++ b/app/jobs/sync_all_cluster_types_job.rb @@ -86,6 +86,7 @@ def sync_data(types) type.version = type_details["last_modified"] type.order = type_details["order"] type.logo_url = type_details["logo_url"] + type.instructions = type_details["instructions"] unless type.save errors << "Unable to #{type.persisted? ? "update" : "create"} type '#{type.descriptive_name}': #{type.errors.full_messages.join("; ")}" end diff --git a/app/jobs/sync_individual_cluster_type_job.rb b/app/jobs/sync_individual_cluster_type_job.rb index c925ebd25..c8a863a70 100644 --- a/app/jobs/sync_individual_cluster_type_job.rb +++ b/app/jobs/sync_individual_cluster_type_job.rb @@ -39,6 +39,7 @@ def sync_data(type_details) @cluster_type.version = type_details["last_modified"] @cluster_type.order = type_details["order"] @cluster_type.logo_url = type_details["logo_url"] + @cluster_type.instructions = type_details["instructions"] unless @cluster_type.save ["Unable to update type '#{@cluster_type.descriptive_name}': #{@cluster_type.errors.full_messages.join("; ")}"] else diff --git a/app/lib/markdown_renderer.rb b/app/lib/markdown_renderer.rb new file mode 100644 index 000000000..51e9873ca --- /dev/null +++ b/app/lib/markdown_renderer.rb @@ -0,0 +1,13 @@ +module MarkdownRenderer + class << self + def render(markdown_text) + Commonmarker.to_html( + markdown_text, + options: { + parse: { smart: true }, + render: { hardbreaks: false }, + } + ).html_safe + end + end +end diff --git a/app/presenters/cluster_type_presenter.rb b/app/presenters/cluster_type_presenter.rb new file mode 100644 index 000000000..39b21bccd --- /dev/null +++ b/app/presenters/cluster_type_presenter.rb @@ -0,0 +1,10 @@ +class ClusterTypePresenter < Presenter + + delegate :instructions, to: :o + + def instruction(id) + o.instructions.detect do |instruction| + instruction['id'] == id + end + end +end diff --git a/app/presenters/rack_presenter.rb b/app/presenters/rack_presenter.rb index 398e0ab3f..212c1c665 100644 --- a/app/presenters/rack_presenter.rb +++ b/app/presenters/rack_presenter.rb @@ -1,3 +1,27 @@ class RackPresenter < Presenter include Costed + + delegate :instructions, :instruction, + to: :cluster_type, + allow_nil: true + + def creation_output + outputs = o.creation_output.split(', ').map { |output| output.split('=') } + Hash[outputs].tap do |h| + if h.key?('web_access') + h['web_access'] = @view_context.link_to(h['web_access'], h['web_access'], target: '_blank') + end + end + end + + private + + def cluster_type + @cluster_type ||= + begin + cluster_type_id = creation_output['concertim_cluster_type'] + ct = ClusterType.find_by(foreign_id: cluster_type_id) + h.presenter_for(ct) if ct + end + end end diff --git a/app/views/clusters/new.html.erb b/app/views/clusters/new.html.erb index c54b666ee..91135272b 100644 --- a/app/views/clusters/new.html.erb +++ b/app/views/clusters/new.html.erb @@ -52,5 +52,12 @@ Complete the details below and click "Launch".
<% end %> + <% presenter = presenter_for(@cluster_type) %> + <% if presenter.instruction('launch').present? %> +
+ <%= MarkdownRenderer.render(presenter.instruction('launch')['text']) %> +
+ <% end %> + <%= f.submit 'Launch' %> <% end %> diff --git a/app/views/racks/_tabbar.html.erb b/app/views/racks/_tabbar.html.erb index 81b468029..8da6c9a64 100644 --- a/app/views/racks/_tabbar.html.erb +++ b/app/views/racks/_tabbar.html.erb @@ -2,5 +2,10 @@ render_tabbar do |tabs| tabs.add 'Overview', rack_path(@rack) tabs.add 'Devices', devices_rack_path(@rack) + + (presenter_for(@rack, RackPresenter).instructions || []).each do |instruction| + next if instruction['id'] == 'launch' + tabs.add(instruction['title'], instructions_rack_path(@rack, instruction['id'])) + end end %> diff --git a/app/views/racks/instructions.html.erb b/app/views/racks/instructions.html.erb new file mode 100644 index 000000000..9618bdce0 --- /dev/null +++ b/app/views/racks/instructions.html.erb @@ -0,0 +1,10 @@ +<% set_title "Rack: #{@rack.name}" -%> +<%= render partial: 'racks/tabbar' %> + +<% rack = presenter_for(@rack, RackPresenter) %> + +<%= render_tab_content do %> +
+ <%= MarkdownRenderer.render(rack.instruction(@instruction_id)['text']) %> +
+<% end %> diff --git a/app/views/racks/show.html.erb b/app/views/racks/show.html.erb index c0c93dbb0..8977a3183 100644 --- a/app/views/racks/show.html.erb +++ b/app/views/racks/show.html.erb @@ -19,9 +19,7 @@ dl.item "Network details", @rack.network_details - dl.sublist "Creation Output" do |sl| - sl.item "Result", @rack.creation_output - end + dl.recurse_items "Creation Output", presenter_for(@rack, RackPresenter).creation_output dl.recurse_items "Metadata", @rack.metadata end diff --git a/config/routes.rb b/config/routes.rb index b356de444..91fd52812 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,6 +27,7 @@ resources :racks, only: [:show] do member do get :devices + get 'instructions/:instruction_id', to: 'racks#instructions', as: :instructions end end diff --git a/db/migrate/20240305173627_add_launch_instruction_to_cluster_type.rb b/db/migrate/20240305173627_add_launch_instruction_to_cluster_type.rb new file mode 100644 index 000000000..ef495715b --- /dev/null +++ b/db/migrate/20240305173627_add_launch_instruction_to_cluster_type.rb @@ -0,0 +1,5 @@ +class AddLaunchInstructionToClusterType < ActiveRecord::Migration[7.1] + def change + add_column :cluster_types, :instructions, :jsonb + end +end diff --git a/db/schema.rb b/db/schema.rb index 84a237fe8..8f356d51f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_03_04_155139) do +ActiveRecord::Schema[7.1].define(version: 2024_03_05_173627) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -58,6 +58,7 @@ t.jsonb "field_groups", default: [], null: false t.integer "order", default: 0, null: false t.string "logo_url", limit: 255 + t.jsonb "instructions" end create_table "data_source_maps", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| From 18628bc88ce7ed2100fd1f91e8f3b25e9553705d Mon Sep 17 00:00:00 2001 From: Ben Armston Date: Thu, 7 Mar 2024 12:17:43 +0000 Subject: [PATCH 6/6] Better launch page styling --- .../stylesheets/components/_cluster_types_view.scss | 1 + app/assets/stylesheets/components/_new_cluster_form.scss | 9 +++++++-- app/views/clusters/new.html.erb | 7 ++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/components/_cluster_types_view.scss b/app/assets/stylesheets/components/_cluster_types_view.scss index e1c547ffa..3cc760cbc 100644 --- a/app/assets/stylesheets/components/_cluster_types_view.scss +++ b/app/assets/stylesheets/components/_cluster_types_view.scss @@ -85,6 +85,7 @@ column-gap: 1.2rem; row-gap: 0.3rem; justify-content: start; + margin-bottom: 0.625rem; .cluster-type-card-title { justify-self: left; diff --git a/app/assets/stylesheets/components/_new_cluster_form.scss b/app/assets/stylesheets/components/_new_cluster_form.scss index b2c42759c..29ddaee89 100644 --- a/app/assets/stylesheets/components/_new_cluster_form.scss +++ b/app/assets/stylesheets/components/_new_cluster_form.scss @@ -19,7 +19,7 @@ } .cluster-field-group { - margin-bottom: 2rem; + margin-bottom: 0.625rem; } .cluster-field-group-label { } .cluster-field-group-description { @@ -39,8 +39,13 @@ } } +.cluster-form-instructions { + padding-left: 10px; + margin-bottom: 0.625rem; +} + .cluster-launch-instructions { max-width: 30rem; - margin-top: -1rem; margin-bottom: 1rem; + margin-left: 1rem; } diff --git a/app/views/clusters/new.html.erb b/app/views/clusters/new.html.erb index 91135272b..b5213eed4 100644 --- a/app/views/clusters/new.html.erb +++ b/app/views/clusters/new.html.erb @@ -11,9 +11,9 @@ <%= render partial: 'cluster_types/card_content', object: @cluster_type %> -

+

Complete the details below and click "Launch". -

+
<%= form_for(@cluster, url: cluster_type_clusters_path(@cluster_type)) do |f| %> @@ -23,7 +23,7 @@ Complete the details below and click "Launch". <% @cluster.field_groups.each do |field_group| %> <% next if field_group.empty? %>
-

<%= field_group.label %>

+

<%= field_group.label %>

<% unless field_group.description.blank? %>
<%= field_group.description %>
<% end %> @@ -54,6 +54,7 @@ Complete the details below and click "Launch". <% presenter = presenter_for(@cluster_type) %> <% if presenter.instruction('launch').present? %> +

<%= presenter.instruction('launch')['title'] %>

<%= MarkdownRenderer.render(presenter.instruction('launch')['text']) %>