saga_forge-dashboard 0.1.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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +47 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +205 -0
  5. data/app/assets/saga_forge/dashboard/cytoscape-dagre.js +397 -0
  6. data/app/assets/saga_forge/dashboard/cytoscape.min.js +32 -0
  7. data/app/assets/saga_forge/dashboard/dagre.min.js +3809 -0
  8. data/app/assets/saga_forge/dashboard/dashboard.css +2 -0
  9. data/app/assets/saga_forge/dashboard/dashboard.js +132 -0
  10. data/app/assets/saga_forge/dashboard/saga_graph.js +143 -0
  11. data/app/assets/saga_forge/dashboard/tailwind.css +299 -0
  12. data/app/assets/saga_forge/dashboard/turbo.min.js +34 -0
  13. data/app/controllers/saga_forge/dashboard/actions_controller.rb +36 -0
  14. data/app/controllers/saga_forge/dashboard/assets_controller.rb +29 -0
  15. data/app/controllers/saga_forge/dashboard/base_controller.rb +29 -0
  16. data/app/controllers/saga_forge/dashboard/definitions_controller.rb +24 -0
  17. data/app/controllers/saga_forge/dashboard/overview_controller.rb +18 -0
  18. data/app/controllers/saga_forge/dashboard/sagas_controller.rb +25 -0
  19. data/app/controllers/saga_forge/dashboard/stalled_controller.rb +29 -0
  20. data/app/controllers/saga_forge/dashboard/suspended_controller.rb +33 -0
  21. data/app/helpers/saga_forge/dashboard/dashboard_helper.rb +77 -0
  22. data/app/jobs/saga_forge/dashboard/bulk_recovery_job.rb +20 -0
  23. data/app/presenters/saga_forge/dashboard/context_presenter.rb +50 -0
  24. data/app/presenters/saga_forge/dashboard/saga_graph.rb +59 -0
  25. data/app/presenters/saga_forge/dashboard/timeline_presenter.rb +68 -0
  26. data/app/queries/saga_forge/dashboard/overview_query.rb +32 -0
  27. data/app/queries/saga_forge/dashboard/sagas_query.rb +83 -0
  28. data/app/queries/saga_forge/dashboard/stats_query.rb +31 -0
  29. data/app/views/layouts/saga_forge/dashboard/application.html.erb +69 -0
  30. data/app/views/saga_forge/dashboard/definitions/missing.html.erb +7 -0
  31. data/app/views/saga_forge/dashboard/definitions/show.html.erb +48 -0
  32. data/app/views/saga_forge/dashboard/overview/_stat_card.html.erb +4 -0
  33. data/app/views/saga_forge/dashboard/overview/classes.html.erb +37 -0
  34. data/app/views/saga_forge/dashboard/overview/index.html.erb +33 -0
  35. data/app/views/saga_forge/dashboard/sagas/_actions.html.erb +27 -0
  36. data/app/views/saga_forge/dashboard/sagas/_compensation.html.erb +41 -0
  37. data/app/views/saga_forge/dashboard/sagas/_context_tree.html.erb +37 -0
  38. data/app/views/saga_forge/dashboard/sagas/_filters.html.erb +9 -0
  39. data/app/views/saga_forge/dashboard/sagas/_saga_row.html.erb +6 -0
  40. data/app/views/saga_forge/dashboard/sagas/_stats.html.erb +16 -0
  41. data/app/views/saga_forge/dashboard/sagas/_summary.html.erb +43 -0
  42. data/app/views/saga_forge/dashboard/sagas/_timeline.html.erb +48 -0
  43. data/app/views/saga_forge/dashboard/sagas/empty.html.erb +6 -0
  44. data/app/views/saga_forge/dashboard/sagas/index.html.erb +46 -0
  45. data/app/views/saga_forge/dashboard/sagas/show.html.erb +19 -0
  46. data/app/views/saga_forge/dashboard/stalled/index.html.erb +51 -0
  47. data/app/views/saga_forge/dashboard/suspended/index.html.erb +57 -0
  48. data/config/routes.rb +39 -0
  49. data/lib/saga_forge/dashboard/configuration.rb +30 -0
  50. data/lib/saga_forge/dashboard/engine.rb +9 -0
  51. data/lib/saga_forge/dashboard/version.rb +5 -0
  52. data/lib/saga_forge/dashboard.rb +33 -0
  53. metadata +142 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7719d60b0d6882e37ba1f707fc29d070fba4a1a54ee5db65deade6e0b6d9c968
4
+ data.tar.gz: 5e984750302fe98e6bd31f34f2f366ced66cb82b6567ba8ea01c8db6849ec6aa
5
+ SHA512:
6
+ metadata.gz: 484a6086e73172225fc30fee4bd8591c8542c9d874fe162c030a1c4e33d589fe9fcace03cbdaa851727bf3391bcb4f6a50967ee091b1e57627b86ea401545dc4
7
+ data.tar.gz: e25cd46d7d0e2ab6b84f79bdbdfc8d094c6e2b59b31a0184b4192f427cdb1e91ff7fc7396283fcc7240606fb8ac0f60c3acf1676d7224040923ee9b373f3c6ec
data/CHANGELOG.md ADDED
@@ -0,0 +1,47 @@
1
+ ## [0.1.0] - 2026-08-26
2
+
3
+ ### Bug Fixes
4
+
5
+ - Test-env CSRF knob, schema staleness note, narrowed asset_digest rescue
6
+ - Working relative/absolute time toggle, a11y chrome, format_bytes guard
7
+ - Render aria-current as a real attribute, not an escaped string
8
+ - Memoize show presenters, deterministic comp order, timeline cap, context preview guard
9
+ - Make TimelinePresenter#truncated? side-effect-free
10
+ - Danger-style Compensate, clarify bulk button covers the whole class
11
+ - Remove em dashes from operator-facing UI copy; realistic seed backtrace
12
+ - Close forward-only guard hole in TimeoutJob; stamp last_active_at on timeout/compensation paths
13
+
14
+ ### Documentation
15
+
16
+ - Comment why the asset route regex stays unanchored + test-env show_exceptions scoping
17
+ - Drop stale cf/task residue comment, fix README port example
18
+
19
+ ### Features
20
+
21
+ - Engine scaffold, config, combustion harness
22
+ - Fail-closed auth tests and controller-served digest-busted assets
23
+ - Layout chrome, view helpers, tailwind styles, poll refresh
24
+ - Sagas index with keyset query and capped stats
25
+ - Saga show with merged timeline and context tree
26
+ - Overview, stalled, and suspended pages
27
+ - State-machine graph with per-instance overlay
28
+ - Operator actions and bulk recovery
29
+ - Preview server, README, packaging
30
+ - Finalized/active filters + stat; drop stay fixture; use persisted finalized_at
31
+
32
+ ### Miscellaneous Tasks
33
+
34
+ - Add cliff-driven release tooling for both gems
35
+ - Tidy gemspecs for release
36
+ - Require MFA to push the gems (rubygems_mfa_required)
37
+
38
+ ### Refactoring
39
+
40
+ - Reuse core State scopes in queries; cover keyset Newer path
41
+ - Reword sibling-gem comments to saga terms; reuse ledger_order scope
42
+
43
+ ### Testing
44
+
45
+ - Cover graph overlay per-node isolation, stale events, namespaced classes
46
+
47
+ ## [Unreleased]
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Stefan Froelich
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # SagaForge::Dashboard
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](MIT-LICENSE)
4
+
5
+ **A mountable Rails engine that answers the question a saga-based workflow
6
+ raises once it's live: which instances are stuck, and why?** It gives
7
+ SagaForge a triage-first dashboard with the recovery controls to act on what
8
+ it surfaces.
9
+
10
+ It is built on the event ledger every saga already keeps. The dashboard reads
11
+ that ledger per instance (every processed, parked, and failed event, with any
12
+ traceback inlined on the event that raised it), rolls it up into a fleet
13
+ overview and per-class lists, and gives you one-click retry, resume, and
14
+ compensate so recovery doesn't mean a Rails console session.
15
+
16
+ > Requires [`saga_forge`](https://github.com/radioactive-labs/saga_forge). See
17
+ > the [main README](https://github.com/radioactive-labs/saga_forge#readme) for
18
+ > saga documentation.
19
+
20
+ Early release: the UI and config API may change before `1.0`.
21
+
22
+ ## Screenshots
23
+
24
+ Not captured yet. The plan is to run `bin/dev` against the seeded preview
25
+ fixtures (every saga health state, across two saga classes) and screenshot
26
+ each page from there, the same way the core gem's site does.
27
+
28
+ ## Installation
29
+
30
+ Add to your application's Gemfile (requires `saga_forge`):
31
+
32
+ ```ruby
33
+ gem "saga_forge-dashboard"
34
+ ```
35
+
36
+ Then run `bundle install`, and mount the engine in `config/routes.rb`:
37
+
38
+ ```ruby
39
+ mount SagaForge::Dashboard::Engine => "/saga_forge"
40
+ ```
41
+
42
+ The dashboard is fail-closed: mount it without configuring
43
+ [authentication](#authentication) and every URL raises. Configure one of the
44
+ auth options below before you rely on it.
45
+
46
+ ## Authentication
47
+
48
+ The dashboard is fail-closed. If you mount it without configuring
49
+ authentication, hitting any dashboard URL raises
50
+ `SagaForge::Dashboard::AuthenticationNotConfigured`. Configure one of the
51
+ following in an initializer (e.g. `config/initializers/saga_forge_dashboard.rb`).
52
+ Resolution order: custom hook, then HTTP Basic, then `:none`, else raise.
53
+
54
+ ### Custom hook
55
+
56
+ ```ruby
57
+ SagaForge::Dashboard.configure do |c|
58
+ c.authenticate { |controller| controller.head(:forbidden) unless controller.current_user&.admin? }
59
+ end
60
+ ```
61
+
62
+ The block runs in the controller's own context (so `instance_exec`, not a
63
+ plain call), and receives the controller instance too. Call `head(:forbidden)`
64
+ or `redirect_to` to deny access; return normally to allow it.
65
+
66
+ ### HTTP Basic Auth
67
+
68
+ ```ruby
69
+ SagaForge::Dashboard.configure do |c|
70
+ c.http_basic = { username: ENV["SF_USER"], password: ENV["SF_PASS"] }
71
+ end
72
+ ```
73
+
74
+ ### Disable (guard the mount point instead)
75
+
76
+ Set authentication to `:none` and guard the mount point with a routing
77
+ constraint:
78
+
79
+ ```ruby
80
+ SagaForge::Dashboard.configure do |c|
81
+ c.authentication = :none
82
+ end
83
+ ```
84
+
85
+ ```ruby
86
+ # config/routes.rb
87
+ authenticate :user, ->(u) { u.admin? } do
88
+ mount SagaForge::Dashboard::Engine => "/saga_forge"
89
+ end
90
+ ```
91
+
92
+ ## Configuration
93
+
94
+ ```ruby
95
+ SagaForge::Dashboard.configure do |c|
96
+ c.polling_interval = 15 # seconds; default auto-refresh interval. 0 to disable.
97
+ c.polling_interval_options = [0, 5, 10, 15, 30, 60, 300] # selectable intervals in the nav "refresh" control
98
+ c.page_size = 50 # sagas per page on the per-class index
99
+ end
100
+ ```
101
+
102
+ | Option | Default | Notes |
103
+ | --- | --- | --- |
104
+ | `polling_interval` | `15` | Seconds between auto-refreshes. A viewer can override it with the nav "refresh" control (stored in a cookie). `0` disables. |
105
+ | `polling_interval_options` | `[0, 5, 10, 15, 30, 60, 300]` | Intervals (seconds; `0` = off) offered by the nav refresh control. |
106
+ | `page_size` | `50` | Sagas per page on the per-class list. |
107
+
108
+ > **The dashboard has no thresholds of its own.** Whether an instance shows up
109
+ > as stalled, suspended, or compensating comes entirely from the saga's own
110
+ > event ledger (`Event#status`) and `State#current_state`, governed by
111
+ > [`SagaForge.config`](https://github.com/radioactive-labs/saga_forge#configuration):
112
+ > `stall_wait` / `stall_budget` decide how long an early event spins before it
113
+ > parks as stalled, and `sweep_interval` decides how quickly a stranded
114
+ > compensation is picked back up. The dashboard only displays what those
115
+ > settings produce; tune them on the core gem, not here.
116
+
117
+ ## Pages
118
+
119
+ - **Sagas** (`/saga_forge`): one saga class at a time (switch classes from the
120
+ dropdown), state badges, filter by state or by the derived `stalled` /
121
+ `suspended` / `compensating` scopes, a correlation-id prefix search, and
122
+ keyset pagination.
123
+ - **Saga detail**: a summary banner (class, version, timestamps, and a warning
124
+ when the instance is stalled, suspended, or stuck mid-compensation), a
125
+ context inspector for the persisted JSON context, and a timeline merging the
126
+ event ledger with compensation progress read from
127
+ `context["__saga_forge"]`, chronological, with the operator actions for that
128
+ instance.
129
+ - **Overview** (`/saga_forge/overview`): fleet-wide totals (all / stalled /
130
+ suspended / compensating), each linking into the matching filtered list,
131
+ plus a per-class breakdown loaded in its own turbo-frame so the cards paint
132
+ before the heavier GROUP BY resolves.
133
+ - **Stalled** (`/saga_forge/stalled`): every instance, across every saga
134
+ class, with at least one parked event, the parked event name(s), and a
135
+ **Retry all stalled** button per class.
136
+ - **Suspended** (`/saga_forge/suspended`): every instance with at least one
137
+ event that ran out of retries, the failed event's error class and message,
138
+ and a **Resume all** button per class.
139
+ - **Definition graph** (`/saga_forge/definitions/:class`): a static diagram of
140
+ one saga class's declared chain (`start_with` / `during` / `finish_with`),
141
+ compiled from the class's own declarations, never executed. Chain edges,
142
+ `transition_to` jumps, and `stay` self-loops are drawn distinctly, and
143
+ passing `?correlation_id=` overlays one instance's actual progress on top:
144
+ which events are processed, stalled, or failed, and where it currently
145
+ sits.
146
+
147
+ ## Operator actions
148
+
149
+ Available from a saga's detail page, and in bulk from the Stalled/Suspended
150
+ lists:
151
+
152
+ - **Retry stalled**: re-delivers every parked event for the instance.
153
+ - **Resume**: clears a failed event's attempts and error, then re-fires it,
154
+ for after you've fixed the bug that caused it.
155
+ - **Compensate**: runs the instance's owed compensations from wherever it
156
+ currently sits, landing it in `:compensated`.
157
+ - **Cancel**: the same rollback, but landing in `:cancelled` instead, with an
158
+ optional reason.
159
+ - **Bulk retry / resume**: the stalled and suspended pages offer a
160
+ class-scoped "act on all" button, fanned out by a background job so the
161
+ request stays fast against a large backlog.
162
+
163
+ Each of these is a thin wrapper over the model methods described in the core
164
+ gem's [Operator API](https://github.com/radioactive-labs/saga_forge#operator-api);
165
+ `retry_stalled!` and `resume!` are no-ops (not errors) on an instance with
166
+ nothing eligible, so a stale button just flashes "Nothing to do."
167
+
168
+ ## Development
169
+
170
+ Run a seeded preview locally (compiles the stylesheet, then boots a demo app
171
+ covering every saga health state across two saga classes):
172
+
173
+ ```bash
174
+ bin/dev # PORT=9090 bin/dev to use a different port
175
+ ```
176
+
177
+ It serves at `http://localhost:9977/saga_forge` by default.
178
+
179
+ ```bash
180
+ bundle exec rake # test + standard
181
+ ```
182
+
183
+ ## Releasing
184
+
185
+ The dashboard is released from the monorepo root with `release:dashboard:*`
186
+ (defined in `../lib/tasks/release.rake`), which tags with the prefix
187
+ `saga_forge-dashboard-v`:
188
+
189
+ ```bash
190
+ cd .. # monorepo root
191
+ rake release:dashboard:version # show the next version (conventional commits)
192
+ rake release:dashboard:prepare # bump + changelog + recompile dashboard.css (uncommitted)
193
+ git diff # review
194
+ rake release:dashboard:publish # commit, build + push gem, tag + push → CI cuts the Release
195
+ ```
196
+
197
+ Release **core before dashboard** — bump the dashboard's `saga_forge` floor
198
+ first if it should require the new core version. `prepare` recompiles
199
+ `dashboard.css` (`bundle exec rake tailwind:build`) so a release never ships a
200
+ stale stylesheet. See the header of `../lib/tasks/release.rake` for the full
201
+ flow.
202
+
203
+ ## License
204
+
205
+ SagaForge::Dashboard is released under the [MIT License](MIT-LICENSE).
@@ -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
+ });