airview 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +65 -0
  6. data/app/assets/javascripts/airview/application.js +425 -0
  7. data/app/assets/stylesheets/airview/application.css +1239 -0
  8. data/app/controllers/airview/application_controller.rb +7 -0
  9. data/app/controllers/airview/resources_controller.rb +261 -0
  10. data/app/controllers/airview/setup_controller.rb +90 -0
  11. data/app/controllers/airview/views_controller.rb +76 -0
  12. data/app/helpers/airview/application_helper.rb +479 -0
  13. data/app/javascript/airview/controllers/grid_controller.js +7 -0
  14. data/app/models/airview/application_record.rb +7 -0
  15. data/app/models/airview/field_definition.rb +22 -0
  16. data/app/models/airview/resource_definition.rb +48 -0
  17. data/app/models/airview/view.rb +27 -0
  18. data/app/views/airview/resources/_delete_modal.html.erb +49 -0
  19. data/app/views/airview/resources/_record_modal.html.erb +62 -0
  20. data/app/views/airview/resources/empty.html.erb +5 -0
  21. data/app/views/airview/resources/show.html.erb +258 -0
  22. data/app/views/airview/setup/_form.html.erb +73 -0
  23. data/app/views/airview/setup/edit.html.erb +16 -0
  24. data/app/views/airview/setup/index.html.erb +52 -0
  25. data/app/views/airview/setup/new.html.erb +14 -0
  26. data/app/views/airview/shared/_sidebar.html.erb +76 -0
  27. data/app/views/layouts/airview/application.html.erb +21 -0
  28. data/config/routes.rb +23 -0
  29. data/db/migrate/20260806000000_create_airview_views.rb +57 -0
  30. data/lib/airview/configuration.rb +6 -0
  31. data/lib/airview/engine.rb +15 -0
  32. data/lib/airview/field.rb +81 -0
  33. data/lib/airview/model_discovery.rb +38 -0
  34. data/lib/airview/query.rb +194 -0
  35. data/lib/airview/resource.rb +51 -0
  36. data/lib/airview/resource_builder.rb +62 -0
  37. data/lib/airview/schema_inference.rb +196 -0
  38. data/lib/airview/version.rb +5 -0
  39. data/lib/airview.rb +93 -0
  40. data/lib/generators/airview/install/install_generator.rb +38 -0
  41. data/lib/generators/airview/install/templates/add_folders_to_airview_views.rb +8 -0
  42. data/lib/generators/airview/install/templates/airview.rb +12 -0
  43. data/lib/generators/airview/install/templates/create_airview_views.rb +57 -0
  44. data/sig/airview.rbs +54 -0
  45. metadata +110 -0
data/lib/airview.rb ADDED
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "airview/version"
4
+ require "active_support/core_ext/object/blank"
5
+ require "active_support/core_ext/object/inclusion"
6
+ require "active_support/core_ext/hash/keys"
7
+ require "active_support/core_ext/string/inflections"
8
+ require "bigdecimal"
9
+ require "json"
10
+ require_relative "airview/configuration"
11
+ require_relative "airview/field"
12
+ require_relative "airview/resource"
13
+ require_relative "airview/query"
14
+ require_relative "airview/model_discovery"
15
+ require_relative "airview/resource_builder"
16
+ require_relative "airview/schema_inference"
17
+ require_relative "airview/engine" if defined?(Rails)
18
+
19
+ module Airview
20
+ class Error < StandardError; end
21
+
22
+ class ConfigurationError < Error; end
23
+ class UnknownResourceError < Error; end
24
+ RECORD_LABEL_METHODS = %i[
25
+ name
26
+ title
27
+ email
28
+ username
29
+ display_name
30
+ company_name
31
+ label
32
+ ].freeze
33
+
34
+ class << self
35
+ def configuration
36
+ @configuration ||= Configuration.new
37
+ end
38
+
39
+ def configure
40
+ yield(configuration)
41
+ end
42
+
43
+ def resources
44
+ if defined?(Airview::ResourceDefinition) && database_resources_available?
45
+ ResourceBuilder.enabled_resources
46
+ else
47
+ {}
48
+ end
49
+ end
50
+
51
+ def resource!(key)
52
+ resources.fetch(key.to_sym) do
53
+ raise UnknownResourceError, "Airview resource #{key.inspect} is not registered"
54
+ end
55
+ end
56
+
57
+ def record_label(record, preferred_method = nil)
58
+ return "" unless record
59
+
60
+ label = label_from_method(record, preferred_method)
61
+ label ||= label_from_method(record, :full_name)
62
+ label ||= label_from_full_name_parts(record)
63
+ label ||= RECORD_LABEL_METHODS.lazy.filter_map { |method_name| label_from_method(record, method_name) }.first
64
+ label.presence || "#{record.class.model_name.human} ##{record.id}"
65
+ end
66
+
67
+ private
68
+
69
+ def label_from_method(record, method_name)
70
+ return nil if method_name.blank? || method_name.to_sym == :to_s
71
+ return nil unless record.respond_to?(method_name)
72
+
73
+ value = record.public_send(method_name).to_s.strip
74
+ return nil if value.blank? || value.start_with?("#<")
75
+
76
+ value
77
+ end
78
+
79
+ def label_from_full_name_parts(record)
80
+ return nil unless record.respond_to?(:first_name) && record.respond_to?(:last_name)
81
+
82
+ [record.public_send(:first_name), record.public_send(:last_name)].compact.join(" ").strip.presence
83
+ end
84
+
85
+ def database_resources_available?
86
+ return false unless ActiveRecord::Base.connected?
87
+
88
+ ActiveRecord::Base.connection.data_source_exists?("airview_resources")
89
+ rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
90
+ false
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record/migration"
5
+
6
+ module Airview
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ def copy_initializer
14
+ template "airview.rb", "config/initializers/airview.rb"
15
+ end
16
+
17
+ def copy_migration
18
+ if migration_exists?("create_airview_views")
19
+ unless migration_exists?("add_folders_to_airview_views")
20
+ migration_template "add_folders_to_airview_views.rb", "db/migrate/add_folders_to_airview_views.rb"
21
+ end
22
+ else
23
+ migration_template "create_airview_views.rb", "db/migrate/create_airview_views.rb"
24
+ end
25
+ end
26
+
27
+ def self.next_migration_number(_dirname)
28
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
29
+ end
30
+
31
+ private
32
+
33
+ def migration_exists?(name)
34
+ Dir.glob(File.join(destination_root, "db/migrate/*_#{name}.rb")).any?
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddFoldersToAirviewViews < ActiveRecord::Migration[7.1]
4
+ def change
5
+ add_column :airview_views, :folder, :string unless column_exists?(:airview_views, :folder)
6
+ add_index :airview_views, %i[resource_key folder] unless index_exists?(:airview_views, %i[resource_key folder])
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Airview is intended for trusted internal admin surfaces.
4
+ # Protect the mount point in config/routes.rb with your app's authentication.
5
+ #
6
+ # authenticate :user, ->(user) { user.admin? } do
7
+ # mount Airview::Engine => "/airview"
8
+ # end
9
+
10
+ Airview.configure do |config|
11
+ # Resource configuration is managed through the Airview UI at /airview/setup.
12
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateAirviewViews < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :airview_resources do |t|
6
+ t.string :key, null: false
7
+ t.string :record_class_name, null: false
8
+ t.string :label, null: false
9
+ t.string :label_method
10
+ t.boolean :enabled, null: false, default: false
11
+ t.integer :position, null: false, default: 0
12
+ t.json :metadata, null: false, default: {}
13
+
14
+ t.timestamps
15
+ end
16
+
17
+ add_index :airview_resources, :key, unique: true
18
+ add_index :airview_resources, :record_class_name, unique: true
19
+ add_index :airview_resources, :enabled
20
+
21
+ create_table :airview_fields do |t|
22
+ t.references :airview_resource, null: false, foreign_key: { to_table: :airview_resources }
23
+ t.string :name, null: false
24
+ t.string :label, null: false
25
+ t.string :field_type, null: false
26
+ t.boolean :visible, null: false, default: true
27
+ t.boolean :read_only, null: false, default: false
28
+ t.integer :position, null: false, default: 0
29
+ t.string :association_name
30
+ t.string :target_model_name
31
+ t.string :target_label_method
32
+ t.json :metadata, null: false, default: {}
33
+
34
+ t.timestamps
35
+ end
36
+
37
+ add_index :airview_fields, %i[airview_resource_id name], unique: true
38
+ add_index :airview_fields, :visible
39
+
40
+ create_table :airview_views do |t|
41
+ t.string :name, null: false
42
+ t.string :resource_key, null: false
43
+ t.string :owner_key
44
+ t.string :folder
45
+ t.json :columns, null: false, default: []
46
+ t.json :filters, null: false, default: {}
47
+ t.json :sorts, null: false, default: {}
48
+ t.json :preferences, null: false, default: {}
49
+
50
+ t.timestamps
51
+ end
52
+
53
+ add_index :airview_views, :resource_key
54
+ add_index :airview_views, %i[resource_key folder]
55
+ add_index :airview_views, %i[resource_key name]
56
+ end
57
+ end
data/sig/airview.rbs ADDED
@@ -0,0 +1,54 @@
1
+ module Airview
2
+ VERSION: String
3
+
4
+ def self.configuration: () -> Configuration
5
+ def self.configure: () { (Configuration) -> void } -> void
6
+ def self.resources: () -> Hash[Symbol, Resource]
7
+ def self.resource!: (Symbol | String key) -> Resource
8
+
9
+ class Error < StandardError
10
+ end
11
+
12
+ class ConfigurationError < Error
13
+ end
14
+
15
+ class UnknownResourceError < Error
16
+ end
17
+
18
+ class Configuration
19
+ end
20
+
21
+ class Field
22
+ attr_reader name: Symbol
23
+ attr_reader label: String
24
+ attr_reader type: Symbol
25
+ attr_reader options: untyped
26
+ attr_reader model: untyped
27
+ attr_reader label_method: Symbol
28
+
29
+ def initialize: (Symbol | String name, type: Symbol | String, ?label: String?, ?readonly: bool, ?options: untyped, ?model: untyped, ?label_method: Symbol?) -> void
30
+ def readonly?: () -> bool
31
+ def editable?: () -> bool
32
+ def association?: () -> bool
33
+ def attribute_name: () -> Symbol
34
+ def validate!: () -> void
35
+ def model_class: () -> untyped
36
+ def option_values: () -> Array[untyped]
37
+ end
38
+
39
+ class Resource
40
+ attr_accessor label: String
41
+ attr_accessor label_method: Symbol
42
+ attr_reader key: Symbol
43
+ attr_reader model: String
44
+ attr_reader fields: Hash[Symbol, Field]
45
+
46
+ def initialize: (Symbol | String key, model: String) -> void
47
+ def field: (Symbol | String name, type: Symbol | String, ?label: String?, **untyped options) -> Field
48
+ def model_class: () -> untyped
49
+ def field!: (Symbol | String name) -> Field
50
+ def editable_fields: () -> Array[Field]
51
+ def validate!: () -> void
52
+ def record_label: (untyped record) -> String
53
+ end
54
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: airview
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - jacksonriso
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-08-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '9.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '7.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9.0'
33
+ description: Airview mounts an internal admin UI for browsing and editing explicitly
34
+ registered ActiveRecord models.
35
+ email:
36
+ - risojackson@gmail.com
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - CHANGELOG.md
42
+ - CODE_OF_CONDUCT.md
43
+ - LICENSE.txt
44
+ - README.md
45
+ - app/assets/javascripts/airview/application.js
46
+ - app/assets/stylesheets/airview/application.css
47
+ - app/controllers/airview/application_controller.rb
48
+ - app/controllers/airview/resources_controller.rb
49
+ - app/controllers/airview/setup_controller.rb
50
+ - app/controllers/airview/views_controller.rb
51
+ - app/helpers/airview/application_helper.rb
52
+ - app/javascript/airview/controllers/grid_controller.js
53
+ - app/models/airview/application_record.rb
54
+ - app/models/airview/field_definition.rb
55
+ - app/models/airview/resource_definition.rb
56
+ - app/models/airview/view.rb
57
+ - app/views/airview/resources/_delete_modal.html.erb
58
+ - app/views/airview/resources/_record_modal.html.erb
59
+ - app/views/airview/resources/empty.html.erb
60
+ - app/views/airview/resources/show.html.erb
61
+ - app/views/airview/setup/_form.html.erb
62
+ - app/views/airview/setup/edit.html.erb
63
+ - app/views/airview/setup/index.html.erb
64
+ - app/views/airview/setup/new.html.erb
65
+ - app/views/airview/shared/_sidebar.html.erb
66
+ - app/views/layouts/airview/application.html.erb
67
+ - config/routes.rb
68
+ - db/migrate/20260806000000_create_airview_views.rb
69
+ - lib/airview.rb
70
+ - lib/airview/configuration.rb
71
+ - lib/airview/engine.rb
72
+ - lib/airview/field.rb
73
+ - lib/airview/model_discovery.rb
74
+ - lib/airview/query.rb
75
+ - lib/airview/resource.rb
76
+ - lib/airview/resource_builder.rb
77
+ - lib/airview/schema_inference.rb
78
+ - lib/airview/version.rb
79
+ - lib/generators/airview/install/install_generator.rb
80
+ - lib/generators/airview/install/templates/add_folders_to_airview_views.rb
81
+ - lib/generators/airview/install/templates/airview.rb
82
+ - lib/generators/airview/install/templates/create_airview_views.rb
83
+ - sig/airview.rbs
84
+ homepage: https://github.com/jacksonriso/airview
85
+ licenses:
86
+ - MIT
87
+ metadata:
88
+ homepage_uri: https://github.com/jacksonriso/airview
89
+ source_code_uri: https://github.com/jacksonriso/airview
90
+ changelog_uri: https://github.com/jacksonriso/airview/blob/main/CHANGELOG.md
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 3.2.0
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 3.5.22
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: A Rails engine for Airtable-like database views inside Rails apps.
110
+ test_files: []