diff --git a/app/components/alchemy/admin/resource_table.rb b/app/components/alchemy/admin/resource_table.rb
new file mode 100644
index 0000000000..daff2014f4
--- /dev/null
+++ b/app/components/alchemy/admin/resource_table.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Alchemy
+ module Admin
+ class ResourceTable < ViewComponent::Base
+ include BaseHelper
+
+ attr_reader :columns, :collection, :nothing_found_label
+
+ erb_template <<~ERB
+ <% if collection.any? %>
+
+
+
+ <% columns.each do |column| %>
+ <%= column.label || column.name %> |
+ <% end %>
+
+
+
+ <% collection.each do |row| %>
+
+ <% columns.each do |column| %>
+
+ <%= view_context.capture(row, &column.block) %>
+ |
+ <% end %>
+
+ <% end %>
+
+
+ <% else %>
+
+ <%= render_icon('info') %>
+ <%= nothing_found_label %>
+
+ <% end %>
+ ERB
+
+ def initialize(collection, nothing_found_label: Alchemy.t("Nothing found"))
+ @collection = collection
+ @nothing_found_label = nothing_found_label
+ @columns = []
+ end
+
+ def add_column(name, label: nil, sortable: true, &block)
+ @columns << Column.new(name, label: label, sortable: sortable, &block)
+ end
+
+ private
+
+ ##
+ # the before_render - method is necessary to force ViewComponent to evaluate the add_column - calls
+ def before_render
+ content
+ end
+
+ class Column
+ attr_reader :block, :label, :name, :sortable
+
+ def initialize(name, sortable:, label: nil, &block)
+ @name = name
+ @label = label
+ @sortable = sortable
+ @block = block || lambda { |item| item[name] }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/components/alchemy/admin/resource_table_spec.rb b/spec/components/alchemy/admin/resource_table_spec.rb
new file mode 100644
index 0000000000..eb460ecfc0
--- /dev/null
+++ b/spec/components/alchemy/admin/resource_table_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe Alchemy::Admin::ResourceTable, type: :component do
+ let(:collection) { [] }
+ before do
+ render
+ end
+
+ subject(:render) do
+ render_inline(described_class.new(collection))
+ end
+
+ context "with data" do
+ let(:collection) {
+ [
+ {name: "Foo", description: "Awesome description"},
+ {name: "Bar", description: "Another description"}
+ ]
+ }
+
+ it "doesn't renders an info message" do
+ expect(page).to_not have_content("Nothing found")
+ end
+
+ context "columns without block" do
+ subject(:render) do
+ render_inline(described_class.new(collection)) do |component|
+ component.add_column(:name)
+ component.add_column(:description)
+ end
+ end
+
+ it "renders a table header" do
+ expect(page).to have_selector("table th", text: "name")
+ expect(page).to have_selector("table th", text: "description")
+ end
+
+ it "renders a table cell" do
+ expect(page).to have_selector("table td.name", text: "Foo")
+ expect(page).to have_selector("table td.description", text: "Awesome description")
+ end
+ end
+
+ context "columns with custom label" do
+ subject(:render) do
+ render_inline(described_class.new(collection)) do |component|
+ component.add_column(:name, label: "Awesome Name")
+ end
+ end
+
+ it "renders a table header with custom label" do
+ expect(page).to have_selector("table th", text: "Awesome Name")
+ end
+ end
+
+ context "columns with a custom block" do
+ subject(:render) do
+ render_inline(described_class.new(collection)) do |component|
+ component.add_column(:description) { |item| item[:description].truncate(10) }
+ end
+ end
+
+ it "renders a table cell with a custom block" do
+ expect(page).to have_selector("table td", text: "Awesome...")
+ end
+ end
+ end
+
+ context "without any data" do
+ it "renders an info message" do
+ expect(page).to have_content("Nothing found")
+ end
+
+ context "with another nothing found - label" do
+ subject(:render) do
+ render_inline(described_class.new(collection, nothing_found_label: "No user found"))
+ end
+
+ it "renders an info message" do
+ expect(page).to have_content("No user found")
+ end
+ end
+ end
+end