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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7120f5d0821cb752001586381ab084a7366aeaa9616b86ef6e648be2b52d152
4
- data.tar.gz: ab541f3705c974e8272cc07db3c9612add6311cb159467e04055eb980fa648b6
3
+ metadata.gz: 7adba77306f5d4a4f8738625ad9b584f13c84c082a307bd4473e82fcba93936e
4
+ data.tar.gz: f8c61cb31ff80d591dac050abe18237aed700532d0b536614fc29b79bade91f5
5
5
  SHA512:
6
- metadata.gz: e3d260f3fcaf3029af874c943ae72b8786e2e86232dbc352a955ae9c4e1add587a0df88507cb86fe5463b308df1ff354b64ebfed00ebe2bcd124924984121797
7
- data.tar.gz: 235208e771305d1085eeea02f5bd762bfb91942901e2b9d7d127281d8e7d04da878fcbf5eb8df4cdec9434c1e5db243fb90b20c79d77ede4136006449cbe2df8
6
+ metadata.gz: 6b4408b95e63107d1650ca3392e20ff2484837f817abbdce2716909e1647478c7562bc379c38283a99825440c258bff7d7b52dcabbc5d2c54ae68aeac946a731
7
+ data.tar.gz: 5a684e7384de30dcc5f4af1d10bb4999a8f104dbc7b3c112998b34ba3018cafb650ab24ef4737c2c11ed8259ec4ed711784e38b9ea31af70011b77b26b5dab44
data/README.md CHANGED
@@ -65,19 +65,19 @@ defn = MatViews::MatViewDefinition.create!(
65
65
 
66
66
  ```ruby
67
67
  # Create
68
- MatViews::Services::CreateView.new(defn, force: true).run
68
+ MatViews::Services::CreateView.new(defn, force: true).call
69
69
  MatViews::CreateViewJob.perform_later(defn.id, force: true)
70
70
 
71
71
  # Refresh
72
- MatViews::Services::RegularRefresh.new(defn, row_count_strategy: :estimated).run
72
+ MatViews::Services::RegularRefresh.new(defn, row_count_strategy: :estimated).call
73
73
  MatViews::RefreshViewJob.perform_later(defn.id, row_count_strategy: :exact)
74
74
 
75
75
  # Delete
76
- MatViews::Services::DeleteView.new(defn, cascade: false, if_exists: true).run
76
+ MatViews::Services::DeleteView.new(defn, cascade: false, if_exists: true).call
77
77
  MatViews::DeleteViewJob.perform_later(defn.id, cascade: true)
78
78
  ```
79
79
 
80
- **Uniform response**: `status`, `payload`, `meta`, `success?` / `error?`.
80
+ **Uniform response**: `status`, `meta`, `success?` / `error?`.
81
81
 
82
82
  ---
83
83
 
@@ -0,0 +1,18 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="128" height="128" viewBox="0 0 256 256" fill="none"
3
+ xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
4
+ <title id="title">MatViews Monogram</title>
5
+ <desc id="desc">Stylized M and V mark for Materialized Views. Transparent background.</desc>
6
+
7
+ <!-- subtle outer ring -->
8
+ <circle cx="128" cy="128" r="110" stroke="#0F172A" stroke-opacity="0.18" stroke-width="10" fill="none"/>
9
+
10
+ <!-- V (accent) -->
11
+ <path d="M64 64 L128 192 L192 64" stroke="#10B981" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/>
12
+
13
+ <!-- M (primary) -->
14
+ <path d="M48 176 L92 80 L128 144 L164 80 L208 176" stroke="#0F172A" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/>
15
+
16
+ <!-- optional tiny dot to balance negative space -->
17
+ <circle cx="128" cy="208" r="4" fill="#10B981" fill-opacity="0.9"/>
18
+ </svg>
@@ -0,0 +1,18 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="128" height="128" viewBox="0 0 256 256" fill="none"
3
+ xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
4
+ <title id="title">MatViews Monogram</title>
5
+ <desc id="desc">Stylized M and V mark for Materialized Views. Transparent background.</desc>
6
+
7
+ <!-- subtle outer ring -->
8
+ <circle cx="128" cy="128" r="110" stroke="#0F172A" stroke-opacity="0.18" stroke-width="10" fill="none"/>
9
+
10
+ <!-- V (accent) -->
11
+ <path d="M64 64 L128 192 L192 64" stroke="#10B981" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/>
12
+
13
+ <!-- M (primary) -->
14
+ <path d="M48 176 L92 80 L128 144 L164 80 L208 176" stroke="#0F172A" stroke-width="20" stroke-linecap="round" stroke-linejoin="round"/>
15
+
16
+ <!-- optional tiny dot to balance negative space -->
17
+ <circle cx="128" cy="208" r="4" fill="#10B981" fill-opacity="0.9"/>
18
+ </svg>
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" fill="none">
3
+ <path d="M64 64 L128 192 L192 64" stroke="currentColor" stroke-width="28" stroke-linecap="round" stroke-linejoin="round"/>
4
+ <path d="M48 176 L92 80 L128 144 L164 80 L208 176" stroke="currentColor" stroke-width="28" stroke-linecap="round" stroke-linejoin="round"/>
5
+ </svg>
@@ -1,15 +1,326 @@
1
1
  /*
2
- * This is a manifest file that'll be compiled into application.css, which will include all the files
3
- * listed below.
4
- *
5
- * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
- * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
- *
8
- * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
- * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
- * files in this directory. Styles in this file should be added after the last require_* statement.
11
- * It is generally better to create a new file per style scope.
12
- *
13
2
  *= require_tree .
14
3
  *= require_self
15
- */
4
+ */
5
+
6
+ /* ─────────────────────────────────────────────────────────────────────────────
7
+ THEME TOKENS
8
+ - Define semantic variables once.
9
+ - Light theme is default on :root.
10
+ - Dark theme overrides via [data-theme="dark"] or prefers-color-scheme.
11
+ ─────────────────────────────────────────────────────────────────────────── */
12
+
13
+ :root{
14
+ /* typography */
15
+ --font-base:14px/1.4 Inter,system-ui;
16
+ --font-head:'Space Grotesk';
17
+ --font-mono:'JetBrains Mono',monospace;
18
+
19
+ /* semantic colors (light defaults) */
20
+ --color-text:#0f172a;
21
+ --color-sub:#475569;
22
+ --color-muted:#334155;
23
+
24
+ --color-bg:#f8fafc;
25
+ --color-surface:#ffffff;
26
+ --color-elev-shadow:rgba(15,23,42,.04);
27
+
28
+ --color-white:#ffffff;
29
+ --color-border:#e2e8f0;
30
+ --color-border-light:#f1f5f9;
31
+ --color-border-mid:#cbd5e1;
32
+ --color-dark:#111827; /* hover for primary in light */
33
+ --color-primary:#0f172a;
34
+
35
+ --color-danger:#dc2626;
36
+ --color-danger-dark:#b91c1c;
37
+
38
+ --color-success-bg:#ecfdf5;
39
+ --color-success:#047857;
40
+ --color-success-border:#a7f3d0;
41
+
42
+ --color-running-bg:#fffbeb;
43
+ --color-running:#92400e;
44
+ --color-running-border:#fde68a;
45
+
46
+ --color-failed-bg:#fef2f2;
47
+ --color-failed:#b91c1c;
48
+ --color-failed-border:#fecaca;
49
+
50
+ --color-link:#3b82f6;
51
+
52
+ /* component-specific surfaces (light) */
53
+ --card-surface:var(--color-surface);
54
+ --table-head-bg:#eaeaea;
55
+ --btn-ghost-hover:#eef2f7;
56
+ --btn-secondary-hover:#f1f5f9;
57
+
58
+ /* overlay / popovers */
59
+ --overlay-scrim:rgba(15,23,42,.35);
60
+ --tooltip-bg:#0f172a;
61
+ --tooltip-shadow:rgba(2,6,23,.20);
62
+
63
+ /* transitions */
64
+ --elev-shadow-lg:rgba(15,23,42,.10);
65
+
66
+ color-scheme: light;
67
+ }
68
+
69
+ /* Dark theme overrides (manual: <html data-theme="dark">) */
70
+ :root[data-theme="dark"]{
71
+ --color-text:#e5e7eb;
72
+ --color-sub:#94a3b8;
73
+ --color-muted:#cbd5e1;
74
+ --color-bg:#0b1020;
75
+ --color-surface:#0f172a;
76
+ --color-elev-shadow:rgba(0,0,0,.25);
77
+ --color-white:#0f172a;
78
+ --color-border:#1f2937;
79
+ --color-border-light:#111827;
80
+ --color-border-mid:#334155;
81
+ --color-dark:#f8fafc;
82
+ --color-primary:#e5e7eb;
83
+ --color-danger:#ef4444;
84
+ --color-danger-dark:#dc2626;
85
+ --color-success-bg:#052e25;
86
+ --color-success:#34d399;
87
+ --color-success-border:#065f46;
88
+ --color-running-bg:#3a2a03;
89
+ --color-running:#fbbf24;
90
+ --color-running-border:#854d0e;
91
+ --color-failed-bg:#3a0d0d;
92
+ --color-failed:#fca5a5;
93
+ --color-failed-border:#7f1d1d;
94
+ --color-link:#60a5fa;
95
+ --card-surface:var(--color-surface);
96
+ --table-head-bg:#0f172a;
97
+ --btn-ghost-hover:#0f172a;
98
+ --btn-secondary-hover:#111827;
99
+ --overlay-scrim:rgba(3,7,18,.55);
100
+ --tooltip-bg:#0b1222;
101
+ --tooltip-shadow:rgba(0,0,0,.35);
102
+
103
+ color-scheme: dark;
104
+ }
105
+
106
+ /* Auto-dark by system setting unless explicitly overridden by data-theme */
107
+ @media (prefers-color-scheme: dark){
108
+ :root:not([data-theme]){
109
+ /* repeat the same overrides as the block above, or factor them with a custom @mixin in SCSS */
110
+ color-scheme: dark;
111
+ }
112
+ }
113
+ :root{ color-scheme: light; }
114
+
115
+ /* ─────────────────────────────────────────────────────────────────────────────
116
+ BASE
117
+ ─────────────────────────────────────────────────────────────────────────── */
118
+
119
+ html,body{height:100%}
120
+ body{
121
+ display:flex;flex-direction:column;min-height:100vh;
122
+ font:var(--font-base);color:var(--color-text);background:var(--color-bg)
123
+ }
124
+ h1,h2,h3,h4,h5,h6{font-weight:600;color:var(--color-text);font-family:var(--font-head)}
125
+ code,pre{font-family:var(--font-mono);font-size:.9rem;color:var(--color-muted)}
126
+ a{color:var(--color-link);text-decoration:none}
127
+ a:hover{text-decoration:underline}
128
+ pre{background:var(--card-surface);border:1px solid var(--color-border);border-radius:8px;box-shadow:0 1px 2px var(--color-elev-shadow);padding:.75rem;overflow:auto;margin: 0;white-space:pre-wrap;}
129
+
130
+ /* ─────────────────────────────────────────────────────────────────────────────
131
+ LAYOUT
132
+ ─────────────────────────────────────────────────────────────────────────── */
133
+
134
+ .mv-main{flex:1 1 auto}
135
+ .mv-container{background:transparent;margin:0 auto;max-width:72rem;padding:1rem 1.25rem}
136
+ .mv-header{background:var(--card-surface);border-bottom:1px solid var(--color-border);color:var(--color-sub);font-size:.9rem;padding:1rem 0}
137
+ .mv-header-row{display:flex;flex-wrap:wrap;gap:12px;justify-content:space-between}
138
+ .mv-header-row.even{justify-content:space-evenly}
139
+ .mv-brand{align-items:center;color:var(--color-text);display:flex;font-weight:600;gap:.6rem;text-decoration:none}
140
+ .mv-logo{height:28px;width:28px}
141
+ .mv-footer{background:var(--card-surface);border-top:1px solid var(--color-border);color:var(--color-sub);font-size:.9rem;margin-top:auto;padding-top:1rem}
142
+ .mv-footer-row{display:flex;flex-wrap:wrap;gap:12px;justify-content:space-between}
143
+ .row-item {display: flex; align-items: center; gap: 6px;}
144
+
145
+ /* ─────────────────────────────────────────────────────────────────────────────
146
+ CARDS
147
+ ─────────────────────────────────────────────────────────────────────────── */
148
+
149
+ .mv-card{background:var(--card-surface);border:1px solid var(--color-border);border-radius:12px;box-shadow:0 1px 2px var(--color-elev-shadow)}
150
+ .mv-card-b{padding:1rem}
151
+ .mv-card-h{border-bottom:1px solid var(--color-border);font-family:var(--font-head);font-weight:600;padding:.75rem 1rem}
152
+ .mv-card-f{border-top:1px solid var(--color-border);padding:.5rem 1rem;text-align:right;font-size:.85rem;}
153
+
154
+ /* ─────────────────────────────────────────────────────────────────────────────
155
+ TOOLBAR
156
+ ─────────────────────────────────────────────────────────────────────────── */
157
+ .mv-toolbar{display:flex;justify-content:flex-end;margin:.5rem 0;}
158
+ .mv-toolbar.start{justify-content: flex-start;}
159
+ .mv-toolbar.center{justify-content: center;flex: 1;}
160
+ /* ─────────────────────────────────────────────────────────────────────────────
161
+ BUTTONS
162
+ ─────────────────────────────────────────────────────────────────────────── */
163
+
164
+ .mv-btn{align-items:center;border:1px solid transparent;border-radius:8px;box-sizing:border-box;cursor:pointer;display:inline-flex;font-weight:500;gap:.5rem;padding:.4rem .7rem;text-decoration:none}
165
+ .mv-btn--ghost{background:transparent;color:var(--color-muted)}
166
+ .mv-btn--ghost:hover{background:var(--btn-ghost-hover)}
167
+ .mv-btn--lg{font-size:.9rem;height:44px;max-height:44px;padding:0 .8rem}
168
+ .mv-btn--md{font-size:.8rem;height:36px;max-height:36px;padding:0 .55rem}
169
+ .mv-btn--negative{background:var(--color-danger);color:#fff}
170
+ .mv-btn--negative:hover{background:var(--color-danger-dark)}
171
+ .mv-btn--primary{background:var(--color-primary);color:#fff}
172
+ [data-theme="dark"] .mv-btn--primary{color:#0b1020} /* ensure contrast on dark */
173
+ .mv-btn--primary:hover{background:var(--color-dark)}
174
+ .mv-btn--secondary{background:var(--card-surface);border-color:var(--color-border-mid);color:var(--color-primary)}
175
+ .mv-btn--secondary:hover{background:var(--btn-secondary-hover)}
176
+ .mv-btn--sm{font-size:.6rem;height:28px;max-height:28px;padding:0 .4rem}
177
+ .mv-btn.underline{text-decoration:underline;}
178
+
179
+ /* ─────────────────────────────────────────────────────────────────────────────
180
+ TABLES
181
+ ─────────────────────────────────────────────────────────────────────────── */
182
+
183
+ .mv-table{background:var(--card-surface);border:1px solid var(--color-border);border-collapse:collapse;border-radius:10px;overflow:hidden;width:100%}
184
+ .mv-th{background:var(--table-head-bg);border-bottom:1px solid var(--color-border);color:var(--color-sub);font-family:var(--font-head);font-weight:600;padding:.5rem .75rem;text-align:left}
185
+ .mv-td{border-bottom:1px solid var(--color-border-light);padding:.5rem .75rem}
186
+ .mv-tr:last-child .mv-td{border-bottom:none}
187
+
188
+ /* ─────────────────────────────────────────────────────────────────────────────
189
+ BADGES
190
+ ─────────────────────────────────────────────────────────────────────────── */
191
+
192
+ .mv-badge{border:1px solid transparent;border-radius:6px;display:inline-block;font-size:.75rem;font-weight:600;padding:.15rem .4rem}
193
+ .mv-badge--failed{background:var(--color-failed-bg);border-color:var(--color-failed-border);color:var(--color-failed)}
194
+ .mv-badge--running{background:var(--color-running-bg);border-color:var(--color-running-border);color:var(--color-running)}
195
+ .mv-badge--success{background:var(--color-success-bg);border-color:var(--color-success-border);color:var(--color-success)}
196
+
197
+ /* ─────────────────────────────────────────────────────────────────────────────
198
+ Details/Summary
199
+ ─────────────────────────────────────────────────────────────────────────── */
200
+ details summary{cursor:pointer;}
201
+ details summary:focus{outline:none}
202
+ .mv-details summary::-webkit-details-marker { display: none; }
203
+ .mv-details summary {display: inline-flex;align-items: center;gap: .5rem;cursor: pointer;}
204
+ .mv-chevron {flex: 0 0 auto;transition: transform 200ms ease;transform-origin: 50% 50%;}
205
+ .mv-details[open] .mv-chevron { transform: rotate(90deg); }
206
+ .mv-details__content {overflow: hidden;will-change: height;margin:0;}
207
+ [dir="rtl"] .mv-details[open] .mv-chevron { transform: rotate(-90deg); }
208
+
209
+ /* ─────────────────────────────────────────────────────────────────────────────
210
+ TABS
211
+ ─────────────────────────────────────────────────────────────────────────── */
212
+
213
+ .mv-tabs{border-bottom:1px solid var(--color-border);display:flex;gap:1rem;margin:1rem 0}
214
+ .mv-tab{border-bottom:2px solid transparent;color:var(--color-muted);font-family:var(--font-head);padding:.5rem .5rem;text-decoration:none}
215
+ .mv-tab--on{border-color:var(--color-primary)}
216
+ .mv-tab:hover{border-color:var(--color-border-mid)}
217
+
218
+ /* ─────────────────────────────────────────────────────────────────────────────
219
+ FORMS
220
+ ─────────────────────────────────────────────────────────────────────────── */
221
+
222
+ .mv-form{display:block;padding:0 1rem;max-width:100%}
223
+ .mv-form--no-pad{padding:0}
224
+ .mv-field{display:flex;flex-direction:column;gap:.25rem;margin:.75rem 0}
225
+ .mv-field-inline-row{display:inline-flex;flex-direction:row;align-items:center;gap:.75rem; margin: 1rem 0;}
226
+ .mv-field-inline{display:inline-flex;flex-direction:row;align-items:center;gap:.5rem}
227
+ .mv-input,.mv-textarea,.mv-select{background:var(--card-surface);border:1px solid var(--color-border-mid);border-radius:8px;padding:.45rem .6rem;color:var(--color-text)}
228
+ .mv-label{color:var(--color-muted);font-size:.9rem;font-weight:600;}
229
+ .mv-label--sublabel{font-weight:300;margin:0;font-size:.85rem;}
230
+ .mv-textarea{min-height:10rem;resize:vertical}
231
+ .mv-hint{color:var(--color-sub);font-size:.75rem;}
232
+
233
+ /* ─────────────────────────────────────────────────────────────────────────────
234
+ FLASH
235
+ ─────────────────────────────────────────────────────────────────────────── */
236
+
237
+ .mv-flash{border-radius:10px;font-size:.9rem;margin:.5rem 0;padding:.5rem .75rem}
238
+ .mv-flash--err{background:var(--color-failed-bg);border:1px solid var(--color-failed-border);color:var(--color-failed)}
239
+ .mv-flash--ok{background:var(--color-success-bg);border:1px solid var(--color-success-border);color:#065f46}
240
+
241
+ /* ─────────────────────────────────────────────────────────────────────────────
242
+ UTILS
243
+ ─────────────────────────────────────────────────────────────────────────── */
244
+
245
+ .mv-text-right{text-align:end} /* logical for LTR/RTL */
246
+
247
+ .mv-td-actions{text-align:end}
248
+ .mv-actions{display:inline-flex;flex-wrap:wrap;gap:12px;justify-content:flex-end}
249
+ .mv-action-group{background:var(--card-surface);border:1px solid var(--color-border);border-radius:12px;box-shadow:0 1px 2px var(--color-elev-shadow);display:inline-flex;flex-direction:column;padding:10px 12px}
250
+ .mv-action-title{align-items:center;color:var(--color-sub);display:flex;font-size:.72rem;font-weight:600;gap:6px;letter-spacing:.02em;margin-bottom:.5rem;text-transform:uppercase}
251
+ .mv-buttons{align-items:center;display:inline-flex;flex-direction:column;gap:6px;align-items: stretch;}
252
+ .mv-buttons .mv-btn{width:100%;justify-content:center;}
253
+ .mv-icon{margin-inline-end:.05rem;margin-inline-start: .05rem;}
254
+ .mv-mv-missing{align-items:center;color:#92400e;display:inline-flex;font-size:.88rem;gap:6px;}
255
+ .mv-status-icon{align-items:center;display:inline-flex;}
256
+ .mv-status-missing{color:#dc2626}
257
+ .mv-status-ok{color:#16a34a}
258
+
259
+ /* ─────────────────────────────────────────────────────────────────────────────
260
+ DRAWER
261
+ ─────────────────────────────────────────────────────────────────────────── */
262
+
263
+ .mv-drawer-root{inset:0;pointer-events:none;position:fixed;z-index:60}
264
+ .mv-drawer-root.is-open{pointer-events:auto}
265
+ .mv-drawer-overlay{background:var(--overlay-scrim);inset:0;opacity:0;position:absolute;transition:opacity .18s ease}
266
+ .mv-drawer-root.is-open .mv-drawer-overlay{opacity:1}
267
+ .mv-drawer{
268
+ background:var(--card-surface);border-left:1px solid var(--color-border);
269
+ box-shadow:-8px 0 24px var(--elev-shadow-lg);display:flex;flex-direction:column;
270
+ height:100%;max-width:90vw;position:absolute;right:0;top:0;
271
+ transform:translateX(100%);transition:transform .22s ease;width:32rem
272
+ }
273
+ .mv-drawer-root.is-open .mv-drawer{transform:translateX(0)}
274
+ .mv-drawer-body{height:100%;overflow:auto;padding:0}
275
+ .mv-drawer-body .mv-card{border-left:0;border-radius:0;border-right:0}
276
+ .mv-drawer-action{background:transparent;border:0;color:var(--color-muted);cursor:pointer;font-size:1.25rem}
277
+ .mv-drawer-head{align-items:center;border-bottom:1px solid var(--color-border);display:flex;justify-content:space-between;padding:.75rem 1rem;font-family:var(--font-head);}
278
+ .mv-drawer-head h2{margin:0}
279
+
280
+ /* ─────────────────────────────────────────────────────────────────────────────
281
+ TOOLTIP + CONFIRM
282
+ ─────────────────────────────────────────────────────────────────────────── */
283
+
284
+ .mv-tooltip{background:var(--tooltip-bg);border-radius:6px;box-shadow:0 4px 16px var(--tooltip-shadow);color:#fff;font-size:12px;line-height:1.2;opacity:0;padding:6px 8px;transition:opacity .15s ease;z-index:9999}
285
+ .mv-tooltip[data-show="true"]{opacity:1}
286
+ .mv-tooltip__arrow{background:var(--tooltip-bg);box-shadow:-1px -1px 1px rgba(2,6,23,.08);height:8px;position:absolute;transform:rotate(45deg);width:8px}
287
+ .mv-tooltip__content{display:block;white-space:nowrap}
288
+ .mv-tooltip--bottom .mv-tooltip__arrow{left:50%;margin-left:-4px;top:-4px}
289
+ .mv-tooltip--left .mv-tooltip__arrow{right:-4px;top:50%;margin-top:-4px}
290
+ .mv-tooltip--right .mv-tooltip__arrow{left:-4px;top:50%;margin-top:-4px}
291
+ .mv-tooltip--top .mv-tooltip__arrow{bottom:-4px;left:50%;margin-left:-4px}
292
+
293
+ .mv-confirm{background:var(--card-surface);border:1px solid var(--color-border);border-radius:8px;box-shadow:0 4px 16px var(--tooltip-shadow);font-size:.85rem;max-width:260px;opacity:0;padding:.75rem;transition:opacity .15s ease;z-index:9999}
294
+ .mv-confirm[data-show="true"]{opacity:1}
295
+ .mv-confirm__content{margin-bottom:.5rem;color:var(--color-text)}
296
+ .mv-confirm__buttons{display:flex;gap:.5rem;justify-content:flex-end}
297
+ .mv-confirm__arrow{background:var(--card-surface);border-left:1px solid var(--color-border);border-top:1px solid var(--color-border);box-shadow:-1px -1px 1px rgba(2,6,23,.08);width:10px;height:10px;position:absolute;transform:rotate(45deg)}
298
+ .mv-confirm--bottom .mv-confirm__arrow{ top:-5px; left:50%; margin-left:-5px }
299
+ .mv-confirm--top .mv-confirm__arrow{ bottom:-5px; left:50%; margin-left:-5px; transform:rotate(225deg) }
300
+ .mv-confirm--left .mv-confirm__arrow{ right:-5px; top:50%; margin-top:-5px; transform:rotate(135deg) }
301
+ .mv-confirm--right .mv-confirm__arrow{ left:-5px; top:50%; margin-top:-5px; transform:rotate(-45deg) }
302
+
303
+ /* ─────────────────────────────────────────────────────────────────────────────
304
+ RTL SUPPORT (no class changes; flip where needed)
305
+ ─────────────────────────────────────────────────────────────────────────── */
306
+
307
+ [dir="rtl"] .mv-brand{flex-direction:row-reverse}
308
+ [dir="rtl"] .mv-status-icon{margin-inline-start:0;margin-inline-end:.5rem}
309
+ [dir="rtl"] .mv-mv-missing{margin-inline-end:0;margin-inline-start:.5rem}
310
+ [dir="rtl"] .mv-th{text-align:right}
311
+ [dir="rtl"] .mv-td-actions{text-align:start}
312
+
313
+ /* Drawer opens from the *left* in RTL */
314
+ [dir="rtl"] .mv-drawer{
315
+ left:0; right:auto;
316
+ border-left:0; border-right:1px solid var(--color-border);
317
+ box-shadow:8px 0 24px var(--elev-shadow-lg);
318
+ transform:translateX(-100%);
319
+ }
320
+ [dir="rtl"] .mv-drawer-root.is-open .mv-drawer{transform:translateX(0)}
321
+
322
+ /* Tooltip/Confirm arrow horizontal flip in RTL */
323
+ [dir="rtl"] .mv-tooltip--left .mv-tooltip__arrow{left:-4px; right:auto}
324
+ [dir="rtl"] .mv-tooltip--right .mv-tooltip__arrow{right:-4px; left:auto}
325
+ [dir="rtl"] .mv-confirm--left .mv-confirm__arrow{left:-5px; right:auto; transform:rotate(-45deg)}
326
+ [dir="rtl"] .mv-confirm--right .mv-confirm__arrow{right:-5px; left:auto; transform:rotate(135deg)}
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Codevedas Inc. 2025-present
4
+ #
5
+ # This source code is licensed under the MIT license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+
8
+ module MatViews
9
+ module Admin
10
+ # MatViews::Admin::ApplicationController
11
+ # --------------------------------------
12
+ # Base controller for the MatViews admin interface.
13
+ #
14
+ # Responsibilities:
15
+ # - Provides authentication and authorization via MatViews::Admin::AuthBridge.
16
+ # - Applies the `mat_views/admin` layout and includes UI helpers.
17
+ # - Manages locale (`I18n.locale`) and enforces language parameter consistency.
18
+ # - Sets the browser time zone around each request when provided via cookies.
19
+ # - Exposes `mat_views_data_theme` (light/dark) for theming via cookies.
20
+ # - Provides frame helpers (`render_frame`, `ensure_frame`) to support Turbo-driven
21
+ # admin UI navigation.
22
+ #
23
+ # Filters:
24
+ # - `before_action`: sets locale and redirects to enforce `lang` consistency.
25
+ # - `around_action`: wraps requests in the browser’s time zone if valid.
26
+ #
27
+ # Methods:
28
+ # - {#default_url_options} ensures `lang` param is included in generated URLs.
29
+ # - {#set_time_zone} runs the request in the cookie-provided time zone if valid.
30
+ # - {#render_frame} renders a UI frame partial given `frame_id`.
31
+ # - {#ensure_frame} requires a `frame_id` param for frame-only actions.
32
+ # - {#redirect_to_lang} redirects when the URL `lang` param differs from `I18n.locale`.
33
+ # - {#set_mat_views_locale} sets the session-defined or default locale.
34
+ # - {#mat_views_data_theme} returns `light`, `dark`, or `nil` for theming.
35
+ #
36
+ class ApplicationController < ActionController::Base
37
+ include MatViews::Admin::AuthBridge
38
+
39
+ helper MatViews::Admin::UiHelper
40
+ helper MatViews::Helpers::UiTestIds
41
+ layout 'mat_views/admin'
42
+
43
+ before_action :set_mat_views_locale, :redirect_to_lang
44
+ helper_method :mat_views_data_theme
45
+ around_action :set_time_zone
46
+
47
+ private
48
+
49
+ # Default URL options, ensuring `lang` is always included.
50
+ #
51
+ # @api private
52
+ #
53
+ # @return [Hash{Symbol => String}]
54
+ def default_url_options
55
+ { lang: params[:lang].presence || I18n.locale }
56
+ end
57
+
58
+ # Wraps the request in the browser’s time zone if one is set in cookies.
59
+ #
60
+ # @api private
61
+ #
62
+ # @yield the block representing the request lifecycle
63
+ # @return [void]
64
+ def set_time_zone(&)
65
+ browser_tz = cookies[:browser_tz]
66
+ if browser_tz.present? && ActiveSupport::TimeZone[browser_tz]
67
+ Time.use_zone(browser_tz, &)
68
+ else
69
+ yield
70
+ end
71
+ end
72
+
73
+ # Ensures a `frame_id` param is present.
74
+ # If missing, redirects to the admin root with an alert.
75
+ #
76
+ # @api private
77
+ #
78
+ # @return [void]
79
+ def ensure_frame
80
+ @frame_id = params[:frame_id]
81
+ if @frame_id.present?
82
+ @frame_action = params[:frame_action]
83
+ return
84
+ end
85
+ redirect_to admin_root_path, alert: I18n.t('mat_views.errors.frame_only')
86
+ end
87
+
88
+ # Redirects to enforce that the `lang` param matches `I18n.locale`.
89
+ #
90
+ # @api private
91
+ #
92
+ # @return [void]
93
+ def redirect_to_lang
94
+ locale_str = locale.to_s
95
+ return if params[:lang] == locale_str
96
+
97
+ lang = locale_str
98
+ redirect_to url_for(params.permit!.to_h.merge(lang: lang)), status: :see_other
99
+ end
100
+
101
+ # Sets the locale for MatViews admin requests.
102
+ # Falls back to default locale if session value is invalid.
103
+ #
104
+ # @api private
105
+ #
106
+ # @return [void]
107
+ def set_mat_views_locale
108
+ I18n.locale = if (loc = session[:mat_views_locale]).present? && MatViews::Engine.available_locales.map(&:to_s).include?(loc)
109
+ loc
110
+ else
111
+ MatViews::Engine.default_locale
112
+ end
113
+ end
114
+
115
+ # Returns the current theme for the admin UI.
116
+ #
117
+ # @api private
118
+ #
119
+ # @return ["light", "dark", nil] the theme stored in cookies, or `nil` if invalid
120
+ def mat_views_data_theme
121
+ theme = cookies[:theme].to_s
122
+ %w[light dark].include?(theme) ? theme : nil
123
+ end
124
+
125
+ # Returns the current locale.
126
+ #
127
+ # @api private
128
+ #
129
+ # @return [Symbol] the current I18n locale
130
+ def locale
131
+ I18n.locale
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Codevedas Inc. 2025-present
4
+ #
5
+ # This source code is licensed under the MIT license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+
8
+ module MatViews
9
+ module Admin
10
+ # MatViews::Admin::DashboardController
11
+ # ------------------------------------
12
+ # Controller for the MatViews admin dashboard.
13
+ #
14
+ # Responsibilities:
15
+ # - Provides the landing page (`index`) for the admin interface.
16
+ # - Authorizes access via {ApplicationController#authorize_mat_views!}.
17
+ # - Prepares placeholder metrics content (future: aggregated refresh metrics).
18
+ #
19
+ class DashboardController < ApplicationController
20
+ # GET /:lang/admin
21
+ #
22
+ # Renders the admin dashboard. Currently sets a placeholder message
23
+ # until metric aggregation is implemented.
24
+ #
25
+ # @return [void]
26
+ def index
27
+ authorize_mat_views!(:read, :mat_views_dashboard)
28
+ @metrics_note = 'Metrics coming soon (see: Aggregate refresh metrics for reporting).'
29
+ end
30
+ end
31
+ end
32
+ end