smriti 0.5.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 (124) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +168 -0
  4. data/Rakefile +15 -0
  5. data/app/assets/images/smriti/android-chrome-192x192.png +0 -0
  6. data/app/assets/images/smriti/android-chrome-512x512.png +0 -0
  7. data/app/assets/images/smriti/apple-touch-icon.png +0 -0
  8. data/app/assets/images/smriti/favicon-16x16.png +0 -0
  9. data/app/assets/images/smriti/favicon-32x32.png +0 -0
  10. data/app/assets/images/smriti/favicon-48x48.png +0 -0
  11. data/app/assets/images/smriti/favicon.ico +0 -0
  12. data/app/assets/images/smriti/favicon.svg +18 -0
  13. data/app/assets/images/smriti/logo.svg +18 -0
  14. data/app/assets/images/smriti/mask-icon.svg +5 -0
  15. data/app/assets/stylesheets/smriti/application.css +1040 -0
  16. data/app/controllers/smriti/admin/application_controller.rb +135 -0
  17. data/app/controllers/smriti/admin/dashboard_controller.rb +32 -0
  18. data/app/controllers/smriti/admin/mat_view_definitions_controller.rb +372 -0
  19. data/app/controllers/smriti/admin/mat_view_runs_controller.rb +185 -0
  20. data/app/controllers/smriti/admin/preferences_controller.rb +91 -0
  21. data/app/helpers/smriti/admin/datatable_helper.rb +249 -0
  22. data/app/helpers/smriti/admin/localized_digit_helper.rb +70 -0
  23. data/app/helpers/smriti/admin/ui_helper.rb +539 -0
  24. data/app/javascript/smriti/application.js +8 -0
  25. data/app/javascript/smriti/controllers/application.js +10 -0
  26. data/app/javascript/smriti/controllers/body_setup_controller.js +120 -0
  27. data/app/javascript/smriti/controllers/datatable_controller.js +351 -0
  28. data/app/javascript/smriti/controllers/details_controller.js +200 -0
  29. data/app/javascript/smriti/controllers/drawer_controller.js +470 -0
  30. data/app/javascript/smriti/controllers/flash_controller.js +112 -0
  31. data/app/javascript/smriti/controllers/index.js +10 -0
  32. data/app/javascript/smriti/controllers/mv_confirm_controller.js +435 -0
  33. data/app/javascript/smriti/controllers/tabs_controller.js +184 -0
  34. data/app/javascript/smriti/controllers/tooltip_controller.js +525 -0
  35. data/app/javascript/smriti/controllers/turbo_frame_lifecycle_controller.js +342 -0
  36. data/app/jobs/smriti/application_job.rb +144 -0
  37. data/app/jobs/smriti/create_view_job.rb +87 -0
  38. data/app/jobs/smriti/delete_view_job.rb +89 -0
  39. data/app/jobs/smriti/refresh_view_job.rb +94 -0
  40. data/app/models/concerns/smriti_i18n.rb +139 -0
  41. data/app/models/concerns/smriti_paginate.rb +70 -0
  42. data/app/models/concerns/smriti_query_helper.rb +36 -0
  43. data/app/models/smriti/application_record.rb +39 -0
  44. data/app/models/smriti/mat_view_definition.rb +254 -0
  45. data/app/models/smriti/mat_view_run.rb +275 -0
  46. data/app/views/layouts/smriti/_footer.html.erb +47 -0
  47. data/app/views/layouts/smriti/_header.html.erb +25 -0
  48. data/app/views/layouts/smriti/admin.html.erb +47 -0
  49. data/app/views/layouts/smriti/turbo_frame.html.erb +3 -0
  50. data/app/views/smriti/admin/dashboard/index.html.erb +38 -0
  51. data/app/views/smriti/admin/mat_view_definitions/_definition_actions.html.erb +94 -0
  52. data/app/views/smriti/admin/mat_view_definitions/_dt-index-empty-row.html.erb +11 -0
  53. data/app/views/smriti/admin/mat_view_definitions/_dt-index-row.html.erb +27 -0
  54. data/app/views/smriti/admin/mat_view_definitions/empty.html.erb +1 -0
  55. data/app/views/smriti/admin/mat_view_definitions/form.html.erb +79 -0
  56. data/app/views/smriti/admin/mat_view_definitions/index.html.erb +10 -0
  57. data/app/views/smriti/admin/mat_view_definitions/show.html.erb +40 -0
  58. data/app/views/smriti/admin/mat_view_runs/_dt-index-empty-row.html.erb +11 -0
  59. data/app/views/smriti/admin/mat_view_runs/_dt-index-row.html.erb +41 -0
  60. data/app/views/smriti/admin/mat_view_runs/index.html.erb +1 -0
  61. data/app/views/smriti/admin/mat_view_runs/show.html.erb +64 -0
  62. data/app/views/smriti/admin/preferences/show.html.erb +49 -0
  63. data/app/views/smriti/admin/ui/_card.html.erb +15 -0
  64. data/app/views/smriti/admin/ui/_datatable.html.erb +34 -0
  65. data/app/views/smriti/admin/ui/_datatable_filters.html.erb +45 -0
  66. data/app/views/smriti/admin/ui/_datatable_tbody.html.erb +11 -0
  67. data/app/views/smriti/admin/ui/_datatable_tfoot.html.erb +70 -0
  68. data/app/views/smriti/admin/ui/_datatable_thead.html.erb +105 -0
  69. data/app/views/smriti/admin/ui/_details.html.erb +10 -0
  70. data/app/views/smriti/admin/ui/_flash.html.erb +6 -0
  71. data/app/views/smriti/admin/ui/_table.html.erb +8 -0
  72. data/config/importmap.rb +9 -0
  73. data/config/locales/ar.yml +223 -0
  74. data/config/locales/de.yml +230 -0
  75. data/config/locales/en-AU-ocker.yml +223 -0
  76. data/config/locales/en-AU.yml +202 -0
  77. data/config/locales/en-BORK.yml +225 -0
  78. data/config/locales/en-CA.yml +223 -0
  79. data/config/locales/en-GB.yml +223 -0
  80. data/config/locales/en-LOL.yml +219 -0
  81. data/config/locales/en-SCOT.yml +223 -0
  82. data/config/locales/en-SHAKESPEARE.yml +225 -0
  83. data/config/locales/en-US-pirate.yml +222 -0
  84. data/config/locales/en-US.yml +225 -0
  85. data/config/locales/en-YODA.yml +221 -0
  86. data/config/locales/en.yml +223 -0
  87. data/config/locales/es.yml +226 -0
  88. data/config/locales/fa.yml +223 -0
  89. data/config/locales/fr-CA.yml +227 -0
  90. data/config/locales/fr.yml +227 -0
  91. data/config/locales/he.yml +218 -0
  92. data/config/locales/hi.yml +223 -0
  93. data/config/locales/it.yml +225 -0
  94. data/config/locales/ja-JP.yml +215 -0
  95. data/config/locales/pt.yml +225 -0
  96. data/config/locales/ru.yml +228 -0
  97. data/config/locales/ur.yml +225 -0
  98. data/config/locales/zh-CN.yml +214 -0
  99. data/config/locales/zh-TW.yml +214 -0
  100. data/config/routes.rb +36 -0
  101. data/lib/ext/exception.rb +20 -0
  102. data/lib/generators/smriti/install/install_generator.rb +86 -0
  103. data/lib/generators/smriti/install/templates/create_mat_view_definitions.rb +29 -0
  104. data/lib/generators/smriti/install/templates/create_mat_view_runs.rb +32 -0
  105. data/lib/generators/smriti/install/templates/smriti_initializer.rb +23 -0
  106. data/lib/smriti/admin/auth_bridge.rb +93 -0
  107. data/lib/smriti/admin/default_auth.rb +62 -0
  108. data/lib/smriti/configuration.rb +58 -0
  109. data/lib/smriti/engine.rb +82 -0
  110. data/lib/smriti/helpers/ui_test_ids.rb +49 -0
  111. data/lib/smriti/jobs/adapter.rb +81 -0
  112. data/lib/smriti/service_response.rb +75 -0
  113. data/lib/smriti/services/base_service.rb +471 -0
  114. data/lib/smriti/services/check_matview_exists.rb +76 -0
  115. data/lib/smriti/services/concurrent_refresh.rb +94 -0
  116. data/lib/smriti/services/create_view.rb +173 -0
  117. data/lib/smriti/services/delete_view.rb +111 -0
  118. data/lib/smriti/services/regular_refresh.rb +90 -0
  119. data/lib/smriti/services/swap_refresh.rb +181 -0
  120. data/lib/smriti/version.rb +21 -0
  121. data/lib/smriti.rb +64 -0
  122. data/lib/tasks/helpers.rb +185 -0
  123. data/lib/tasks/smriti_tasks.rake +151 -0
  124. metadata +206 -0
@@ -0,0 +1,275 @@
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
+ ##
9
+ # Top-level namespace for the smriti engine.
10
+ module Smriti
11
+ ##
12
+ # ActiveRecord model that tracks the lifecycle of *runs* for
13
+ # materialised views.
14
+ #
15
+ # Each record corresponds to a single attempt to mutate a materialised view
16
+ # from a {Smriti::MatViewDefinition}, storing its status, timing, and
17
+ # any associated error or metadata.
18
+ #
19
+ # This model provides an auditable history of view provisioning across
20
+ # environments, useful for telemetry, dashboards, and debugging.
21
+ #
22
+ # @see Smriti::MatViewDefinition
23
+ # @see Smriti::CreateViewJob
24
+ #
25
+ # @example Query recent successful runs
26
+ # Smriti::MatViewRun.status_success.order(created_at: :desc).limit(10)
27
+ #
28
+ # @example Check if a definition has any failed runs
29
+ # definition.mat_view_runs.status_failed.any?
30
+ #
31
+ class MatViewRun < ApplicationRecord
32
+ ##
33
+ # Underlying database table name.
34
+ self.table_name = 'mat_view_runs'
35
+
36
+ ##
37
+ # The definition this run belongs to.
38
+ #
39
+ # @return [Smriti::MatViewDefinition]
40
+ #
41
+ belongs_to :mat_view_definition, class_name: 'Smriti::MatViewDefinition'
42
+
43
+ ##
44
+ # Status of the create run.
45
+ #
46
+ # @!attribute [r] status
47
+ # @return [Symbol] One of:
48
+ # - `:running` - currently executing
49
+ # - `:success` - completed successfully
50
+ # - `:failed` - encountered an error
51
+ #
52
+ enum :status, {
53
+ running: 0,
54
+ success: 1,
55
+ failed: 2
56
+ }, prefix: :status
57
+
58
+ # Operation type of the run.
59
+ #
60
+ # @!attribute [r] operation
61
+ # @return [Symbol] One of:
62
+ # - `:create` - initial creation of the materialised view
63
+ # - `:refresh` - refreshing an existing view
64
+ # - `:drop` - dropping the materialised view
65
+ enum :operation, {
66
+ create: 0,
67
+ refresh: 1,
68
+ drop: 2
69
+ }, prefix: :operation
70
+
71
+ ##
72
+ # Validations
73
+ #
74
+ # Ensures that a status is always present.
75
+ validates :status, presence: true
76
+
77
+ # ───────────────────────────────────────────────────────────────
78
+ # Scopes for runs, ordering, searching, filtering
79
+ # ───────────────────────────────────────────────────────────────
80
+
81
+ ##
82
+ # Scope create runs
83
+ # All runs with `operation: :create`.
84
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
85
+ scope :create_runs, -> { where(operation: :create) }
86
+
87
+ ##
88
+ # Scope refresh runs
89
+ # All runs with `operation: :refresh`.
90
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
91
+ scope :refresh_runs, -> { where(operation: :refresh) }
92
+
93
+ ##
94
+ # Scope drop runs
95
+ # All runs with `operation: :drop`.
96
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
97
+ scope :drop_runs, -> { where(operation: :drop) }
98
+
99
+ ##
100
+ # Scope ordered by operation
101
+ # Orders by the `operation` attribute using humanized enum labels.
102
+ #
103
+ # @param dir [Symbol, String] `:asc` or `:desc`
104
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
105
+ scope :ordered_by_operation, ->(dir) { ordered_by_enum(enum_values: operations, enum_name: :operation, direction: dir) }
106
+
107
+ ##
108
+ # Scope ordered by definition name
109
+ # Orders by the associated definition's `name` attribute.
110
+ #
111
+ # @param dir [Symbol, String] `:asc` or `:desc`
112
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
113
+ scope :ordered_by_definition, ->(dir) { left_joins(:mat_view_definition).order("mat_view_definitions.name #{dir.to_s.upcase}") }
114
+
115
+ ##
116
+ # Scope ordered by started_at
117
+ # Orders by the `started_at` attribute.
118
+ #
119
+ # @param dir [Symbol, String] `:asc` or `:desc`
120
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
121
+ scope :ordered_by_started_at, ->(dir) { order("started_at #{dir.to_s.upcase}") }
122
+
123
+ ##
124
+ # Scope ordered by the 'status' attribute using humanized enum labels.
125
+ #
126
+ # @param dir [Symbol, String] `:asc` or `:desc`
127
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
128
+ scope :ordered_by_status, ->(dir) { ordered_by_enum(enum_values: statuses, enum_name: :status, direction: dir) }
129
+
130
+ ##
131
+ # Scope ordered by duration_ms
132
+ # Orders by the `duration_ms` attribute.
133
+ #
134
+ # @param dir [Symbol, String] `:asc` or `:desc`
135
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
136
+ scope :ordered_by_duration_ms, ->(dir) { order("duration_ms #{dir.to_s.upcase}") }
137
+
138
+ ##
139
+ # Scope search by operation
140
+ # Searches the `operation` attribute using humanized enum labels.
141
+ #
142
+ # @param term [String] search term
143
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
144
+ scope :search_by_operation, ->(term) { search_by_enum(enum_values: operations, enum_name: :operation, term: term) }
145
+
146
+ ##
147
+ # Scope search by definition name
148
+ # Searches by the associated definition's `name` attribute using ILIKE.
149
+ #
150
+ # @param term [String] search term
151
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
152
+ scope :search_by_definition, lambda { |term|
153
+ where(<<~SQL, like: "%#{term}%")
154
+ EXISTS (
155
+ SELECT 1
156
+ FROM mat_view_definitions d
157
+ WHERE d.id = mat_view_runs.mat_view_definition_id
158
+ AND d.name ILIKE :like
159
+ )
160
+ SQL
161
+ }
162
+
163
+ ##
164
+ # Scope search by status
165
+ # Searches the `status` attribute using humanized enum labels.
166
+ #
167
+ # @param term [String] search term
168
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
169
+ scope :search_by_status, ->(term) { search_by_enum(enum_values: statuses, enum_name: :status, term: term) }
170
+
171
+ # Scope search by duration_ms
172
+ # Searches the `duration_ms` attribute by casting to text and using ILIKE.
173
+ # Also supports searching with localized "X milliseconds" format.
174
+ #
175
+ # @param term [String] search term
176
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
177
+ scope :search_by_duration_ms, lambda { |term|
178
+ term_with_ms = I18n.t('smriti.x_miliseconds', count: term)
179
+ where('CAST(duration_ms AS TEXT) ILIKE ?', "%#{term}%")
180
+ .or(where('CAST(duration_ms AS TEXT) ILIKE ?', "%#{term_with_ms}%"))
181
+ }
182
+
183
+ ##
184
+ # Scope filter by operation
185
+ # Filters by the `operation` attribute.
186
+ #
187
+ # @param operation [String, Symbol] operation value
188
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
189
+ scope :filtered_by_operation, ->(operation) { where(operation:) }
190
+
191
+ ##
192
+ # Scope filter by definition
193
+ # Filters by the associated definition's ID.
194
+ #
195
+ # @param definition_id [Integer] definition ID
196
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
197
+ scope :filtered_by_definition, lambda { |definition_id|
198
+ where(mat_view_definition_id: definition_id)
199
+ }
200
+
201
+ ##
202
+ # Scope filter by status
203
+ # Filters by the `status` attribute.
204
+ #
205
+ # @param status [String, Symbol] status value
206
+ # @return [ActiveRecord::Relation<Smriti::MatViewRun>]
207
+ scope :filtered_by_status, ->(status) { where(status:) }
208
+
209
+ # ──────────────────────────────────────────────────────────────
210
+ # Class methods
211
+ # ──────────────────────────────────────────────────────────────
212
+
213
+ class << self
214
+ # Options for filtering by operation
215
+ #
216
+ # @return [Array<Array(String, String)>] array of `[label, value]` pairs
217
+ def filter_options_for_operation
218
+ order(:operation).distinct.pluck(:operation).compact.map { |operation| [human_enum_name(:operation, operation), operation] }
219
+ end
220
+
221
+ # Options for filtering by definition
222
+ #
223
+ # @return [Array<Array(String, Integer)>] array of `[name, id]` pairs
224
+ def filter_options_for_definition
225
+ Smriti::MatViewDefinition.order(:name).pluck(:name, :id)
226
+ end
227
+
228
+ # Options for filtering by status
229
+ #
230
+ # @return [Array<Array(String, String)>] array of `[label, value]` pairs
231
+ def filter_options_for_status
232
+ order(:status).distinct.pluck(:status).compact.map { |status| [human_enum_name(:status, status), status] }
233
+ end
234
+ end
235
+
236
+ # ──────────────────────────────────────────────────────────────
237
+ # Instance methods
238
+ # ──────────────────────────────────────────────────────────────
239
+
240
+ ##
241
+ # Metadata associated with the run.
242
+ #
243
+ # This is a JSONB column storing arbitrary structured data about the run,
244
+ # such as database responses, row counts, etc.
245
+ #
246
+ # @return [Hash] Parsed JSON metadata.
247
+ def meta
248
+ self[:meta] || {}
249
+ end
250
+
251
+ ##
252
+ # Error message if the run failed.
253
+ #
254
+ # Extracted from `meta['error']['message']` if present.
255
+ #
256
+ # @return [String, nil] Error message or `nil` if none.
257
+ def error_message
258
+ meta.dig('error', 'message')
259
+ end
260
+
261
+ ##
262
+
263
+ # row count before the operation, if applicable
264
+ # @return [Integer, nil]
265
+ def row_count_before
266
+ meta.dig('response', 'row_count_before')
267
+ end
268
+
269
+ # row count after the operation, if applicable
270
+ # @return [Integer, nil]
271
+ def row_count_after
272
+ meta.dig('response', 'row_count_after')
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,47 @@
1
+ <footer class="mv-footer">
2
+ <div class="mv-container mv-footer-row">
3
+ <div>
4
+ <%= t("smriti.footer.tagline") %><br>
5
+ <%= t("smriti.footer.copyright", year: Time.current.year, company: Smriti::Engine.company_name) %>
6
+ </div>
7
+ <div class="mv-text-right">
8
+ <%= mv_link_to Smriti::Engine.rubygems_uri, tooltip: t("smriti.footer.tooltip.gem_version"), testid: 'GEM_LINK' do %>
9
+ <strong><%= Smriti::Engine.project_name %></strong>
10
+ <%= t("smriti.footer.version", version: Smriti::Engine.project_version) %><br>
11
+ <% end %>
12
+ <%= mv_link_to t("smriti.footer.project_homepage"),
13
+ Smriti::Engine.project_homepage,
14
+ tooltip: t("smriti.footer.tooltip.project_homepage"),
15
+ testid: "PROJECT_HOMEPAGE_LINK" %>
16
+ <%= mv_link_to t("smriti.footer.open_issue"), Smriti::Engine.bug_tracker_uri, tooltip: t("smriti.footer.tooltip.open_issue"), testid: "OPEN_ISSUE_LINK" %>
17
+ <%= mv_link_to t("smriti.footer.documentation"),
18
+ Smriti::Engine.documentation_uri,
19
+ tooltip: t("smriti.footer.tooltip.documentation"),
20
+ testid: "DOCUMENTATION_LINK" %>
21
+ <br/>
22
+ <span class="text-rose-700"><%= t("smriti.footer.need_help") %></span>
23
+ <%= mv_link_to t("smriti.footer.support"), Smriti::Engine.support_uri, tooltip: t("smriti.footer.tooltip.support"), testid: "SUPPORT_LINK" %>
24
+ </div>
25
+ </div>
26
+ </footer>
27
+ <div class="mv-drawer-root" aria-hidden="true" data-drawer-target="root">
28
+ <div data-drawer-target="overlay" class="mv-drawer-overlay" data-action="click->drawer#close"></div>
29
+ <aside data-drawer-target="panel" class="mv-drawer" role="dialog" aria-modal="true">
30
+ <div class="mv-drawer-head" aria-label="<%= t('smriti.details') %>" data-drawer-target="header">
31
+ <h2 id="mv-drawer-title" data-drawer-target="title">
32
+ <%= t("smriti.details") %>
33
+ </h2>
34
+ <div class='row-item'>
35
+ <%= mv_drawer_action_button(t("smriti.refresh"), "refresh", t("smriti.refresh_contents"), "left", testid: "DRAWER_REFRESH_LINK") { mv_icon(:refresh) } %>
36
+ <%= mv_drawer_action_button(t("smriti.close"), "close", t("smriti.close_window"), "left", testid: "DRAWER_CLOSE_LINK") { mv_icon(:x_circle) } %>
37
+ </div>
38
+ </div>
39
+ <div class="mv-drawer-body">
40
+ <turbo-frame id="mv-drawer" data-drawer-target="frame" data-turbo-frame-lifecycle-target="frame">
41
+ <div class="mv-card">
42
+ <div class="mv-card-b"><%= t("smriti.loading") %></div>
43
+ </div>
44
+ </turbo-frame>
45
+ </div>
46
+ </aside>
47
+ </div>
@@ -0,0 +1,25 @@
1
+ <header class="mv-header">
2
+ <div class="mv-container mv-header-row">
3
+ <div class="row-item">
4
+ <%= mv_link_to admin_root_path, class: "mv-brand", underline: false, testid: 'HEADER_LINK' do %>
5
+ <%= image_tag "smriti/logo.svg", alt: t("smriti.title"), class: "mv-logo" %>
6
+ <span><%= t("smriti.title") %></span>
7
+ <% end %>
8
+ </div>
9
+ <div class='row-item'>
10
+ <% if user %>
11
+ <span>
12
+ <%= t("smriti.header.signed_in_as", email: user.respond_to?(:email) ? user.email : user.to_s) %>
13
+ </span>
14
+ <% end %>
15
+ <%= mv_drawer_link(
16
+ admin_preferences_path(frame_id: "mv-drawer"),
17
+ t("smriti.settings.title"),
18
+ variant: :ghost,
19
+ tooltip: t("smriti.settings.title"),
20
+ tooltip_placement: "bottom",
21
+ testid: "PREFERENCES_LINK",
22
+ ) { mv_icon(:gear) } %>
23
+ </div>
24
+ </div>
25
+ </header>
@@ -0,0 +1,47 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= I18n.locale %>" dir="<%= %i(ar he fa ur).include?(I18n.locale.to_sym) ? 'rtl' : 'ltr' %>" data-theme="<%= smriti_data_theme || 'auto' %>">
3
+ <head>
4
+ <title><%= t("smriti.title") %><%= content_for?(:page_title) ? " | #{content_for(:page_title)}" : "" %></title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+ <meta charset="utf-8">
8
+ <meta name="description" content="<%= t('smriti.project_description') %>">
9
+ <meta name="author" content="<%= t('smriti.project_author') %>">
10
+ <meta name="keywords" content="<%= t('smriti.project_tags') %>">
11
+ <meta name="viewport" content="width=device-width, initial-scale=1">
12
+ <meta name="color-scheme" content="<%= %w[light dark].include?(smriti_data_theme) ? smriti_data_theme : 'light dark' %>">
13
+ <meta http-equiv="Content-Language" content="<%= I18n.locale %>">
14
+ <link rel="preconnect" href="https://fonts.googleapis.com">
15
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16
+ <link
17
+ href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Space+Grotesk:wght@300..700&family=Work+Sans:ital,wght@0,100..900;1,100..900&display=swap"
18
+ rel="stylesheet"
19
+ >
20
+ <%= favicon_link_tag "smriti/favicon.svg", type: "image/svg+xml" %>
21
+ <%= favicon_link_tag "smriti/favicon.ico", rel: "icon", sizes: "16x16 32x32 48x48" %>
22
+ <%= favicon_link_tag "smriti/favicon-16x16.png", rel: "icon", type: "image/png", sizes: "16x16" %>
23
+ <%= favicon_link_tag "smriti/favicon-32x32.png", rel: "icon", type: "image/png", sizes: "32x32" %>
24
+ <%= favicon_link_tag "smriti/favicon-48x48.png", rel: "icon", type: "image/png", sizes: "48x48" %>
25
+ <%= favicon_link_tag "smriti/apple-touch-icon.png", rel: "apple-touch-icon" %>
26
+ <%= favicon_link_tag "smriti/mask-icon.svg", rel: "mask-icon", color: "#0F172A" %>
27
+ <%= favicon_link_tag "smriti/android-chrome-192x192.png", rel: "icon", type: "image/png", sizes: "192x192" %>
28
+ <%= favicon_link_tag "smriti/android-chrome-512x512.png", rel: "icon", type: "image/png", sizes: "512x512" %>
29
+ <%= tag.meta name: "theme-color", content: "#0F172A" %>
30
+ <%= stylesheet_link_tag "smriti/application.css", media: "all", "data-turbo-track": "reload" %>
31
+ <%= javascript_importmap_tags "smriti/application", importmap: Smriti.importmap %>
32
+
33
+ <%= javascript_tag do %>
34
+ window.SmritiRoutes = { definitionsPath: "<%= admin_mat_view_definitions_path %>", runsPath: "<%= admin_mat_view_runs_path %>", preferencesPath: "<%= admin_preferences_path %>" }
35
+ <% end %>
36
+ </head>
37
+ <body class="mv-shell" data-controller="body-setup turbo-frame-lifecycle drawer mv-confirm" data-drawer-open-class="is-open">
38
+ <%= render "layouts/smriti/header" %>
39
+ <main class="mv-main">
40
+ <div class="mv-container">
41
+ <%= render "smriti/admin/ui/flash" %>
42
+ <%= yield %>
43
+ </div>
44
+ </main>
45
+ <%= render "layouts/smriti/footer" %>
46
+ </body>
47
+ </html>
@@ -0,0 +1,3 @@
1
+ <turbo-frame id="<%= @frame_id %>" class="mv-frame" data-turbo-frame-lifecycle-target="frame">
2
+ <%= yield %>
3
+ </turbo-frame>
@@ -0,0 +1,38 @@
1
+ <h1><%= t("smriti.dashboard.title") %></h1>
2
+ <div class="mv-card" style="margin-bottom:12px;">
3
+ <div class="mv-card-h"><%= t("smriti.dashboard.metrics.title") %></div>
4
+ <div class="mv-card-b">
5
+ <div class="mv-flash mv-flash--ok">
6
+ <%= @metrics_note %>
7
+ </div>
8
+ </div>
9
+ </div>
10
+ <div data-controller="tabs" data-tabs-active-link-class="mv-tab--on" data-tabs-default-tab-value="definitions">
11
+ <nav class="mv-tabs" style="margin-bottom:1rem;">
12
+ <%= mv_tab_link 'definitions', selected: true, testid: 'DEFINITIONS_TAB_LINK' do %>
13
+ <%= t("smriti.definitions") %>
14
+ <% end %>
15
+ <%= mv_tab_link 'runs', selected: false, testid: 'RUNS_TAB_LINK' do %>
16
+ <%= t("smriti.runs") %>
17
+ <% end %>
18
+ </nav>
19
+ <div data-tabs-target="panel" data-name="definitions">
20
+ <turbo-frame
21
+ id="dash-definitions"
22
+ data-src="<%= admin_mat_view_definitions_path(frame_id: 'dash-definitions') %>"
23
+ data-turbo-frame-lifecycle-target="frame"
24
+ data-turbo-temporary
25
+ >
26
+ <div class="mv-card">
27
+ <div class="mv-card-b"><%= t("smriti.loading_definitions") %></div>
28
+ </div>
29
+ </turbo-frame>
30
+ </div>
31
+ <div data-tabs-target="panel" data-name="runs" hidden>
32
+ <turbo-frame id="dash-runs" data-src="<%= admin_mat_view_runs_path(frame_id: 'dash-runs') %>" data-turbo-frame-lifecycle-target="frame" data-turbo-temporary>
33
+ <div class="mv-card">
34
+ <div class="mv-card-b"><%= t("smriti.loading_runs") %></div>
35
+ </div>
36
+ </turbo-frame>
37
+ </div>
38
+ </div>
@@ -0,0 +1,94 @@
1
+ <div class="mv-actions">
2
+ <!-- GROUP: Definition -->
3
+ <div class="mv-action-group">
4
+ <div class="mv-action-title"><%= mv_icon(:layers) %>
5
+ <%= t("smriti.definition") %></div>
6
+ <div class="mv-buttons">
7
+ <%= mv_button_link admin_root_path(tab: 'runs', dtfilter: "definition:#{defn.id}"),
8
+ variant: :ghost,
9
+ testid: 'VIEW_HISTORY_LINK',
10
+ testid_identifier: "defn-#{defn.id}",
11
+ data: { turbo: false, turbo_frame: "_top" } do %>
12
+ <%= mv_icon(:history) %>
13
+ <%= t("smriti.history") %>
14
+ <% end %>
15
+ <%= mv_drawer_link(edit_admin_mat_view_definition_path(defn, frame_id: 'mv-drawer'),
16
+ t("smriti.edit_var", name: defn.name),
17
+ testid: 'EDIT_LINK',
18
+ testid_identifier: "defn-#{defn.id}",
19
+ tooltip: t("smriti.mat_view_definition.edit_tooltip")) do %>
20
+ <%= mv_icon(:edit) %>
21
+ <%= t("smriti.edit") %>
22
+ <% end %>
23
+ <%= mv_button_to admin_mat_view_definition_path(defn, frame_id: frame_id),
24
+ method: :delete,
25
+ variant: :negative,
26
+ testid: 'DELETE_LINK',
27
+ testid_identifier: "defn-#{defn.id}",
28
+ tooltip: t("smriti.mat_view_definition.delete_tooltip"), tooltip_placement: "bottom",
29
+ confirm: t("smriti.mat_view_definition.delete_confirm", name: defn.name) do %>
30
+ <%= mv_icon(:trash) %>
31
+ <%= t("smriti.delete") %>
32
+ <% end %>
33
+ </div>
34
+ </div>
35
+ <!-- GROUP: Materialized View -->
36
+ <div class="mv-action-group">
37
+ <div class="mv-action-title">
38
+ <%= mv_icon(:database) %>
39
+ <%= t("smriti.mat_view_definition.materialized_view") %>
40
+ <% if mv_exists %>
41
+ <span class="mv-status-icon" data-controller="tooltip" data-tooltip-text-value="<%= t("smriti.mat_view_definition.materialized_view_exists") %>">
42
+ <%= mv_icon(:check_circle, class_name: "mv-status-ok") %>
43
+ </span>
44
+ <% else %>
45
+ <span class="mv-status-icon" data-controller="tooltip" data-tooltip-text-value="<%= t("smriti.mat_view_definition.materialized_view_not_exists") %>">
46
+ <%= mv_icon(:x_circle, class_name: "mv-status-missing") %>
47
+ </span>
48
+ <% end %>
49
+ </div>
50
+ <div class="mv-buttons">
51
+ <% if mv_exists %>
52
+ <%= mv_button_to refresh_admin_mat_view_definition_path(defn, frame_id: frame_id),
53
+ variant: :primary,
54
+ method: :post,
55
+ testid: 'REFRESH_LINK',
56
+ testid_identifier: "defn-#{defn.id}",
57
+ tooltip: t("smriti.mat_view_definition.refresh_tooltip") do %>
58
+ <%= mv_icon(:refresh) %>
59
+ <%= t("smriti.refresh") %>
60
+ <% end %>
61
+ <%= mv_button_to delete_now_admin_mat_view_definition_path(defn, frame_id: frame_id),
62
+ method: :post,
63
+ variant: :negative,
64
+ testid: 'DROP_LINK',
65
+ testid_identifier: "defn-#{defn.id}",
66
+ tooltip: t("smriti.mat_view_definition.drop_mv_tooltip"), tooltip_placement: "bottom",
67
+ confirm: t("smriti.mat_view_definition.drop_mv_confirm", name: defn.name) do %>
68
+ <%= mv_icon(:x_circle) %>
69
+ <%= t("smriti.mat_view_definition.drop_mv") %>
70
+ <% end %>
71
+ <%= mv_button_to delete_now_admin_mat_view_definition_path(defn, frame_id: frame_id, cascade: true),
72
+ method: :post,
73
+ variant: :negative,
74
+ testid: 'DROP_CASCADE_LINK',
75
+ testid_identifier: "defn-#{defn.id}",
76
+ tooltip: t("smriti.mat_view_definition.drop_mv_cascade_tooltip"), tooltip_placement: "bottom",
77
+ confirm: t("smriti.mat_view_definition.drop_mv_cascade_confirm", name: defn.name) do %>
78
+ <%= mv_icon(:x_circle) %>
79
+ <%= t("smriti.mat_view_definition.drop_mv_cascade") %>
80
+ <% end %>
81
+ <% else %>
82
+ <%= mv_button_to create_now_admin_mat_view_definition_path(defn, frame_id: frame_id),
83
+ method: :post,
84
+ variant: :secondary,
85
+ testid: 'CREATE_MV_LINK',
86
+ testid_identifier: "defn-#{defn.id}",
87
+ tooltip: t("smriti.mat_view_definition.create_mv_tooltip"), tooltip_placement: "bottom" do %>
88
+ <%= mv_icon(:hammer) %>
89
+ <%= t("smriti.mat_view_definition.create_mv") %>
90
+ <% end %>
91
+ <% end %>
92
+ </div>
93
+ </div>
94
+ </div>
@@ -0,0 +1,11 @@
1
+ <tr class="mv-tr">
2
+ <td class="mv-td" colspan="<%= dt_config[:columns].size %>">
3
+ <div class="mv-cell">
4
+ <% if params[:dtfilter] || params[:dtsearch] %>
5
+ <%= t("smriti.mat_view_definition.no_definitions_if_filtered") %>
6
+ <% else %>
7
+ <%= t("smriti.mat_view_definition.no_definitions") %>
8
+ <% end %>
9
+ </div>
10
+ </td>
11
+ </tr>
@@ -0,0 +1,27 @@
1
+ <% dt_humanize_ref = dt_config[:dt_humanize_ref]
2
+ dt_humanize_ref_klass = dt_humanize_ref.constantize
3
+ humanize_attr = ->(attr) { dt_humanize_ref_klass.human_attribute_name(attr) }
4
+ humanize_enum_attr = ->(col, val) { dt_humanize_ref_klass.human_enum_name(col, val) }
5
+ mv_exists_map ||= {} %>
6
+ <tr class="mv-tr">
7
+ <td class="mv-td">
8
+ <div class="mv-cell">
9
+ <%= mv_drawer_link(admin_mat_view_definition_path(row_value, frame_id: 'mv-drawer'), t("smriti.view_var", name: row_value.name),
10
+ variant: :ghost,
11
+ testid: 'VIEW_LINK',
12
+ testid_identifier: "defn-#{row_value.id}",
13
+ underline: true,
14
+ tooltip: t("smriti.mat_view_definition.view_tooltip")) do %>
15
+ <%= row_value.name %>
16
+ <% end %>
17
+ </div>
18
+ </td>
19
+ <td class="mv-td"><div class="mv-cell"><%= humanize_enum_attr.call(:refresh_strategy, row_value.refresh_strategy) %></div></td>
20
+ <td class="mv-td"><div class="mv-cell"><%= row_value.schedule_cron.presence || "-" %></div></td>
21
+ <td class="mv-td"><div class="mv-cell"><%= row_value.last_run ? l_with_digits(row_value.last_run.started_at.in_time_zone, format: :datetime12hour) : "-" %></div></td>
22
+ <td class="mv-td mv-td-actions">
23
+ <div class="mv-cell">
24
+ <%= render "definition_actions", defn: row_value, mv_exists: mv_exists_map[row_value.name], frame_id: "dash-definitions" %>
25
+ </div>
26
+ </td>
27
+ </tr>
@@ -0,0 +1 @@
1
+ <h1><%= t("smriti.reloading") %></h1>
@@ -0,0 +1,79 @@
1
+ <% action = @definition.new_record? ? admin_mat_view_definitions_path(frame_id: "mv-drawer") : admin_mat_view_definition_path(@definition, frame_id: "mv-drawer") %>
2
+ <% method = @definition.new_record? ? :post : :patch %>
3
+
4
+ <input
5
+ type="hidden"
6
+ id="mv-drawer-title-text"
7
+ value="<%= @definition.new_record? ? t('smriti.mat_view_definition.new_definition') : t("smriti.edit_var", name: @definition.name) %>"
8
+ />
9
+
10
+ <input type="hidden" id="mv-drawer-open-url-identifier" value="definitions_<%= @definition.new_record? ? 'new' : "edit_#{@definition.id}" %>"/>
11
+
12
+ <%= form_with model: @definition, url: action, method: method, class: "mv-form" do |f| %>
13
+ <% if @definition.errors.any? %>
14
+ <div class="mv-flash mv-flash--err">
15
+ <strong><%= t("smriti.errors.prevented_saving", count: @definition.errors.count) %></strong>
16
+ <ul style="margin:.25rem 1rem;">
17
+ <% @definition.errors.full_messages.each do |msg| %>
18
+ <li><%= msg %></li>
19
+ <% end %>
20
+ </ul>
21
+ </div>
22
+ <% end %>
23
+
24
+ <div class="mv-field">
25
+ <%= f.label :name, class: "mv-label" %>
26
+ <%= f.text_field :name, class: "mv-input", required: true, placeholder: Smriti::MatViewDefinition.placeholder_for(:name) %>
27
+ <%= content_tag :small, Smriti::MatViewDefinition.hint_for(:name), class: "mv-hint" %>
28
+
29
+ </div>
30
+
31
+ <div class="mv-field">
32
+ <%= f.label :sql, class: "mv-label" %>
33
+ <%= f.text_area :sql, class: "mv-textarea", placeholder: Smriti::MatViewDefinition.placeholder_for(:sql), required: true %>
34
+ <%= content_tag :small, Smriti::MatViewDefinition.hint_for(:sql), class: "mv-hint" %>
35
+ </div>
36
+
37
+ <div class="mv-field">
38
+ <%= f.label :refresh_strategy, class: "mv-label" %>
39
+ <%= f.select :refresh_strategy,
40
+ options_for_select(Smriti::MatViewDefinition.human_enum_options(:refresh_strategy), @definition.refresh_strategy),
41
+ {},
42
+ class: "mv-select" %>
43
+ <%= content_tag :small, Smriti::MatViewDefinition.hint_for(:refresh_strategy), class: "mv-hint" %>
44
+ </div>
45
+
46
+ <div class="mv-field">
47
+ <%= f.label :schedule_cron, class: "mv-label" %>
48
+ <%= f.text_field :schedule_cron, class: "mv-input", placeholder: Smriti::MatViewDefinition.placeholder_for(:schedule_cron) %>
49
+ <%= content_tag :small, Smriti::MatViewDefinition.hint_for(:schedule_cron), class: "mv-hint" %>
50
+ </div>
51
+
52
+ <div class="mv-field">
53
+ <%= f.label :unique_index_columns, class: "mv-label" %>
54
+ <% current_uic = Array(@definition.unique_index_columns).join(", ") %>
55
+ <%= f.text_field :unique_index_columns, value: current_uic, class: "mv-input", placeholder: Smriti::MatViewDefinition.placeholder_for(:unique_index_columns) %>
56
+ <%= content_tag :small, Smriti::MatViewDefinition.hint_for(:unique_index_columns), class: "mv-hint" %>
57
+ </div>
58
+
59
+ <div class="mv-field">
60
+ <%= f.label :dependencies, class: "mv-label" %>
61
+ <% current_dep = Array(@definition.dependencies).join(", ") %>
62
+ <%= f.text_field :dependencies, value: current_dep, class: "mv-input", placeholder: Smriti::MatViewDefinition.placeholder_for(:dependencies) %>
63
+ <%= content_tag :small, Smriti::MatViewDefinition.hint_for(:dependencies), class: "mv-hint" %>
64
+ </div>
65
+
66
+ <div style="display:flex;gap:.5rem;margin-top:.75rem;">
67
+ <%= mv_submit_button class: "mv-btn mv-btn--primary",
68
+ testid: 'SUBMIT_BUTTON',
69
+ testid_identifier: "defn-#{@definition.id || 'new'}" do %>
70
+ <%= @definition.new_record? ? t("smriti.create") : t("smriti.save_changes") %>
71
+ <% end %>
72
+
73
+ <%= mv_cancel_button variant: :ghost, data: { action: "click->drawer#close" },
74
+ testid: 'CANCEL_BUTTON',
75
+ testid_identifier: "defn-#{@definition.id || 'new'}" do %>
76
+ <%= t("smriti.cancel") %>
77
+ <% end %>
78
+ </div>
79
+ <% end %>