Rails app blueprint

Jumpstart Pro

Add Avo 3 to your Jumpstart Pro app blazing fast!

We are migrating from ActiveAdmin to Avo and both in terms of Developer experience and User experience, it is a game changer!

Adrien Poly
Adrien Poly
CTO, Plume
Run the following command to quickly apply this app blueprint

If you think this blueprint needs changes please submit a PR here.

Full blueprint

# Blueprint info:
#   Link: https://avohq.io/blueprints/jumpstart-pro
#   Repository: https://github.com/avo-hq/jumpstart-pro-app-template

# List of all Avo files with path and contents
files = {"app/avo/cards/active_subscriptions.rb"=>"class Avo::Cards::ActiveSubscriptions < Avo::Cards::MetricCard\n  self.id = \"active_subscriptions\"\n  self.label = \"Active subscriptions\"\n  self.description = \"Total number of active subscriptions\"\n\n  def query\n    result ::Pay::Subscription.active.count\n  end\nend\n", "app/avo/cards/total_revenue.rb"=>"class Avo::Cards::TotalRevenue < Avo::Cards::MetricCard\n  self.id = \"total_revenue\"\n  self.label = \"Total revenue\"\n  self.prefix = \"$\"\n\n  def query\n    case arguments[:period]\n    when :last_12_months\n      result last_12_months\n    when :last_month\n      result last_month\n    when :this_month\n      result this_month\n    else\n      result total_revenue\n    end\n  end\n\n  def total_revenue\n    revenue_in_cents = ::Pay::Charge.sum(:amount)\n    refunds_in_cents = ::Pay::Charge.sum(:amount_refunded)\n    (revenue_in_cents - refunds_in_cents) / 100.0\n  end\n\n  def last_12_months\n    revenue_for_range 12.months.ago..Time.current\n  end\n\n  def last_month\n    month = Time.current.prev_month\n    revenue_for_range month.beginning_of_month..month.end_of_month\n  end\n\n  def this_month\n    month = Time.current\n    revenue_for_range month.beginning_of_month..month.end_of_month\n  end\n\n  def revenue_for_range(range)\n    revenue_in_cents = ::Pay::Charge.where(created_at: range).sum(:amount)\n    refunds_in_cents = ::Pay::Charge.where(created_at: range).sum(:amount_refunded)\n    (revenue_in_cents - refunds_in_cents) / 100.0\n  end\nend\n", "app/avo/cards/users_count.rb"=>"class Avo::Cards::UsersCount < Avo::Cards::MetricCard\n  self.id = \"users_count\"\n  self.label = \"Users count\"\n  self.description = \"Total number of users registered\"\n\n  def query\n    result ::User.all.count\n  end\nend\n", "app/avo/dashboards/overview.rb"=>"class Avo::Dashboards::Overview < Avo::Dashboards::BaseDashboard\n  self.id = \"overview\"\n  self.name = \"Overview\"\n  self.grid_cols = 4\n\n  def cards\n    divider label: \"Revenue\"\n    card Avo::Cards::TotalRevenue,\n      arguments: {\n        period: :this_month\n      },\n      label: \"This month\"\n    card Avo::Cards::TotalRevenue,\n      arguments: {\n        period: :last_month\n      },\n      label: \"Last month\"\n    card Avo::Cards::TotalRevenue,\n      arguments: {\n        period: :last_12_months\n      },\n      label: \"Last 12 months\"\n    card Avo::Cards::TotalRevenue\n\n    divider\n\n    card Avo::Cards::UsersCount\n    card Avo::Cards::ActiveSubscriptions\n  end\nend\n", "app/avo/resources/account.rb"=>"class Avo::Resources::Account < Avo::BaseResource\n  self.title = :name\n  self.includes = [:owner, :users]\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], name_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.name,\n        image_url: record.avatar\n      }\n    }\n  }\n\n  def fields\n    main_panel do\n      field :id, as: :id\n      field :avatar,\n        as: :file,\n        is_image: true,\n        link_to_resource: true,\n        rounded: true,\n        hide_on: :forms\n      field :owner, as: :belongs_to\n      field :name, as: :text\n      field :personal, as: :boolean\n      field :users_count, as: :text do\n        record.users.length\n      end\n\n      sidebar do\n        field :extra_billing_info, as: :textarea\n        field :created_at, as: :date_time\n        field :updated_at, as: :date_time\n      end\n    end\n\n    tabs do\n      field :subscriptions, as: :has_many\n      field :pay_customers, as: :has_many\n      field :charges, as: :has_many\n      field :account_users, as: :has_many\n      field :users, as: :has_many\n    end\n  end\nend\n", "app/avo/resources/account_user.rb"=>"class Avo::Resources::AccountUser < Avo::BaseResource\n  self.title = :id\n  self.includes = []\n\n  def fields\n    main_panel do\n      field :id, as: :id\n      field :account, as: :belongs_to\n      field :user, as: :belongs_to\n      field :roles,\n        as: :boolean_group,\n        options: {\n          admin: \"Administrator\",\n          member: \"Member\",\n        }\n      field :admin?, as: :boolean do\n        record.roles[\"admin\"] == true\n      end\n\n      sidebar do\n        field :created_at, as: :date_time\n        field :updated_at, as: :date_time\n      end\n    end\n  end\nend\n", "app/avo/resources/announcement.rb"=>"class Avo::Resources::Announcement < Avo::BaseResource\n  self.title = :title\n  self.includes = []\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], title_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.title,\n      }\n    }\n  }\n\n  def fields\n    field :id, as: :id\n    field :title, as: :text\n    field :description, as: :trix, always_show: true\n    field :kind, as: :badge, options: {success: \"new\", info: \"improvement\", danger: \"fix\", warning: \"update\"}, hide_on: :forms\n    field :kind, as: :select, options: Announcement::TYPES.map { |t| [t, t] }.to_h, show_on: :forms\n    field :published_at, as: :date_time\n  end\nend\n", "app/avo/resources/charge.rb"=>"class Avo::Resources::Charge < Avo::BaseResource\n  self.title = :processor_id\n  self.includes = []\n  self.model_class = Pay::Charge\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], processor_id_cont: params[:q], m: \"or\").result(distinct: false)},\n    item: -> {\n      {\n        title: record.processor_id\n      }\n    }\n  }\n\n  def fields\n    field :id, as: :id\n    field :customer, as: :belongs_to\n    field :amount, as: :text\n    field :amount_refunded, as: :text\n    field :created_at, as: :date_time\n\n    with_options only_on: :show do\n      field :processor_id, as: :text, format_using: -> do\n        link_to value, view_context.controller.charge_processor_url(model), target: :_blank\n      rescue\n        value\n      end\n      field :subscription, as: :belongs_to\n      field :updated_at, as: :date_time\n    end\n  end\nend\n", "app/avo/resources/connected_account.rb"=>"class Avo::Resources::ConnectedAccount < Avo::BaseResource\n  self.title = :id\n  self.includes = []\n  self.model_class = ConnectedAccount\n\n  def fields\n    field :id, as: :id\n    field :user, as: :belongs_to\n    field :image_url, as: :external_image\n    field :expired?, as: :boolean, hide_on: :forms\n  end\nend\n", "app/avo/resources/customer.rb"=>"class Avo::Resources::Customer < Avo::BaseResource\n  self.title = :name\n  self.includes = [:owner]\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], processor_id_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.name,\n      }\n    }\n  }\n  self.model_class = Pay::Customer\n\n  def fields\n    main_panel do\n      field :id, as: :id\n      field :owner, as: :belongs_to, polymorphic_as: :owner, types: [::Account]\n\n      field :processor, as: :badge, options: {success: \"stripe\"}\n      field :processor_id, as: :text, format_using: -> do\n        link_to(value, view_context.controller.customer_processor_url(model), target: :_blank)\n      rescue\n        value\n      end\n      field :default, as: :boolean\n      field :data, as: :key_value\n\n      sidebar do\n        field :created_at, as: :date_time\n        field :updated_at, as: :date_time\n        field :deleted_at, as: :date_time\n      end\n    end\n\n    tabs do\n      field :subscriptions, as: :has_many\n      field :charges, as: :has_many\n    end\n  end\nend\n", "app/avo/resources/payment_method.rb"=>"class Avo::Resources::PaymentMethod < Avo::BaseResource\n  self.includes = []\n  self.model_class = Pay::PaymentMethod\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], processor_id_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.id,\n      }\n    }\n  }\n\n  def fields\n    main_panel do\n      field :id, as: :id\n      field :customer, as: :belongs_to\n      field :processor_id, as: :text\n      field :type, as: :text\n      field :default, as: :boolean\n      field :data, as: :key_value\n\n      sidebar do\n        field :extra_billing_info, as: :key_value\n        field :created_at, as: :date_time\n        field :updated_at, as: :date_time\n      end\n    end\n  end\nend\n", "app/avo/resources/plan.rb"=>"class Avo::Resources::Plan < Avo::BaseResource\n  self.title = :name\n  self.includes = []\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], name_cont: params[:q], description_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.name,\n      }\n    }\n  }\n\n  def fields\n    main_panel do\n      field :id, as: :id\n      field :name, as: :text\n      field :description, as: :text, hide_on: :index\n      field :hidden, as: :boolean\n      field :amount, as: :number, help: \"Price in cents\", format_using: -> do\n        if view == :edit || view == :new\n          value\n        else\n          (BigDecimal(value.to_s) / 100).round(2)\n        end\n      end\n      field :currency, as: :select, options: Pay::Currency.all.map { |iso, v| [\"\#{iso.upcase} - \#{v[\"name\"]}\", iso] }.to_h\n      field :interval, as: :select, options: [:month, :year]\n      field :interval_count, as: :number\n      field :trial_period_days, as: :number\n\n      field :features, as: :tags\n\n      sidebar do\n        field :created_at, as: :date_time\n        field :updated_at, as: :date_time\n        field :processor_details, as: :heading, label: \"Processor details\"\n        field :stripe_id, as: :text, format_using: -> do\n          if view == :edit || view == :new\n            value\n          else\n            link_to value, \"https://dashboard.stripe.com/plans/\#{value}\", target: :_blank\n          end\n        rescue\n          value\n        end\n        field :braintree_id, as: :text, format_using: -> do\n          if view == :edit || view == :new\n            value\n          else\n            link_to value, \"https://sandbox.braintreegateway.com/merchants/\#{Pay::Braintree.merchant_id}/plans/\#{id}\", target: :_blank\n          end\n        rescue\n          value\n        end\n        field :paddle_id, as: :text\n        field :fake_processor_id, as: :text\n      end\n    end\n  end\nend\n", "app/avo/resources/subscription.rb"=>"class Avo::Resources::Subscription < Avo::BaseResource\n  self.title = :processor_id\n  self.includes = [:owner, :customer]\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], name_cont: params[:q], processor_id_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.processor_id,\n      }\n    }\n  }\n  self.model_class = Pay::Subscription\n\n  def fields\n    main_panel do\n      field :id, as: :id\n      with_options only_on: :index do\n        field :customer, as: :belongs_to\n        field :name, as: :text\n        field :active?, as: :boolean, name: \"Active\"\n        field :cancelled?, as: :boolean, name: \"Cancelled\"\n      end\n      field :owner, as: :has_one\n      field :processor_id, as: :text, format_using: ->(id) do\n        link_to id, view_context.controller.subscription_processor_url(model), target: :_blank\n      rescue\n        id\n      end\n      field :active?, as: :boolean\n      field :processor, as: :badge, options: {success: \"Stripe\"} do |model, *args|\n        name = record.payment_processor.class.to_s.split(\"::\").second\n        if name.downcase.include? \"fake\"\n          \"Fake\"\n        else\n          name\n        end\n      rescue\n        \"Unknown\"\n      end\n\n      sidebar do\n        field :status, as: :status, readonly: true, failed_when: [:canceled], loading_when: [:trialing]\n        field :trial_ends_at_ago, as: :text\n        field :created_at, as: :date_time, readonly: true\n        field :ends_at, as: :date_time, readonly: true\n        field :processor_id, as: :text, readonly: true, as_html: true do\n          case record.customer.processor\n          when \"stripe\"\n            link_to(record.processor_id, \"https://dashboard.stripe.com/subscriptions/\#{record.processor_id}\", target: :_blank)\n          else\n            record.processor_id\n          end\n        end\n        field :processor_plan, as: :text, readonly: true\n        field :quantity, as: :number, readonly: true\n      end\n    end\n  end\nend\n", "app/avo/resources/user.rb"=>"class Avo::Resources::User < Avo::BaseResource\n  self.title = :name\n  self.includes = [:accounts]\n  self.search = {\n    query: -> { query.ransack(id_eq: params[:q], name_cont: params[:q], email_cont: params[:q], m: \"or\").result(distinct: false) },\n    item: -> {\n      {\n        title: record.name,\n      }\n    }\n  }\n\n  def fields\n    field :id, as: :id\n    field :avatar, as: :file, is_image: true, hide_on: :forms, link_to_resource: true\n    field :name, as: :text\n    field :email, as: :text\n    field :admin, as: :boolean, help: \"Whether the user has admin capabilities.\"\n\n    with_options hide_on: :forms do\n      field :number_of_accounts, as: :text do\n        record.accounts.length\n      end\n    end\n\n    tabs do\n      field :accounts, as: :has_many\n      field :connected_accounts, as: :has_many\n    end\n\n    tabs do\n      tab \"General\" do\n        panel do\n          field :time_zone, as: :text\n          field :invitations_count, as: :text\n        end\n      end\n      tab \"Account management\" do\n        panel do\n          field :password, as: :password\n          field :password_confirmation, as: :password\n          field :reset_password_token, as: :password\n          field :reset_password_sent_at, as: :date_time\n          field :remember_created_at, as: :date_time\n          field :confirmation_token, as: :text\n          field :confirmed_at, as: :date_time\n          field :confirmation_sent_at, as: :date_time\n          field :unconfirmed_email, as: :text\n          field :created_at, as: :date_time\n          field :updated_at, as: :date_time\n          field :invitation_token, as: :text\n          field :invitation_created_at, as: :date_time\n          field :invitation_sent_at, as: :date_time\n          field :invitation_accepted_at, as: :date_time\n          field :invitation_limit, as: :number\n          field :terms_of_service, as: :boolean\n          field :accepted_terms_at, as: :date_time\n          field :accepted_privacy_at, as: :date_time\n        end\n      end\n    end\n  end\nend\n", "app/controllers/avo/account_users_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# You shouldn't need to modify it in order to use Avo.\nclass Avo::AccountUsersController < Avo::ResourcesController\nend\n", "app/controllers/avo/accounts_controller.rb"=>"class Avo::AccountsController < Avo::ResourcesController\nend\n", "app/controllers/avo/announcements_controller.rb"=>"class Avo::AnnouncementsController < Avo::ResourcesController\nend\n", "app/controllers/avo/charges_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# You shouldn't need to modify it in order to use Avo.\nclass Avo::ChargesController < Avo::ResourcesController\n  include Jumpstart::AdministrateHelpers\nend\n", "app/controllers/avo/connected_accounts_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# You shouldn't need to modify it in order to use Avo.\nclass Avo::ConnectedAccountsController < Avo::ResourcesController\nend\n", "app/controllers/avo/customers_controller.rb"=>"class Avo::CustomersController < Avo::ResourcesController\n  include Jumpstart::AdministrateHelpers\nend\n", "app/controllers/avo/payment_methods_controller.rb"=>"# This controller has been generated to enable Rails' resource routes.\n# You shouldn't need to modify it in order to use Avo.\nclass Avo::PaymentMethodsController < Avo::ResourcesController\nend\n", "app/controllers/avo/plans_controller.rb"=>"class Avo::PlansController < Avo::ResourcesController\n  include Jumpstart::AdministrateHelpers\nend\n", "app/controllers/avo/subscriptions_controller.rb"=>"class Avo::SubscriptionsController < Avo::ResourcesController\n  include Jumpstart::AdministrateHelpers\nend\n", "app/controllers/avo/users_controller.rb"=>"class Avo::UsersController < Avo::ResourcesController\nend\n", "config/initializers/avo.rb"=>"# For more information regarding these settings check out our docs https://docs.avohq.io\nAvo.configure do |config|\n  ## == Routing ==\n  config.root_path = \"/avo\"\n  # used only when you have custom `map` configuration in your config.ru\n  # config.prefix_path = \"/internal\"\n\n  # Where should the user be redirected when visting the `/avo` url\n  config.home_path = defined?(Avo::Pro) ? \"/avo/dashboards/overview\" : nil\n\n  ## == Licensing ==\n  # Add your license key here for Pro or Advanced licenses\n  # config.license_key = ENV['AVO_LICENSE_KEY']\n\n  ## == Set the context ==\n  config.set_context do\n    # Return a context object that gets evaluated in Avo::ApplicationController\n    {}\n  end\n\n  ## == Authentication ==\n  # config.current_user_method = {}\n  # config.authenticate_with do\n  # end\n\n  ## == Authorization ==\n  # config.authorization_methods = {\n  #   index: 'index?',\n  #   show: 'show?',\n  #   edit: 'edit?',\n  #   new: 'new?',\n  #   update: 'update?',\n  #   create: 'create?',\n  #   destroy: 'destroy?',\n  #   search: 'search?',\n  # }\n  # config.raise_error_on_missing_policy = false\n  # config.authorization_client = :pundit\n\n  ## == Localization ==\n  # config.locale = 'en-US'\n\n  ## == Resource options ==\n  # config.resource_controls_placement = :right\n  # config.model_resource_mapping = {}\n  # config.default_view_type = :table\n  # config.per_page = 24\n  # config.per_page_steps = [12, 24, 48, 72]\n  # config.via_per_page = 8\n  config.id_links_to_resource = true\n  # config.cache_resources_on_index_view = true\n  ## permanent enable or disable cache_resource_filters, default value is false\n  # config.cache_resource_filters = false\n  ## provide a lambda to enable or disable cache_resource_filters per user/resource.\n  # config.cache_resource_filters = ->(current_user:, resource:) { current_user.cache_resource_filters?}\n\n  ## == Customization ==\n  # config.app_name = 'Avocadelicious'\n  # config.timezone = 'UTC'\n  # config.currency = 'USD'\n  # config.hide_layout_when_printing = false\n  # config.full_width_container = false\n  # config.full_width_index_view = false\n  # config.search_debounce = 300\n  # config.view_component_path = \"app/components\"\n  # config.display_license_request_timeout_error = true\n  # config.disabled_features = []\n  # config.tabs_style = :tabs # can be :tabs or :pills\n  # config.buttons_on_form_footers = true\n  # config.field_wrapper_layout = true\n\n  ## == Branding ==\n  # config.branding = {\n  #   colors: {\n  #     background: \"248 246 242\",\n  #     100 => \"#CEE7F8\",\n  #     400 => \"#399EE5\",\n  #     500 => \"#0886DE\",\n  #     600 => \"#066BB2\",\n  #   },\n  #   chart_colors: [\"#0B8AE2\", \"#34C683\", \"#2AB1EE\", \"#34C6A8\"],\n  #   logo: \"/avo-assets/logo.png\",\n  #   logomark: \"/avo-assets/logomark.png\",\n  #   placeholder: \"/avo-assets/placeholder.svg\",\n  #   favicon: \"/avo-assets/favicon.ico\"\n  # }\n\n  ## == Breadcrumbs ==\n  # config.display_breadcrumbs = true\n  # config.set_initial_breadcrumbs do\n  #   add_breadcrumb \"Home\", '/avo'\n  # end\n\n  ## == Menus ==\n  config.main_menu = -> {\n    section \"Dashboards\", icon: \"dashboards\" do\n      all_dashboards\n    end\n\n    section \"Users\", icon: \"heroicons/outline/user-group\" do\n      resource :announcement\n      resource :user\n      resource :account\n      resource :account_user\n      resource :plan\n    end\n\n    section \"Pay\", icon: \"heroicons/outline/currency-dollar\" do\n      resource :customer\n      resource :charge\n      resource :payment_method\n      resource :subscription\n    end\n\n    if Rails.env.development?\n      link \"Jumpstart Config\", path: Avo::Current.view_context.main_app.jumpstart_path\n    end\n  }\n  config.profile_menu = -> {\n    link \"Profile\", path: \"/avo/profile\", icon: \"user-circle\"\n  }\nend\n"}

if ARGV.include? "--community-edition"
  edition = "community"
elsif ARGV.include? "--pro-edition"
  edition = "pro"
elsif ARGV.include? "--advanced-edition"
  edition = "advanced"
end

unless edition
  # === Fetch the Avo edition ===
  question = <<~QUESTION


    Which version of Avo would you like to install?
    1. Avo Community (default)
    2. Avo Pro
    3. Avo Advanced

    More information about version features here:
    https://avohq.io/pricing


  QUESTION
  puts question

  answer = ask("Which version of Avo would you like to install?", default: "1", limited_to: ["1", "2", "3"])

  edition = case answer
  when "1"
    "community"
  when "2"
    "pro"
  when "3"
    "advanced"
  end
end

# === Add gem to Gemfile ===
case edition
when "community"
  gem "avo", ">= 3.1.0"
when "pro"
  gem "avo", ">= 3.1.0"
  gem "avo-pro", source: "https://packager.dev/avo-hq"
when "advanced"
  gem "avo", ">= 3.1.0"
  gem "avo-advanced", source: "https://packager.dev/avo-hq"
end

# === Run bundle install ===
Bundler.with_unbundled_env { run "bundle install" }

# === Add route ===
route_contents = <<-ROUTES
  # Avo admin panel
  if defined?(Avo::Engine)
    authenticated :user, lambda { |u| u.admin? } do
      mount Avo::Engine, at: Avo.configuration.root_path
    end
  end
ROUTES
route route_contents

# === Copy template files ===
files.each do |path, contents|
  file path, contents
end