chrono_forge-dashboard 0.1.0 → 0.2.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +43 -24
  4. data/app/assets/chrono_forge/dashboard/cytoscape-dagre.js +397 -0
  5. data/app/assets/chrono_forge/dashboard/cytoscape.min.js +32 -0
  6. data/app/assets/chrono_forge/dashboard/dagre.min.js +3809 -0
  7. data/app/assets/chrono_forge/dashboard/dashboard.css +1 -1
  8. data/app/assets/chrono_forge/dashboard/dashboard.js +37 -0
  9. data/app/assets/chrono_forge/dashboard/definition_graph.js +161 -0
  10. data/app/controllers/chrono_forge/dashboard/assets_controller.rb +8 -1
  11. data/app/controllers/chrono_forge/dashboard/definitions_controller.rb +35 -0
  12. data/app/controllers/chrono_forge/dashboard/workflows_controller.rb +6 -2
  13. data/app/helpers/chrono_forge/dashboard/dashboard_helper.rb +33 -0
  14. data/app/presenters/chrono_forge/dashboard/branch_presenter.rb +38 -1
  15. data/app/presenters/chrono_forge/dashboard/branches_presenter.rb +59 -4
  16. data/app/presenters/chrono_forge/dashboard/cytoscape_graph.rb +48 -0
  17. data/app/presenters/chrono_forge/dashboard/definition_overlay.rb +128 -0
  18. data/app/queries/chrono_forge/dashboard/workflows_query.rb +10 -1
  19. data/app/views/chrono_forge/dashboard/branch_children/show.html.erb +56 -11
  20. data/app/views/chrono_forge/dashboard/definitions/show.html.erb +42 -0
  21. data/app/views/chrono_forge/dashboard/workflows/_branches.html.erb +14 -4
  22. data/app/views/chrono_forge/dashboard/workflows/_filters.html.erb +12 -1
  23. data/app/views/chrono_forge/dashboard/workflows/_stats.html.erb +5 -9
  24. data/app/views/chrono_forge/dashboard/workflows/index.html.erb +3 -3
  25. data/app/views/chrono_forge/dashboard/workflows/show.html.erb +1 -0
  26. data/app/views/layouts/chrono_forge/dashboard/application.html.erb +18 -10
  27. data/config/routes.rb +6 -1
  28. data/lib/chrono_forge/dashboard/configuration.rb +2 -2
  29. data/lib/chrono_forge/dashboard/version.rb +1 -1
  30. metadata +10 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbe30b442414b1aa18a74c822c26e579c9c6295cd115de58479dcdb408922b1a
4
- data.tar.gz: 0b03195d459b7e9919a38ca2bf58e026a0a8c05e595bcd86f6ca8c2a227e9db8
3
+ metadata.gz: 8ebe7cacdd958bfd807d5fe695f44f8d46fde5bc5a672ffafc02578134f549f9
4
+ data.tar.gz: d4ac11de2827303f91d54c5bade906c4dd78cebf0357eb68ea61d56af2b4a95c
5
5
  SHA512:
6
- metadata.gz: '052338fbc1da61418b261d93db5555c6413c8ebbe5e72253fe31acc3a9c8db6925399656dafc09f5a78c5c89b070413628e85db737bff464fc6c445c588b76df'
7
- data.tar.gz: dee584ae0d82ea86aec8a12f57dd18df3f32c6176db80bee4401aeeb26c195c9d3a5f094f41abf8ebd250eaaf1640e5af6204a35a4324c30889897b23205e76b
6
+ metadata.gz: 010b3752d94b172520326e68d2893f83fe424198970c2c6a66023bff1420080b421b8d3f86825f776d61103f93d143386fe5bf86f045927c66071a9cf6266eb2
7
+ data.tar.gz: 238f10e0646cf3bd6a927d10debe3fb15e42edd33c807c1dd82e3baf809c8adcfd3eb601d7a97d7f2a98770790dd272529df605e7a93c2c439cc600e7b72df25
data/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to `chrono_forge-dashboard` are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/).
4
4
 
5
+ ## [0.2.0] - 2026-07-04
6
+
7
+ ### Bug Fixes
8
+
9
+ - Preserve filter input and focus across polling refresh
10
+ - Converge merges promptly via drain-ETA cadence and never-started-count rekick ([#12](https://github.com/radioactive-labs/chrono_forge/issues/12))
11
+ - Overlay/analyzer correctness, detail-panel XSS, and UX polish
12
+
13
+ ### Documentation
14
+
15
+ - Refresh dashboard screenshots, badges; fix upgrade note and API reference
16
+ - Restructure READMEs — promote branches, refine cadence note, dashboard cross-link
17
+
18
+ ### Features
19
+
20
+ - Hide branch children by default with a filter toggle
21
+ - Blocked filter on the index + state dots on branch chips
22
+ - Surface merge poll schedule on the branches panel
23
+ - Workflow definition graph with static DAG and live run overlay ([#13](https://github.com/radioactive-labs/chrono_forge/issues/13))
24
+
25
+ ### Miscellaneous Tasks
26
+
27
+ - Replace bin/release with per-gem rake release flow
28
+
29
+ ### Styling
30
+
31
+ - Apply standardrb blank-line formatting to existing files
32
+
5
33
  ## [0.1.0] - 2026-06-27
6
34
 
7
35
  Initial release — a free, mountable, server-rendered dashboard for ChronoForge workflows. Requires `chrono_forge >= 0.10.0` (for the branches feature and the `durably_repeat` fast-forward catch-up it surfaces). Adds no migrations of its own, no host asset pipeline, and no new indexes on hot tables (branch views read the core's `parent_execution_log_id` index).
data/README.md CHANGED
@@ -1,29 +1,16 @@
1
1
  # ChronoForge::Dashboard
2
2
 
3
- A mountable Rails engine that provides visibility and operational controls for ChronoForge workflows.
4
-
5
- Version: `0.1.0` (early release). The UI and config API may change before `1.0`.
3
+ [![Gem Version](https://badge.fury.io/rb/chrono_forge-dashboard.svg)](https://badge.fury.io/rb/chrono_forge-dashboard)
4
+ [![Ruby](https://github.com/radioactive-labs/chrono_forge/actions/workflows/main.yml/badge.svg)](https://github.com/radioactive-labs/chrono_forge/actions/workflows/main.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
- ## Screenshots
8
-
9
- | Workflow list | Analytics |
10
- | --- | --- |
11
- | [![Workflow list](docs/screenshots/workflows.png)](docs/screenshots/workflows.png) | [![Analytics](docs/screenshots/analytics.png)](docs/screenshots/analytics.png) |
12
- | Filter by state/class/key, keyset pagination, capped state counts. | Completion/failure rate, throughput, top errors, queue health — per class. |
13
-
14
- | Waiting | Repetitions |
15
- | --- | --- |
16
- | [![Waiting workflows](docs/screenshots/waiting.png)](docs/screenshots/waiting.png) | [![Repetitions](docs/screenshots/repetitions.png)](docs/screenshots/repetitions.png) |
17
- | Oldest unresolved `continue_if` (event) wait per class — the silent stall. | A `durably_repeat` step's per-iteration runs, with tombstones and lateness. |
7
+ A mountable Rails engine that provides visibility and operational controls for ChronoForge workflows.
18
8
 
19
- | Branches | Branch children |
20
- | --- | --- |
21
- | [![Branches panel](docs/screenshots/branches.png)](docs/screenshots/branches.png) | [![Branch children](docs/screenshots/branch-children.png)](docs/screenshots/branch-children.png) |
22
- | Fan-out branches with capped dispatched/pending/blocked counts and merge state. | One branch's children — blocked-first triage, capped filters, retry per child. |
9
+ > Requires [`chrono_forge`](https://github.com/radioactive-labs/chrono_forge). See the [main README](https://github.com/radioactive-labs/chrono_forge#readme) for workflow documentation.
23
10
 
24
- **Workflow detail** step-replay timeline with errors inlined on the step that failed, periodic-task health, and arguments/context:
11
+ Version: `0.1.0` (early release). The UI and config API may change before `1.0`.
25
12
 
26
- [![Workflow detail](docs/screenshots/workflow-detail.png)](docs/screenshots/workflow-detail.png)
13
+ [![ChronoForge dashboard](docs/screenshots/workflows.png)](docs/screenshots/workflows.png)
27
14
 
28
15
  ## Installation
29
16
 
@@ -47,6 +34,35 @@ Add to `config/routes.rb`:
47
34
  mount ChronoForge::Dashboard::Engine, at: "/chrono_forge"
48
35
  ```
49
36
 
37
+ ## Screenshots
38
+
39
+ | Workflow list | Analytics |
40
+ | --- | --- |
41
+ | [![Workflow list](docs/screenshots/workflows.png)](docs/screenshots/workflows.png) | [![Analytics](docs/screenshots/analytics.png)](docs/screenshots/analytics.png) |
42
+ | Filter by state/class/key, keyset pagination, capped state counts. | Completion/failure rate, throughput, top errors, queue health — per class. |
43
+
44
+ | Waiting | Repetitions |
45
+ | --- | --- |
46
+ | [![Waiting workflows](docs/screenshots/waiting.png)](docs/screenshots/waiting.png) | [![Repetitions](docs/screenshots/repetitions.png)](docs/screenshots/repetitions.png) |
47
+ | Oldest unresolved `continue_if` (event) wait per class — the silent stall. | A `durably_repeat` step's per-iteration runs, with catch-up skips (per-tick tombstones or a "caught up ×N" summary) and lateness. |
48
+
49
+ | Branches | Branch children |
50
+ | --- | --- |
51
+ | [![Branches panel](docs/screenshots/branches.png)](docs/screenshots/branches.png) | [![Branch children](docs/screenshots/branch-children.png)](docs/screenshots/branch-children.png) |
52
+ | Fan-out branches with exact **spawned / pending / never-started** counts (recorded by the poller — the immutable spawned total is counted once and cached when the branch seals) plus blocked count and merge state, and in-flight merges showing **live throughput (children/s) and ETA** while draining. | One branch's children with a **live stats header** (throughput/ETA, spawned, pending, never-started, dropped-child recovery) — blocked-first triage, capped state filters, retry per child. |
53
+
54
+ **Workflow detail** — step-replay timeline with errors inlined on the step that failed, periodic-task health, and arguments/context:
55
+
56
+ [![Workflow detail](docs/screenshots/workflow-detail.png)](docs/screenshots/workflow-detail.png)
57
+
58
+ **Definition graph** — a per-run static DAG of the durable steps a workflow *will* run (parsed from `perform` with Prism, never executed), with the run's status painted on each node — done / in progress / failed / not-yet-reached — plus guarded edges, early-`return` exits, and unmapped steps. Tap a node or edge to inspect its step name / guard:
59
+
60
+ [![Definition graph](docs/screenshots/definition-graph.png)](docs/screenshots/definition-graph.png)
61
+
62
+ A scheduled-payment recurrence with three reminder-ordering branches that reconverge on the charge. This run took the auto-charge branch (green); the other two branches were never reached (dimmed), the past-dismiss guard exits early to `halt`, and the final `process_payment` failed:
63
+
64
+ [![Definition graph — scheduled payment](docs/screenshots/definition-graph-scheduled-payment.png)](docs/screenshots/definition-graph-scheduled-payment.png)
65
+
50
66
  ## Authentication
51
67
 
52
68
  The dashboard is fail-closed. If you mount it without configuring authentication, hitting any dashboard URL raises `ChronoForge::Dashboard::AuthenticationNotConfigured`. Configure one of the following in an initializer (e.g. `config/initializers/chrono_forge_dashboard.rb`).
@@ -94,8 +110,8 @@ All options go in the same `configure` block as auth:
94
110
 
95
111
  ```ruby
96
112
  ChronoForge::Dashboard.configure do |c|
97
- c.polling_interval = 5 # seconds; the default auto-refresh interval. 0 to disable.
98
- c.polling_interval_options = [0, 5, 10, 30, 60, 300] # selectable intervals in the nav "refresh" control
113
+ c.polling_interval = 15 # seconds; the default auto-refresh interval. 0 to disable.
114
+ c.polling_interval_options = [0, 5, 10, 15, 30, 60, 300] # selectable intervals in the nav "refresh" control
99
115
  c.page_size = 50 # workflows per page
100
116
  c.long_wait_threshold = 3600 # seconds; wait-state ages above this are flagged
101
117
  end
@@ -103,8 +119,8 @@ end
103
119
 
104
120
  | Option | Default | Notes |
105
121
  | --- | --- | --- |
106
- | `polling_interval` | `5` | Seconds between auto-refreshes (the default). A viewer can override it with the nav "refresh" control (stored in a cookie). `0` disables. |
107
- | `polling_interval_options` | `[0, 5, 10, 30, 60, 300]` | Intervals (seconds; `0` = off) offered by the nav refresh control. |
122
+ | `polling_interval` | `15` | Seconds between auto-refreshes (the default). Most pages refresh in place (preserving filter text, focus, and scroll), so a draining fan-out's live throughput/ETA and counts update without a manual reload. The definition graph opts out: its interactive Cytoscape canvas can't survive an in-place swap, so it reloads fully instead and hides the refresh control. A viewer can override the interval with the nav "refresh" control (stored in a cookie). `0` disables. |
123
+ | `polling_interval_options` | `[0, 5, 10, 15, 30, 60, 300]` | Intervals (seconds; `0` = off) offered by the nav refresh control. |
108
124
  | `page_size` | `50` | Workflows per page on the index. |
109
125
  | `long_wait_threshold` | `3600` | Wait-state age in seconds above which a warning is shown. |
110
126
 
@@ -112,6 +128,7 @@ end
112
128
 
113
129
  - **Workflow list**: state badges, filter by state/job class/workflow key, stats header showing counts by state
114
130
  - **Workflow detail**: step replay timeline showing every `durably_execute`, `wait`, `continue_if`, and `durably_repeat` run; repetitions from `durably_repeat` appear nested under their coordination step
131
+ - **Definition graph**: a per-run static DAG of the durable steps a workflow *will* run — parsed from the `perform` method source with [Prism](https://github.com/ruby/prism) (never executed, never touches the DB) — with the run's live status overlaid on each node (done / in progress / pending / not-yet-reached / failed / unmapped, with per-node repeat counts and fan-out child tallies). Rendered client-side with [Cytoscape](https://js.cytoscape.org) (dagre layout): pan/zoom, and tap a node or edge to inspect its step name / guard. Reached from a "Definition graph" link on the workflow detail page. The analysis is deliberately *conservative*: `if`/`unless`/`case`/`continue_if` become guarded edges, an early `return` a dashed exit, `branch`/`spawn_each` a fan-out node, `durably_repeat` a loop node, and anything it can't resolve statically (a computed step name, a data-dependent loop, a durable call behind an unknown method) becomes a `dynamic` node with a warning rather than a confident-but-wrong graph. It also follows durable calls into helper methods in the same class, assignments, `&&`/`||`, and `case`/`in`, so a step one expression deep isn't missed. A workflow whose source can't be analyzed, or whose `perform` has no durable steps, degrades to a note, never an error.
115
132
  - **Context inspector**: JSON tree view of the workflow's persistent context
116
133
  - **Per-step error logs**: errors attributed to the step and attempt that raised them
117
134
  - **Periodic-task health**: summary of each `durably_repeat` task (last run, next run, missed executions)
@@ -130,6 +147,8 @@ bundle exec rake tailwind:build
130
147
 
131
148
  Assets are cache-busted by a content digest, so a gem upgrade is picked up without a hard refresh.
132
149
 
150
+ **One exception:** the **Definition graph** page loads [Cytoscape](https://js.cytoscape.org) + [dagre](https://github.com/dagrejs/dagre) to lay out the DAG client-side (~670 KB total, loaded only on that page). All three libraries plus the init module (`definition_graph.js`) are vendored into the gem and served from the engine — no external host / CDN, and no inline `<script>` (the init is an external file), so the page stays CSP-friendly. The graph is passed as JSON in a `data-` attribute (ERB-escaped in, `JSON.parse`d out) and Cytoscape renders labels onto a canvas, so the graph itself has no HTML-injection surface. Unlike the old Mermaid text grammar, guards containing `()`, `<`, and `&&` round-trip untouched. The one place author-controlled text (labels, step names, guards) reaches the DOM is the tap-to-inspect detail panel, which HTML-escapes it before insertion. Every other page remains dependency-free vanilla JS.
151
+
133
152
  ## Development
134
153
 
135
154
  Run a seeded preview locally (compiles the stylesheet, then boots a demo app on `http://localhost:9876/chrono_forge`):
@@ -0,0 +1,397 @@
1
+ (function webpackUniversalModuleDefinition(root, factory) {
2
+ if(typeof exports === 'object' && typeof module === 'object')
3
+ module.exports = factory(require("dagre"));
4
+ else if(typeof define === 'function' && define.amd)
5
+ define(["dagre"], factory);
6
+ else if(typeof exports === 'object')
7
+ exports["cytoscapeDagre"] = factory(require("dagre"));
8
+ else
9
+ root["cytoscapeDagre"] = factory(root["dagre"]);
10
+ })(this, function(__WEBPACK_EXTERNAL_MODULE__4__) {
11
+ return /******/ (function(modules) { // webpackBootstrap
12
+ /******/ // The module cache
13
+ /******/ var installedModules = {};
14
+ /******/
15
+ /******/ // The require function
16
+ /******/ function __webpack_require__(moduleId) {
17
+ /******/
18
+ /******/ // Check if module is in cache
19
+ /******/ if(installedModules[moduleId]) {
20
+ /******/ return installedModules[moduleId].exports;
21
+ /******/ }
22
+ /******/ // Create a new module (and put it into the cache)
23
+ /******/ var module = installedModules[moduleId] = {
24
+ /******/ i: moduleId,
25
+ /******/ l: false,
26
+ /******/ exports: {}
27
+ /******/ };
28
+ /******/
29
+ /******/ // Execute the module function
30
+ /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
31
+ /******/
32
+ /******/ // Flag the module as loaded
33
+ /******/ module.l = true;
34
+ /******/
35
+ /******/ // Return the exports of the module
36
+ /******/ return module.exports;
37
+ /******/ }
38
+ /******/
39
+ /******/
40
+ /******/ // expose the modules object (__webpack_modules__)
41
+ /******/ __webpack_require__.m = modules;
42
+ /******/
43
+ /******/ // expose the module cache
44
+ /******/ __webpack_require__.c = installedModules;
45
+ /******/
46
+ /******/ // define getter function for harmony exports
47
+ /******/ __webpack_require__.d = function(exports, name, getter) {
48
+ /******/ if(!__webpack_require__.o(exports, name)) {
49
+ /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
50
+ /******/ }
51
+ /******/ };
52
+ /******/
53
+ /******/ // define __esModule on exports
54
+ /******/ __webpack_require__.r = function(exports) {
55
+ /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
56
+ /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
57
+ /******/ }
58
+ /******/ Object.defineProperty(exports, '__esModule', { value: true });
59
+ /******/ };
60
+ /******/
61
+ /******/ // create a fake namespace object
62
+ /******/ // mode & 1: value is a module id, require it
63
+ /******/ // mode & 2: merge all properties of value into the ns
64
+ /******/ // mode & 4: return value when already ns object
65
+ /******/ // mode & 8|1: behave like require
66
+ /******/ __webpack_require__.t = function(value, mode) {
67
+ /******/ if(mode & 1) value = __webpack_require__(value);
68
+ /******/ if(mode & 8) return value;
69
+ /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
70
+ /******/ var ns = Object.create(null);
71
+ /******/ __webpack_require__.r(ns);
72
+ /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
73
+ /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
74
+ /******/ return ns;
75
+ /******/ };
76
+ /******/
77
+ /******/ // getDefaultExport function for compatibility with non-harmony modules
78
+ /******/ __webpack_require__.n = function(module) {
79
+ /******/ var getter = module && module.__esModule ?
80
+ /******/ function getDefault() { return module['default']; } :
81
+ /******/ function getModuleExports() { return module; };
82
+ /******/ __webpack_require__.d(getter, 'a', getter);
83
+ /******/ return getter;
84
+ /******/ };
85
+ /******/
86
+ /******/ // Object.prototype.hasOwnProperty.call
87
+ /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
88
+ /******/
89
+ /******/ // __webpack_public_path__
90
+ /******/ __webpack_require__.p = "";
91
+ /******/
92
+ /******/
93
+ /******/ // Load entry module and return exports
94
+ /******/ return __webpack_require__(__webpack_require__.s = 0);
95
+ /******/ })
96
+ /************************************************************************/
97
+ /******/ ([
98
+ /* 0 */
99
+ /***/ (function(module, exports, __webpack_require__) {
100
+
101
+ var impl = __webpack_require__(1); // registers the extension on a cytoscape lib ref
102
+
103
+
104
+ var register = function register(cytoscape) {
105
+ if (!cytoscape) {
106
+ return;
107
+ } // can't register if cytoscape unspecified
108
+
109
+
110
+ cytoscape('layout', 'dagre', impl); // register with cytoscape.js
111
+ };
112
+
113
+ if (typeof cytoscape !== 'undefined') {
114
+ // expose to global cytoscape (i.e. window.cytoscape)
115
+ register(cytoscape);
116
+ }
117
+
118
+ module.exports = register;
119
+
120
+ /***/ }),
121
+ /* 1 */
122
+ /***/ (function(module, exports, __webpack_require__) {
123
+
124
+ function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
125
+
126
+ var isFunction = function isFunction(o) {
127
+ return typeof o === 'function';
128
+ };
129
+
130
+ var defaults = __webpack_require__(2);
131
+
132
+ var assign = __webpack_require__(3);
133
+
134
+ var dagre = __webpack_require__(4); // constructor
135
+ // options : object containing layout options
136
+
137
+
138
+ function DagreLayout(options) {
139
+ this.options = assign({}, defaults, options);
140
+ } // runs the layout
141
+
142
+
143
+ DagreLayout.prototype.run = function () {
144
+ var options = this.options;
145
+ var layout = this;
146
+ var cy = options.cy; // cy is automatically populated for us in the constructor
147
+
148
+ var eles = options.eles;
149
+
150
+ var getVal = function getVal(ele, val) {
151
+ return isFunction(val) ? val.apply(ele, [ele]) : val;
152
+ };
153
+
154
+ var bb = options.boundingBox || {
155
+ x1: 0,
156
+ y1: 0,
157
+ w: cy.width(),
158
+ h: cy.height()
159
+ };
160
+
161
+ if (bb.x2 === undefined) {
162
+ bb.x2 = bb.x1 + bb.w;
163
+ }
164
+
165
+ if (bb.w === undefined) {
166
+ bb.w = bb.x2 - bb.x1;
167
+ }
168
+
169
+ if (bb.y2 === undefined) {
170
+ bb.y2 = bb.y1 + bb.h;
171
+ }
172
+
173
+ if (bb.h === undefined) {
174
+ bb.h = bb.y2 - bb.y1;
175
+ }
176
+
177
+ var g = new dagre.graphlib.Graph({
178
+ multigraph: true,
179
+ compound: true
180
+ });
181
+ var gObj = {};
182
+
183
+ var setGObj = function setGObj(name, val) {
184
+ if (val != null) {
185
+ gObj[name] = val;
186
+ }
187
+ };
188
+
189
+ setGObj('nodesep', options.nodeSep);
190
+ setGObj('edgesep', options.edgeSep);
191
+ setGObj('ranksep', options.rankSep);
192
+ setGObj('rankdir', options.rankDir);
193
+ setGObj('align', options.align);
194
+ setGObj('ranker', options.ranker);
195
+ setGObj('acyclicer', options.acyclicer);
196
+ g.setGraph(gObj);
197
+ g.setDefaultEdgeLabel(function () {
198
+ return {};
199
+ });
200
+ g.setDefaultNodeLabel(function () {
201
+ return {};
202
+ }); // add nodes to dagre
203
+
204
+ var nodes = eles.nodes();
205
+
206
+ if (isFunction(options.sort)) {
207
+ nodes = nodes.sort(options.sort);
208
+ }
209
+
210
+ for (var i = 0; i < nodes.length; i++) {
211
+ var node = nodes[i];
212
+ var nbb = node.layoutDimensions(options);
213
+ g.setNode(node.id(), {
214
+ width: nbb.w,
215
+ height: nbb.h,
216
+ name: node.id()
217
+ }); // console.log( g.node(node.id()) );
218
+ } // set compound parents
219
+
220
+
221
+ for (var _i = 0; _i < nodes.length; _i++) {
222
+ var _node = nodes[_i];
223
+
224
+ if (_node.isChild()) {
225
+ g.setParent(_node.id(), _node.parent().id());
226
+ }
227
+ } // add edges to dagre
228
+
229
+
230
+ var edges = eles.edges().stdFilter(function (edge) {
231
+ return !edge.source().isParent() && !edge.target().isParent(); // dagre can't handle edges on compound nodes
232
+ });
233
+
234
+ if (isFunction(options.sort)) {
235
+ edges = edges.sort(options.sort);
236
+ }
237
+
238
+ for (var _i2 = 0; _i2 < edges.length; _i2++) {
239
+ var edge = edges[_i2];
240
+ g.setEdge(edge.source().id(), edge.target().id(), {
241
+ minlen: getVal(edge, options.minLen),
242
+ weight: getVal(edge, options.edgeWeight),
243
+ name: edge.id()
244
+ }, edge.id()); // console.log( g.edge(edge.source().id(), edge.target().id(), edge.id()) );
245
+ }
246
+
247
+ dagre.layout(g);
248
+ var gNodeIds = g.nodes();
249
+
250
+ for (var _i3 = 0; _i3 < gNodeIds.length; _i3++) {
251
+ var id = gNodeIds[_i3];
252
+ var n = g.node(id);
253
+ cy.getElementById(id).scratch().dagre = n;
254
+ }
255
+
256
+ var dagreBB;
257
+
258
+ if (options.boundingBox) {
259
+ dagreBB = {
260
+ x1: Infinity,
261
+ x2: -Infinity,
262
+ y1: Infinity,
263
+ y2: -Infinity
264
+ };
265
+ nodes.forEach(function (node) {
266
+ var dModel = node.scratch().dagre;
267
+ dagreBB.x1 = Math.min(dagreBB.x1, dModel.x);
268
+ dagreBB.x2 = Math.max(dagreBB.x2, dModel.x);
269
+ dagreBB.y1 = Math.min(dagreBB.y1, dModel.y);
270
+ dagreBB.y2 = Math.max(dagreBB.y2, dModel.y);
271
+ });
272
+ dagreBB.w = dagreBB.x2 - dagreBB.x1;
273
+ dagreBB.h = dagreBB.y2 - dagreBB.y1;
274
+ } else {
275
+ dagreBB = bb;
276
+ }
277
+
278
+ var constrainPos = function constrainPos(p) {
279
+ if (options.boundingBox) {
280
+ var xPct = dagreBB.w === 0 ? 0 : (p.x - dagreBB.x1) / dagreBB.w;
281
+ var yPct = dagreBB.h === 0 ? 0 : (p.y - dagreBB.y1) / dagreBB.h;
282
+ return {
283
+ x: bb.x1 + xPct * bb.w,
284
+ y: bb.y1 + yPct * bb.h
285
+ };
286
+ } else {
287
+ return p;
288
+ }
289
+ };
290
+
291
+ nodes.layoutPositions(layout, options, function (ele) {
292
+ ele = _typeof(ele) === "object" ? ele : this;
293
+ var dModel = ele.scratch().dagre;
294
+ return constrainPos({
295
+ x: dModel.x,
296
+ y: dModel.y
297
+ });
298
+ });
299
+ return this; // chaining
300
+ };
301
+
302
+ module.exports = DagreLayout;
303
+
304
+ /***/ }),
305
+ /* 2 */
306
+ /***/ (function(module, exports) {
307
+
308
+ var defaults = {
309
+ // dagre algo options, uses default value on undefined
310
+ nodeSep: undefined,
311
+ // the separation between adjacent nodes in the same rank
312
+ edgeSep: undefined,
313
+ // the separation between adjacent edges in the same rank
314
+ rankSep: undefined,
315
+ // the separation between adjacent nodes in the same rank
316
+ rankDir: undefined,
317
+ // 'TB' for top to bottom flow, 'LR' for left to right,
318
+ align: undefined,
319
+ // alignment for rank nodes. Can be 'UL', 'UR', 'DL', or 'DR', where U = up, D = down, L = left, and R = right
320
+ acyclicer: undefined,
321
+ // If set to 'greedy', uses a greedy heuristic for finding a feedback arc set for a graph.
322
+ // A feedback arc set is a set of edges that can be removed to make a graph acyclic.
323
+ ranker: undefined,
324
+ // Type of algorithm to assigns a rank to each node in the input graph.
325
+ // Possible values: network-simplex, tight-tree or longest-path
326
+ minLen: function minLen(edge) {
327
+ return 1;
328
+ },
329
+ // number of ranks to keep between the source and target of the edge
330
+ edgeWeight: function edgeWeight(edge) {
331
+ return 1;
332
+ },
333
+ // higher weight edges are generally made shorter and straighter than lower weight edges
334
+ // general layout options
335
+ fit: true,
336
+ // whether to fit to viewport
337
+ padding: 30,
338
+ // fit padding
339
+ spacingFactor: undefined,
340
+ // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
341
+ nodeDimensionsIncludeLabels: false,
342
+ // whether labels should be included in determining the space used by a node
343
+ animate: false,
344
+ // whether to transition the node positions
345
+ animateFilter: function animateFilter(node, i) {
346
+ return true;
347
+ },
348
+ // whether to animate specific nodes when animation is on; non-animated nodes immediately go to their final positions
349
+ animationDuration: 500,
350
+ // duration of animation in ms if enabled
351
+ animationEasing: undefined,
352
+ // easing of animation if enabled
353
+ boundingBox: undefined,
354
+ // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
355
+ transform: function transform(node, pos) {
356
+ return pos;
357
+ },
358
+ // a function that applies a transform to the final node position
359
+ ready: function ready() {},
360
+ // on layoutready
361
+ sort: undefined,
362
+ // a sorting function to order the nodes and edges; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
363
+ // because cytoscape dagre creates a directed graph, and directed graphs use the node order as a tie breaker when
364
+ // defining the topology of a graph, this sort function can help ensure the correct order of the nodes/edges.
365
+ // this feature is most useful when adding and removing the same nodes and edges multiple times in a graph.
366
+ stop: function stop() {} // on layoutstop
367
+
368
+ };
369
+ module.exports = defaults;
370
+
371
+ /***/ }),
372
+ /* 3 */
373
+ /***/ (function(module, exports) {
374
+
375
+ // Simple, internal Object.assign() polyfill for options objects etc.
376
+ module.exports = Object.assign != null ? Object.assign.bind(Object) : function (tgt) {
377
+ for (var _len = arguments.length, srcs = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
378
+ srcs[_key - 1] = arguments[_key];
379
+ }
380
+
381
+ srcs.forEach(function (src) {
382
+ Object.keys(src).forEach(function (k) {
383
+ return tgt[k] = src[k];
384
+ });
385
+ });
386
+ return tgt;
387
+ };
388
+
389
+ /***/ }),
390
+ /* 4 */
391
+ /***/ (function(module, exports) {
392
+
393
+ module.exports = __WEBPACK_EXTERNAL_MODULE__4__;
394
+
395
+ /***/ })
396
+ /******/ ]);
397
+ });