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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +47 -0
- data/MIT-LICENSE +21 -0
- data/README.md +205 -0
- data/app/assets/saga_forge/dashboard/cytoscape-dagre.js +397 -0
- data/app/assets/saga_forge/dashboard/cytoscape.min.js +32 -0
- data/app/assets/saga_forge/dashboard/dagre.min.js +3809 -0
- data/app/assets/saga_forge/dashboard/dashboard.css +2 -0
- data/app/assets/saga_forge/dashboard/dashboard.js +132 -0
- data/app/assets/saga_forge/dashboard/saga_graph.js +143 -0
- data/app/assets/saga_forge/dashboard/tailwind.css +299 -0
- data/app/assets/saga_forge/dashboard/turbo.min.js +34 -0
- data/app/controllers/saga_forge/dashboard/actions_controller.rb +36 -0
- data/app/controllers/saga_forge/dashboard/assets_controller.rb +29 -0
- data/app/controllers/saga_forge/dashboard/base_controller.rb +29 -0
- data/app/controllers/saga_forge/dashboard/definitions_controller.rb +24 -0
- data/app/controllers/saga_forge/dashboard/overview_controller.rb +18 -0
- data/app/controllers/saga_forge/dashboard/sagas_controller.rb +25 -0
- data/app/controllers/saga_forge/dashboard/stalled_controller.rb +29 -0
- data/app/controllers/saga_forge/dashboard/suspended_controller.rb +33 -0
- data/app/helpers/saga_forge/dashboard/dashboard_helper.rb +77 -0
- data/app/jobs/saga_forge/dashboard/bulk_recovery_job.rb +20 -0
- data/app/presenters/saga_forge/dashboard/context_presenter.rb +50 -0
- data/app/presenters/saga_forge/dashboard/saga_graph.rb +59 -0
- data/app/presenters/saga_forge/dashboard/timeline_presenter.rb +68 -0
- data/app/queries/saga_forge/dashboard/overview_query.rb +32 -0
- data/app/queries/saga_forge/dashboard/sagas_query.rb +83 -0
- data/app/queries/saga_forge/dashboard/stats_query.rb +31 -0
- data/app/views/layouts/saga_forge/dashboard/application.html.erb +69 -0
- data/app/views/saga_forge/dashboard/definitions/missing.html.erb +7 -0
- data/app/views/saga_forge/dashboard/definitions/show.html.erb +48 -0
- data/app/views/saga_forge/dashboard/overview/_stat_card.html.erb +4 -0
- data/app/views/saga_forge/dashboard/overview/classes.html.erb +37 -0
- data/app/views/saga_forge/dashboard/overview/index.html.erb +33 -0
- data/app/views/saga_forge/dashboard/sagas/_actions.html.erb +27 -0
- data/app/views/saga_forge/dashboard/sagas/_compensation.html.erb +41 -0
- data/app/views/saga_forge/dashboard/sagas/_context_tree.html.erb +37 -0
- data/app/views/saga_forge/dashboard/sagas/_filters.html.erb +9 -0
- data/app/views/saga_forge/dashboard/sagas/_saga_row.html.erb +6 -0
- data/app/views/saga_forge/dashboard/sagas/_stats.html.erb +16 -0
- data/app/views/saga_forge/dashboard/sagas/_summary.html.erb +43 -0
- data/app/views/saga_forge/dashboard/sagas/_timeline.html.erb +48 -0
- data/app/views/saga_forge/dashboard/sagas/empty.html.erb +6 -0
- data/app/views/saga_forge/dashboard/sagas/index.html.erb +46 -0
- data/app/views/saga_forge/dashboard/sagas/show.html.erb +19 -0
- data/app/views/saga_forge/dashboard/stalled/index.html.erb +51 -0
- data/app/views/saga_forge/dashboard/suspended/index.html.erb +57 -0
- data/config/routes.rb +39 -0
- data/lib/saga_forge/dashboard/configuration.rb +30 -0
- data/lib/saga_forge/dashboard/engine.rb +9 -0
- data/lib/saga_forge/dashboard/version.rb +5 -0
- data/lib/saga_forge/dashboard.rb +33 -0
- 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
|
+
[](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
|
+
});
|