Skip to content

Instantly share code, notes, and snippets.

@usutani
Last active September 26, 2021 08:14
Show Gist options
  • Save usutani/cac74984ac386c45fcaa455770c8efdf to your computer and use it in GitHub Desktop.
Save usutani/cac74984ac386c45fcaa455770c8efdf to your computer and use it in GitHub Desktop.
Hotwire: Rails: バックグラウンドで処理したファイルをダウンロードする時のUI
<%= tag.li id: convert.id do %>
<%= tag.span convert.status_summary %>
<%= tag.span do %>
<% out_file_path = rails_blob_path(convert.out_file) if convert.out_file.attached? %>
<%= link_to_if(convert.succeeded?, 'Download', out_file_path) { "N/A" } %>
<% end %>
<%= tag.span convert.message %>
<% end %>
<%= form_with(model: convert) do |form| %>
<% if convert.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(convert.errors.count, "error") %> prohibited this convert from being saved:</h2>
<ul>
<% convert.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :in_file %>
<%= form.file_field :in_file %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
class Convert < ApplicationRecord
RELOAD_INTERVAL_MARGIN = 2.seconds
enum status: {
waiting: 0,
validating: 1,
converting: 2,
succeeded: 3,
failed: 4
}
has_one_attached :in_file
has_one_attached :out_file
validates :in_file, presence: true
scope :running, -> { where(status: [:waiting, :validating, :converting]) }
scope :need_to_notify, -> { where('updated_at >= ?', Time.zone.now - RELOAD_INTERVAL_MARGIN) }
def self.need_to_reload?
running.exists?
end
def status_summary
"#{id}:#{status}"
end
end
class ConvertsController < ApplicationController
before_action :set_convert, only: [:show, :edit, :update, :destroy]
# GET /converts
def index
@need_to_reload = Convert.need_to_reload?
@convert = Convert.new
@converts = Convert.all
end
# GET /converts/1
def show
end
# GET /converts/new
def new
@convert = Convert.new
end
# GET /converts/1/edit
def edit
end
# POST /converts
def create
@convert = Convert.new(convert_params)
if @convert.save
FileConvertJob.perform_later(@convert)
@new_convert = Convert.new
@notice = 'Convert was successfully created.'
else
@need_to_reload = Convert.need_to_reload?
@converts = Convert.all
render :index, status: :unprocessable_entity
end
end
# PATCH/PUT /converts/1
def update
if @convert.update(convert_params)
redirect_to @convert, notice: 'Convert was successfully updated.'
else
render :edit
end
end
# DELETE /converts/1
def destroy
@convert.destroy
redirect_to converts_url, notice: 'Convert was successfully destroyed.'
end
private
# Use callbacks to share common setup or constraints between actions.
def set_convert
@convert = Convert.find(params[:id])
end
# Only allow a list of trusted parameters through.
def convert_params
params.require(:convert).permit(:in_file)
rescue ActionController::ParameterMissing
nil
end
end
<%= turbo_stream.update 'notice', @notice %>
<%= turbo_stream.update 'reload', 'true' %>
<%= turbo_stream.replace 'new_convert' do %>
<div id="new_convert">
<%= render 'form', convert: @new_convert %>
</div>
<% end %>
<%= turbo_stream.append 'converts' do %>
<%= render 'converts/convert', convert: @convert %>
<% end %>
class FileConvertJob < ApplicationJob
queue_as :default
def perform(convert)
unless convert.waiting?
logger.info "Convert #{@convert.id} is already performed."
return
end
unless pseudo_validate_input_file(convert)
message = 'Failed to validate the input file.'
logger.info message
convert.update(status: :failed, message: message)
return
end
pseudo_convert_file(convert)
end
def pseudo_validate_input_file(convert)
convert.update(status: :validating)
logger.info convert.status
sleep 5
true
end
def pseudo_convert_file(convert)
convert.update(status: :converting)
logger.info convert.status
attach_out_file(convert)
sleep 5
message = "The process was successful. #{Time.zone.now.iso8601}"
convert.update(status: :succeeded, message: message)
logger.info convert.status
end
def attach_out_file(convert)
convert.in_file.open(tmpdir: Dir.tmpdir) do |file|
convert.out_file.attach(io: File.open(file.path), filename: 'out_file.csv')
end
end
end
<p id="notice"><%= notice %></p>
<%= tag.div @need_to_reload.to_s, id: 'reload', hidden: false %>
<div hidden
data-controller="reload"
data-reload-url-value="/converts/status"
data-reload-interval-value="1000"
data-action="turbo:submit-end@document->reload#startReloading"></div>
<h1>New Convert</h1>
<div id="new_convert">
<%= render 'form', convert: @convert %>
</div>
<h1>Converts</h1>
<ul>
<div id="converts">
<%= render @converts %>
</div>
</ul>
<% @converts.each do |convert| %>
<%= turbo_stream.replace convert.id do %>
<%= render 'converts/convert', convert: convert %>
<% end %>
<% end %>
<%= turbo_stream.update "reload", @need_to_reload.to_s %>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = {
url: String,
interval: Number
}
initialize() {
this.intervalId = 0
}
connect() {
this.startReloading()
}
disconnect() {
this.stopReloading()
}
startReloading() {
if (this.intervalId !== 0) {
return
}
this.intervalId = setInterval(() => {
if (this.canReload()) {
this.updateElements()
} else {
this.stopReloading()
}
}, this.intervalValue);
}
updateElements() {
fetch(this.urlValue, { headers: { 'Accept': 'text/vnd.turbo-stream.html' } })
.then(response => response.text())
.then(message => Turbo.renderStreamMessage(message))
.catch (() => this.stopReloading())
}
canReload() {
const reload = document.getElementById('reload').textContent
return (reload === 'true')
}
stopReloading() {
if (this.intervalId !== 0) {
clearInterval(this.intervalId)
this.intervalId = 0
}
}
}
Rails.application.routes.draw do
namespace :converts do
resources :status, only: :index
end
resources :converts
end
class Converts::StatusController < ApplicationController
def index
@need_to_reload = Convert.need_to_reload?
@converts = Convert.running.or(Convert.need_to_notify)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment