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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +43 -24
- data/app/assets/chrono_forge/dashboard/cytoscape-dagre.js +397 -0
- data/app/assets/chrono_forge/dashboard/cytoscape.min.js +32 -0
- data/app/assets/chrono_forge/dashboard/dagre.min.js +3809 -0
- data/app/assets/chrono_forge/dashboard/dashboard.css +1 -1
- data/app/assets/chrono_forge/dashboard/dashboard.js +37 -0
- data/app/assets/chrono_forge/dashboard/definition_graph.js +161 -0
- data/app/controllers/chrono_forge/dashboard/assets_controller.rb +8 -1
- data/app/controllers/chrono_forge/dashboard/definitions_controller.rb +35 -0
- data/app/controllers/chrono_forge/dashboard/workflows_controller.rb +6 -2
- data/app/helpers/chrono_forge/dashboard/dashboard_helper.rb +33 -0
- data/app/presenters/chrono_forge/dashboard/branch_presenter.rb +38 -1
- data/app/presenters/chrono_forge/dashboard/branches_presenter.rb +59 -4
- data/app/presenters/chrono_forge/dashboard/cytoscape_graph.rb +48 -0
- data/app/presenters/chrono_forge/dashboard/definition_overlay.rb +128 -0
- data/app/queries/chrono_forge/dashboard/workflows_query.rb +10 -1
- data/app/views/chrono_forge/dashboard/branch_children/show.html.erb +56 -11
- data/app/views/chrono_forge/dashboard/definitions/show.html.erb +42 -0
- data/app/views/chrono_forge/dashboard/workflows/_branches.html.erb +14 -4
- data/app/views/chrono_forge/dashboard/workflows/_filters.html.erb +12 -1
- data/app/views/chrono_forge/dashboard/workflows/_stats.html.erb +5 -9
- data/app/views/chrono_forge/dashboard/workflows/index.html.erb +3 -3
- data/app/views/chrono_forge/dashboard/workflows/show.html.erb +1 -0
- data/app/views/layouts/chrono_forge/dashboard/application.html.erb +18 -10
- data/config/routes.rb +6 -1
- data/lib/chrono_forge/dashboard/configuration.rb +2 -2
- data/lib/chrono_forge/dashboard/version.rb +1 -1
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ebe7cacdd958bfd807d5fe695f44f8d46fde5bc5a672ffafc02578134f549f9
|
|
4
|
+
data.tar.gz: d4ac11de2827303f91d54c5bade906c4dd78cebf0357eb68ea61d56af2b4a95c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/chrono_forge-dashboard)
|
|
4
|
+
[](https://github.com/radioactive-labs/chrono_forge/actions/workflows/main.yml)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
| Workflow list | Analytics |
|
|
10
|
-
| --- | --- |
|
|
11
|
-
| [](docs/screenshots/workflows.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
|
-
| [](docs/screenshots/waiting.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
|
-
|
|
20
|
-
| --- | --- |
|
|
21
|
-
| [](docs/screenshots/branches.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
|
-
|
|
11
|
+
Version: `0.1.0` (early release). The UI and config API may change before `1.0`.
|
|
25
12
|
|
|
26
|
-
[](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
|
+
| [](docs/screenshots/workflows.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
|
+
| [](docs/screenshots/waiting.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
|
+
| [](docs/screenshots/branches.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
|
+
[](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
|
+
[](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
|
+
[](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 =
|
|
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` | `
|
|
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
|
+
});
|