Skip to content

Instantly share code, notes, and snippets.

@julianrubisch
Last active May 13, 2021 15:25
Show Gist options
  • Save julianrubisch/e8c10576a368213a4b6713e89ff66eb3 to your computer and use it in GitHub Desktop.
Save julianrubisch/e8c10576a368213a4b6713e89ff66eb3 to your computer and use it in GitHub Desktop.
StimulusReflex Patterns - 2 Imports
<!-- app/views/embed_imports/_form.html.erb -->
<%= form_with(model: [board, embed_import]) do |form| %>
<%= form.file_field :upload %>
<%= form.submit "Upload Links", class: "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-lime-600 hover:bg-lime-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-lime-500" %>
<% end %>
# app/models/embed_import.rb
class EmbedImport < ApplicationRecord
has_one_attached :upload
belongs_to :board
after_create_commit :start
private
def start
return if finished_at
ImportJob.perform_later self
end
end
# app/models/embed_import.rb
class EmbedImport < ApplicationRecord
# ...
# When given a block, yields progress (value between 0 and 100) and embed
# for each row of imported CSV file.
def process(&block)
yield 0, nil if block_given?
upload.open do |file|
# don't do this if your file is large, i.e. in production!
# use this: size = upload.byte_size
line_count = file.readlines.size - 1
file.rewind
yield 20, nil # arbitrary; download to local disk done
import_csv(file) do |embed, index|
next unless block
# simulate a slow running process
sleep 1
# file.pos will not change per row but per chunk of data read from the file
# use in connection with size: file.pos.to_d / size
progress = ((index.to_d / line_count) * 75).floor + 20
block.call(progress, embed)
end
end
update! finished_at: Time.now
yield 100, nil if block_given?
end
private
# ...
def import_csv(io)
CSV.new(io, headers: true).each.with_index do |row, index|
embed = import(row)
yield embed, index if block_given?
end
end
def import(params)
Embed.create! params.to_h.merge(board_id: board_id)
end
end
# app/jobs/import_job.rb
class ImportJob < ApplicationJob
include CableReady::Broadcaster
queue_as :default
def perform(import)
cable_ready["import"].dispatch_event(
name: "import:start"
).broadcast
import.process do |progress, imported|
cable_ready["import"].dispatch_event(
name: "import:progress",
detail: {progress: progress, imported: imported}
).broadcast
yield progress, imported
end
end
end
# app/jobs/embed_import_job.rb
class EmbedImportJob < ImportJob
def perform(import)
super do |progress, imported|
if imported.present?
StreamBoardJob.perform_now(board: imported.board)
end
end
end
end
# app/models/embed_import.rb
class EmbedImport < ApplicationRecord
# ...
private
def start
return if finished_at
EmbedImportJob.perform_later self
end
# ...
end
<div class="hidden">
<div class="relative pt-1">
<div class="overflow-hidden h-2 mb-4 text-xs flex rounded bg-orange-200">
<div id="progress-bar" style="width:0%" class="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-orange-500 transition-all ease-in-out"></div>
</div>
</div>
</div>
<div data-controller="import" data-action="import:progress@document->import#onProgress" class="hidden">
<div class="relative pt-1">
<div class="overflow-hidden h-2 mb-4 text-xs flex rounded bg-orange-200">
<div style="width:0%" data-import-target="progressBar" class="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-orange-500 transition-all ease-in-out"></div>
</div>
</div>
</div>
# app/controllers/embed_imports_controller.rb
class EmbedImportsController < ApplicationController
def create
@embed_import = EmbedImport.new(embed_imports_params)
@embed_import.board = Board.friendly.find(params[:board_id])
@embed_import.save
end
private
def embed_imports_params
params.require(:embed_import).permit(:upload)
end
end
# app/models/embed_import.rb
class EmbedImport < ApplicationRecord
# ...
# When given a block, yields progress (value between 0 and 100) and embed
# for each row of imported CSV file.
def process(&block)
yield 0, nil if block_given?
upload.open do |file|
# don't do this if your file is large, i.e. in production!
# use this: size = upload.byte_size
line_count = file.readlines.size - 1
file.rewind
yield 20, nil # arbitrary; download to local disk done
import_csv(file) do |embed, index|
next unless block
# simulate a slow running process
sleep 1
# file.pos will not change per row but per chunk of data read from the file
# use in connection with size: file.pos.to_d / size
progress = ((index.to_d / line_count) * 75).floor + 20
block.call(progress, embed)
end
end
update! finished_at: Time.now
yield 100, nil if block_given?
end
private
# ...
def import_csv(io)
CSV.new(io, headers: true).each.with_index do |row, index|
embed = import(row)
yield embed, index if block_given?
end
end
def import(params)
Embed.create! params.to_h.merge(board_id: board_id)
end
end
$ bin/rails g migration AddUserReferenceToEmbedImports user:references
# app/channels/import_channel.rb
class ImportChannel < ApplicationCable::Channel
def subscribed
stream_from "import_#{current_user.id}"
end
end
// app/javascript/controllers/import_controller.js
import { Controller } from "stimulus";
export default class extends Controller {
static targets = ["progressBar"];
onProgress(e) {
if (e.detail.progress > 0) {
this.element.classList.remove("hidden");
}
this.progressBarTarget.setAttribute(
"style",
`width: ${e.detail.progress}%`
);
if (e.detail.progress === 100) {
this.element.classList.add("hidden");
}
}
}
class ImportJob < ApplicationJob
include CableReady::Broadcaster
queue_as :default
def perform(import)
import.process do |progress, imported|
# TODO
end
end
end
class ImportJob < ApplicationJob
include CableReady::Broadcaster
queue_as :default
def perform(import)
import.process do |progress, imported|
cable_ready["import"].set_style( # <-- update style here
name: "width",
selector: "#progress-bar",
value: progress
).broadcast
end
end
end
class ImportJob < ApplicationJob
include CableReady::Broadcaster
queue_as :default
def perform(import)
cable_ready["import"].dispatch_event(
name: "import:start"
).broadcast
import.process do |progress, imported|
cable_ready["import"].dispatch_event(
name: "import:progress",
detail: {progress: progress, imported: imported}
).broadcast
if imported.present?
StreamBoardJob.perform_now(board: imported.board)
end
end
end
end
# app/jobs/import_job.rb
class ImportJob < ApplicationJob
include CableReady::Broadcaster
queue_as :default
def perform(import)
cable_ready["import_#{import.user_id}"].dispatch_event(
name: "import:start"
).broadcast
import.process do |progress, imported|
cable_ready["import_#{import.user_id}"].dispatch_event(
name: "import:progress",
detail: {progress: progress, imported: imported}
).broadcast
yield progress, imported
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment