From 4a3f7d5e6535ab0a24732dc0e36b93269ef0acdc Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 4 Nov 2024 13:06:04 +0100 Subject: [PATCH] Implement live points count update for imports --- app/channels/imports_channel.rb | 7 +++ app/javascript/channels/imports_channel.js | 15 ++++++ app/javascript/channels/index.js | 1 + .../controllers/imports_controller.js | 51 +++++++++++++++++++ app/services/geojson/import_parser.rb | 6 ++- app/services/gpx/track_parser.rb | 14 +++-- app/services/imports/broadcaster.rb | 16 ++++++ app/views/imports/index.html.erb | 19 ++++--- config/importmap.rb | 1 + spec/channels/imports_channel_spec.rb | 5 ++ spec/services/gpx/track_parser_spec.rb | 18 +++++++ 11 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 app/channels/imports_channel.rb create mode 100644 app/javascript/channels/imports_channel.js create mode 100644 app/javascript/controllers/imports_controller.js create mode 100644 app/services/imports/broadcaster.rb create mode 100644 spec/channels/imports_channel_spec.rb diff --git a/app/channels/imports_channel.rb b/app/channels/imports_channel.rb new file mode 100644 index 00000000..fed59af9 --- /dev/null +++ b/app/channels/imports_channel.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ImportsChannel < ApplicationCable::Channel + def subscribed + stream_for current_user + end +end diff --git a/app/javascript/channels/imports_channel.js b/app/javascript/channels/imports_channel.js new file mode 100644 index 00000000..45749452 --- /dev/null +++ b/app/javascript/channels/imports_channel.js @@ -0,0 +1,15 @@ +import consumer from "./consumer" + +consumer.subscriptions.create("ImportsChannel", { + connected() { + console.log("Connected to the imports channel!"); + }, + + disconnected() { + // Called when the subscription has been terminated by the server + }, + + received(data) { + // Called when there's incoming data on the websocket for this channel + } +}); diff --git a/app/javascript/channels/index.js b/app/javascript/channels/index.js index ed929ca4..0c2237ee 100644 --- a/app/javascript/channels/index.js +++ b/app/javascript/channels/index.js @@ -1,3 +1,4 @@ // Import all the channels to be used by Action Cable import "notifications_channel" import "points_channel" +import "imports_channel" diff --git a/app/javascript/controllers/imports_controller.js b/app/javascript/controllers/imports_controller.js new file mode 100644 index 00000000..fe302ac7 --- /dev/null +++ b/app/javascript/controllers/imports_controller.js @@ -0,0 +1,51 @@ +import { Controller } from "@hotwired/stimulus"; +import consumer from "../channels/consumer"; + +export default class extends Controller { + static targets = ["index"]; + + connect() { + console.log("Imports controller connected", { + hasIndexTarget: this.hasIndexTarget, + element: this.element, + userId: this.element.dataset.userId + }); + this.setupSubscription(); + } + + setupSubscription() { + const userId = this.element.dataset.userId; + console.log("Setting up subscription with userId:", userId); + + this.channel = consumer.subscriptions.create( + { channel: "ImportsChannel" }, + { + connected: () => { + console.log("Successfully connected to ImportsChannel"); + // Test that we can receive messages + console.log("Subscription object:", this.channel); + }, + disconnected: () => { + console.log("Disconnected from ImportsChannel"); + }, + received: (data) => { + console.log("Received data:", data); + const row = this.element.querySelector(`tr[data-import-id="${data.import.id}"]`); + + if (row) { + const pointsCell = row.querySelector('[data-points-count]'); + if (pointsCell) { + pointsCell.textContent = new Intl.NumberFormat().format(data.import.points_count); + } + } + } + } + ); + } + + disconnect() { + if (this.channel) { + this.channel.unsubscribe(); + } + } +} diff --git a/app/services/geojson/import_parser.rb b/app/services/geojson/import_parser.rb index ba3d333f..ff78e6f6 100644 --- a/app/services/geojson/import_parser.rb +++ b/app/services/geojson/import_parser.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Geojson::ImportParser + include Imports::Broadcaster + attr_reader :import, :json, :user_id def initialize(import, user_id) @@ -12,10 +14,12 @@ class Geojson::ImportParser def call data = Geojson::Params.new(json).call - data.each do |point| + data.each.with_index(1) do |point, index| next if point_exists?(point, user_id) Point.create!(point.merge(user_id:, import_id: import.id)) + + broadcast_import_progress(import, index) end end diff --git a/app/services/gpx/track_parser.rb b/app/services/gpx/track_parser.rb index 3fde5a4b..27cf1ae2 100644 --- a/app/services/gpx/track_parser.rb +++ b/app/services/gpx/track_parser.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Gpx::TrackParser + include Imports::Broadcaster + attr_reader :import, :json, :user_id def initialize(import, user_id) @@ -13,7 +15,9 @@ class Gpx::TrackParser tracks = json['gpx']['trk'] tracks_arr = tracks.is_a?(Array) ? tracks : [tracks] - tracks_arr.map { parse_track(_1) }.flatten + tracks_arr.map { parse_track(_1) }.flatten.each.with_index(1) do |point, index| + create_point(point, index) + end end private @@ -22,12 +26,10 @@ class Gpx::TrackParser segments = track['trkseg'] segments_array = segments.is_a?(Array) ? segments : [segments] - segments_array.map do |segment| - segment['trkpt'].each { create_point(_1) } - end + segments_array.map { |segment| segment['trkpt'] } end - def create_point(point) + def create_point(point, index) return if point['lat'].blank? || point['lon'].blank? || point['time'].blank? return if point_exists?(point) @@ -40,6 +42,8 @@ class Gpx::TrackParser raw_data: point, user_id: ) + + broadcast_import_progress(import, index) end def point_exists?(point) diff --git a/app/services/imports/broadcaster.rb b/app/services/imports/broadcaster.rb new file mode 100644 index 00000000..1c7f54bb --- /dev/null +++ b/app/services/imports/broadcaster.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Imports::Broadcaster + def broadcast_import_progress(import, index) + ImportsChannel.broadcast_to( + import.user, + { + action: 'update', + import: { + id: import.id, + points_count: index + } + } + ) + end +end diff --git a/app/views/imports/index.html.erb b/app/views/imports/index.html.erb index d74c6182..134a8f26 100644 --- a/app/views/imports/index.html.erb +++ b/app/views/imports/index.html.erb @@ -39,18 +39,23 @@ Created at - + <% @imports.each do |import| %> - + - <%= link_to import.name, import, class: 'underline hover:no-underline' %> (<%= import.source %>) -   + <%= link_to import.name, import, class: 'underline hover:no-underline' %> + (<%= import.source %>) +   <%= link_to 'πŸ—ΊοΈ', map_path(import_id: import.id) %> -   +   <%= link_to 'πŸ“‹', points_path(import_id: import.id) %> - - <%= "#{number_with_delimiter import.points_count}" %> + + <%= number_with_delimiter import.points_count %> <%= import.created_at.strftime("%d.%m.%Y, %H:%M") %> diff --git a/config/importmap.rb b/config/importmap.rb index 4832b78f..a389cb18 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -21,3 +21,4 @@ pin '@rails/actioncable', to: 'actioncable.esm.js' pin_all_from 'app/javascript/channels', under: 'channels' pin 'notifications_channel', to: 'channels/notifications_channel.js' pin 'points_channel', to: 'channels/points_channel.js' +pin 'imports_channel', to: 'channels/imports_channel.js' diff --git a/spec/channels/imports_channel_spec.rb b/spec/channels/imports_channel_spec.rb new file mode 100644 index 00000000..061152e0 --- /dev/null +++ b/spec/channels/imports_channel_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe ImportsChannel, type: :channel do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/services/gpx/track_parser_spec.rb b/spec/services/gpx/track_parser_spec.rb index b9282f84..9e9a47c1 100644 --- a/spec/services/gpx/track_parser_spec.rb +++ b/spec/services/gpx/track_parser_spec.rb @@ -16,6 +16,12 @@ RSpec.describe Gpx::TrackParser do it 'creates points' do expect { parser }.to change { Point.count }.by(301) end + + it 'broadcasts importing progress' do + expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(301).times + + parser + end end context 'when file has multiple segments' do @@ -24,6 +30,12 @@ RSpec.describe Gpx::TrackParser do it 'creates points' do expect { parser }.to change { Point.count }.by(558) end + + it 'broadcasts importing progress' do + expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(558).times + + parser + end end end @@ -33,6 +45,12 @@ RSpec.describe Gpx::TrackParser do it 'creates points' do expect { parser }.to change { Point.count }.by(407) end + + it 'broadcasts importing progress' do + expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).exactly(407).times + + parser + end end end end