From 7fbeebd534da398f14fd85a3bd3c1397d7ce4c7d Mon Sep 17 00:00:00 2001 From: sensei100 Date: Mon, 16 Feb 2026 12:00:17 -0500 Subject: [PATCH 1/2] IMT-195 Add badge counts to folders --- app/assets/stylesheets/application.css.scss | 18 ++++++++++++ .../controllers/wunderbaum_controller.js | 10 +++++++ app/models/isilon_asset.rb | 28 +++++++++++++++++++ app/serializers/isilon_folder_serializer.rb | 6 +++- ...scendant_assets_count_to_isilon_folders.rb | 6 ++++ db/schema.rb | 4 ++- lib/tasks/backfill_folder_counts.rake | 10 +++++++ spec/models/isilon_asset_spec.rb | 21 ++++++++++++++ spec/requests/volumes/file_tree_spec.rb | 12 ++++++++ .../isilon_folder_serializer_spec.rb | 6 ++++ 10 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20260216154353_add_descendant_assets_count_to_isilon_folders.rb create mode 100644 lib/tasks/backfill_folder_counts.rake diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index 13abd701..075d8fa2 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -338,6 +338,24 @@ div.wunderbaum div.wb-header span.wb-col.wb-active { vertical-align: -0.15em; } +div.wunderbaum .wb-title .wb-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + font-size: 11px; + font-weight: 600; + padding: 0 6px; + border-radius: 999px; + background-color: #3b82f6; + color: #ffffff; + margin-left: 6px; + line-height: 1; + position: relative; + top: -2px; +} + .dashboard-title { font-weight: 400; } diff --git a/app/javascript/controllers/wunderbaum_controller.js b/app/javascript/controllers/wunderbaum_controller.js index 34a434f2..a1fd0740 100644 --- a/app/javascript/controllers/wunderbaum_controller.js +++ b/app/javascript/controllers/wunderbaum_controller.js @@ -262,6 +262,16 @@ export default class extends Controller { if (isFolder) { titleElem.textContent = node.title || ""; + + const count = node.data?.descendant_assets_count; + if (count != null) { + const badge = document.createElement("span"); + badge.className = "wb-badge"; + badge.textContent = count; + badge.title = `${count} assets`; + badge.style.marginLeft = "6px"; + titleElem.appendChild(badge); + } } else { titleElem.innerHTML = `${node.title}`; diff --git a/app/models/isilon_asset.rb b/app/models/isilon_asset.rb index 02582318..871d3afb 100644 --- a/app/models/isilon_asset.rb +++ b/app/models/isilon_asset.rb @@ -13,6 +13,10 @@ class IsilonAsset < ApplicationRecord before_validation :set_default_migration_status, on: :create before_validation :sync_volume_from_parent + after_create :increment_folder_counts + after_destroy :decrement_folder_counts + after_update :handle_folder_move + def full_path_with_volume volume_name = parent_folder&.volume&.name return isilon_path unless volume_name.present? @@ -22,6 +26,30 @@ def full_path_with_volume "/#{volume_name}#{path}".gsub(%r{//+}, "/") end + def increment_folder_counts + update_ancestors(parent_folder_id, 1) + end + + def decrement_folder_counts + update_ancestors(parent_folder_id, -1) + end + + def handle_folder_move + if saved_change_to_parent_folder_id? + old_id, new_id = saved_change_to_parent_folder_id + update_ancestors(old_id, -1) if old_id + update_ancestors(new_id, 1) if new_id + end + end + + def update_ancestors(folder_id, delta) + folder = IsilonFolder.find_by(id: folder_id) + while folder + folder.increment!(:descendant_assets_count, delta) + folder = folder.parent_folder + end + end + private def set_default_migration_status diff --git a/app/serializers/isilon_folder_serializer.rb b/app/serializers/isilon_folder_serializer.rb index b66672db..c630e705 100644 --- a/app/serializers/isilon_folder_serializer.rb +++ b/app/serializers/isilon_folder_serializer.rb @@ -1,6 +1,6 @@ class IsilonFolderSerializer < ActiveModel::Serializer attributes :title, :full_path, :folder, :id, :lazy, - :assigned_to_id, :assigned_to, + :assigned_to_id, :assigned_to, :descendant_assets_count, :parent_folder_id, :path, :key, :notes def title @@ -28,6 +28,10 @@ def assigned_to object.assigned_to&.name.to_s.presence || "Unassigned" end + def descendant_assets_count + return nil unless object.is_a?(IsilonFolder) + object.descendant_assets_count + end def path return [] if object.respond_to?(:parent_folder_id) && object.parent_folder_id.nil? diff --git a/db/migrate/20260216154353_add_descendant_assets_count_to_isilon_folders.rb b/db/migrate/20260216154353_add_descendant_assets_count_to_isilon_folders.rb new file mode 100644 index 00000000..232850da --- /dev/null +++ b/db/migrate/20260216154353_add_descendant_assets_count_to_isilon_folders.rb @@ -0,0 +1,6 @@ +class AddDescendantAssetsCountToIsilonFolders < ActiveRecord::Migration[7.2] + def change + add_column :isilon_folders, :descendant_assets_count, :integer, default: 0, null: false + add_index :isilon_folders, :descendant_assets_count + end +end diff --git a/db/schema.rb b/db/schema.rb index a7a39df3..f13065e7 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.2].define(version: 2026_02_13_143000) do +ActiveRecord::Schema[7.2].define(version: 2026_02_16_154353) do create_table "admins", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false @@ -97,7 +97,9 @@ t.datetime "updated_at", null: false t.integer "assigned_to_id" t.text "notes" + t.integer "descendant_assets_count", default: 0, null: false t.index ["assigned_to_id"], name: "index_isilon_folders_on_assigned_to_id" + t.index ["descendant_assets_count"], name: "index_isilon_folders_on_descendant_assets_count" t.index ["parent_folder_id"], name: "index_isilon_folders_on_parent_folder_id" t.index ["volume_id", "full_path"], name: "index_isilon_folders_on_volume_id_and_full_path", unique: true t.index ["volume_id"], name: "index_isilon_folders_on_volume_id" diff --git a/lib/tasks/backfill_folder_counts.rake b/lib/tasks/backfill_folder_counts.rake new file mode 100644 index 00000000..ef842f84 --- /dev/null +++ b/lib/tasks/backfill_folder_counts.rake @@ -0,0 +1,10 @@ +namespace :folders do + desc "Backfill descendant_assets_count" + task backfill_counts: :environment do + IsilonFolder.find_each do |folder| + ids = [ folder.id ] + folder.descendant_folders.map(&:id) + count = IsilonAsset.where(parent_folder_id: ids).count + folder.update_column(:descendant_assets_count, count) + end + end +end diff --git a/spec/models/isilon_asset_spec.rb b/spec/models/isilon_asset_spec.rb index c715896d..9fa17849 100644 --- a/spec/models/isilon_asset_spec.rb +++ b/spec/models/isilon_asset_spec.rb @@ -65,4 +65,25 @@ expect(asset.volume).to eq(volume) end end + + describe "descendant_assets_count" do + let!(:root) { create(:isilon_folder) } + let!(:child) { create(:isilon_folder, parent_folder: root) } + + it "increments counts up the tree when asset is created" do + expect { + create(:isilon_asset, parent_folder: child) + }.to change { child.reload.descendant_assets_count }.by(1) + .and change { root.reload.descendant_assets_count }.by(1) + end + + it "decrements counts when asset is destroyed" do + asset = create(:isilon_asset, parent_folder: child) + + expect { + asset.destroy + }.to change { child.reload.descendant_assets_count }.by(-1) + .and change { root.reload.descendant_assets_count }.by(-1) + end + end end diff --git a/spec/requests/volumes/file_tree_spec.rb b/spec/requests/volumes/file_tree_spec.rb index d9574307..b353c6e7 100644 --- a/spec/requests/volumes/file_tree_spec.rb +++ b/spec/requests/volumes/file_tree_spec.rb @@ -60,6 +60,18 @@ def parsed ids = body.map { |h| h["id"] } expect(ids).to include(root.id) end + + let!(:folder) { create(:isilon_folder, volume: volume) } + + it "returns correct descendant_assets_count" do + create_list(:isilon_asset, 3, parent_folder: folder) + + get file_tree_volume_path(volume, format: :json) + + json = JSON.parse(response.body) + match = json.find { |h| h["id"] == folder.id } + expect(match["descendant_assets_count"]).to eq(3) + end end describe "GET /volumes/:id/file_tree_assets" do diff --git a/spec/serializers/isilon_folder_serializer_spec.rb b/spec/serializers/isilon_folder_serializer_spec.rb index 1c66f340..91100126 100644 --- a/spec/serializers/isilon_folder_serializer_spec.rb +++ b/spec/serializers/isilon_folder_serializer_spec.rb @@ -10,4 +10,10 @@ expect(serialized[:notes]).to eq("Serializer note") end + + it "includes descendant_assets_count for folders" do + folder = create(:isilon_folder, descendant_assets_count: 5) + json = IsilonFolderSerializer.new(folder).as_json + expect(json[:descendant_assets_count]).to eq(5) + end end From 7a7c75b13995203bdd3a147c354708bfce0bb9c8 Mon Sep 17 00:00:00 2001 From: Chris Doyle Date: Mon, 16 Feb 2026 12:25:30 -0500 Subject: [PATCH 2/2] adds post_ingest task to run 3 post_ingest tasks --- README.md | 5 +++-- db/schema.rb | 1 + lib/tasks/isilon_import.rake | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a18909ab..7e56c35e 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,11 @@ bundle exec rails runner "pwd = SecureRandom.alphanumeric(16); u = User.create!( bundle exec rails sync:assets[scan_output.applications-backup.csv] # Run these after full assets ingest is complete. -bundle exec rails sync:cleanup_folder_assets -bundle exec rails duplicates:detect +bundle exec rails sync:post_ingest ``` +`sync:post_ingest` runs cleanup_folder_assets, folders:backfill_counts, and duplicates:detect. + In some zsh shells with nomatch turned on, escaping the brackets in this command may be necessary. Alternatively, quote the entire task portion of the command, or add "setopt +o nomatch" to your ~/.zshrc profile to prevent zsh from requiring bracket escaping in Rails commands. diff --git a/db/schema.rb b/db/schema.rb index f13065e7..211bc999 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -97,6 +97,7 @@ t.datetime "updated_at", null: false t.integer "assigned_to_id" t.text "notes" + t.boolean "has_descendant_assets", default: false, null: false t.integer "descendant_assets_count", default: 0, null: false t.index ["assigned_to_id"], name: "index_isilon_folders_on_assigned_to_id" t.index ["descendant_assets_count"], name: "index_isilon_folders_on_descendant_assets_count" diff --git a/lib/tasks/isilon_import.rake b/lib/tasks/isilon_import.rake index 20747620..09efc58e 100644 --- a/lib/tasks/isilon_import.rake +++ b/lib/tasks/isilon_import.rake @@ -47,6 +47,26 @@ namespace :sync do puts "Deleted #{count} asset(s)" end + desc "Post-ingest housekeeping: cleanup folder-assets, backfill folder counts, detect duplicates" + task :post_ingest, [ :volume_name ] => :environment do |_t, args| + args.with_defaults(volume_name: nil) + + volume_name = args[:volume_name] + + puts "Running post-ingest housekeeping..." + + Rake::Task["sync:cleanup_folder_assets"].reenable + Rake::Task["sync:cleanup_folder_assets"].invoke(volume_name) + + Rake::Task["folders:backfill_counts"].reenable + Rake::Task["folders:backfill_counts"].invoke + + Rake::Task["duplicates:detect"].reenable + Rake::Task["duplicates:detect"].invoke + + puts "Post-ingest housekeeping complete." + end + desc "Export TIFF rule matches without updating migration_status" task :tiffs_export, [ :output_path, :volume_name ] => :environment do |_t, args| args.with_defaults(output_path: nil, volume_name: nil)