good_pipeline 0.4.0 → 0.6.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/Appraisals +25 -0
- data/CHANGELOG.md +58 -0
- data/README.md +67 -11
- data/app/controllers/good_pipeline/frontends_controller.rb +18 -1
- data/app/controllers/good_pipeline/pipelines_controller.rb +254 -36
- data/app/controllers/good_pipeline/themes_controller.rb +15 -0
- data/app/frontend/good_pipeline/dashboard.css +407 -0
- data/app/frontend/good_pipeline/dashboard.js +542 -0
- data/app/helpers/good_pipeline/mermaid_diagram_builder.rb +225 -66
- data/app/helpers/good_pipeline/pipelines_helper.rb +89 -5
- data/app/jobs/good_pipeline/pipeline_callback_job.rb +2 -2
- data/app/jobs/good_pipeline/pipeline_reconciliation_job.rb +1 -2
- data/app/jobs/good_pipeline/step_finished_job.rb +6 -2
- data/app/models/good_pipeline/pipeline_record.rb +7 -4
- data/app/models/good_pipeline/step_record.rb +6 -3
- data/app/views/good_pipeline/pipelines/_actions.html.erb +28 -0
- data/app/views/good_pipeline/pipelines/_chain_links.html.erb +17 -29
- data/app/views/good_pipeline/pipelines/_diagram.html.erb +101 -0
- data/app/views/good_pipeline/pipelines/_filter_bar.html.erb +51 -0
- data/app/views/good_pipeline/pipelines/_kpi_strip.html.erb +74 -0
- data/app/views/good_pipeline/pipelines/_pager.html.erb +31 -0
- data/app/views/good_pipeline/pipelines/_row.html.erb +103 -0
- data/app/views/good_pipeline/pipelines/_row_detail.html.erb +93 -0
- data/app/views/good_pipeline/pipelines/_sidebar.html.erb +115 -0
- data/app/views/good_pipeline/pipelines/_steps_table.html.erb +46 -33
- data/app/views/good_pipeline/pipelines/_table.html.erb +33 -0
- data/app/views/good_pipeline/pipelines/_timeline.html.erb +55 -0
- data/app/views/good_pipeline/pipelines/_topbar.html.erb +54 -0
- data/app/views/good_pipeline/pipelines/definitions.html.erb +88 -55
- data/app/views/good_pipeline/pipelines/index.html.erb +31 -42
- data/app/views/good_pipeline/pipelines/show.html.erb +54 -47
- data/app/views/layouts/good_pipeline/application.html.erb +24 -153
- data/config/routes.rb +4 -0
- data/demo/config/environments/development.rb +3 -0
- data/demo/config/environments/test.rb +2 -1
- data/demo/db/migrate/20260319205325_create_good_jobs.rb +1 -1
- data/demo/db/migrate/20260319205326_create_good_pipeline_tables.rb +1 -1
- data/demo/db/migrate/20260810000000_add_good_pipeline_dashboard_indexes.rb +20 -0
- data/demo/db/seeds.rb +325 -11
- data/demo/docs/screenshots/definitions.png +0 -0
- data/demo/docs/screenshots/show.png +0 -0
- data/demo/test/good_pipeline/test_bulk_enqueue.rb +54 -1
- data/demo/test/good_pipeline/test_cancellation.rb +312 -0
- data/demo/test/good_pipeline/test_cleanup.rb +31 -1
- data/demo/test/good_pipeline/test_coordinator.rb +39 -0
- data/demo/test/good_pipeline/test_dashboard.rb +667 -0
- data/demo/test/good_pipeline/test_dashboard_rerun.rb +244 -0
- data/demo/test/good_pipeline/test_pipeline_callback_job.rb +11 -0
- data/demo/test/good_pipeline/test_pipeline_record.rb +30 -0
- data/demo/test/good_pipeline/test_runner.rb +40 -1
- data/demo/test/good_pipeline/test_step_record.rb +37 -0
- data/demo/test/integration/test_barrier_execution.rb +502 -0
- data/demo/test/integration/test_chain_locking.rb +54 -0
- data/demo/test/integration/test_pipeline_chaining.rb +96 -0
- data/demo/test/test_helper.rb +20 -0
- data/docs/architecture.md +45 -29
- data/docs/branching.md +4 -0
- data/docs/callbacks.md +9 -7
- data/docs/cleanup.md +2 -2
- data/docs/dag-validation.md +16 -0
- data/docs/dashboard.md +84 -22
- data/docs/defining-pipelines.md +34 -1
- data/docs/failure-strategies.md +24 -1
- data/docs/getting-started.md +9 -1
- data/docs/index.md +1 -1
- data/docs/introduction.md +4 -4
- data/docs/monitoring.md +8 -3
- data/docs/pipeline-chaining.md +17 -9
- data/docs/public/screenshots/definitions.png +0 -0
- data/docs/public/screenshots/index.png +0 -0
- data/docs/public/screenshots/show.png +0 -0
- data/docs/screenshots/definitions.png +0 -0
- data/docs/screenshots/index.png +0 -0
- data/docs/screenshots/show.png +0 -0
- data/gemfiles/rails_7_2.gemfile +24 -0
- data/gemfiles/rails_7_2.gemfile.lock +412 -0
- data/gemfiles/rails_8_0.gemfile +24 -0
- data/gemfiles/rails_8_0.gemfile.lock +411 -0
- data/gemfiles/rails_8_1.gemfile +24 -0
- data/gemfiles/rails_8_1.gemfile.lock +415 -0
- data/lib/generators/good_pipeline/install/templates/create_good_pipeline_tables.rb.erb +9 -0
- data/lib/generators/good_pipeline/upgrade/templates/add_good_pipeline_dashboard_indexes.rb.erb +27 -0
- data/lib/generators/good_pipeline/upgrade/upgrade_generator.rb +32 -0
- data/lib/good_pipeline/barrier_compiler.rb +106 -0
- data/lib/good_pipeline/chain_coordinator.rb +9 -5
- data/lib/good_pipeline/constants.rb +2 -0
- data/lib/good_pipeline/coordinator.rb +269 -140
- data/lib/good_pipeline/dashboard/connection_info.rb +67 -0
- data/lib/good_pipeline/dashboard/definition_stages.rb +59 -0
- data/lib/good_pipeline/dashboard/filter_set.rb +77 -0
- data/lib/good_pipeline/dashboard/kpi_calculator.rb +143 -0
- data/lib/good_pipeline/dashboard/sparkline.rb +73 -0
- data/lib/good_pipeline/dashboard/stage_lanes.rb +307 -0
- data/lib/good_pipeline/dashboard/step_timings.rb +50 -0
- data/lib/good_pipeline/dashboard.rb +21 -0
- data/lib/good_pipeline/errors.rb +11 -0
- data/lib/good_pipeline/pipeline.rb +14 -2
- data/lib/good_pipeline/step_definition.rb +16 -1
- data/lib/good_pipeline/version.rb +1 -1
- data/lib/good_pipeline.rb +7 -1
- data/mise.toml +1 -1
- metadata +48 -13
- data/app/frontend/good_pipeline/style.css +0 -597
- data/app/views/good_pipeline/pipelines/_pagination.html.erb +0 -24
- data/app/views/good_pipeline/pipelines/_pipeline_row.html.erb +0 -7
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
/* GoodPipeline dashboard — dependency-free browser behavior, no build step. */
|
|
2
|
+
(function () {
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
var SWAP_REGIONS = ["gp-kpi", "gp-filters", "gp-table"];
|
|
6
|
+
var MIN_SCALE = 0.5;
|
|
7
|
+
var MAX_SCALE = 4;
|
|
8
|
+
var ZOOM_STEP = 0.25;
|
|
9
|
+
var searchTimer = null;
|
|
10
|
+
var navSequence = 0;
|
|
11
|
+
var navController = null;
|
|
12
|
+
var latestRequestedUrl = window.location.href;
|
|
13
|
+
var viewportStates = new WeakMap();
|
|
14
|
+
var renderTokens = new WeakMap();
|
|
15
|
+
var renderSequence = 0;
|
|
16
|
+
var mermaidId = 0;
|
|
17
|
+
var mermaidBusy = false;
|
|
18
|
+
var queuedRender = null;
|
|
19
|
+
|
|
20
|
+
function csrfToken() {
|
|
21
|
+
var meta = document.querySelector('meta[name="csrf-token"]');
|
|
22
|
+
return meta ? meta.content : "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function replaceContents(target, source) {
|
|
26
|
+
while (target.firstChild) target.removeChild(target.firstChild);
|
|
27
|
+
Array.from(source.childNodes).forEach(function (child) {
|
|
28
|
+
target.appendChild(child.cloneNode(true));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function syncSearchFromUrl() {
|
|
33
|
+
var input = document.querySelector("form[data-gp-search-form] input[name=q]");
|
|
34
|
+
if (!input || document.activeElement === input) return;
|
|
35
|
+
var value = new URL(window.location.href).searchParams.get("q") || "";
|
|
36
|
+
if (input.value !== value) input.value = value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setLoading(loading) {
|
|
40
|
+
SWAP_REGIONS.forEach(function (id) {
|
|
41
|
+
var region = document.getElementById(id);
|
|
42
|
+
if (region) region.classList.toggle("is-loading", loading);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function partialNav(url, options) {
|
|
47
|
+
options = options || {};
|
|
48
|
+
url = new URL(url, window.location.origin).toString();
|
|
49
|
+
latestRequestedUrl = url;
|
|
50
|
+
var targets = SWAP_REGIONS.map(function (id) { return document.getElementById(id); });
|
|
51
|
+
if (targets.some(function (node) { return !node; })) {
|
|
52
|
+
window.location.href = url;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var sequence = ++navSequence;
|
|
57
|
+
if (navController) navController.abort();
|
|
58
|
+
navController = new AbortController();
|
|
59
|
+
var controller = navController;
|
|
60
|
+
if (!options.silent) setLoading(true);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
var response = await fetch(url, {
|
|
64
|
+
method: "GET",
|
|
65
|
+
signal: controller.signal,
|
|
66
|
+
credentials: "same-origin",
|
|
67
|
+
headers: {
|
|
68
|
+
"Accept": "text/html",
|
|
69
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
70
|
+
"X-GoodPipeline-Partial": "true"
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) throw new Error("HTTP " + response.status + " " + response.statusText);
|
|
74
|
+
var html = await response.text();
|
|
75
|
+
if (controller.signal.aborted || sequence !== navSequence) return;
|
|
76
|
+
|
|
77
|
+
var doc = new DOMParser().parseFromString(html, "text/html");
|
|
78
|
+
var sources = SWAP_REGIONS.map(function (id) { return doc.getElementById(id); });
|
|
79
|
+
if (sources.some(function (node) { return !node; })) throw new Error("partial response is missing a dashboard region");
|
|
80
|
+
|
|
81
|
+
targets.forEach(function (target, index) { replaceContents(target, sources[index]); });
|
|
82
|
+
if (options.history === "replace") history.replaceState({}, "", url);
|
|
83
|
+
if (options.history !== "replace" && options.history !== "none") history.pushState({}, "", url);
|
|
84
|
+
syncSearchFromUrl();
|
|
85
|
+
initializeDynamicContent();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error && error.name === "AbortError") return;
|
|
88
|
+
if (sequence !== navSequence) return;
|
|
89
|
+
console.error("[good_pipeline] partial navigation failed", error);
|
|
90
|
+
window.location.href = url;
|
|
91
|
+
} finally {
|
|
92
|
+
if (sequence === navSequence) setLoading(false);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function searchUrl(form) {
|
|
97
|
+
var url = new URL(form.action || window.location.href, window.location.origin);
|
|
98
|
+
var params = new URL(latestRequestedUrl, window.location.origin).searchParams;
|
|
99
|
+
params.delete("q");
|
|
100
|
+
params.delete("page");
|
|
101
|
+
params.delete("expanded");
|
|
102
|
+
var data = new FormData(form);
|
|
103
|
+
var query = String(data.get("q") || "").trim();
|
|
104
|
+
if (query) params.set("q", query);
|
|
105
|
+
url.search = params.toString();
|
|
106
|
+
return url.toString();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function segmentUrl(link) {
|
|
110
|
+
var url = new URL(latestRequestedUrl, window.location.origin);
|
|
111
|
+
var key = link.getAttribute("data-gp-segment");
|
|
112
|
+
var value = link.getAttribute("data-gp-segment-value");
|
|
113
|
+
if (key && value) url.searchParams.set(key, value);
|
|
114
|
+
url.searchParams.delete("page");
|
|
115
|
+
url.searchParams.delete("expanded");
|
|
116
|
+
|
|
117
|
+
var input = document.querySelector("form[data-gp-search-form] input[name=q]");
|
|
118
|
+
if (input) {
|
|
119
|
+
var query = input.value.trim();
|
|
120
|
+
if (query) url.searchParams.set("q", query);
|
|
121
|
+
else url.searchParams.delete("q");
|
|
122
|
+
}
|
|
123
|
+
return url.toString();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
var COPY_ICON = '<svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8l3 3 7-7"></path></svg>';
|
|
127
|
+
|
|
128
|
+
function announce(message) {
|
|
129
|
+
var region = document.getElementById("gp-aria-live");
|
|
130
|
+
if (!region) return;
|
|
131
|
+
region.textContent = "";
|
|
132
|
+
requestAnimationFrame(function () { region.textContent = message; });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function flashCopied(button) {
|
|
136
|
+
var original = button.innerHTML;
|
|
137
|
+
var label = button.getAttribute("data-gp-copy-label") || "text";
|
|
138
|
+
button.innerHTML = COPY_ICON;
|
|
139
|
+
announce("Copied " + label);
|
|
140
|
+
if (button._gpCopyTimer) clearTimeout(button._gpCopyTimer);
|
|
141
|
+
button._gpCopyTimer = setTimeout(function () {
|
|
142
|
+
if (button.isConnected) button.innerHTML = original;
|
|
143
|
+
}, 1100);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function fallbackCopy(value) {
|
|
147
|
+
var area = document.createElement("textarea");
|
|
148
|
+
area.value = value;
|
|
149
|
+
area.setAttribute("readonly", "");
|
|
150
|
+
area.setAttribute("aria-hidden", "true");
|
|
151
|
+
area.style.position = "fixed";
|
|
152
|
+
area.style.left = "-9999px";
|
|
153
|
+
area.style.top = "0";
|
|
154
|
+
document.body.appendChild(area);
|
|
155
|
+
area.select();
|
|
156
|
+
area.setSelectionRange(0, value.length);
|
|
157
|
+
var copied = false;
|
|
158
|
+
try { copied = document.execCommand("copy"); } catch (_error) { copied = false; }
|
|
159
|
+
area.remove();
|
|
160
|
+
return copied;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function copyValue(button) {
|
|
164
|
+
var value = button.getAttribute("data-gp-copy");
|
|
165
|
+
if (value == null) return;
|
|
166
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
167
|
+
navigator.clipboard.writeText(value).then(function () {
|
|
168
|
+
flashCopied(button);
|
|
169
|
+
}).catch(function () {
|
|
170
|
+
if (fallbackCopy(value)) flashCopied(button);
|
|
171
|
+
else announce("Copy failed");
|
|
172
|
+
});
|
|
173
|
+
} else if (fallbackCopy(value)) {
|
|
174
|
+
flashCopied(button);
|
|
175
|
+
} else {
|
|
176
|
+
announce("Copy failed");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function currentTheme() {
|
|
181
|
+
return document.documentElement.getAttribute("data-gp-theme") === "light" ? "light" : "dark";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function updateThemeButtons(theme) {
|
|
185
|
+
document.querySelectorAll("[data-gp-theme-toggle]").forEach(function (button) {
|
|
186
|
+
button.textContent = theme === "dark" ? "◐" : "◑";
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function setTheme(theme) {
|
|
191
|
+
document.documentElement.setAttribute("data-gp-theme", theme);
|
|
192
|
+
updateThemeButtons(theme);
|
|
193
|
+
renderVisibleDiagrams(true);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function toggleTheme() {
|
|
197
|
+
var previous = currentTheme();
|
|
198
|
+
var next = previous === "dark" ? "light" : "dark";
|
|
199
|
+
setTheme(next);
|
|
200
|
+
var meta = document.querySelector('meta[name="good-pipeline-theme-url"]');
|
|
201
|
+
var url = meta ? meta.content : "theme";
|
|
202
|
+
fetch(url, {
|
|
203
|
+
method: "PATCH",
|
|
204
|
+
credentials: "same-origin",
|
|
205
|
+
headers: { "Accept": "application/json", "Content-Type": "application/json", "X-CSRF-Token": csrfToken() },
|
|
206
|
+
body: JSON.stringify({ theme: next })
|
|
207
|
+
}).then(function (response) {
|
|
208
|
+
if (!response.ok) throw new Error("HTTP " + response.status);
|
|
209
|
+
}).catch(function (error) {
|
|
210
|
+
setTheme(previous);
|
|
211
|
+
console.warn("[good_pipeline] theme persistence failed", error);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function graphFrom(container) {
|
|
216
|
+
var value = container.getAttribute("data-graph") || "";
|
|
217
|
+
try {
|
|
218
|
+
var parsed = JSON.parse(value);
|
|
219
|
+
return parsed && typeof parsed === "object" ? String(parsed.graph || "") : String(parsed || "");
|
|
220
|
+
} catch (_error) {
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function diagramPalette(theme) {
|
|
226
|
+
if (theme === "light") {
|
|
227
|
+
return { bg:"#f5f6f8",node:"#eef0f4",text:"#0f1216",border:"#cdd2da",line:"#8a92a0",success:"#2f8a52",running:"#2563d4",failed:"#c43a3a",halted:"#b16a1f",skipped:"#6b7384",branch:"#b16a1f",terminal:"#1a1d22",terminalText:"#ffffff" };
|
|
228
|
+
}
|
|
229
|
+
return { bg:"#0c0d10",node:"#1a1e24",text:"#e6e9ef",border:"#2a2f38",line:"#5d6573",success:"#4ea36b",running:"#6694e8",failed:"#d96565",halted:"#c98a3f",skipped:"#6b7384",branch:"#c98a3f",terminal:"#d6deea",terminalText:"#0c0d10" };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function graphClasses(palette) {
|
|
233
|
+
return [
|
|
234
|
+
" classDef step fill:" + palette.node + ",color:" + palette.text + ",stroke:" + palette.border,
|
|
235
|
+
" classDef pending fill:" + palette.node + ",color:" + palette.skipped + ",stroke:" + palette.skipped,
|
|
236
|
+
" classDef running fill:" + palette.running + ",color:#fff,stroke:" + palette.running,
|
|
237
|
+
" classDef enqueued fill:" + palette.running + ",color:#fff,stroke:" + palette.running,
|
|
238
|
+
" classDef succeeded fill:" + palette.success + ",color:#fff,stroke:" + palette.success,
|
|
239
|
+
" classDef failed fill:" + palette.failed + ",color:#fff,stroke:" + palette.failed,
|
|
240
|
+
" classDef halted fill:" + palette.halted + ",color:#fff,stroke:" + palette.halted,
|
|
241
|
+
" classDef canceled fill:" + palette.node + ",color:" + palette.skipped + ",stroke:" + palette.skipped,
|
|
242
|
+
" classDef skipped fill:" + palette.node + ",color:" + palette.skipped + ",stroke:" + palette.skipped,
|
|
243
|
+
" classDef skipped_by_branch fill:" + palette.node + ",color:" + palette.skipped + ",stroke:" + palette.skipped,
|
|
244
|
+
" classDef branch fill:" + palette.branch + ",color:#fff,stroke:" + palette.branch,
|
|
245
|
+
" classDef barrier fill:" + palette.node + ",color:" + palette.text + ",stroke:" + palette.line + ",stroke-width:2px",
|
|
246
|
+
" classDef terminal fill:" + palette.terminal + ",color:" + palette.terminalText + ",stroke:" + palette.terminal
|
|
247
|
+
].join("\n");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function diagramIsVisible(container) {
|
|
251
|
+
var definition = container.closest(".gp-definition-panel");
|
|
252
|
+
if (definition && !definition.classList.contains("is-active")) return false;
|
|
253
|
+
var panel = container.closest(".gp-graph-panel");
|
|
254
|
+
return !panel || !panel.hidden;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function diagramError(container, message) {
|
|
258
|
+
var canvas = container.querySelector(".gp-diagram-canvas");
|
|
259
|
+
if (!canvas) return;
|
|
260
|
+
while (canvas.firstChild) canvas.removeChild(canvas.firstChild);
|
|
261
|
+
var error = document.createElement("div");
|
|
262
|
+
error.className = "gp-diagram-error";
|
|
263
|
+
error.setAttribute("role", "alert");
|
|
264
|
+
error.textContent = message;
|
|
265
|
+
canvas.appendChild(error);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function performRender(task) {
|
|
269
|
+
var container = task.container;
|
|
270
|
+
if (!container.isConnected || !diagramIsVisible(container)) return;
|
|
271
|
+
if (!window.mermaid || typeof window.mermaid.render !== "function") {
|
|
272
|
+
diagramError(container, "The diagram renderer could not be loaded.");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
var graph = graphFrom(container);
|
|
276
|
+
if (!graph) {
|
|
277
|
+
diagramError(container, "No graph definition is available.");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
var edgeCount = Number(container.getAttribute("data-edge-count") || 0);
|
|
281
|
+
if (edgeCount > 1000) {
|
|
282
|
+
diagramError(container, "Full graph disabled: " + edgeCount + " edges exceeds Mermaid's 1,000-edge safety limit.");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
var theme = currentTheme();
|
|
286
|
+
var palette = diagramPalette(theme);
|
|
287
|
+
var definition = graph + "\n" + graphClasses(palette);
|
|
288
|
+
window.mermaid.initialize({
|
|
289
|
+
startOnLoad:false,
|
|
290
|
+
securityLevel:"strict",
|
|
291
|
+
theme:"base",
|
|
292
|
+
maxEdges:1000,
|
|
293
|
+
themeVariables:{ background:"transparent",primaryColor:palette.node,primaryTextColor:palette.text,primaryBorderColor:palette.border,lineColor:palette.line,fontFamily:"JetBrains Mono, monospace",fontSize:"11px",edgeLabelBackground:palette.bg },
|
|
294
|
+
flowchart:{ curve:"basis",nodeSpacing:26,rankSpacing:38,useMaxWidth:false }
|
|
295
|
+
});
|
|
296
|
+
var result = await window.mermaid.render("gp-dag-" + (++mermaidId), definition);
|
|
297
|
+
if (!container.isConnected || renderTokens.get(container) !== task.token) return;
|
|
298
|
+
var canvas = container.querySelector(".gp-diagram-canvas");
|
|
299
|
+
if (!canvas) return;
|
|
300
|
+
canvas.innerHTML = result.svg;
|
|
301
|
+
var svg = canvas.querySelector("svg");
|
|
302
|
+
if (svg) { svg.style.maxWidth = "100%"; svg.style.height = "auto"; }
|
|
303
|
+
container.setAttribute("data-rendered-theme", theme);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function drainRenderQueue() {
|
|
307
|
+
if (mermaidBusy || !queuedRender) return;
|
|
308
|
+
var task = queuedRender;
|
|
309
|
+
queuedRender = null;
|
|
310
|
+
mermaidBusy = true;
|
|
311
|
+
performRender(task).catch(function (error) {
|
|
312
|
+
if (task.container.isConnected && renderTokens.get(task.container) === task.token) {
|
|
313
|
+
diagramError(task.container, "Diagram render failed: " + (error && error.message ? error.message : "unknown error"));
|
|
314
|
+
}
|
|
315
|
+
}).finally(function () {
|
|
316
|
+
mermaidBusy = false;
|
|
317
|
+
drainRenderQueue();
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function requestDiagramRender(container, force) {
|
|
322
|
+
if (!container || !diagramIsVisible(container)) return;
|
|
323
|
+
if (!force && container.getAttribute("data-rendered-theme") === currentTheme() && container.querySelector("svg")) return;
|
|
324
|
+
var token = ++renderSequence;
|
|
325
|
+
renderTokens.set(container, token);
|
|
326
|
+
queuedRender = { container:container, token:token };
|
|
327
|
+
drainRenderQueue();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function renderVisibleDiagrams(force) {
|
|
331
|
+
var diagrams = Array.from(document.querySelectorAll(".gp-diagram-container[data-graph]"));
|
|
332
|
+
// Each screen has one visible graph. Iteration order still makes the newest
|
|
333
|
+
// visible request win if host markup accidentally exposes more than one.
|
|
334
|
+
diagrams.forEach(function (container) {
|
|
335
|
+
if (diagramIsVisible(container)) requestDiagramRender(container, force);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function viewportState(container) {
|
|
340
|
+
if (!viewportStates.has(container)) viewportStates.set(container, { scale:1, panX:0, panY:0 });
|
|
341
|
+
return viewportStates.get(container);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function applyViewport(container) {
|
|
345
|
+
var state = viewportState(container);
|
|
346
|
+
var canvas = container.querySelector(".gp-diagram-canvas");
|
|
347
|
+
if (canvas) canvas.style.transform = "translate(" + state.panX + "px," + state.panY + "px) scale(" + state.scale + ")";
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function resetViewport(container) {
|
|
351
|
+
if (!container) return;
|
|
352
|
+
viewportStates.set(container, { scale:1, panX:0, panY:0 });
|
|
353
|
+
applyViewport(container);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function zoomViewport(container, delta, centerX, centerY) {
|
|
357
|
+
var state = viewportState(container);
|
|
358
|
+
var oldScale = state.scale;
|
|
359
|
+
var next = Math.max(MIN_SCALE, Math.min(MAX_SCALE, oldScale + delta));
|
|
360
|
+
if (next === oldScale) return;
|
|
361
|
+
var ratio = next / oldScale;
|
|
362
|
+
state.panX = centerX - (centerX - state.panX) * ratio;
|
|
363
|
+
state.panY = centerY - (centerY - state.panY) * ratio;
|
|
364
|
+
state.scale = next;
|
|
365
|
+
applyViewport(container);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function bindViewport(viewport) {
|
|
369
|
+
if (viewport.dataset.gpViewportBound === "true") return;
|
|
370
|
+
viewport.dataset.gpViewportBound = "true";
|
|
371
|
+
viewport.addEventListener("wheel", function (event) {
|
|
372
|
+
event.preventDefault();
|
|
373
|
+
var container = viewport.closest(".gp-diagram-container");
|
|
374
|
+
var rect = viewport.getBoundingClientRect();
|
|
375
|
+
zoomViewport(container, event.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP, event.clientX - rect.left, event.clientY - rect.top);
|
|
376
|
+
}, { passive:false });
|
|
377
|
+
viewport.addEventListener("pointerdown", function (event) {
|
|
378
|
+
if (event.button !== 0) return;
|
|
379
|
+
var container = viewport.closest(".gp-diagram-container");
|
|
380
|
+
var canvas = container.querySelector(".gp-diagram-canvas");
|
|
381
|
+
var state = viewportState(container);
|
|
382
|
+
var startX = event.clientX - state.panX;
|
|
383
|
+
var startY = event.clientY - state.panY;
|
|
384
|
+
if (canvas) canvas.classList.add("is-dragging");
|
|
385
|
+
viewport.setPointerCapture(event.pointerId);
|
|
386
|
+
function move(moveEvent) {
|
|
387
|
+
state.panX = moveEvent.clientX - startX;
|
|
388
|
+
state.panY = moveEvent.clientY - startY;
|
|
389
|
+
applyViewport(container);
|
|
390
|
+
}
|
|
391
|
+
function up() {
|
|
392
|
+
if (canvas) canvas.classList.remove("is-dragging");
|
|
393
|
+
viewport.removeEventListener("pointermove", move);
|
|
394
|
+
viewport.removeEventListener("pointerup", up);
|
|
395
|
+
viewport.removeEventListener("pointercancel", up);
|
|
396
|
+
}
|
|
397
|
+
viewport.addEventListener("pointermove", move);
|
|
398
|
+
viewport.addEventListener("pointerup", up);
|
|
399
|
+
viewport.addEventListener("pointercancel", up);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function initializeDynamicContent() {
|
|
404
|
+
document.querySelectorAll(".gp-diagram-viewport").forEach(bindViewport);
|
|
405
|
+
renderVisibleDiagrams(false);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function switchDefinition(button) {
|
|
409
|
+
var targetId = button.getAttribute("data-gp-definition-target");
|
|
410
|
+
var panel = document.getElementById(targetId);
|
|
411
|
+
if (!panel) return;
|
|
412
|
+
document.querySelectorAll(".gp-definition-item").forEach(function (item) {
|
|
413
|
+
var active = item === button;
|
|
414
|
+
item.classList.toggle("is-active", active);
|
|
415
|
+
item.setAttribute("aria-selected", active ? "true" : "false");
|
|
416
|
+
});
|
|
417
|
+
document.querySelectorAll(".gp-definition-panel").forEach(function (item) { item.classList.toggle("is-active", item === panel); });
|
|
418
|
+
var crumb = document.querySelector("[data-gp-active-crumb]");
|
|
419
|
+
var meta = document.querySelector("[data-gp-active-meta]");
|
|
420
|
+
if (crumb) crumb.textContent = button.getAttribute("data-gp-definition-name") || "";
|
|
421
|
+
if (meta) meta.textContent = button.getAttribute("data-gp-definition-meta") || "";
|
|
422
|
+
var diagram = panel.querySelector(".gp-diagram-container");
|
|
423
|
+
resetViewport(diagram);
|
|
424
|
+
requestDiagramRender(diagram, false);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function toggleGraph(button) {
|
|
428
|
+
var card = button.closest(".gp-card");
|
|
429
|
+
if (!card) return;
|
|
430
|
+
var graphPanel = card.querySelector(".gp-graph-panel");
|
|
431
|
+
var stagePanel = card.querySelector(".gp-stage-panel");
|
|
432
|
+
if (!graphPanel || !stagePanel) return;
|
|
433
|
+
var showGraph = graphPanel.hidden;
|
|
434
|
+
graphPanel.hidden = !showGraph;
|
|
435
|
+
stagePanel.hidden = showGraph;
|
|
436
|
+
button.textContent = showGraph ? "stage view" : "render full graph";
|
|
437
|
+
button.setAttribute("aria-pressed", showGraph ? "true" : "false");
|
|
438
|
+
var diagram = graphPanel.querySelector(".gp-diagram-container");
|
|
439
|
+
resetViewport(diagram);
|
|
440
|
+
if (showGraph) requestDiagramRender(diagram, false);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
document.addEventListener("click", function (event) {
|
|
444
|
+
var copy = event.target.closest("[data-gp-copy]");
|
|
445
|
+
if (copy) { event.preventDefault(); event.stopPropagation(); copyValue(copy); return; }
|
|
446
|
+
|
|
447
|
+
var theme = event.target.closest("[data-gp-theme-toggle]");
|
|
448
|
+
if (theme) { event.preventDefault(); toggleTheme(); return; }
|
|
449
|
+
|
|
450
|
+
var definition = event.target.closest("[data-gp-definition-target]");
|
|
451
|
+
if (definition) { event.preventDefault(); switchDefinition(definition); return; }
|
|
452
|
+
|
|
453
|
+
var graphToggle = event.target.closest("[data-gp-graph-toggle]");
|
|
454
|
+
if (graphToggle && !graphToggle.disabled) { event.preventDefault(); toggleGraph(graphToggle); return; }
|
|
455
|
+
|
|
456
|
+
var diagramAction = event.target.closest("[data-gp-diagram-action]");
|
|
457
|
+
if (diagramAction) {
|
|
458
|
+
event.preventDefault();
|
|
459
|
+
var container = diagramAction.closest(".gp-diagram-container");
|
|
460
|
+
if (!container) {
|
|
461
|
+
var diagramCard = diagramAction.closest(".gp-card");
|
|
462
|
+
container = diagramCard && diagramCard.querySelector(".gp-diagram-container");
|
|
463
|
+
}
|
|
464
|
+
var viewport = container && container.querySelector(".gp-diagram-viewport");
|
|
465
|
+
var action = diagramAction.getAttribute("data-gp-diagram-action");
|
|
466
|
+
if (action === "reset") resetViewport(container);
|
|
467
|
+
if ((action === "in" || action === "out") && viewport) {
|
|
468
|
+
var rect = viewport.getBoundingClientRect();
|
|
469
|
+
zoomViewport(container, action === "in" ? ZOOM_STEP : -ZOOM_STEP, rect.width / 2, rect.height / 2);
|
|
470
|
+
}
|
|
471
|
+
if (action === "fullscreen" && container) {
|
|
472
|
+
var fullscreenTarget = container.closest(".gp-card") || container;
|
|
473
|
+
if (document.fullscreenElement === fullscreenTarget) document.exitFullscreen();
|
|
474
|
+
else if (fullscreenTarget.requestFullscreen) fullscreenTarget.requestFullscreen();
|
|
475
|
+
}
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
var partial = event.target.closest("a[data-gp-partial-link]");
|
|
480
|
+
if (partial) {
|
|
481
|
+
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || partial.target === "_blank") return;
|
|
482
|
+
event.preventDefault();
|
|
483
|
+
var href = partial.href;
|
|
484
|
+
if (partial.hasAttribute("data-gp-segment")) {
|
|
485
|
+
if (searchTimer) { clearTimeout(searchTimer); searchTimer = null; }
|
|
486
|
+
href = segmentUrl(partial);
|
|
487
|
+
}
|
|
488
|
+
partialNav(href, { history:"push" });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
var row = event.target.closest("tr[data-gp-expand-url]");
|
|
493
|
+
if (row && !event.target.closest("a,button,input,select,textarea")) {
|
|
494
|
+
partialNav(row.getAttribute("data-gp-expand-url"), { history:"push" });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
document.addEventListener("input", function (event) {
|
|
500
|
+
var form = event.target.closest("form[data-gp-search-form]");
|
|
501
|
+
if (!form) return;
|
|
502
|
+
if (searchTimer) clearTimeout(searchTimer);
|
|
503
|
+
searchTimer = setTimeout(function () {
|
|
504
|
+
partialNav(searchUrl(form), { history:"replace", silent:true });
|
|
505
|
+
}, 300);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
document.addEventListener("submit", function (event) {
|
|
509
|
+
var confirmable = event.target.closest("form[data-gp-confirm]");
|
|
510
|
+
if (confirmable) {
|
|
511
|
+
var message = confirmable.getAttribute("data-gp-confirm");
|
|
512
|
+
if (message && !window.confirm(message)) event.preventDefault();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
var form = event.target.closest("form[data-gp-search-form]");
|
|
517
|
+
if (!form) return;
|
|
518
|
+
event.preventDefault();
|
|
519
|
+
if (searchTimer) { clearTimeout(searchTimer); searchTimer = null; }
|
|
520
|
+
partialNav(searchUrl(form), { history:"push" });
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
document.addEventListener("keydown", function (event) {
|
|
524
|
+
if (event.key === "Escape") {
|
|
525
|
+
var collapse = document.querySelector("[data-collapse-expand]");
|
|
526
|
+
if (collapse) { event.preventDefault(); collapse.click(); }
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (event.target.matches("input,textarea,select") || event.target.isContentEditable) return;
|
|
530
|
+
if (event.key === "/") {
|
|
531
|
+
var search = document.querySelector("form[data-gp-search-form] input[type=search]");
|
|
532
|
+
if (search) { event.preventDefault(); search.focus(); }
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
window.addEventListener("popstate", function () {
|
|
537
|
+
latestRequestedUrl = window.location.href;
|
|
538
|
+
partialNav(window.location.href, { history:"none", silent:true });
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
initializeDynamicContent();
|
|
542
|
+
})();
|