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,139 @@
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
+ # MatViewsI18n
10
+ # ------------
11
+ # Concern that adds convenient **class-level** helpers for model I18n:
12
+ # - Humanized attribute names
13
+ # - Humanized enum values
14
+ # - Select-friendly enum option arrays
15
+ # - Placeholders and hints for forms
16
+ #
17
+ # These helpers rely on Rails’ standard i18n model keys using the model’s
18
+ # `model_name.i18n_key` (e.g. `MatViews::MatViewDefinition` → `mat_views/mat_view_definition`).
19
+ #
20
+ # ## Expected i18n structure (examples)
21
+ #
22
+ # ```yml
23
+ # en-US:
24
+ # activerecord:
25
+ # attributes:
26
+ # mat_views/mat_view_definition:
27
+ # name: "View name"
28
+ # sql: "SQL"
29
+ # enums:
30
+ # mat_views/mat_view_definition:
31
+ # refresh_strategy:
32
+ # regular: "Regular"
33
+ # concurrent: "Concurrent"
34
+ # swap: "Swap"
35
+ # placeholders:
36
+ # mat_views/mat_view_definition:
37
+ # name: "e.g. monthly_sales_mv"
38
+ # hints:
39
+ # mat_views/mat_view_definition:
40
+ # sql: "Use a SELECT statement; no trailing semicolon."
41
+ # ```
42
+ #
43
+ # ## Usage
44
+ # ```ruby
45
+ # MatViews::MatViewDefinition.human_name(:name) # => "View name"
46
+ # MatViews::MatViewDefinition.human_enum_name(:refresh_strategy, :regular) # => "Regular"
47
+ # MatViews::MatViewDefinition.human_enum_options(:refresh_strategy)
48
+ # # => [["Regular","regular"], ["Concurrent","concurrent"], ["Swap","swap"]]
49
+ # MatViews::MatViewDefinition.placeholder_for(:name) # => "e.g. monthly_sales_mv"
50
+ # MatViews::MatViewDefinition.hint_for(:sql) # => "Use a SELECT statement..."
51
+ # ```
52
+ #
53
+ # @note Methods are added as **class methods** to the including model.
54
+ #
55
+ # @!method self.human_name(attribute)
56
+ # Humanized (translated) attribute label for this model.
57
+ # Falls back to `attribute.to_s.humanize` when missing.
58
+ # @param attribute [Symbol, String]
59
+ # @return [String]
60
+ #
61
+ # @!method self.human_enum_name(enum_name, enum_value)
62
+ # Humanized (translated) enum value label.
63
+ # Falls back to `enum_value.to_s.humanize` when missing.
64
+ # @param enum_name [Symbol, String] the enum definition name
65
+ # @param enum_value [Symbol, String, Integer] the value/key of the enum
66
+ # @return [String]
67
+ #
68
+ # @!method self.human_enum_options(enum_name)
69
+ # Options array suitable for Rails `options_for_select`.
70
+ # @param enum_name [Symbol, String]
71
+ # @return [Array<Array(String, String)>] each item is `[label, value]`
72
+ #
73
+ # @!method self.placeholder_for(attribute)
74
+ # Form placeholder for the given attribute.
75
+ # Returns empty string if not defined.
76
+ # @param attribute [Symbol, String]
77
+ # @return [String]
78
+ #
79
+ # @!method self.hint_for(attribute)
80
+ # Form hint/help text for the given attribute.
81
+ # Returns empty string if not defined.
82
+ # @param attribute [Symbol, String]
83
+ # @return [String]
84
+ #
85
+ module MatViewsI18n
86
+ extend ActiveSupport::Concern
87
+
88
+ class_methods do
89
+ # @return [String]
90
+ def human_name(attribute)
91
+ I18n.t(
92
+ "activerecord.attributes.#{model_name.i18n_key}.#{attribute}",
93
+ default: attribute.to_s.humanize
94
+ )
95
+ end
96
+
97
+ # human_enum_name(:refresh_strategy, :regular) → "Regular"
98
+ #
99
+ # @param enum_name [Symbol, String]
100
+ # @param enum_value [Symbol, String, Integer]
101
+ # @return [String]
102
+ def human_enum_name(enum_name, enum_value)
103
+ key = enum_value.to_s
104
+ I18n.t(
105
+ "activerecord.enums.#{model_name.i18n_key}.#{enum_name}.#{key}",
106
+ default: key.humanize
107
+ )
108
+ end
109
+
110
+ # human_enum_options(:refresh_strategy)
111
+ # → [["Regular","regular"], ["Concurrent","concurrent"], ["Swap","swap"]]
112
+ #
113
+ # @param enum_name [Symbol, String]
114
+ # @return [Array<Array(String, String)>]
115
+ def human_enum_options(enum_name)
116
+ public_send(enum_name.to_s.pluralize).keys.map do |val|
117
+ [human_enum_name(enum_name, val), val]
118
+ end
119
+ end
120
+
121
+ # @param attribute [Symbol, String]
122
+ # @return [String]
123
+ def placeholder_for(attribute)
124
+ I18n.t(
125
+ "activerecord.placeholders.#{model_name.i18n_key}.#{attribute}",
126
+ default: ''
127
+ )
128
+ end
129
+
130
+ # @param attribute [Symbol, String]
131
+ # @return [String]
132
+ def hint_for(attribute)
133
+ I18n.t(
134
+ "activerecord.hints.#{model_name.i18n_key}.#{attribute}",
135
+ default: ''
136
+ )
137
+ end
138
+ end
139
+ end
@@ -30,5 +30,6 @@ module MatViews
30
30
  # @return [void]
31
31
  #
32
32
  self.abstract_class = true
33
+ include MatViewsI18n
33
34
  end
34
35
  end
@@ -9,9 +9,9 @@
9
9
  # Top-level namespace for the mat_views engine.
10
10
  module MatViews
11
11
  ##
12
- # Represents a **materialized view definition** managed by the engine.
12
+ # Represents a **materialised view definition** managed by the engine.
13
13
  #
14
- # A definition stores the canonical name and SQL for a materialized view and
14
+ # A definition stores the canonical name and SQL for a materialised view and
15
15
  # drives lifecycle operations (create, refresh, delete) via background jobs
16
16
  # and services. It also tracks operational history through associated
17
17
  # run models.
@@ -60,8 +60,7 @@ module MatViews
60
60
 
61
61
  ##
62
62
  # @!attribute name
63
- # @return [String] PostgreSQL identifier for the materialized view.
64
- #
63
+ # validates :name that must be present, unique, and a valid identifier.
65
64
  validates :name,
66
65
  presence: true,
67
66
  uniqueness: true,
@@ -69,11 +68,17 @@ module MatViews
69
68
 
70
69
  ##
71
70
  # @!attribute sql
72
- # @return [String] SELECT statement used to materialize the view.
73
- #
71
+ # validates :sql that must be present and begin with SELECT.
74
72
  validates :sql,
75
73
  presence: true,
76
- format: { with: /\A\s*SELECT/i, message: 'must begin with a SELECT' }
74
+ format: { with: /\A\s*SELECT/i, message: :invalid }
75
+
76
+ ##
77
+ # @!attribute unique_index_columns
78
+ # validates :unique_index_columns to be non-empty when using `refresh_strategy=concurrent`.
79
+ validates :unique_index_columns,
80
+ length: { minimum: 1, message: :at_least_one },
81
+ if: -> { refresh_strategy == 'concurrent' }
77
82
 
78
83
  # ────────────────────────────────────────────────────────────────
79
84
  # Enums / configuration
@@ -10,9 +10,9 @@
10
10
  module MatViews
11
11
  ##
12
12
  # ActiveRecord model that tracks the lifecycle of *runs* for
13
- # materialized views.
13
+ # materialised views.
14
14
  #
15
- # Each record corresponds to a single attempt to mutate a materialized view
15
+ # Each record corresponds to a single attempt to mutate a materialised view
16
16
  # from a {MatViews::MatViewDefinition}, storing its status, timing, and
17
17
  # any associated error or metadata.
18
18
  #
@@ -45,25 +45,23 @@ module MatViews
45
45
  #
46
46
  # @!attribute [r] status
47
47
  # @return [Symbol] One of:
48
- # - `:pending` queued but not yet started
49
- # - `:running` currently executing
50
- # - `:success` completed successfully
51
- # - `:failed` — encountered an error
48
+ # - `:running` - currently executing
49
+ # - `:success` - completed successfully
50
+ # - `:failed` - encountered an error
52
51
  #
53
52
  enum :status, {
54
- pending: 0,
55
- running: 1,
56
- success: 2,
57
- failed: 3
53
+ running: 0,
54
+ success: 1,
55
+ failed: 2
58
56
  }, prefix: :status
59
57
 
60
58
  # Operation type of the run.
61
59
  #
62
60
  # @!attribute [r] operation
63
61
  # @return [Symbol] One of:
64
- # - `:create` initial creation of the materialized view
65
- # - `:refresh` refreshing an existing view
66
- # - `:drop` dropping the materialized view
62
+ # - `:create` - initial creation of the materialised view
63
+ # - `:refresh` - refreshing an existing view
64
+ # - `:drop` - dropping the materialised view
67
65
  enum :operation, {
68
66
  create: 0,
69
67
  refresh: 1,
@@ -0,0 +1,41 @@
1
+ <footer class="mv-footer">
2
+ <div class="mv-container mv-footer-row">
3
+ <div>
4
+ <%= mv_t("footer.tagline") %><br>
5
+ <%= mv_t("footer.copyright", year: Time.current.year, company: MatViews::Engine.company_name) %>
6
+ </div>
7
+ <div class="mv-text-right">
8
+ <%= mv_link_to MatViews::Engine.rubygems_uri, tooltip: mv_t("footer.tooltip.gem_version"), testid: 'GEM_LINK' do %>
9
+ <strong><%= MatViews::Engine.project_name %></strong>
10
+ <%= mv_t("footer.version", version: MatViews::Engine.project_version) %><br>
11
+ <% end %>
12
+ <%= mv_link_to mv_t("footer.project_homepage"), MatViews::Engine.project_homepage, tooltip: mv_t("footer.tooltip.project_homepage"), testid: "PROJECT_HOMEPAGE_LINK" %>
13
+ <%= mv_link_to mv_t("footer.open_issue"), MatViews::Engine.bug_tracker_uri, tooltip: mv_t("footer.tooltip.open_issue"), testid: "OPEN_ISSUE_LINK" %>
14
+ <%= mv_link_to mv_t("footer.documentation"), MatViews::Engine.documentation_uri, tooltip: mv_t("footer.tooltip.documentation"), testid: "DOCUMENTATION_LINK" %>
15
+ <br/>
16
+ <span class="text-rose-700"><%= mv_t("footer.need_help") %></span>
17
+ <%= mv_link_to mv_t("footer.support"), MatViews::Engine.support_uri, tooltip: mv_t("footer.tooltip.support"), testid: "SUPPORT_LINK" %>
18
+ </div>
19
+ </div>
20
+ </footer>
21
+ <div class="mv-drawer-root" aria-hidden="true">
22
+ <div data-drawer-target="overlay" class="mv-drawer-overlay" data-action="click->drawer#close"></div>
23
+ <aside data-drawer-target="panel" class="mv-drawer" role="dialog" aria-modal="true">
24
+ <div class="mv-drawer-head" aria-label="<%= mv_t('details') %>">
25
+ <h2 id="mv-drawer-title">
26
+ <%= mv_t("details") %>
27
+ </h2>
28
+ <div class='row-item'>
29
+ <%= mv_drawer_action_button(mv_t("refresh"), "refresh", mv_t("refresh_contents"), "left", testid: "DRAWER_REFRESH_LINK") { mv_icon(:refresh) } %>
30
+ <%= mv_drawer_action_button(mv_t("close"), "close", mv_t("close_window"), "left", testid: "DRAWER_CLOSE_LINK") { mv_icon(:x_circle) } %>
31
+ </div>
32
+ </div>
33
+ <div class="mv-drawer-body">
34
+ <turbo-frame id="mv-drawer" data-drawer-target="frame">
35
+ <div class="mv-card">
36
+ <div class="mv-card-b"><%= mv_t("loading") %></div>
37
+ </div>
38
+ </turbo-frame>
39
+ </div>
40
+ </aside>
41
+ </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 "mat_views/logo.svg", alt: mv_t("title"), class: "mv-logo" %>
6
+ <span><%= mv_t("title") %></span>
7
+ <% end %>
8
+ </div>
9
+ <div class='row-item'>
10
+ <% if user %>
11
+ <span>
12
+ <%= mv_t("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
+ mv_t("settings.title"),
18
+ variant: :ghost,
19
+ tooltip: mv_t("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="<%= mat_views_data_theme || 'auto' %>">
3
+ <head>
4
+ <title><%= t("mat_views.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="<%= mv_t('project_description') %>">
9
+ <meta name="author" content="<%= mv_t('project_author') %>">
10
+ <meta name="keywords" content="<%= mv_t('project_tags') %>">
11
+ <meta name="viewport" content="width=device-width, initial-scale=1">
12
+ <meta name="color-scheme" content="<%= mat_views_data_theme || 'auto' %>">
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 "mat_views/favicon.svg", type: "image/svg+xml" %>
21
+ <%= favicon_link_tag "mat_views/favicon.ico", rel: "icon", sizes: "16x16 32x32 48x48" %>
22
+ <%= favicon_link_tag "mat_views/favicon-16x16.png", rel: "icon", type: "image/png", sizes: "16x16" %>
23
+ <%= favicon_link_tag "mat_views/favicon-32x32.png", rel: "icon", type: "image/png", sizes: "32x32" %>
24
+ <%= favicon_link_tag "mat_views/favicon-48x48.png", rel: "icon", type: "image/png", sizes: "48x48" %>
25
+ <%= favicon_link_tag "mat_views/apple-touch-icon.png", rel: "apple-touch-icon" %>
26
+ <%= favicon_link_tag "mat_views/mask-icon.svg", rel: "mask-icon", color: "#0F172A" %>
27
+ <%= favicon_link_tag "mat_views/android-chrome-192x192.png", rel: "icon", type: "image/png", sizes: "192x192" %>
28
+ <%= favicon_link_tag "mat_views/android-chrome-512x512.png", rel: "icon", type: "image/png", sizes: "512x512" %>
29
+ <%= tag.meta name: "theme-color", content: "#0F172A" %>
30
+ <%= stylesheet_link_tag "mat_views/application.css", media: "all", "data-turbo-track": "reload" %>
31
+ <%= javascript_importmap_tags "mat_views/application", importmap: MatViews.importmap %>
32
+
33
+ <%= javascript_tag do %>
34
+ window.MatViewsRoutes = { definitionsPath: "<%= admin_mat_view_definitions_path %>", runsPath: "<%= admin_runs_path %>", preferencesPath: "<%= admin_preferences_path %>" }
35
+ <% end %>
36
+ </head>
37
+ <body class="mv-shell" data-controller="turbo-frame-lifecycle drawer timezone mv-confirm">
38
+ <%= render "layouts/mat_views/header" %>
39
+ <main class="mv-main">
40
+ <div class="mv-container">
41
+ <%= render "mat_views/admin/ui/flash" %>
42
+ <%= yield %>
43
+ </div>
44
+ </main>
45
+ <%= render "layouts/mat_views/footer" %>
46
+ </body>
47
+ </html>
@@ -0,0 +1,3 @@
1
+ <turbo-frame id="<%= @frame_id %>" class="mv-frame">
2
+ <%= yield %>
3
+ </turbo-frame>
@@ -0,0 +1,33 @@
1
+ <h1><%= t("mat_views.dashboard.title") %></h1>
2
+ <div class="mv-card" style="margin-bottom:12px;">
3
+ <div class="mv-card-h"><%= t("mat_views.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">
11
+ <nav class="mv-tabs" style="margin-bottom:1rem;">
12
+ <%= mv_tab_link 'definitions', selected: true, testid: 'DEFINITIONS_TAB_LINK' do %>
13
+ <%= t("mat_views.definitions") %>
14
+ <% end %>
15
+ <%= mv_tab_link 'runs', selected: false, testid: 'RUNS_TAB_LINK' do %>
16
+ <%= t("mat_views.runs") %>
17
+ <% end %>
18
+ </nav>
19
+ <div data-tabs-target="panel" data-name="definitions">
20
+ <turbo-frame id="dash-definitions" data-src="<%= admin_mat_view_definitions_path(frame_id: 'dash-definitions') %>" data-turbo-temporary>
21
+ <div class="mv-card">
22
+ <div class="mv-card-b"><%= t("mat_views.loading_definitions") %></div>
23
+ </div>
24
+ </turbo-frame>
25
+ </div>
26
+ <div data-tabs-target="panel" data-name="runs" hidden>
27
+ <turbo-frame id="dash-runs" data-src="<%= admin_runs_path(frame_id: 'dash-runs') %>" data-turbo-temporary>
28
+ <div class="mv-card">
29
+ <div class="mv-card-b"><%= t("mat_views.loading_runs") %></div>
30
+ </div>
31
+ </turbo-frame>
32
+ </div>
33
+ </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
+ <%= mv_t("definition") %></div>
6
+ <div class="mv-buttons">
7
+ <%= mv_button_link admin_root_path(tab: 'runs', mat_view_definition_id: 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
+ <%= mv_t("history") %>
14
+ <% end %>
15
+ <%= mv_drawer_link(edit_admin_mat_view_definition_path(defn, frame_id: 'mv-drawer'),
16
+ mv_t("edit_var", name: defn.name),
17
+ testid: 'EDIT_LINK',
18
+ testid_identifier: "defn-#{defn.id}",
19
+ tooltip: mv_t("mat_view_definition.edit_tooltip")) do %>
20
+ <%= mv_icon(:edit) %>
21
+ <%= mv_t("edit") %>
22
+ <% end %>
23
+ <%= mv_button_to admin_mat_view_definition_path(defn, frame_id: frame_id, frame_action: 'close-and-refresh'),
24
+ method: :delete,
25
+ variant: :negative,
26
+ testid: 'DELETE_LINK',
27
+ testid_identifier: "defn-#{defn.id}",
28
+ tooltip: mv_t("mat_view_definition.delete_tooltip"), tooltip_placement: "bottom",
29
+ confirm: mv_t("mat_view_definition.delete_confirm", name: defn.name) do %>
30
+ <%= mv_icon(:trash) %>
31
+ <%= mv_t("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
+ <%= mv_t("mat_view_definition.materialized_view") %>
40
+ <% if mv_exists %>
41
+ <span class="mv-status-icon" data-controller="tooltip" data-tooltip-text-value="<%= mv_t("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="<%= mv_t("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: mv_t("mat_view_definition.refresh_tooltip") do %>
58
+ <%= mv_icon(:refresh) %>
59
+ <%= mv_t("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: mv_t("mat_view_definition.drop_mv_tooltip"), tooltip_placement: "bottom",
67
+ confirm: mv_t("mat_view_definition.drop_mv_confirm", name: defn.name) do %>
68
+ <%= mv_icon(:x_circle) %>
69
+ <%= mv_t("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: mv_t("mat_view_definition.drop_mv_cascade_tooltip"), tooltip_placement: "bottom",
77
+ confirm: mv_t("mat_view_definition.drop_mv_cascade_confirm", name: defn.name) do %>
78
+ <%= mv_icon(:x_circle) %>
79
+ <%= mv_t("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: mv_t("mat_view_definition.create_mv_tooltip"), tooltip_placement: "bottom" do %>
88
+ <%= mv_icon(:hammer) %>
89
+ <%= mv_t("mat_view_definition.create_mv") %>
90
+ <% end %>
91
+ <% end %>
92
+ </div>
93
+ </div>
94
+ </div>
@@ -0,0 +1,48 @@
1
+ <% humanize_attr = ->(attr) { MatViews::MatViewDefinition.human_attribute_name(attr) } %>
2
+ <% humanize_enum_attr = ->(col, val) { MatViews::MatViewDefinition.human_enum_name(col, val) } %>
3
+ <table class="mv-table">
4
+ <colgroup>
5
+ <col class="col-name">
6
+ <col class="col-strategy">
7
+ <col class="col-schedule">
8
+ <col class="col-refreshed">
9
+ <col class="col-actions">
10
+ </colgroup>
11
+ <thead>
12
+ <tr class="mv-tr">
13
+ <th class="mv-th"><%= humanize_attr.call(:name) %></th>
14
+ <th class="mv-th"><%= humanize_attr.call(:refresh_strategy) %></th>
15
+ <th class="mv-th"><%= humanize_attr.call(:schedule_cron) %></th>
16
+ <th class="mv-th"><%= humanize_attr.call(:last_run) %></th>
17
+ <th class="mv-th" style="text-align:right;"><%= mv_t("actions") %></th>
18
+ </tr>
19
+ </thead>
20
+ <tbody>
21
+ <% if definitions.any? %>
22
+ <% definitions.each do |defn| %>
23
+ <tr class="mv-tr">
24
+ <td class="mv-td">
25
+ <%= mv_drawer_link(admin_mat_view_definition_path(defn, frame_id: 'mv-drawer'), mv_t("view_var", name: defn.name),
26
+ variant: :ghost,
27
+ testid: 'VIEW_LINK',
28
+ testid_identifier: "defn-#{defn.id}",
29
+ underline: true,
30
+ tooltip: mv_t("mat_view_definition.view_tooltip")) do %>
31
+ <%= defn.name %>
32
+ <% end %>
33
+ </td>
34
+ <td class="mv-td"><%= humanize_enum_attr.call(:refresh_strategy, defn.refresh_strategy) %></td>
35
+ <td class="mv-td"><%= defn.schedule_cron.presence || "-" %></td>
36
+ <td class="mv-td"><%= defn.last_run ? l(defn.last_run.started_at.in_time_zone, format: :datetime12hour) : "-" %></td>
37
+ <td class="mv-td mv-td-actions">
38
+ <%= render "definition_actions", defn: defn, mv_exists: mv_exists_map[defn], frame_id: "dash-definitions" %>
39
+ </td>
40
+ </tr>
41
+ <% end %>
42
+ <% else %>
43
+ <tr class="mv-tr">
44
+ <td class="mv-td" colspan="5"><%= mv_t("mat_view_definition.no_definitions") %></td>
45
+ </tr>
46
+ <% end %>
47
+ </tbody>
48
+ </table>
@@ -0,0 +1 @@
1
+ <h1><%= mv_t("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? ? mv_t('mat_view_definition.new_definition') : mv_t("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("mat_views.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: MatViews::MatViewDefinition.placeholder_for(:name) %>
27
+ <%= content_tag :small, MatViews::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: MatViews::MatViewDefinition.placeholder_for(:sql), required: true %>
34
+ <%= content_tag :small, MatViews::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(MatViews::MatViewDefinition.human_enum_options(:refresh_strategy), @definition.refresh_strategy),
41
+ {},
42
+ class: "mv-select" %>
43
+ <%= content_tag :small, MatViews::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: MatViews::MatViewDefinition.placeholder_for(:schedule_cron) %>
49
+ <%= content_tag :small, MatViews::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: MatViews::MatViewDefinition.placeholder_for(:unique_index_columns) %>
56
+ <%= content_tag :small, MatViews::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: MatViews::MatViewDefinition.placeholder_for(:dependencies) %>
63
+ <%= content_tag :small, MatViews::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("mat_views.create") : t("mat_views.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
+ <%= mv_t("cancel") %>
77
+ <% end %>
78
+ </div>
79
+ <% end %>
@@ -0,0 +1,10 @@
1
+ <div class="mv-toolbar start">
2
+ <%= mv_drawer_link(new_admin_mat_view_definition_path(frame_id: 'mv-drawer'), mv_t("mat_view_definition.new_definition"),
3
+ variant: :primary,
4
+ testid: 'NEW_DEFINITION_LINK',
5
+ tooltip: mv_t("mat_view_definition.new_definition_tooltip")) do %>
6
+ <%= mv_icon(:plus_circle) %>
7
+ <%= mv_t("mat_view_definition.new_definition") %>
8
+ <% end %>
9
+ </div>
10
+ <%= render "table", definitions: @definitions, mv_exists_map: @mv_exists_map %>