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,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,7 +29,7 @@ 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
|
@@ -9,7 +9,7 @@
|
|
9
9
|
# Top-level namespace for the mat_views engine.
|
10
10
|
module MatViews
|
11
11
|
##
|
12
|
-
# ActiveJob that handles *creation* of PostgreSQL
|
12
|
+
# ActiveJob that handles *creation* of PostgreSQL materialised views for a
|
13
13
|
# given {MatViews::MatViewDefinition}.
|
14
14
|
#
|
15
15
|
# The job:
|
@@ -17,7 +17,7 @@ module MatViews
|
|
17
17
|
# 2. Looks up the target {MatViews::MatViewDefinition}.
|
18
18
|
# 3. Starts a {MatViews::MatViewRun} row to track lifecycle/timing, with `operation: :create`.
|
19
19
|
# 4. Executes {MatViews::Services::CreateView}.
|
20
|
-
# 5. Finalizes the run with success/failure, duration, and
|
20
|
+
# 5. Finalizes the run with success/failure, duration, and meta.
|
21
21
|
#
|
22
22
|
# @see MatViews::Services::CreateView
|
23
23
|
# @see MatViews::MatViewDefinition
|
@@ -40,28 +40,29 @@ module MatViews
|
|
40
40
|
queue_as { MatViews.configuration.job_queue || :default }
|
41
41
|
|
42
42
|
##
|
43
|
-
# Perform the create job for the given
|
43
|
+
# Perform the create job for the given materialised view definition.
|
44
44
|
#
|
45
45
|
# @api public
|
46
46
|
#
|
47
|
-
# @param
|
47
|
+
# @param mat_view_definition_id [Integer, String] ID of {MatViews::MatViewDefinition}.
|
48
48
|
# @param force_arg [Boolean, Hash, nil] Optional flag or hash (`{ force: true }`)
|
49
49
|
# @param row_count_strategy_arg [:Symbol, String] One of: `:estimated`, `:exact`, `:none` or `nil`.
|
50
50
|
#
|
51
51
|
# @return [Hash] Serialized {MatViews::ServiceResponse#to_h}:
|
52
52
|
# - `:status` [Symbol]
|
53
|
-
# - `:payload` [Hash]
|
54
53
|
# - `:error` [String, nil]
|
55
54
|
# - `:duration_ms` [Integer]
|
56
55
|
# - `:meta` [Hash]
|
57
56
|
#
|
58
57
|
# @raise [StandardError] Re-raised on unexpected failure after marking the run failed.
|
59
58
|
#
|
60
|
-
def perform(
|
61
|
-
definition = MatViews::MatViewDefinition.find(
|
59
|
+
def perform(mat_view_definition_id, force_arg = nil, row_count_strategy_arg = nil)
|
60
|
+
definition = MatViews::MatViewDefinition.find(mat_view_definition_id)
|
62
61
|
|
63
62
|
record_run(definition, :create) do
|
64
|
-
MatViews::Services::CreateView.new(definition,
|
63
|
+
MatViews::Services::CreateView.new(definition,
|
64
|
+
force: force?(force_arg),
|
65
|
+
row_count_strategy: normalize_strategy(row_count_strategy_arg)).call
|
65
66
|
end
|
66
67
|
end
|
67
68
|
|
@@ -9,7 +9,7 @@
|
|
9
9
|
# Top-level namespace for the mat_views engine.
|
10
10
|
module MatViews
|
11
11
|
##
|
12
|
-
# ActiveJob that handles *deletion* of PostgreSQL
|
12
|
+
# ActiveJob that handles *deletion* of PostgreSQL materialised views via
|
13
13
|
# {MatViews::Services::DeleteView}.
|
14
14
|
#
|
15
15
|
# This job mirrors {MatViews::CreateViewJob} and {MatViews::RefreshViewJob}:
|
@@ -39,28 +39,28 @@ module MatViews
|
|
39
39
|
queue_as { MatViews.configuration.job_queue || :default }
|
40
40
|
|
41
41
|
##
|
42
|
-
# Perform the job for the given
|
42
|
+
# Perform the job for the given materialised view definition.
|
43
43
|
#
|
44
44
|
# @api public
|
45
45
|
#
|
46
|
-
# @param
|
46
|
+
# @param mat_view_definition_id [Integer, String] ID of {MatViews::MatViewDefinition}.
|
47
47
|
# @param cascade_arg [Boolean, String, Integer, Hash, nil] Cascade option.
|
48
48
|
# @param row_count_strategy_arg [:Symbol, String] One of: `:estimated`, `:exact`, `:none` or `nil`.
|
49
49
|
#
|
50
50
|
# @return [Hash] Serialized {MatViews::ServiceResponse#to_h}:
|
51
51
|
# - `:status` [Symbol]
|
52
|
-
# - `:payload` [Hash]
|
53
52
|
# - `:error` [String, nil]
|
54
53
|
# - `:duration_ms` [Integer]
|
55
54
|
# - `:meta` [Hash]
|
56
55
|
#
|
57
56
|
# @raise [StandardError] Re-raised on unexpected failure after marking the run failed.
|
58
57
|
#
|
59
|
-
def perform(
|
60
|
-
definition = MatViews::MatViewDefinition.find(
|
58
|
+
def perform(mat_view_definition_id, cascade_arg = nil, row_count_strategy_arg = nil)
|
59
|
+
definition = MatViews::MatViewDefinition.find(mat_view_definition_id)
|
61
60
|
record_run(definition, :drop) do
|
62
|
-
MatViews::Services::DeleteView.new(definition,
|
63
|
-
|
61
|
+
MatViews::Services::DeleteView.new(definition,
|
62
|
+
cascade: cascade?(cascade_arg),
|
63
|
+
row_count_strategy: normalize_strategy(row_count_strategy_arg)).call
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
@@ -23,9 +23,9 @@ module MatViews
|
|
23
23
|
# - otherwise → {MatViews::Services::RegularRefresh}
|
24
24
|
#
|
25
25
|
# Row count reporting can be controlled via `row_count_strategy`:
|
26
|
-
# - `:estimated` (default)
|
27
|
-
# - `:exact`
|
28
|
-
# - `nil`
|
26
|
+
# - `:estimated` (default) - fast, approximate via reltuples
|
27
|
+
# - `:exact` - accurate `COUNT(*)`
|
28
|
+
# - `nil` - skip counting
|
29
29
|
#
|
30
30
|
# @see MatViews::MatViewDefinition
|
31
31
|
# @see MatViews::MatViewRun
|
@@ -48,26 +48,25 @@ module MatViews
|
|
48
48
|
queue_as { MatViews.configuration.job_queue || :default }
|
49
49
|
|
50
50
|
##
|
51
|
-
# Perform the refresh job for the given
|
51
|
+
# Perform the refresh job for the given materialised view definition.
|
52
52
|
#
|
53
53
|
# @api public
|
54
54
|
#
|
55
|
-
# @param
|
55
|
+
# @param mat_view_definition_id [Integer, String] ID of {MatViews::MatViewDefinition}.
|
56
56
|
# @param row_count_strategy_arg [:Symbol, String] One of: `:estimated`, `:exact`, `:none` or `nil`.
|
57
57
|
#
|
58
58
|
# @return [Hash] Serialized {MatViews::ServiceResponse#to_h}:
|
59
59
|
# - `:status` [Symbol]
|
60
|
-
# - `:payload` [Hash]
|
61
60
|
# - `:error` [String, nil]
|
62
61
|
# - `:duration_ms` [Integer]
|
63
62
|
# - `:meta` [Hash]
|
64
63
|
#
|
65
64
|
# @raise [StandardError] Re-raised on unexpected failure after marking the run failed.
|
66
65
|
#
|
67
|
-
def perform(
|
68
|
-
definition = MatViews::MatViewDefinition.find(
|
66
|
+
def perform(mat_view_definition_id, row_count_strategy_arg = nil)
|
67
|
+
definition = MatViews::MatViewDefinition.find(mat_view_definition_id)
|
69
68
|
record_run(definition, :refresh) do
|
70
|
-
service(definition).new(definition, row_count_strategy: normalize_strategy(row_count_strategy_arg)).
|
69
|
+
service(definition).new(definition, row_count_strategy: normalize_strategy(row_count_strategy_arg)).call
|
71
70
|
end
|
72
71
|
end
|
73
72
|
|