Skip to content

Add devise + experiment with turbo, ActionCable and combining these with Vue #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,5 @@ gem 'sidekiq', '~> 6.4'
gem 'vite_rails', '~> 3.0.10'

gem 'nokogiri', '>= 1.13.6'

gem "devise", "~> 4.9"
14 changes: 14 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ GEM
tzinfo (~> 2.0)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
bcrypt (3.1.19)
bootsnap (1.16.0)
msgpack (~> 1.2)
builder (3.2.4)
Expand All @@ -86,6 +87,12 @@ GEM
connection_pool (2.4.0)
crass (1.0.6)
date (3.3.3)
devise (4.9.2)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
diff-lcs (1.5.0)
dry-cli (1.0.0)
erubi (1.12.0)
Expand Down Expand Up @@ -137,6 +144,7 @@ GEM
racc (~> 1.4)
nokogiri (1.15.2-x86_64-darwin)
racc (~> 1.4)
orm_adapter (0.5.0)
playwright-ruby-client (1.35.0)
concurrent-ruby (>= 1.1.6)
mime-types (>= 3.0)
Expand Down Expand Up @@ -183,6 +191,9 @@ GEM
ffi (~> 1.0)
redis (4.8.1)
regexp_parser (2.7.0)
responders (3.1.0)
actionpack (>= 5.2)
railties (>= 5.2)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.3)
Expand Down Expand Up @@ -227,6 +238,8 @@ GEM
dry-cli (>= 0.7, < 2)
rack-proxy (~> 0.6, >= 0.6.1)
zeitwerk (~> 2.2)
warden (1.2.9)
rack (>= 2.0.9)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
Expand All @@ -244,6 +257,7 @@ DEPENDENCIES
byebug
capybara (>= 3.35)
chunky_png (~> 1.4)
devise (~> 4.9)
jbuilder (~> 2.7)
name_of_person (~> 1.1, >= 1.1.1)
nokogiri (>= 1.13.6)
Expand Down
1 change: 1 addition & 0 deletions Procfile.dev
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
web: bin/rails server -p 3000
vite: bundle exec bin/vite dev
redis: redis-server
15 changes: 15 additions & 0 deletions app/channels/application_cable/connection.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user

def connect
self.current_user = find_verified_user
end

private

def find_verified_user
if (verified_user = env['warden'].user)
verified_user
else
reject_unauthorized_connection
end
end
end
end
15 changes: 15 additions & 0 deletions app/channels/import_users_progress_channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class ImportUsersProgressChannel < ApplicationCable::Channel
def subscribed
# stream_from "some_channel"
# stream_from "import_user_#{param[:user_id]}_progress_channel"
stream_from 'import_users_progress_channel'
end

def unsubscribed
# Any cleanup needed when channel is unsubscribed
end

def get_progress
ActionCable.server.broadcast 'import_users_progress_channel', { progress: 0 }
end
end
25 changes: 25 additions & 0 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class UsersController < ApplicationController
before_action :authenticate_user!

def index
# if Rails.cache.read('users/import_progress') != "in_progress"
# ImportContactsJob.perform_later
# end

# Rails.cache.write('users/import_progress', "in_progress")
#
@data = {
users: User.all
}
end

def csv_import_modal; end

def import_from_csv
@import_result = ::Users::CsvUploadParser.new(params[:users_csv].read).import_users_from_csv
end

def show
@user = User.find(params[:id])
end
end
3 changes: 3 additions & 0 deletions app/frontend/channels/consumer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createConsumer } from "@rails/actioncable";

export default createConsumer();
5 changes: 5 additions & 0 deletions app/frontend/entrypoints/application.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

dialog::backdrop {
backdrop-filter: blur(10px);
transition: backdrop-filter .5s ease;
}
17 changes: 15 additions & 2 deletions app/frontend/entrypoints/turbo-vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { createApp, type App, type Component } from "vue";

let components: App[] = [];

const onTurboLoad = async (e: Event) => {
await mountApp(e);

document.addEventListener("turbo:frame-render", openModal);
};

const mountApp = async (e: Event) => {
const vueComponentsForPage = getVueComponents(window.location.pathname);
let app: App;
Expand All @@ -26,7 +32,6 @@ const mountApp = async (e: Event) => {
if (component !== undefined) {
component[1]()
.then((c: Component) => {
console.debug(c);
props = rootContainer.dataset.props;
app = createApp(c, props ? JSON.parse(props) : undefined);
components.push(app);
Expand Down Expand Up @@ -70,7 +75,7 @@ const mountApp = async (e: Event) => {
}
};

document.addEventListener("turbo:load", mountApp);
document.addEventListener("turbo:load", onTurboLoad);

document.addEventListener("turbo:visit", () => {
if (components.length > 0) {
Expand All @@ -80,8 +85,16 @@ document.addEventListener("turbo:visit", () => {

components = [];
}

document.removeEventListener("turbo:frame-render", openModal);
});

const openModal = (e: Event) => {
if (typeof e.target === "object" && e.target instanceof Element) {
e.target.querySelector("dialog")?.showModal();
}
};

function clearInitialPropsFromDOM(element: HTMLElement) {
element.removeAttribute("data-props");
}
Expand Down
73 changes: 73 additions & 0 deletions app/frontend/entrypoints/views/users/Index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from "vue";
import { type Subscription } from "@rails/actioncable";
import consumer from "@/channels/consumer.ts";

type User = {
id: number;
email: string;
};

defineProps({
users: {
type: Array as () => User[],
required: true,
},
});

const progress = ref(null);
let subscription: Subscription | null = null;

const createImportChannelSubscription = () => {
return consumer.subscriptions.create("ImportUsersProgressChannel", {
connected() {
console.log("connected");
},
disconnected() {
console.log("disconnected");
},
received(data) {
progress.value = data?.progress;
},
});
};

onMounted(() => {
subscription = createImportChannelSubscription();
});

onBeforeUnmount(() => {
subscription?.unsubscribe();
});
</script>

<template>
<section class="w-full">
<header class="flex justify-between">
<h1 class="text-3xl">Users</h1>

<a
href="/users/csv_import_modal"
class="bg-purple-500 hover:bg-purple-600 text-white py-1.5 px-3 font-medium whitespace-nowrap rounded"
role="button"
data-turbo-frame="modal"
>
Import Users from CSV
</a>
</header>


<article class="mt-8">
<ol v-if="users.length > 0" class="list-decimal list-inside">
<li class="list-item" v-for="user in users" :key="user.id">
<a
class="text-purple-600 hover:text-purple-800 dark:hover:text-purple-300"
:href="`/users/${user.id}`"
>{{ user.email }}</a
>
</li>
</ol>
<div v-else>No users found</div>
</article>
</section>
</template>
4 changes: 4 additions & 0 deletions app/frontend/helpers/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ const Zap = async () =>
const PandasApp = async () =>
(await import("@/entrypoints/views/pandas/index/App.vue")).default;

const UsersIndex = async () =>
(await import("@/entrypoints/views/users/Index.vue")).default;

const routes = {
"/users": [["#vue-root", UsersIndex]],
"/": [["#root-view", RootApp]],
"/pandas": [
["#pandas-view", PandasApp],
Expand Down
2 changes: 2 additions & 0 deletions app/helpers/users_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module UsersHelper
end
12 changes: 12 additions & 0 deletions app/jobs/import_contacts_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class ImportContactsJob < ApplicationJob
queue_as :default

def perform(*_args)
100.times do |i|
ActionCable.server.broadcast 'import_users_progress_channel', { progress: i + 1 }
sleep 0.1
end

Rails.cache.delete('users/import_progress')
end
end
6 changes: 6 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
end
60 changes: 60 additions & 0 deletions app/services/users/csv_upload_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
require 'csv'

module Users

class CsvUploadParser
def initialize(csv_string)
@csv_string = csv_string
end

def import_users_from_csv
errors = {}
successfully_imported_count = 0
row_index = 1
header_rows_count = 1

total = @csv_string.lines.count - header_rows_count

CSV.parse(@csv_string, headers: true) do |row|
user = User.create(
email: row["Email"],
password: row["Password"],
password_confirmation: row["Password"]
)

if user.valid?
user.save
successfully_imported_count += 1
else
errors["Row #{row_index}"] = user.errors.objects.first.full_message
end

row_index += 1
processed_count = row_index - header_rows_count

# ActionCable.server.broadcast 'import_users_progress_channel', { pct_complete: percentage_complete(processed_count, total) }
Turbo::StreamsChannel.broadcast_replace_later_to(
:users,
target: "users_progress_bar",
partial: "users/progress_bar",
locals: {
processed_count: processed_count,
total: total,
percentage_complete: percentage_complete(processed_count, total)
}
)
end

{
errors: errors,
successfully_imported_count: successfully_imported_count
}
end

private

def percentage_complete(row_index, total)
((row_index.to_f / total.to_f) * 100).round(2)
end
end
end
4 changes: 3 additions & 1 deletion app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
<body class="font-sans font-normal leading-normal text-slate-800 dark:text-gray-50">
<%= render "shared/header" %>

<main class="px-4 lg:px-10 mt-10 max-w-screen-md mx-auto">
<turbo-frame id="modal"> </turbo-frame>

<main class="px-4 lg:px-10 mt-10 max-w-screen-lg mx-auto">
<%= content_for?(:content) ? yield(:content) : yield %>
</main>
</body>
Expand Down
3 changes: 3 additions & 0 deletions app/views/shared/_github_icon_link.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<a href="https://github.com/HonuLife/rails-turbo-vue-experiments.git" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 496 512" class="fill-black hover:fill-purple-600 dark:fill-white dark:hover:fill-purple-300"><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg>
</a>
12 changes: 11 additions & 1 deletion app/views/shared/_header.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
<nav class="flex flex-wrap items-center justify-between px-3 py-3 bg-gray-100 lg:px-10 dark:bg-slate-900">
<%= link_to "Rails + Vue", root_path, class: "text-2xl font-bold text-gray-900 dark:text-green-600 dark:hover:text-green-200" %>

<%= link_to "Pandas", pandas_path, class: "text-xl font-bold text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200" %>
<div class="flex gap-3 items-center">
<%= link_to "Users", users_path, class: "text-xl font-bold text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200" %>
<%= link_to "Pandas", pandas_path, class: "text-xl font-bold text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200" %>
<%= render "shared/github_icon_link" %>

<% if current_user %>
<%= link_to "Logout", destroy_user_session_path, data: { turbo_method: :delete }, class: "text-sm font-bold text-primary hover:text-purple-600" %>
<% else %>
<%= link_to "Login", new_user_session_path, class: "text-xl font-bold hover:text-blue-300" %>
<% end %>
</div>
</nav>
</header>
Loading