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.
- checksums.yaml +4 -4
- data/README.md +4 -4
- data/app/assets/images/mat_views/android-chrome-192x192.png +0 -0
- data/app/assets/images/mat_views/android-chrome-512x512.png +0 -0
- data/app/assets/images/mat_views/apple-touch-icon.png +0 -0
- data/app/assets/images/mat_views/favicon-16x16.png +0 -0
- data/app/assets/images/mat_views/favicon-32x32.png +0 -0
- data/app/assets/images/mat_views/favicon-48x48.png +0 -0
- data/app/assets/images/mat_views/favicon.ico +0 -0
- data/app/assets/images/mat_views/favicon.svg +18 -0
- data/app/assets/images/mat_views/logo.svg +18 -0
- data/app/assets/images/mat_views/mask-icon.svg +5 -0
- data/app/assets/stylesheets/mat_views/application.css +323 -12
- data/app/controllers/mat_views/admin/application_controller.rb +135 -0
- data/app/controllers/mat_views/admin/dashboard_controller.rb +32 -0
- data/app/controllers/mat_views/admin/mat_view_definitions_controller.rb +248 -0
- data/app/controllers/mat_views/admin/preferences_controller.rb +91 -0
- data/app/controllers/mat_views/admin/runs_controller.rb +74 -0
- data/app/helpers/mat_views/admin/ui_helper.rb +385 -0
- data/app/javascript/mat_views/application.js +8 -0
- data/app/javascript/mat_views/controllers/application.js +10 -0
- data/app/javascript/mat_views/controllers/details_controller.js +122 -0
- data/app/javascript/mat_views/controllers/drawer_controller.js +252 -0
- data/app/javascript/mat_views/controllers/filter_controller.js +90 -0
- data/app/javascript/mat_views/controllers/flash_controller.js +13 -0
- data/app/javascript/mat_views/controllers/index.js +10 -0
- data/app/javascript/mat_views/controllers/mv_confirm_controller.js +281 -0
- data/app/javascript/mat_views/controllers/submitter_controller.js +15 -0
- data/app/javascript/mat_views/controllers/tabs_controller.js +67 -0
- data/app/javascript/mat_views/controllers/timezone_controller.js +16 -0
- data/app/javascript/mat_views/controllers/tooltip_controller.js +328 -0
- data/app/javascript/mat_views/controllers/turbo_frame_lifecycle_controller.js +49 -0
- data/app/jobs/mat_views/application_job.rb +2 -2
- data/app/jobs/mat_views/create_view_job.rb +9 -8
- data/app/jobs/mat_views/delete_view_job.rb +8 -8
- data/app/jobs/mat_views/refresh_view_job.rb +8 -9
- data/app/models/concerns/mat_views_i18n.rb +139 -0
- data/app/models/mat_views/application_record.rb +1 -0
- data/app/models/mat_views/mat_view_definition.rb +12 -7
- data/app/models/mat_views/mat_view_run.rb +11 -13
- data/app/views/layouts/mat_views/_footer.html.erb +41 -0
- data/app/views/layouts/mat_views/_header.html.erb +25 -0
- data/app/views/layouts/mat_views/admin.html.erb +47 -0
- data/app/views/layouts/mat_views/turbo_frame.html.erb +3 -0
- data/app/views/mat_views/admin/dashboard/index.html.erb +33 -0
- data/app/views/mat_views/admin/mat_view_definitions/_definition_actions.html.erb +94 -0
- data/app/views/mat_views/admin/mat_view_definitions/_table.html.erb +48 -0
- data/app/views/mat_views/admin/mat_view_definitions/empty.html.erb +1 -0
- data/app/views/mat_views/admin/mat_view_definitions/form.html.erb +79 -0
- data/app/views/mat_views/admin/mat_view_definitions/index.html.erb +10 -0
- data/app/views/mat_views/admin/mat_view_definitions/show.html.erb +40 -0
- data/app/views/mat_views/admin/preferences/show.html.erb +50 -0
- data/app/views/mat_views/admin/runs/_table.html.erb +61 -0
- data/app/views/mat_views/admin/runs/index.html.erb +38 -0
- data/app/views/mat_views/admin/runs/show.html.erb +64 -0
- data/app/views/mat_views/admin/ui/_card.html.erb +15 -0
- data/app/views/mat_views/admin/ui/_details.html.erb +10 -0
- data/app/views/mat_views/admin/ui/_flash.html.erb +6 -0
- data/app/views/mat_views/admin/ui/_table.html.erb +8 -0
- data/config/importmap.rb +9 -0
- data/config/locales/en-AU-ocker.yml +187 -0
- data/config/locales/en-AU.yml +187 -0
- data/config/locales/en-BB.yml +187 -0
- data/config/locales/en-BD.yml +187 -0
- data/config/locales/en-BE.yml +187 -0
- data/config/locales/en-BORK.yml +187 -0
- data/config/locales/en-BS.yml +187 -0
- data/config/locales/en-BZ.yml +187 -0
- data/config/locales/en-CA.yml +187 -0
- data/config/locales/en-CM.yml +187 -0
- data/config/locales/en-CY.yml +187 -0
- data/config/locales/en-EG.yml +187 -0
- data/config/locales/en-FJ.yml +187 -0
- data/config/locales/en-GB.yml +187 -0
- data/config/locales/en-GH.yml +187 -0
- data/config/locales/en-GI.yml +187 -0
- data/config/locales/en-GM.yml +187 -0
- data/config/locales/en-GY.yml +187 -0
- data/config/locales/en-HK.yml +187 -0
- data/config/locales/en-IE.yml +187 -0
- data/config/locales/en-IN.yml +187 -0
- data/config/locales/en-JM.yml +187 -0
- data/config/locales/en-KE.yml +187 -0
- data/config/locales/en-LK.yml +187 -0
- data/config/locales/en-LOL.yml +187 -0
- data/config/locales/en-LR.yml +187 -0
- data/config/locales/en-MS.yml +187 -0
- data/config/locales/en-MT.yml +187 -0
- data/config/locales/en-MW.yml +187 -0
- data/config/locales/en-MY.yml +187 -0
- data/config/locales/en-NG.yml +187 -0
- data/config/locales/en-NP.yml +187 -0
- data/config/locales/en-NZ.yml +187 -0
- data/config/locales/en-PG.yml +187 -0
- data/config/locales/en-PH.yml +187 -0
- data/config/locales/en-PK.yml +187 -0
- data/config/locales/en-RW.yml +187 -0
- data/config/locales/en-SCOT.yml +187 -0
- data/config/locales/en-SG.yml +187 -0
- data/config/locales/en-SHAKESPEARE.yml +187 -0
- data/config/locales/en-SL.yml +187 -0
- data/config/locales/en-SS.yml +187 -0
- data/config/locales/en-TH.yml +187 -0
- data/config/locales/en-TT.yml +187 -0
- data/config/locales/en-TZ.yml +187 -0
- data/config/locales/en-UG.yml +187 -0
- data/config/locales/en-US-pirate.yml +187 -0
- data/config/locales/en-US.yml +187 -0
- data/config/locales/en-YODA.yml +187 -0
- data/config/locales/en-ZA.yml +187 -0
- data/config/locales/en-ZW.yml +187 -0
- data/config/locales/en.yml +187 -0
- data/config/routes.rb +27 -3
- data/lib/generators/mat_views/install/templates/create_mat_view_definitions.rb +7 -7
- data/lib/generators/mat_views/install/templates/create_mat_view_runs.rb +5 -5
- data/lib/mat_views/admin/auth_bridge.rb +93 -0
- data/lib/mat_views/admin/default_auth.rb +61 -0
- data/lib/mat_views/configuration.rb +9 -0
- data/lib/mat_views/engine.rb +50 -2
- data/lib/mat_views/helpers/ui_test_ids.rb +43 -0
- data/lib/mat_views/services/base_service.rb +46 -38
- data/lib/mat_views/services/check_matview_exists.rb +76 -0
- data/lib/mat_views/services/concurrent_refresh.rb +9 -6
- data/lib/mat_views/services/create_view.rb +15 -15
- data/lib/mat_views/services/delete_view.rb +8 -11
- data/lib/mat_views/services/regular_refresh.rb +6 -5
- data/lib/mat_views/services/swap_refresh.rb +11 -9
- data/lib/mat_views/version.rb +1 -1
- data/lib/mat_views.rb +10 -4
- data/lib/tasks/helpers.rb +13 -13
- data/lib/tasks/mat_views_tasks.rake +15 -15
- 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
|
@@ -9,9 +9,9 @@
|
|
9
9
|
# Top-level namespace for the mat_views engine.
|
10
10
|
module MatViews
|
11
11
|
##
|
12
|
-
# Represents a **
|
12
|
+
# Represents a **materialised view definition** managed by the engine.
|
13
13
|
#
|
14
|
-
# A definition stores the canonical name and SQL for a
|
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
|
-
#
|
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
|
-
#
|
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:
|
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
|
-
#
|
13
|
+
# materialised views.
|
14
14
|
#
|
15
|
-
# Each record corresponds to a single attempt to mutate a
|
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
|
-
# - `:
|
49
|
-
# - `:
|
50
|
-
# - `:
|
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
|
-
|
55
|
-
|
56
|
-
|
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`
|
65
|
-
# - `:refresh`
|
66
|
-
# - `:drop`
|
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,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 %>
|