mat_views 0.2.0 → 0.3.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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/app/assets/images/mat_views/android-chrome-192x192.png +0 -0
  4. data/app/assets/images/mat_views/android-chrome-512x512.png +0 -0
  5. data/app/assets/images/mat_views/apple-touch-icon.png +0 -0
  6. data/app/assets/images/mat_views/favicon-16x16.png +0 -0
  7. data/app/assets/images/mat_views/favicon-32x32.png +0 -0
  8. data/app/assets/images/mat_views/favicon-48x48.png +0 -0
  9. data/app/assets/images/mat_views/favicon.ico +0 -0
  10. data/app/assets/images/mat_views/favicon.svg +18 -0
  11. data/app/assets/images/mat_views/logo.svg +18 -0
  12. data/app/assets/images/mat_views/mask-icon.svg +5 -0
  13. data/app/assets/stylesheets/mat_views/application.css +323 -12
  14. data/app/controllers/mat_views/admin/application_controller.rb +135 -0
  15. data/app/controllers/mat_views/admin/dashboard_controller.rb +32 -0
  16. data/app/controllers/mat_views/admin/mat_view_definitions_controller.rb +248 -0
  17. data/app/controllers/mat_views/admin/preferences_controller.rb +91 -0
  18. data/app/controllers/mat_views/admin/runs_controller.rb +74 -0
  19. data/app/helpers/mat_views/admin/ui_helper.rb +385 -0
  20. data/app/javascript/mat_views/application.js +8 -0
  21. data/app/javascript/mat_views/controllers/application.js +10 -0
  22. data/app/javascript/mat_views/controllers/details_controller.js +122 -0
  23. data/app/javascript/mat_views/controllers/drawer_controller.js +252 -0
  24. data/app/javascript/mat_views/controllers/filter_controller.js +90 -0
  25. data/app/javascript/mat_views/controllers/flash_controller.js +13 -0
  26. data/app/javascript/mat_views/controllers/index.js +10 -0
  27. data/app/javascript/mat_views/controllers/mv_confirm_controller.js +281 -0
  28. data/app/javascript/mat_views/controllers/submitter_controller.js +15 -0
  29. data/app/javascript/mat_views/controllers/tabs_controller.js +67 -0
  30. data/app/javascript/mat_views/controllers/timezone_controller.js +16 -0
  31. data/app/javascript/mat_views/controllers/tooltip_controller.js +328 -0
  32. data/app/javascript/mat_views/controllers/turbo_frame_lifecycle_controller.js +49 -0
  33. data/app/jobs/mat_views/application_job.rb +2 -2
  34. data/app/jobs/mat_views/create_view_job.rb +9 -8
  35. data/app/jobs/mat_views/delete_view_job.rb +8 -8
  36. data/app/jobs/mat_views/refresh_view_job.rb +8 -9
  37. data/app/models/concerns/mat_views_i18n.rb +139 -0
  38. data/app/models/mat_views/application_record.rb +1 -0
  39. data/app/models/mat_views/mat_view_definition.rb +12 -7
  40. data/app/models/mat_views/mat_view_run.rb +11 -13
  41. data/app/views/layouts/mat_views/_footer.html.erb +41 -0
  42. data/app/views/layouts/mat_views/_header.html.erb +25 -0
  43. data/app/views/layouts/mat_views/admin.html.erb +47 -0
  44. data/app/views/layouts/mat_views/turbo_frame.html.erb +3 -0
  45. data/app/views/mat_views/admin/dashboard/index.html.erb +33 -0
  46. data/app/views/mat_views/admin/mat_view_definitions/_definition_actions.html.erb +94 -0
  47. data/app/views/mat_views/admin/mat_view_definitions/_table.html.erb +48 -0
  48. data/app/views/mat_views/admin/mat_view_definitions/empty.html.erb +1 -0
  49. data/app/views/mat_views/admin/mat_view_definitions/form.html.erb +79 -0
  50. data/app/views/mat_views/admin/mat_view_definitions/index.html.erb +10 -0
  51. data/app/views/mat_views/admin/mat_view_definitions/show.html.erb +40 -0
  52. data/app/views/mat_views/admin/preferences/show.html.erb +50 -0
  53. data/app/views/mat_views/admin/runs/_table.html.erb +61 -0
  54. data/app/views/mat_views/admin/runs/index.html.erb +38 -0
  55. data/app/views/mat_views/admin/runs/show.html.erb +64 -0
  56. data/app/views/mat_views/admin/ui/_card.html.erb +15 -0
  57. data/app/views/mat_views/admin/ui/_details.html.erb +10 -0
  58. data/app/views/mat_views/admin/ui/_flash.html.erb +6 -0
  59. data/app/views/mat_views/admin/ui/_table.html.erb +8 -0
  60. data/config/importmap.rb +9 -0
  61. data/config/locales/en-AU-ocker.yml +187 -0
  62. data/config/locales/en-AU.yml +187 -0
  63. data/config/locales/en-BB.yml +187 -0
  64. data/config/locales/en-BD.yml +187 -0
  65. data/config/locales/en-BE.yml +187 -0
  66. data/config/locales/en-BORK.yml +187 -0
  67. data/config/locales/en-BS.yml +187 -0
  68. data/config/locales/en-BZ.yml +187 -0
  69. data/config/locales/en-CA.yml +187 -0
  70. data/config/locales/en-CM.yml +187 -0
  71. data/config/locales/en-CY.yml +187 -0
  72. data/config/locales/en-EG.yml +187 -0
  73. data/config/locales/en-FJ.yml +187 -0
  74. data/config/locales/en-GB.yml +187 -0
  75. data/config/locales/en-GH.yml +187 -0
  76. data/config/locales/en-GI.yml +187 -0
  77. data/config/locales/en-GM.yml +187 -0
  78. data/config/locales/en-GY.yml +187 -0
  79. data/config/locales/en-HK.yml +187 -0
  80. data/config/locales/en-IE.yml +187 -0
  81. data/config/locales/en-IN.yml +187 -0
  82. data/config/locales/en-JM.yml +187 -0
  83. data/config/locales/en-KE.yml +187 -0
  84. data/config/locales/en-LK.yml +187 -0
  85. data/config/locales/en-LOL.yml +187 -0
  86. data/config/locales/en-LR.yml +187 -0
  87. data/config/locales/en-MS.yml +187 -0
  88. data/config/locales/en-MT.yml +187 -0
  89. data/config/locales/en-MW.yml +187 -0
  90. data/config/locales/en-MY.yml +187 -0
  91. data/config/locales/en-NG.yml +187 -0
  92. data/config/locales/en-NP.yml +187 -0
  93. data/config/locales/en-NZ.yml +187 -0
  94. data/config/locales/en-PG.yml +187 -0
  95. data/config/locales/en-PH.yml +187 -0
  96. data/config/locales/en-PK.yml +187 -0
  97. data/config/locales/en-RW.yml +187 -0
  98. data/config/locales/en-SCOT.yml +187 -0
  99. data/config/locales/en-SG.yml +187 -0
  100. data/config/locales/en-SHAKESPEARE.yml +187 -0
  101. data/config/locales/en-SL.yml +187 -0
  102. data/config/locales/en-SS.yml +187 -0
  103. data/config/locales/en-TH.yml +187 -0
  104. data/config/locales/en-TT.yml +187 -0
  105. data/config/locales/en-TZ.yml +187 -0
  106. data/config/locales/en-UG.yml +187 -0
  107. data/config/locales/en-US-pirate.yml +187 -0
  108. data/config/locales/en-US.yml +187 -0
  109. data/config/locales/en-YODA.yml +187 -0
  110. data/config/locales/en-ZA.yml +187 -0
  111. data/config/locales/en-ZW.yml +187 -0
  112. data/config/locales/en.yml +187 -0
  113. data/config/routes.rb +27 -3
  114. data/lib/generators/mat_views/install/templates/create_mat_view_definitions.rb +7 -7
  115. data/lib/generators/mat_views/install/templates/create_mat_view_runs.rb +5 -5
  116. data/lib/mat_views/admin/auth_bridge.rb +93 -0
  117. data/lib/mat_views/admin/default_auth.rb +61 -0
  118. data/lib/mat_views/configuration.rb +9 -0
  119. data/lib/mat_views/engine.rb +50 -2
  120. data/lib/mat_views/helpers/ui_test_ids.rb +43 -0
  121. data/lib/mat_views/services/base_service.rb +46 -38
  122. data/lib/mat_views/services/check_matview_exists.rb +76 -0
  123. data/lib/mat_views/services/concurrent_refresh.rb +9 -6
  124. data/lib/mat_views/services/create_view.rb +15 -15
  125. data/lib/mat_views/services/delete_view.rb +8 -11
  126. data/lib/mat_views/services/regular_refresh.rb +6 -5
  127. data/lib/mat_views/services/swap_refresh.rb +11 -9
  128. data/lib/mat_views/version.rb +1 -1
  129. data/lib/mat_views.rb +10 -4
  130. data/lib/tasks/helpers.rb +13 -13
  131. data/lib/tasks/mat_views_tasks.rake +15 -15
  132. metadata +130 -5
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Codevedas Inc. 2025-present
4
+ #
5
+ # This source code is licensed under the MIT license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+
8
+ module MatViews
9
+ module Admin
10
+ # MatViews::Admin::MatViewDefinitionsController
11
+ # ---------------------------------------------
12
+ # Admin CRUD controller for {MatViews::MatViewDefinition} records.
13
+ #
14
+ # Responsibilities:
15
+ # - Full CRUD lifecycle: index, show, new, create, edit, update, destroy.
16
+ # - Admin-only actions to trigger materialised view operations:
17
+ # - {#create_now} → enqueues {MatViews::CreateViewJob}
18
+ # - {#refresh} → enqueues {MatViews::RefreshViewJob}
19
+ # - {#delete_now} → enqueues {MatViews::DeleteViewJob}
20
+ # - Integrates with Turbo Frames (uses frame-aware redirects/responses).
21
+ # - Normalizes array fields (`unique_index_columns`, `dependencies`) from
22
+ # comma-separated params into arrays.
23
+ #
24
+ # Filters:
25
+ # - `before_action :set_definition` for member actions.
26
+ # - `before_action :normalize_array_fields` for create/update.
27
+ # - `before_action :ensure_frame` to enforce frame context.
28
+ #
29
+ class MatViewDefinitionsController < ApplicationController
30
+ before_action :set_definition, only: %i[show edit update destroy create_now refresh delete_now]
31
+ before_action :normalize_array_fields, only: %i[create update]
32
+ before_action :ensure_frame
33
+
34
+ # GET /:lang/admin/definitions
35
+ #
36
+ # Lists all definitions with existence checks against the database.
37
+ #
38
+ # @return [void]
39
+ def index
40
+ # sleep 20
41
+ authorize_mat_views!(:read, MatViews::MatViewDefinition)
42
+ @definitions = MatViews::MatViewDefinition.order(:name).to_a
43
+ @mv_exists_map = @definitions.index_with do |defn|
44
+ MatViews::Services::CheckMatviewExists.new(defn).call.response[:exists]
45
+ end
46
+ render 'index', formats: :html, layout: 'mat_views/turbo_frame'
47
+ end
48
+
49
+ # GET /:lang/admin/definitions/:id
50
+ #
51
+ # Shows a single definition, including run history.
52
+ #
53
+ # @return [void]
54
+ def show
55
+ authorize_mat_views!(:read, @definition)
56
+ @mv_exists = MatViews::Services::CheckMatviewExists.new(@definition).call.response[:exists]
57
+ @runs = @definition.mat_view_runs.order(created_at: :desc).to_a
58
+ render 'show', formats: :html, layout: 'mat_views/turbo_frame'
59
+ end
60
+
61
+ # GET /:lang/admin/definitions/new
62
+ #
63
+ # Renders the new definition form.
64
+ #
65
+ # @return [void]
66
+ def new
67
+ authorize_mat_views!(:create, MatViews::MatViewDefinition)
68
+ @definition = MatViews::MatViewDefinition.new
69
+ render 'form', formats: :html, layout: 'mat_views/turbo_frame'
70
+ end
71
+
72
+ # POST /:lang/admin/definitions
73
+ #
74
+ # Creates a new definition from params.
75
+ #
76
+ # @return [void]
77
+ def create
78
+ authorize_mat_views!(:create, MatViews::MatViewDefinition)
79
+ @definition = MatViews::MatViewDefinition.new(definition_params)
80
+ if @definition.save
81
+ handle_frame_response(status: 298)
82
+ else
83
+ render 'form', formats: :html, layout: 'mat_views/turbo_frame', status: :unprocessable_content
84
+ end
85
+ end
86
+
87
+ # GET /:lang/admin/definitions/:id/edit
88
+ #
89
+ # Renders the edit form for an existing definition.
90
+ #
91
+ # @return [void]
92
+ def edit
93
+ authorize_mat_views!(:update, @definition)
94
+ render 'form', formats: :html, layout: 'mat_views/turbo_frame'
95
+ end
96
+
97
+ # PATCH/PUT /:lang/admin/definitions/:id
98
+ #
99
+ # Updates an existing definition.
100
+ #
101
+ # @return [void]
102
+ def update
103
+ authorize_mat_views!(:update, @definition)
104
+ if @definition.update(definition_params)
105
+ handle_frame_response(status: 298)
106
+ else
107
+ render 'form', formats: :html, layout: 'mat_views/turbo_frame', status: :unprocessable_content
108
+ end
109
+ end
110
+
111
+ # DELETE /:lang/admin/definitions/:id
112
+ #
113
+ # Destroys the definition. Frame-specific redirect/empty response.
114
+ #
115
+ # @return [void]
116
+ def destroy
117
+ authorize_mat_views!(:destroy, @definition)
118
+ @definition.destroy!
119
+ if @frame_id == 'dash-definitions'
120
+ redirect_to admin_mat_view_definitions_path(frame_id: @frame_id, frame_action: @frame_action), status: :see_other
121
+ else
122
+ render 'empty', formats: :html, layout: 'mat_views/turbo_frame', status: 298
123
+ end
124
+ end
125
+
126
+ # POST /:lang/admin/definitions/:id/create_now
127
+ #
128
+ # Immediately enqueues a background job to create the materialised view.
129
+ #
130
+ # @return [void]
131
+ def create_now
132
+ force = params[:force].to_s.downcase == 'true'
133
+ authorize_mat_views!(:create_view, @definition)
134
+ MatViews::Jobs::Adapter.enqueue(
135
+ MatViews::CreateViewJob,
136
+ queue: MatViews.configuration.job_queue,
137
+ args: [@definition.id, force, row_count_strategy]
138
+ )
139
+ handle_frame_response
140
+ end
141
+
142
+ # POST /:lang/admin/definitions/:id/refresh
143
+ #
144
+ # Immediately enqueues a background job to refresh the materialised view.
145
+ #
146
+ # @return [void]
147
+ def refresh
148
+ authorize_mat_views!(:refresh, @definition)
149
+ MatViews::Jobs::Adapter.enqueue(
150
+ MatViews::RefreshViewJob,
151
+ queue: MatViews.configuration.job_queue,
152
+ args: [@definition.id, row_count_strategy]
153
+ )
154
+ handle_frame_response
155
+ end
156
+
157
+ # POST /:lang/admin/definitions/:id/delete_now
158
+ #
159
+ # Immediately enqueues a background job to delete the materialised view.
160
+ #
161
+ # @return [void]
162
+ def delete_now
163
+ authorize_mat_views!(:delete_view, @definition)
164
+
165
+ cascade = params[:cascade].to_s.downcase == 'true'
166
+ MatViews::Jobs::Adapter.enqueue(
167
+ MatViews::DeleteViewJob,
168
+ queue: MatViews.configuration.job_queue,
169
+ args: [@definition.id, cascade, row_count_strategy]
170
+ )
171
+ handle_frame_response
172
+ end
173
+
174
+ private
175
+
176
+ # Returns the configured row count strategy for admin UI operations.
177
+ #
178
+ # @api private
179
+ #
180
+ # @return [Symbol, nil] row count strategy (e.g., :estimated, :exact, :none)
181
+ def row_count_strategy
182
+ MatViews.configuration.admin_ui[:row_count_strategy] || :none
183
+ end
184
+
185
+ # Handles redirect/response after a frame-based action.
186
+ #
187
+ # @api private
188
+ #
189
+ # @param status [Symbol,Integer] the HTTP status for redirect
190
+ # @return [void]
191
+ def handle_frame_response(status: :see_other)
192
+ if @frame_id == 'dash-definitions'
193
+ redirect_to admin_mat_view_definitions_path(frame_id: @frame_id, frame_action: @frame_action), status: :see_other
194
+ else
195
+ redirect_to admin_mat_view_definition_path(@definition, frame_id: @frame_id, frame_action: @frame_action), status: status
196
+ end
197
+ end
198
+
199
+ # Loads a definition by `params[:id]`.
200
+ #
201
+ # @api private
202
+ #
203
+ # @return [void]
204
+ def set_definition
205
+ @definition = MatViews::MatViewDefinition.find(params[:id])
206
+ end
207
+
208
+ # Normalizes array fields (unique_index_columns, dependencies) from
209
+ # comma-separated strings into arrays.
210
+ #
211
+ # @api private
212
+ #
213
+ # @return [void]
214
+ def normalize_array_fields
215
+ normalize_array_field(:mat_view_definition, :unique_index_columns)
216
+ normalize_array_field(:mat_view_definition, :dependencies)
217
+ end
218
+
219
+ # Normalizes a specific array field from a comma-separated string into an array.
220
+ #
221
+ # @api private
222
+ #
223
+ # @param object_key [Symbol] the params object key (e.g., :mat_view_definition)
224
+ # @param array_key [Symbol] the specific array field key (e.g., :unique_index_columns)
225
+ #
226
+ # @return [void]
227
+ def normalize_array_field(object_key, array_key)
228
+ object = params[object_key]
229
+ values = object[array_key]
230
+ return if values.nil?
231
+
232
+ object[array_key] = values.split(',').map(&:strip).reject(&:blank?)
233
+ end
234
+
235
+ # Strong params for mat view definitions.
236
+ #
237
+ # @api private
238
+ #
239
+ # @return [ActionController::Parameters]
240
+ def definition_params
241
+ params.require(:mat_view_definition).permit(
242
+ :name, :sql, :refresh_strategy, :schedule_cron,
243
+ unique_index_columns: [], dependencies: []
244
+ )
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Codevedas Inc. 2025-present
4
+ #
5
+ # This source code is licensed under the MIT license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+
8
+ module MatViews
9
+ module Admin
10
+ # MatViews::Admin::PreferencesController
11
+ # --------------------------------------
12
+ # Controller for managing user preferences in the MatViews admin UI.
13
+ #
14
+ # Responsibilities:
15
+ # - Allows users to view and update UI preferences such as theme and locale.
16
+ # - Stores theme in cookies and locale in session.
17
+ # - Provides a force-reload response option to update Turbo frames dynamically.
18
+ #
19
+ # Filters:
20
+ # - `before_action :authorize!` → ensures user can access preferences.
21
+ # - `before_action :ensure_frame` → requires Turbo frame context for `show`.
22
+ #
23
+ class PreferencesController < ApplicationController
24
+ before_action :authorize!
25
+ before_action :ensure_frame
26
+
27
+ # GET /:lang/admin/preferences
28
+ #
29
+ # Displays the current preferences (theme + locale) and available locales.
30
+ # If `force_reload=1` is passed, sets a non-standard status code (299) and
31
+ # a custom header to signal the client to reload.
32
+ #
33
+ # @return [void]
34
+ def show
35
+ @theme = read_theme
36
+ @locale = I18n.locale.to_s
37
+ @locales = MatViews::Engine.locale_code_mapping.sort_by { |_key, name| name }.map { |code, _name| code.to_s }.uniq
38
+
39
+ # force reload frame if requested
40
+ if params[:force_reload].to_s == '1'
41
+ response.status = 299
42
+ response.set_header('X-Status-Name', 'Success force reload')
43
+ end
44
+
45
+ render 'show', formats: :html, layout: 'mat_views/turbo_frame'
46
+ end
47
+
48
+ # PATCH/PUT /:lang/admin/preferences
49
+ #
50
+ # Updates preferences:
51
+ # - Theme (`light`, `dark`, or deleted if invalid) stored in cookies.
52
+ # - Locale stored in session if valid.
53
+ # Redirects back to preferences with `force_reload=1` to trigger a refresh.
54
+ #
55
+ # @return [void]
56
+ def update
57
+ theme_param = params[:theme].to_s
58
+ case theme_param
59
+ when 'light', 'dark' then cookies[:theme] = { value: theme_param, expires: 1.year.from_now, httponly: false }
60
+ else cookies.delete(:theme)
61
+ end
62
+
63
+ locale = params[:locale].to_s.presence || MatViews::Engine.default_locale.to_s
64
+ session[:mat_views_locale] = locale if MatViews::Engine.available_locales.map(&:to_s).include?(locale)
65
+
66
+ redirect_to "#{admin_preferences_path}?force_reload=1&frame_id=#{@frame_id}", status: :see_other
67
+ end
68
+
69
+ private
70
+
71
+ # Authorizes access to preferences.
72
+ #
73
+ # @api private
74
+ #
75
+ # @return [void]
76
+ def authorize!
77
+ authorize_mat_views!(:read, MatViews::MatViewDefinition)
78
+ end
79
+
80
+ # Reads the theme from cookies.
81
+ #
82
+ # @api private
83
+ #
84
+ # @return ["light", "dark", "auto"] theme preference or "auto" if unset/invalid
85
+ def read_theme
86
+ t = cookies[:theme].to_s
87
+ %w[light dark].include?(t) ? t : 'auto'
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Codevedas Inc. 2025-present
4
+ #
5
+ # This source code is licensed under the MIT license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+
8
+ module MatViews
9
+ module Admin
10
+ # MatViews::Admin::RunsController
11
+ # -------------------------------
12
+ # Controller for viewing materialised view run history in the admin UI.
13
+ #
14
+ # Responsibilities:
15
+ # - Provides a list of recent runs (create/refresh/delete) for all definitions.
16
+ # - Shows details for an individual run in a Turbo frame/drawer context.
17
+ #
18
+ # Filters:
19
+ # - `before_action :ensure_frame` → enforces frame-only access.
20
+ # - `before_action :set_mat_view_run` → loads and authorizes a single run.
21
+ #
22
+ # Views:
23
+ # - Index renders `mat_views/admin/runs/embed_frame` partial.
24
+ # - Show renders `mat_views/admin/runs/embed_show_drawer` partial.
25
+ #
26
+ class RunsController < ApplicationController
27
+ before_action :ensure_frame
28
+ helper_method :definition
29
+ before_action :ensure_frame, only: %i[index show]
30
+ before_action :set_mat_view_run, only: %i[show]
31
+
32
+ # GET /:lang/admin/runs
33
+ #
34
+ # Lists all materialised view runs ordered by start time.
35
+ #
36
+ # @return [void]
37
+ def index
38
+ authorize_mat_views!(:read, :mat_views_runs)
39
+
40
+ @definitions = MatViews::MatViewDefinition.order(:name).to_a
41
+ @runs = MatViews::MatViewRun.order(started_at: :desc)
42
+
43
+ %i[mat_view_definition_id operation status].each do |param|
44
+ param_value = params[param]
45
+ next unless param_value.present?
46
+
47
+ @runs = @runs.where(param => param_value)
48
+ end
49
+ render 'index', formats: :html, layout: 'mat_views/turbo_frame'
50
+ end
51
+
52
+ # GET /:lang/admin/runs/:id
53
+ #
54
+ # Displays details for a single run.
55
+ #
56
+ # @return [void]
57
+ def show
58
+ render 'show', formats: :html, layout: 'mat_views/turbo_frame'
59
+ end
60
+
61
+ private
62
+
63
+ # Loads the requested run and checks authorization.
64
+ #
65
+ # @api private
66
+ #
67
+ # @return [void]
68
+ def set_mat_view_run
69
+ @run = MatViews::MatViewRun.find(params[:id])
70
+ authorize_mat_views!(:read, @run)
71
+ end
72
+ end
73
+ end
74
+ end