mat_views 0.1.2 → 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 +10 -10
- 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 +107 -2
- data/app/jobs/mat_views/create_view_job.rb +21 -122
- data/app/jobs/mat_views/delete_view_job.rb +22 -129
- data/app/jobs/mat_views/refresh_view_job.rb +12 -133
- 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 +34 -16
- 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/ext/exception.rb +20 -0
- 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 +7 -7
- 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/jobs/adapter.rb +8 -5
- data/lib/mat_views/service_response.rb +30 -15
- data/lib/mat_views/services/base_service.rb +204 -41
- data/lib/mat_views/services/check_matview_exists.rb +76 -0
- data/lib/mat_views/services/concurrent_refresh.rb +38 -121
- data/lib/mat_views/services/create_view.rb +72 -55
- data/lib/mat_views/services/delete_view.rb +46 -95
- data/lib/mat_views/services/regular_refresh.rb +38 -94
- data/lib/mat_views/services/swap_refresh.rb +83 -123
- data/lib/mat_views/version.rb +1 -1
- data/lib/mat_views.rb +13 -6
- data/lib/tasks/helpers.rb +27 -27
- data/lib/tasks/mat_views_tasks.rake +48 -42
- metadata +131 -5
@@ -0,0 +1,67 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright Codevedas Inc. 2025-present
|
3
|
+
*
|
4
|
+
* This source code is licensed under the MIT license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
import { Controller } from "@hotwired/stimulus";
|
8
|
+
|
9
|
+
export default class extends Controller {
|
10
|
+
static targets = ["link", "panel"];
|
11
|
+
|
12
|
+
connect() {
|
13
|
+
const qs = new URLSearchParams(window.location.search);
|
14
|
+
const name = qs.get("tab") || this.linkTargets[0]?.dataset?.name;
|
15
|
+
setTimeout(() => {
|
16
|
+
if (name) this.showByName(name, false);
|
17
|
+
else if (this.panelTargets[0])
|
18
|
+
this._ensureFrameLoaded(this.panelTargets[0]);
|
19
|
+
});
|
20
|
+
}
|
21
|
+
|
22
|
+
show(e) {
|
23
|
+
e.preventDefault();
|
24
|
+
const name = e.currentTarget.dataset.name;
|
25
|
+
this.showByName(name, true);
|
26
|
+
}
|
27
|
+
|
28
|
+
showByName(name, pushState) {
|
29
|
+
this.linkTargets.forEach((a) => {
|
30
|
+
const on = a.dataset.name === name;
|
31
|
+
a.classList.toggle("mv-tab--on", on);
|
32
|
+
a.setAttribute("aria-selected", on ? "true" : "false");
|
33
|
+
});
|
34
|
+
|
35
|
+
this.panelTargets.forEach((p) => {
|
36
|
+
p.hidden = p.dataset.name !== name;
|
37
|
+
});
|
38
|
+
|
39
|
+
const active = this.panelTargets.find((p) => p.dataset.name === name);
|
40
|
+
if (active) this._ensureFrameLoaded(active);
|
41
|
+
|
42
|
+
if (pushState) {
|
43
|
+
const url = new URL(window.location);
|
44
|
+
url.searchParams.set("tab", name);
|
45
|
+
|
46
|
+
if (name == "definitions") {
|
47
|
+
url.searchParams.delete("mat_view_definition_id");
|
48
|
+
url.searchParams.delete("operation");
|
49
|
+
url.searchParams.delete("status");
|
50
|
+
}
|
51
|
+
|
52
|
+
history.replaceState({}, "", url);
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
_ensureFrameLoaded(panel) {
|
57
|
+
const frame = panel.querySelector("turbo-frame");
|
58
|
+
|
59
|
+
if (!frame) return;
|
60
|
+
if (!frame.getAttribute("src")) {
|
61
|
+
const src = frame.dataset.src;
|
62
|
+
if (src) frame.setAttribute("src", src);
|
63
|
+
}
|
64
|
+
|
65
|
+
frame.reload();
|
66
|
+
}
|
67
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright Codevedas Inc. 2025-present
|
3
|
+
*
|
4
|
+
* This source code is licensed under the MIT license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
import { Controller } from "@hotwired/stimulus";
|
8
|
+
|
9
|
+
export default class extends Controller {
|
10
|
+
connect() {
|
11
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
12
|
+
if (document.cookie.indexOf(`browser_tz=${tz}`) === -1) {
|
13
|
+
document.cookie = `browser_tz=${tz}; path=/`;
|
14
|
+
}
|
15
|
+
}
|
16
|
+
}
|
@@ -0,0 +1,328 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright Codevedas Inc. 2025-present
|
3
|
+
*
|
4
|
+
* This source code is licensed under the MIT license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
import { Controller } from "@hotwired/stimulus";
|
8
|
+
|
9
|
+
/**
|
10
|
+
* Usage:
|
11
|
+
* <button
|
12
|
+
* data-controller="tooltip"
|
13
|
+
* data-tooltip-text-value="Save"
|
14
|
+
* data-tooltip-placement="bottom"
|
15
|
+
* >
|
16
|
+
* Save
|
17
|
+
* </button>
|
18
|
+
*/
|
19
|
+
export default class extends Controller {
|
20
|
+
static values = {
|
21
|
+
text: String,
|
22
|
+
placement: { type: String, default: "top" },
|
23
|
+
delay: { type: Number, default: 120 },
|
24
|
+
hideDelay: { type: Number, default: 80 },
|
25
|
+
disabled: { type: Boolean, default: false },
|
26
|
+
gap: { type: Number, default: 8 },
|
27
|
+
margin: { type: Number, default: 8 },
|
28
|
+
};
|
29
|
+
|
30
|
+
// ──────────────────────────────────────────────
|
31
|
+
// Lifecycle
|
32
|
+
// ──────────────────────────────────────────────
|
33
|
+
connect() {
|
34
|
+
this.tooltipEl = this._ensureTooltipEl();
|
35
|
+
this.contentEl = this.tooltipEl.querySelector(".mv-tooltip__content");
|
36
|
+
this.element.classList.add("mv-cursor-pointer");
|
37
|
+
this._bindHandlers();
|
38
|
+
this._addListeners();
|
39
|
+
}
|
40
|
+
|
41
|
+
disconnect() {
|
42
|
+
this._clearAllTimers();
|
43
|
+
this._removeListeners();
|
44
|
+
this._restoreTitle();
|
45
|
+
this._clearDescribedBy();
|
46
|
+
}
|
47
|
+
|
48
|
+
// ──────────────────────────────────────────────
|
49
|
+
// Public actions
|
50
|
+
// ──────────────────────────────────────────────
|
51
|
+
show() {
|
52
|
+
if (this.disabledValue) return;
|
53
|
+
this._clearTimer("_hideTimer");
|
54
|
+
this._schedule("_showTimer", () => this._actuallyShow(), this.delayValue);
|
55
|
+
}
|
56
|
+
|
57
|
+
hide() {
|
58
|
+
this._clearTimer("_showTimer");
|
59
|
+
this._schedule(
|
60
|
+
"_hideTimer",
|
61
|
+
() => this._actuallyHide(),
|
62
|
+
this.hideDelayValue,
|
63
|
+
);
|
64
|
+
}
|
65
|
+
|
66
|
+
toggle() {
|
67
|
+
this._isVisible() ? this.hide() : this.show();
|
68
|
+
}
|
69
|
+
|
70
|
+
forceHide() {
|
71
|
+
this._clearAllTimers();
|
72
|
+
this._actuallyHide(true);
|
73
|
+
}
|
74
|
+
|
75
|
+
// ──────────────────────────────────────────────
|
76
|
+
// Internals: show/hide
|
77
|
+
// ──────────────────────────────────────────────
|
78
|
+
_actuallyShow() {
|
79
|
+
const text = this._resolveText();
|
80
|
+
if (!text) return;
|
81
|
+
|
82
|
+
// Remove native title to avoid default browser tooltip
|
83
|
+
this._saveAndRemoveTitle();
|
84
|
+
|
85
|
+
// Fill content
|
86
|
+
this.contentEl.textContent = text;
|
87
|
+
|
88
|
+
// Prep, place, show
|
89
|
+
this._prepareForMeasure();
|
90
|
+
const rect = this.element.getBoundingClientRect();
|
91
|
+
const ttRect = this.tooltipEl.getBoundingClientRect();
|
92
|
+
const placement = this._resolvePlacement();
|
93
|
+
const { x, y } = this._computePosition(
|
94
|
+
rect,
|
95
|
+
ttRect,
|
96
|
+
placement,
|
97
|
+
this.gapValue,
|
98
|
+
this.marginValue,
|
99
|
+
);
|
100
|
+
|
101
|
+
this._applyPlacementClass(placement);
|
102
|
+
this._setTransform(x, y);
|
103
|
+
this._setVisible(true);
|
104
|
+
this._setDescribedBy();
|
105
|
+
}
|
106
|
+
|
107
|
+
_actuallyHide(immediate = false) {
|
108
|
+
this._setVisible(false);
|
109
|
+
|
110
|
+
if (immediate) {
|
111
|
+
this._offscreen();
|
112
|
+
} else {
|
113
|
+
// Allow CSS transition to finish before moving offscreen (if not reopened)
|
114
|
+
setTimeout(() => {
|
115
|
+
if (!this._isVisible()) this._offscreen();
|
116
|
+
}, 150);
|
117
|
+
}
|
118
|
+
|
119
|
+
this._restoreTitle();
|
120
|
+
this._clearDescribedBy();
|
121
|
+
}
|
122
|
+
|
123
|
+
// ──────────────────────────────────────────────
|
124
|
+
// Internals: listeners and handlers
|
125
|
+
// ──────────────────────────────────────────────
|
126
|
+
_bindHandlers() {
|
127
|
+
this._onEnter = this.show.bind(this);
|
128
|
+
this._onLeave = this.hide.bind(this);
|
129
|
+
this._onFocus = this.show.bind(this);
|
130
|
+
this._onBlur = this.hide.bind(this);
|
131
|
+
this._onKey = (e) => {
|
132
|
+
if (e.key === "Escape") this.forceHide();
|
133
|
+
};
|
134
|
+
this._onTouch = (e) => {
|
135
|
+
e.preventDefault();
|
136
|
+
this.show();
|
137
|
+
this._clearTimer("_touchTimer");
|
138
|
+
this._touchTimer = setTimeout(() => this.hide(), 1500);
|
139
|
+
};
|
140
|
+
}
|
141
|
+
|
142
|
+
_addListeners() {
|
143
|
+
const el = this.element;
|
144
|
+
el.addEventListener("mouseenter", this._onEnter);
|
145
|
+
el.addEventListener("mouseleave", this._onLeave);
|
146
|
+
el.addEventListener("focus", this._onFocus);
|
147
|
+
el.addEventListener("blur", this._onBlur);
|
148
|
+
el.addEventListener("keydown", this._onKey);
|
149
|
+
// preventDefault() requires passive: false
|
150
|
+
el.addEventListener("touchstart", this._onTouch, { passive: false });
|
151
|
+
}
|
152
|
+
|
153
|
+
_removeListeners() {
|
154
|
+
const el = this.element;
|
155
|
+
if (!el) return;
|
156
|
+
el.removeEventListener("mouseenter", this._onEnter);
|
157
|
+
el.removeEventListener("mouseleave", this._onLeave);
|
158
|
+
el.removeEventListener("focus", this._onFocus);
|
159
|
+
el.removeEventListener("blur", this._onBlur);
|
160
|
+
el.removeEventListener("keydown", this._onKey);
|
161
|
+
el.removeEventListener("touchstart", this._onTouch, { passive: false });
|
162
|
+
}
|
163
|
+
|
164
|
+
// ──────────────────────────────────────────────
|
165
|
+
// Internals: DOM helpers
|
166
|
+
// ──────────────────────────────────────────────
|
167
|
+
_ensureTooltipEl() {
|
168
|
+
let el = document.getElementById("mv-tooltip");
|
169
|
+
if (el) return el;
|
170
|
+
|
171
|
+
el = document.createElement("div");
|
172
|
+
el.id = "mv-tooltip";
|
173
|
+
el.setAttribute("role", "tooltip");
|
174
|
+
el.className = "mv-tooltip";
|
175
|
+
Object.assign(el.style, {
|
176
|
+
position: "fixed",
|
177
|
+
top: "0",
|
178
|
+
left: "0",
|
179
|
+
pointerEvents: "none",
|
180
|
+
opacity: "0",
|
181
|
+
transform: "translate(-9999px,-9999px)",
|
182
|
+
});
|
183
|
+
el.innerHTML =
|
184
|
+
'<div class="mv-tooltip__content"></div><div class="mv-tooltip__arrow" data-arrow></div>';
|
185
|
+
document.body.appendChild(el);
|
186
|
+
return el;
|
187
|
+
}
|
188
|
+
|
189
|
+
_prepareForMeasure() {
|
190
|
+
this.tooltipEl.style.opacity = "0";
|
191
|
+
this._offscreen();
|
192
|
+
this._removeAllPlacementClasses();
|
193
|
+
}
|
194
|
+
|
195
|
+
_applyPlacementClass(placement) {
|
196
|
+
this._removeAllPlacementClasses();
|
197
|
+
this.tooltipEl.classList.add(`mv-tooltip--${placement}`);
|
198
|
+
}
|
199
|
+
|
200
|
+
_removeAllPlacementClasses() {
|
201
|
+
this.tooltipEl.classList.remove(
|
202
|
+
"mv-tooltip--top",
|
203
|
+
"mv-tooltip--right",
|
204
|
+
"mv-tooltip--bottom",
|
205
|
+
"mv-tooltip--left",
|
206
|
+
);
|
207
|
+
}
|
208
|
+
|
209
|
+
_setTransform(x, y) {
|
210
|
+
this.tooltipEl.style.transform = `translate(${x}px, ${y}px)`;
|
211
|
+
}
|
212
|
+
|
213
|
+
_offscreen() {
|
214
|
+
this.tooltipEl.style.transform = "translate(-9999px,-9999px)";
|
215
|
+
}
|
216
|
+
|
217
|
+
_setVisible(visible) {
|
218
|
+
if (visible) {
|
219
|
+
this.tooltipEl.style.opacity = "1";
|
220
|
+
this.tooltipEl.setAttribute("data-show", "true");
|
221
|
+
} else {
|
222
|
+
this.tooltipEl.removeAttribute("data-show");
|
223
|
+
this.tooltipEl.style.opacity = "0";
|
224
|
+
}
|
225
|
+
}
|
226
|
+
|
227
|
+
_setDescribedBy() {
|
228
|
+
this.element.setAttribute("aria-describedby", "mv-tooltip");
|
229
|
+
}
|
230
|
+
|
231
|
+
_clearDescribedBy() {
|
232
|
+
this.element.removeAttribute("aria-describedby");
|
233
|
+
}
|
234
|
+
|
235
|
+
// ──────────────────────────────────────────────
|
236
|
+
// Internals: data/text/title
|
237
|
+
// ──────────────────────────────────────────────
|
238
|
+
_resolveText() {
|
239
|
+
return (
|
240
|
+
this.textValue ||
|
241
|
+
this.element.getAttribute("aria-label") ||
|
242
|
+
this.element.getAttribute("title")
|
243
|
+
);
|
244
|
+
}
|
245
|
+
|
246
|
+
_resolvePlacement() {
|
247
|
+
return (
|
248
|
+
this.element.getAttribute("data-tooltip-placement") || this.placementValue
|
249
|
+
);
|
250
|
+
}
|
251
|
+
|
252
|
+
_saveAndRemoveTitle() {
|
253
|
+
if (this.element.hasAttribute("title")) {
|
254
|
+
this._savedTitle = this.element.getAttribute("title");
|
255
|
+
this.element.removeAttribute("title");
|
256
|
+
}
|
257
|
+
}
|
258
|
+
|
259
|
+
_restoreTitle() {
|
260
|
+
if (this._savedTitle) {
|
261
|
+
this.element.setAttribute("title", this._savedTitle);
|
262
|
+
this._savedTitle = null;
|
263
|
+
}
|
264
|
+
}
|
265
|
+
|
266
|
+
// ──────────────────────────────────────────────
|
267
|
+
// Internals: timers & state helpers
|
268
|
+
// ──────────────────────────────────────────────
|
269
|
+
_schedule(name, fn, ms) {
|
270
|
+
this._clearTimer(name);
|
271
|
+
this[name] = setTimeout(fn, ms);
|
272
|
+
}
|
273
|
+
|
274
|
+
_clearTimer(name) {
|
275
|
+
if (this[name]) {
|
276
|
+
clearTimeout(this[name]);
|
277
|
+
this[name] = null;
|
278
|
+
}
|
279
|
+
}
|
280
|
+
|
281
|
+
_clearAllTimers() {
|
282
|
+
this._clearTimer("_showTimer");
|
283
|
+
this._clearTimer("_hideTimer");
|
284
|
+
this._clearTimer("_touchTimer");
|
285
|
+
}
|
286
|
+
|
287
|
+
_isVisible() {
|
288
|
+
return this.tooltipEl?.getAttribute("data-show") === "true";
|
289
|
+
}
|
290
|
+
|
291
|
+
// ──────────────────────────────────────────────
|
292
|
+
// Geometry
|
293
|
+
// ──────────────────────────────────────────────
|
294
|
+
_computePosition(targetRect, ttRect, placement, gap, margin) {
|
295
|
+
const coords = this._coordsFor(placement, targetRect, ttRect, gap);
|
296
|
+
const x = this._clampX(coords.x, ttRect.width, margin);
|
297
|
+
const y = this._clampY(coords.y, ttRect.height, margin);
|
298
|
+
return { x: Math.round(x), y: Math.round(y) };
|
299
|
+
}
|
300
|
+
|
301
|
+
_coordsFor(placement, r, tt, gap) {
|
302
|
+
switch (placement) {
|
303
|
+
case "top":
|
304
|
+
return {
|
305
|
+
x: r.left + (r.width - tt.width) / 2,
|
306
|
+
y: r.top - tt.height - gap,
|
307
|
+
};
|
308
|
+
case "bottom":
|
309
|
+
return { x: r.left + (r.width - tt.width) / 2, y: r.bottom + gap };
|
310
|
+
case "left":
|
311
|
+
return {
|
312
|
+
x: r.left - tt.width - gap,
|
313
|
+
y: r.top + (r.height - tt.height) / 2,
|
314
|
+
};
|
315
|
+
case "right":
|
316
|
+
default:
|
317
|
+
return { x: r.right + gap, y: r.top + (r.height - tt.height) / 2 };
|
318
|
+
}
|
319
|
+
}
|
320
|
+
|
321
|
+
_clampX(x, width, margin) {
|
322
|
+
return Math.max(margin, Math.min(x, window.innerWidth - width - margin));
|
323
|
+
}
|
324
|
+
|
325
|
+
_clampY(y, height, margin) {
|
326
|
+
return Math.max(margin, Math.min(y, window.innerHeight - height - margin));
|
327
|
+
}
|
328
|
+
}
|
@@ -0,0 +1,49 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright Codevedas Inc. 2025-present
|
3
|
+
*
|
4
|
+
* This source code is licensed under the MIT license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
import { Controller } from "@hotwired/stimulus";
|
8
|
+
|
9
|
+
export default class extends Controller {
|
10
|
+
connect() {
|
11
|
+
this.elements = document.querySelectorAll("turbo-frame");
|
12
|
+
this.elements.forEach((el) => {
|
13
|
+
el.addEventListener("turbo:before-fetch-request", this._addBusyAttribute);
|
14
|
+
el.addEventListener("turbo:frame-load", this._removeBusyAttribute);
|
15
|
+
el.addEventListener(
|
16
|
+
"turbo:fetch-request-error",
|
17
|
+
this._removeBusyAttribute,
|
18
|
+
);
|
19
|
+
});
|
20
|
+
}
|
21
|
+
|
22
|
+
disconnect() {
|
23
|
+
this.elements.forEach((el) => {
|
24
|
+
el.removeEventListener(
|
25
|
+
"turbo:before-fetch-request",
|
26
|
+
this._addBusyAttribute,
|
27
|
+
);
|
28
|
+
el.removeEventListener("turbo:frame-load", this._removeBusyAttribute);
|
29
|
+
el.removeEventListener(
|
30
|
+
"turbo:fetch-request-error",
|
31
|
+
this._removeBusyAttribute,
|
32
|
+
);
|
33
|
+
});
|
34
|
+
}
|
35
|
+
|
36
|
+
_addBusyAttribute = (event) => {
|
37
|
+
document.body.setAttribute("aria-busy", "true");
|
38
|
+
document.body.setAttribute("busy", "");
|
39
|
+
event.target.setAttribute("aria-busy", "true");
|
40
|
+
event.target.setAttribute("busy", "");
|
41
|
+
};
|
42
|
+
|
43
|
+
_removeBusyAttribute = (event) => {
|
44
|
+
document.body.removeAttribute("aria-busy");
|
45
|
+
document.body.removeAttribute("busy");
|
46
|
+
event.target.removeAttribute("aria-busy");
|
47
|
+
event.target.removeAttribute("busy");
|
48
|
+
};
|
49
|
+
}
|
@@ -8,7 +8,7 @@
|
|
8
8
|
##
|
9
9
|
# Top-level namespace for the mat_views engine.
|
10
10
|
#
|
11
|
-
# All classes, modules, and services for
|
11
|
+
# All classes, modules, and services for materialised view management
|
12
12
|
# are defined under this namespace.
|
13
13
|
#
|
14
14
|
# @example Accessing a job
|
@@ -29,11 +29,116 @@ module MatViews
|
|
29
29
|
#
|
30
30
|
# @example Defining a custom job
|
31
31
|
# class MyCustomJob < MatViews::ApplicationJob
|
32
|
-
# def perform(
|
32
|
+
# def perform(mat_view_definition_id)
|
33
33
|
# # custom logic here
|
34
34
|
# end
|
35
35
|
# end
|
36
36
|
#
|
37
37
|
class ApplicationJob < ActiveJob::Base
|
38
|
+
private
|
39
|
+
|
40
|
+
def record_run(definition, operation, &)
|
41
|
+
start = monotime
|
42
|
+
run = start_run(definition, operation)
|
43
|
+
response = yield
|
44
|
+
finalize_run(run, response, elapsed_ms(start))
|
45
|
+
response.to_h
|
46
|
+
rescue StandardError => e
|
47
|
+
fail_run(run, e, elapsed_ms(start))
|
48
|
+
raise e
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Begin a {MatViews::MatViewRun} row for lifecycle tracking.
|
53
|
+
#
|
54
|
+
# @api private
|
55
|
+
#
|
56
|
+
# @return [MatViews::MatViewRun]
|
57
|
+
#
|
58
|
+
def start_run(definition, operation)
|
59
|
+
MatViews::MatViewRun.create!(
|
60
|
+
mat_view_definition: definition,
|
61
|
+
status: :running,
|
62
|
+
started_at: Time.current,
|
63
|
+
operation: operation
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Finalize the run with success/failure, timing, and meta from the response.
|
69
|
+
#
|
70
|
+
# @api private
|
71
|
+
#
|
72
|
+
# @param run [MatViews::MatViewRun]
|
73
|
+
# @param response [MatViews::ServiceResponse, nil] may be nil if exception raised
|
74
|
+
# @param duration_ms [Integer]
|
75
|
+
# @return [void]
|
76
|
+
#
|
77
|
+
def finalize_run(run, response, duration_ms)
|
78
|
+
base_attrs = {
|
79
|
+
finished_at: Time.current,
|
80
|
+
duration_ms: duration_ms,
|
81
|
+
meta: { request: response.request, response: response.response }.compact
|
82
|
+
}
|
83
|
+
|
84
|
+
if response.success?
|
85
|
+
run.update!(base_attrs.merge(status: :success, error: nil))
|
86
|
+
else
|
87
|
+
run.update!(base_attrs.merge(status: :failed, error: response.error))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# Mark the run failed due to an exception.
|
93
|
+
#
|
94
|
+
# @api private
|
95
|
+
#
|
96
|
+
# @param run [MatViews::MatViewRun]
|
97
|
+
# @param exception [Exception]
|
98
|
+
# @param duration_ms [Integer]
|
99
|
+
# @return [void]
|
100
|
+
#
|
101
|
+
def fail_run(run, exception, duration_ms)
|
102
|
+
run&.update!(
|
103
|
+
error: exception.mv_serialize_error,
|
104
|
+
finished_at: Time.current,
|
105
|
+
duration_ms: duration_ms,
|
106
|
+
status: :failed
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Monotonic clock getter (for elapsed-time measurement).
|
112
|
+
#
|
113
|
+
# @api private
|
114
|
+
# @return [Float] seconds
|
115
|
+
#
|
116
|
+
def monotime = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
117
|
+
|
118
|
+
##
|
119
|
+
# Convert monotonic start time to elapsed milliseconds.
|
120
|
+
#
|
121
|
+
# @api private
|
122
|
+
# @param start [Float]
|
123
|
+
# @return [Integer] elapsed ms
|
124
|
+
#
|
125
|
+
def elapsed_ms(start) = ((monotime - start) * 1000).round
|
126
|
+
|
127
|
+
##
|
128
|
+
# Normalize the strategy argument into a symbol or default.
|
129
|
+
#
|
130
|
+
# @api private
|
131
|
+
#
|
132
|
+
# @param arg [Symbol, String, nil]
|
133
|
+
# @return [Symbol] One of `:estimated`, `:exact`, or `:none` by default.
|
134
|
+
#
|
135
|
+
def normalize_strategy(arg)
|
136
|
+
case arg
|
137
|
+
when String, Symbol
|
138
|
+
arg.to_sym
|
139
|
+
else
|
140
|
+
:none
|
141
|
+
end
|
142
|
+
end
|
38
143
|
end
|
39
144
|
end
|