Skip to content

Commit

Permalink
feat(ui): redesign VM listing
Browse files Browse the repository at this point in the history
Make the UI less crowded, consolidate some information behind hoverable slabs

Other notes:
* show customization specs list
* show only connect address, others in modal
* show clustered/sequential count in list
* hide delete button unless hovering on the row
* add copy to clipboard buttons
* add search by tags
  • Loading branch information
mromulus committed Feb 16, 2024
1 parent 887305f commit bce2ba1
Show file tree
Hide file tree
Showing 32 changed files with 230 additions and 110 deletions.
3 changes: 2 additions & 1 deletion app/calculation/hostname_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
class HostnameGenerator < Patterns::Calculation
private
def result
subject.virtual_machine = virtual_machine
hostname_sequence_suffix = '{{ seq }}' if virtual_machine.clustered?
hostname_team_suffix = '{{ team_nr_str }}' if virtual_machine.numbered_actor && (!nic || !nic.network&.numbered?)

Expand All @@ -21,7 +22,7 @@ def result
end

def virtual_machine
subject.virtual_machine
options[:vm] || subject.virtual_machine
end

def nic
Expand Down
2 changes: 2 additions & 0 deletions app/calculation/liquid_range_substitution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def numbering_actor
subject.network.actor
when VirtualMachine
vm_numbering_source(subject)
when CustomizationSpec
vm_numbering_source(subject.virtual_machine)
else
subject.actor
end
Expand Down
18 changes: 18 additions & 0 deletions app/components/actor_avatar_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

class ActorAvatarComponent < ViewComponent::Base
def initialize(actor:)
@actor = actor
end

private
def attributes
{
class: helpers.actor_color_classes(@actor),
data: {
controller: 'tippy',
tooltip: @actor.name
}
}
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
%figure.rounded-full.inline-block.text-xs.leading-8.w-8.h-8.text-center.inline-block{attributes}= @actor.initials
2 changes: 1 addition & 1 deletion app/components/actor_chip_component.haml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
%span.rounded-full.px-3.py-1.items-center.whitespace-nowrap{class: css_classes}
%span.rounded-full.px-3.py-1.whitespace-nowrap{class: css_classes}
= content
= @text || @actor.name
5 changes: 4 additions & 1 deletion app/components/chip_component.html.haml
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
%span.text-sm.rounded-md.px-3.py-0.bg-cyan-200.text-cyan-800.dark:bg-cyan-700.dark:text-cyan-300{title: @name}= @name.truncate(16)
%span.text-xs.rounded-md.px-2.inline-flex.items-center.gap-x-1{title: @name, class: color_classes + ' py-0.5'}
- if @icon
%i.fas{class: "fa-#{@icon}"}
= @name
9 changes: 8 additions & 1 deletion app/components/chip_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
class ChipComponent < ViewComponent::Base
with_collection_parameter :name

def initialize(name:)
def initialize(name:, icon: nil, flavor: 'stone')
@name = name
@flavor = flavor
@icon = icon
end

private
def color_classes
"bg-#{@flavor}-200 text-#{@flavor}-800 dark:bg-#{@flavor}-700 dark:text-#{@flavor}-300"
end
end
2 changes: 1 addition & 1 deletion app/components/liquid_address_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ def render?

private
def template_text
AddressValues.result_for(@object) || UnsubstitutedAddress.result_for(@object)
AddressValues.result_for(@object) || UnsubstitutedAddress.result_for(@object) || 'N/A or dynamic'
end
end
11 changes: 10 additions & 1 deletion app/components/liquid_fqdn_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
class LiquidFQDNComponent < LiquidTooltipSnippetComponent
private
def template_text
HostnameGenerator.result_for(@object.host_spec).fqdn
HostnameGenerator.result_for(spec, *options).fqdn
end

def spec
case @object
when CustomizationSpec
@object
when VirtualMachine
@object.host_spec
end
end
end
7 changes: 0 additions & 7 deletions app/components/liquid_text_component.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
# frozen_string_literal: true

class LiquidTextComponent < LiquidTooltipSnippetComponent
attr_reader :actor

def initialize(object:, actor:)
@object = object
@actor = actor
end

private
def template_text
@object
Expand Down
7 changes: 5 additions & 2 deletions app/components/liquid_tooltip_snippet_component.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# frozen_string_literal: true

class LiquidTooltipSnippetComponent < ViewComponent::Base
def initialize(object:)
attr_reader :options

def initialize(options = {}, object:)
@object = object
@options = options
end

def call
Expand All @@ -13,7 +16,7 @@ def call
{
data: {
controller: 'tippy',
tooltip: LiquidRangeSubstitution.result_for(@object, node: variable_node, actor: @actor)
tooltip: LiquidRangeSubstitution.result_for(@object, node: variable_node, actor: options[:actor])
}
}
)
Expand Down
2 changes: 1 addition & 1 deletion app/components/network_interface_form_component.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
%i.fas.fa-cloud-upload-alt.text-sm
- if network_interface.connection?
.flex.items-center.justify-center.rounded-full.bg-orange-200.text-orange-800.w-7.h-7{title: 'Connection interface'}
%i.fas.fa-cogs.text-sm
%i.fas.fa-satellite-dish.text-sm

- if network_interface.persisted?
= form_with(url: [network_interface.exercise, network_interface.virtual_machine, network_interface], method: :delete, data: { turbo_confirm: 'Are you sure?' }) do |form|
Expand Down
18 changes: 13 additions & 5 deletions app/controllers/virtual_machines_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ class VirtualMachinesController < ApplicationController

def index
@virtual_machines = policy_scope(@exercise.virtual_machines)
.includes({ customization_specs: [:capabilities, :tags] })
.includes({ connection_nic: { addresses: [:address_pool] } })
.preload(
:actor, :operating_system, :system_owner,
:connection_nic, :exercise,
network_interfaces: [
{ network: [:actor, :exercise] }
]
:actor, :numbered_by,
:host_spec, :operating_system, :system_owner,
connection_nic: [:addresses],
)
.order(:name)

Expand Down Expand Up @@ -54,6 +54,14 @@ def show
authorize @virtual_machine
end

def address_preview
@virtual_machine ||= @exercise
.virtual_machines
.find(params[:id])

authorize @virtual_machine, :show?
end

def update
@virtual_machine.numbered_by = get_numbered
@virtual_machine.assign_attributes(virtual_machine_params)
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const controllers = import.meta.glob("./**/*_controller.js", { eager: true });
registerControllers(application, controllers);

import { Dropdown, Modal } from "tailwindcss-stimulus-components";
import Clipboard from "stimulus-clipboard";

application.register("dropdown", Dropdown);
application.register("modal", Modal);
application.register("clipboard", Clipboard);
21 changes: 18 additions & 3 deletions app/javascript/src/fonts.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
faBook,
faFlask,
faBox,
faCogs,
faSatelliteDish,
faClipboard,
faCopy,
faClone,
faAnglesRight,
Expand All @@ -27,6 +28,9 @@ import {
faSearch,
faUpRightFromSquare,
faIdBadge,
faUserTag,
faTags,
faFileLines,
faKey,
faLayerGroup,
faProjectDiagram,
Expand All @@ -40,7 +44,12 @@ import {
faCircleQuestion,
} from "@fortawesome/free-solid-svg-icons";

import { faUbuntu, faWindows } from "@fortawesome/free-brands-svg-icons";
import {
faUbuntu,
faDebian,
faLinux,
faWindows,
} from "@fortawesome/free-brands-svg-icons";

// Make sure this is before any other `fontawesome` API calls
config.autoAddCss = false;
Expand All @@ -52,7 +61,8 @@ library.add(
faBook,
faFlask,
faBox,
faCogs,
faSatelliteDish,
faClipboard,
faCopy,
faClone,
faAnglesRight,
Expand All @@ -71,13 +81,18 @@ library.add(
faSearch,
faUpRightFromSquare,
faIdBadge,
faUserTag,
faTags,
faFileLines,
faKey,
faLayerGroup,
faProjectDiagram,
faNetworkWired,
faHdd,
faUsers,
faUbuntu,
faLinux,
faDebian,
faWindows,
faServer,
faDatabase,
Expand Down
4 changes: 4 additions & 0 deletions app/models/actor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ def downcased_name
name.downcase
end

def initials
name.split(' ').map(&:first).map(&:upcase).join
end

def numbering
return unless prefs['numbered']
count = prefs.dig('numbered', 'count').presence || 0
Expand Down
1 change: 1 addition & 0 deletions app/models/address.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Address < ApplicationRecord
belongs_to :address_pool, touch: true, optional: true
has_one :virtual_machine, through: :network_interface
has_one :network, through: :network_interface
has_one :actor, through: :network

delegate :exercise, to: :network

Expand Down
4 changes: 4 additions & 0 deletions app/models/operating_system.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,14 @@ def applied_primary_disk_size

def to_icon
case slug
when /debian/
'fa-brands fa-debian'
when /ubuntu/
'fa-brands fa-ubuntu'
when /win/
'fa-brands fa-windows'
when /linux/
'fa-brands fa-linux'
else
'fa-solid fa-server'
end
Expand Down
19 changes: 12 additions & 7 deletions app/models/virtual_machine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,24 @@ class VirtualMachine < ApplicationRecord
belongs_to :numbered_by, polymorphic: true, optional: true
has_many :network_interfaces, dependent: :destroy
has_many :customization_specs, dependent: :destroy
has_one :connection_nic, -> { connectable },
class_name: 'NetworkInterface', foreign_key: :virtual_machine_id
has_one :host_spec, -> { mode_host },
class_name: 'CustomizationSpec', foreign_key: :virtual_machine_id

has_many :networks, through: :network_interfaces
has_many :addresses, through: :network_interfaces
has_and_belongs_to_many :services,
after_add: :invalidate_cache, after_remove: :invalidate_cache
has_and_belongs_to_many :capabilities,
after_add: :invalidate_cache, after_remove: :invalidate_cache

has_one :connection_nic, -> { connectable },
class_name: 'NetworkInterface', foreign_key: :virtual_machine_id
has_one :host_spec, -> { mode_host },
class_name: 'CustomizationSpec', foreign_key: :virtual_machine_id

accepts_nested_attributes_for :network_interfaces,
reject_if: proc { |attributes| attributes.all? { |key, value| value.blank? || value == '0' } }

scope :search, ->(query) {
columns = %w{virtual_machines.name customization_specs.dns_name users.name operating_systems.name}
left_outer_joins(:system_owner, :operating_system, :customization_specs)
columns = %w{virtual_machines.name customization_specs.dns_name users.name operating_systems.name tags.name}
left_outer_joins(:system_owner, :operating_system, customization_specs: { taggings: [:tag] })
.where(
columns
.map { |c| "lower(#{c}) ilike :search" }
Expand Down Expand Up @@ -97,6 +97,11 @@ def clustered?
custom_instance_count.to_i > 0
end

def connection_address
return unless connection_nic
connection_nic.addresses.detect(&:connection?)
end

private
def lowercase_fields
name.downcase! if name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
= form.collection_select :matcher, actor_number_config.actor.all_numbers, :to_s, :to_s, { include_blank: true }, { class: 'form-input mt-1', multiple: true, data: { controller: 'select'} }

.pt-9
= link_to 'javascript:;', class: 'text-white bg-gradient-to-r from-cyan-400 via-cyan-500 to-cyan-600 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-cyan-300 dark:focus:ring-cyan-800 font-medium rounded-lg text-sm text-center px-5 py-2.5', data: { action: "click->modal#open" } do
= link_to 'javascript:;', class: 'text-white bg-gradient-to-r from-purple-500 to-pink-500 hover:bg-gradient-to-l focus:ring-4 focus:outline-none focus:ring-purple-200 dark:focus:ring-purple-800 font-medium rounded-lg text-sm text-center px-5 py-2.5', data: { action: "click->modal#open" } do
Config map
%i.fas.fa-up-right-from-square

Expand Down
4 changes: 2 additions & 2 deletions app/views/addresses/_address.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
.text-center.text-gray-500 Automatic configuration
- if address.connection?
.shrink-0.self-center.inline-flex.items-center.justify-center.rounded-full.bg-orange-200.text-orange-800.w-7.h-7{title: Address.human_attribute_name(:connection)}
%i.fas.fa-cogs.text-sm
%i.fas.fa-satellite-dish.text-sm

.relative.inline-flex{"data-controller" => "dropdown"}
.inline-flex.justify-center.items-center.group.select-none{"data-action" => "click->dropdown#toggle click@window->dropdown#hide", "data-dropdown-target" => "button", :role => "button", :tabindex => "0"}
Expand Down Expand Up @@ -61,7 +61,7 @@

- if address.connection? && address.fixed?
.self-center.inline-flex.items-center.justify-center.rounded-full.bg-orange-200.text-orange-800.w-7.h-7{title: Address.human_attribute_name(:connection)}
%i.fas.fa-cogs.text-sm
%i.fas.fa-satellite-dish.text-sm

- if address.needs_pool? && address.offset
.self-center.flex.items-center.justify-center.rounded-full.bg-blue-200.text-blue-800.w-7.h-7{class: !address.dns_enabled ? 'opacity-70' : ''}
Expand Down
2 changes: 1 addition & 1 deletion app/views/checks/_check.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
= f.label :special_label, class: 'font-bold'
= f.text_field :special_label, class: 'form-input', disabled: !policy(@service).update?
.pt-9.ml-2
= link_to 'javascript:;', class: 'text-white bg-gradient-to-r from-cyan-400 via-cyan-500 to-cyan-600 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-cyan-300 dark:focus:ring-cyan-800 font-medium rounded-lg text-sm text-center px-5 py-2.5', data: { action: "click->modal#open" } do
= link_to 'javascript:;', class: 'text-white bg-gradient-to-r from-purple-500 to-pink-500 hover:bg-gradient-to-l focus:ring-4 focus:outline-none focus:ring-purple-200 dark:focus:ring-purple-800 font-medium rounded-lg text-sm text-center px-5 py-2.5', data: { action: "click->modal#open" } do
Config map
%i.fas.fa-up-right-from-square

Expand Down
4 changes: 2 additions & 2 deletions app/views/networks/_result_set.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
%td.px-6.py-4.whitespace-nowrap
.text-sm.light:text-gray-900
- if network.cloud_id
= render LiquidTextComponent.new(object: network.cloud_id, actor: network.actor)
= render LiquidTextComponent.new({ actor: network.actor }, object: network.cloud_id)
- else
%em (None)
.text-sm.text-gray-500.dark:text-gray-400.gap-x-6.flex
Expand All @@ -33,7 +33,7 @@

%td.px-6.py-4.whitespace-nowrap
- if network.domain.present?
= render LiquidTextComponent.new(object: network.domain, actor: network.actor)
= render LiquidTextComponent.new({ actor: network.actor }, object: network.domain)
- unless network.ignore_root_domain
%span<>= ".#{network.exercise.root_domain}"
- else
Expand Down
4 changes: 2 additions & 2 deletions app/views/networks/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@
.col-span-4
%dt.text-sm.text-gray-500.dark:text-gray-400= Network.human_attribute_name(:domain)
%dd.mt-1.text-sm.text-gray-900.dark:text-white.mt-0
= render LiquidTextComponent.new(object: @network.full_domain, actor: @network.actor)
= render LiquidTextComponent.new({actor: @network.actor}, object: @network.full_domain)

.py-1
.border-t.border-gray-200.dark:border-gray-500

.px-6.py-5
%dt.text-sm.text-gray-500.dark:text-gray-400= Network.human_attribute_name(:cloud_id)
%dd.mt-1.text-sm.text-gray-900.dark:text-white.mt-0.col-span-2
= render LiquidTextComponent.new(object: @network.cloud_id, actor: @network.actor)
= render LiquidTextComponent.new({actor: @network.actor}, object: @network.cloud_id)

= render 'used_addresses'
Loading

0 comments on commit bce2ba1

Please sign in to comment.