mbeditor 0.12.1 → 0.12.3
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 +77 -0
- data/README.md +38 -8
- data/app/assets/javascripts/mbeditor/collaboration_service.js +62 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +78 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +89 -0
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +233 -59
- data/app/assets/javascripts/mbeditor/file_service.js +9 -0
- data/app/assets/javascripts/mbeditor/websocket_service.js +17 -0
- data/app/assets/stylesheets/mbeditor/editor.css +88 -0
- data/app/channels/mbeditor/channel_authentication.rb +31 -2
- data/app/controllers/mbeditor/editors_controller.rb +13 -0
- data/app/services/mbeditor/model_graph_service.rb +13 -3
- data/app/services/mbeditor/route_service.rb +79 -0
- data/lib/mbeditor/configuration.rb +15 -2
- data/lib/mbeditor/engine.rb +17 -3
- data/lib/mbeditor/route_map.rb +1 -0
- data/lib/mbeditor/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 88d963e43c97cf6974b3e38c079ffd2dba642dc1c13a68237f1a3a4f7c21e9a0
|
|
4
|
+
data.tar.gz: 9e32a366d6ea7ce5014fff94ee89adba7f6909d09aa29f6404142b4155688a3a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 100805339d3956837f0ec5349de86b5bcf4315ef5dd261e76b80c0acb589cd9036264b70bb65677be3d27be9575007daee02e08c1cbad86ac3136bd331312a6b
|
|
7
|
+
data.tar.gz: d1d0681c8962251ceb0d9f18db0937e20f3c1ae1e2d30754a08a88ee10a47b6a6492641de84b9e83c0b2ab81963e79d1f3d86bb61a985bfa7b4a5eb579d0548d
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,83 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.3] - 2026-08-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`config.cable_authenticate_with`** — authentication for the collaboration
|
|
14
|
+
WebSocket, falling back to `authenticate_with` when unset.
|
|
15
|
+
- **A "Pairing off" chip and diagnostics panel.** When collaboration cannot
|
|
16
|
+
work, the editor now says so and lists every condition it depends on — the
|
|
17
|
+
vendored libraries, Action Cable's JavaScript, whether the server advertises
|
|
18
|
+
it, whether the socket actually connected, and whether anyone else is here —
|
|
19
|
+
with the fix written beside each failure. Nothing is shown when everything is
|
|
20
|
+
healthy and you are simply alone.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **A WebSocket subscribe runs no controller, and this made pairing silently
|
|
24
|
+
impossible for most authenticated apps.** `authenticate_with` is evaluated on
|
|
25
|
+
the cable against a probe exposing `session`, `cookies`, `request` and
|
|
26
|
+
`params` — so a hook reading `Current.user`, calling `UserSession.find`, or
|
|
27
|
+
using a memoised `current_user` gets `nil` or a `NameError`, denies, and the
|
|
28
|
+
socket is rejected. The hook keeps working perfectly over HTTP, so nothing
|
|
29
|
+
looks wrong; collaboration simply never connects.
|
|
30
|
+
|
|
31
|
+
Rejections are now logged as `[mbeditor] WebSocket subscription rejected: …`
|
|
32
|
+
naming the cause, `cable_authenticate_with` provides an escape hatch when one
|
|
33
|
+
proc cannot serve both contexts, and the README says this plainly instead of
|
|
34
|
+
noting that request-scoped state "may be narrower". The README's own example
|
|
35
|
+
used `UserSession.find`, which is exactly the pattern that cannot work.
|
|
36
|
+
|
|
37
|
+
## [0.12.2] - 2026-08-03
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- **Inline route hints in controllers.** Every action in a controller file is
|
|
41
|
+
annotated with the verb and path that reach it — `GET /orders/:id` beside
|
|
42
|
+
`def show` — and a public action nothing routes to is flagged `no route`.
|
|
43
|
+
Hovering adds the named-route helper. Answering "is this actually reachable?"
|
|
44
|
+
previously meant reading `config/routes.rb` and expanding `resources` in your
|
|
45
|
+
head.
|
|
46
|
+
|
|
47
|
+
Routes come from the host app's own route set rather than by parsing
|
|
48
|
+
`config/routes.rb`. mbeditor runs inside the app, so the routes are already
|
|
49
|
+
built — and they are the only source that accounts for `resources` expansion,
|
|
50
|
+
`member`/`collection` blocks, scopes, constraints and mounted engines.
|
|
51
|
+
|
|
52
|
+
- **`config.model_graph_max_models`** (default 1000, was a hard-coded 300).
|
|
53
|
+
A schema over the cap silently lost models and only said "(truncated)".
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
- **Model boxes are sized to their contents** rather than all being one width,
|
|
57
|
+
so a long model or column name is no longer truncated while a model called
|
|
58
|
+
`Tag` wastes most of its box. Layer positions accumulate per layer, since a
|
|
59
|
+
fixed stride would let a wide box overlap the next layer.
|
|
60
|
+
- **Cluster blocks are packed in two dimensions** instead of stacked in a single
|
|
61
|
+
column, which left a schema with one big core and several small islands
|
|
62
|
+
running off the bottom with the right-hand side empty.
|
|
63
|
+
- **The dummy app now carries a real ActiveRecord schema** — 20 models and 44
|
|
64
|
+
associations covering a hub, a chain, a self-reference, a join table, a
|
|
65
|
+
polymorphic association and unconnected islands — so the model graph can be
|
|
66
|
+
demonstrated and tested against something representative.
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
- **mbeditor could stop a host app from booting.** The pending-migrations
|
|
70
|
+
middleware was installed whenever `ActiveRecord::Migration::CheckPending` was
|
|
71
|
+
defined, but Rails only puts that middleware in the stack when
|
|
72
|
+
`config.active_record.migration_error` is `:page_load`. Any app loading
|
|
73
|
+
ActiveRecord with a different setting raised "No such middleware to insert
|
|
74
|
+
before" during boot. It now tests the same condition Rails does.
|
|
75
|
+
- **Long labels drew straight out past the edge of their model box.** SVG text
|
|
76
|
+
neither wraps nor ellipsises; labels are now measured against the font the
|
|
77
|
+
theme actually resolves and cut to fit.
|
|
78
|
+
- **"no database connection" hung below the box it belonged to** — the box
|
|
79
|
+
height did not count that placeholder line.
|
|
80
|
+
- **The model search is a real dropdown.** The native `<datalist>` rendered in
|
|
81
|
+
the browser's own chrome: unstyleable, and unable to show the table name and
|
|
82
|
+
column count beside each model. Arrow keys and Enter work as before.
|
|
83
|
+
- **The titlebar wrapped to two lines in a narrow window**, pushing the toolbar
|
|
84
|
+
out of its 32px row. It no longer wraps, and below the width where the toolbar
|
|
85
|
+
drops its button labels the title gives way to the icon alone.
|
|
86
|
+
|
|
10
87
|
## [0.12.1] - 2026-08-03
|
|
11
88
|
|
|
12
89
|
### Added
|
data/README.md
CHANGED
|
@@ -67,8 +67,12 @@ Mbeditor.configure do |config|
|
|
|
67
67
|
config.excluded_paths = %w[.git tmp log node_modules .bundle coverage vendor/bundle]
|
|
68
68
|
config.rubocop_command = "bundle exec rubocop"
|
|
69
69
|
|
|
70
|
-
# Optional authentication (runs as a before_action in the engine controllers
|
|
71
|
-
#
|
|
70
|
+
# Optional authentication (runs as a before_action in the engine controllers,
|
|
71
|
+
# and on the collaboration WebSocket subscribe). Resolve the user from
|
|
72
|
+
# `session` — a WebSocket subscribe runs no controller, so Current.user,
|
|
73
|
+
# UserSession.find and a memoised current_user are all unavailable there.
|
|
74
|
+
# config.authenticate_with = proc { redirect_to login_path unless User.find_by(id: session[:user_id]) }
|
|
75
|
+
# config.cable_authenticate_with = proc { ... } # when one proc cannot serve both
|
|
72
76
|
|
|
73
77
|
# Optional test runner (Minitest or RSpec)
|
|
74
78
|
# config.test_framework = :minitest # :minitest or :rspec — auto-detected when nil
|
|
@@ -123,6 +127,7 @@ end
|
|
|
123
127
|
| Option | Default | Description |
|
|
124
128
|
|--------|---------|-------------|
|
|
125
129
|
| `authenticate_with` | `nil` | Proc run as a `before_action` in all engine controllers. Executed via `instance_exec` inside the controller, so it has access to `session`, `cookies`, `redirect_to`, and auth-library class methods (e.g. Authlogic's `UserSession.find`) — but not helper methods from the host's `ApplicationController`. The same hook is also evaluated when the collaboration / editor **WebSocket** subscribes (see [Collaborative pairing](#collaborative-pairing-optional)); if it halts or raises, that subscription is rejected (fail-closed). Over the cable the proc runs against a request-derived probe, so request-scoped state may be narrower than over HTTP. |
|
|
130
|
+
| `cable_authenticate_with` | `nil` | Authentication for the collaboration WebSocket; `nil` falls back to `authenticate_with`. Set this when the HTTP hook cannot work on the cable — a WebSocket subscribe runs no controller, so `Current.*`, an Authlogic `UserSession` and a memoised `current_user` are `nil` or raise, and the hook then denies the socket while working perfectly over HTTP. See [Collaborative pairing](#collaborative-pairing-optional). |
|
|
126
131
|
| `authentication_cache_ttl` | `0` | Seconds to cache the auth result in the session (`0` = no caching). Set e.g. `300` to avoid calling `authenticate_with` on every request when the proc is expensive. Trade-off: after host logout, mbeditor stays accessible for up to TTL seconds. |
|
|
127
132
|
|
|
128
133
|
### Test runner
|
|
@@ -335,20 +340,45 @@ should be able to reach the port.
|
|
|
335
340
|
|
|
336
341
|
**2. Set an authentication hook — it runs on the WebSocket handshake.**
|
|
337
342
|
Configure `authenticate_with` (see the [Authentication](#authentication) options).
|
|
338
|
-
The same hook that gates the HTTP editor is
|
|
343
|
+
The same hook that gates the HTTP editor is also evaluated when the collaboration /
|
|
339
344
|
editor WebSocket subscribes: if it halts (e.g. `redirect_to`/`render`/`head`) — or raises —
|
|
340
345
|
the socket subscription is **rejected (fail-closed)**, so pairing cannot bypass your auth.
|
|
341
346
|
|
|
347
|
+
> **A WebSocket subscribe runs no controller.** This is the single most common reason
|
|
348
|
+
> pairing appears to do nothing. Anything a `before_action` populates is unavailable on
|
|
349
|
+
> the cable: `Current.user` (and any other `ActiveSupport::CurrentAttributes`) is `nil`,
|
|
350
|
+
> `UserSession.find` raises because Authlogic's controller adapter was never activated,
|
|
351
|
+
> and a memoised `current_user` is undefined. The hook then denies — correctly, given it
|
|
352
|
+
> was handed nobody — and the socket is rejected. It works perfectly over HTTP the whole
|
|
353
|
+
> time, so nothing looks wrong until you notice collaboration never connects.
|
|
354
|
+
|
|
355
|
+
The hook is evaluated against a probe exposing `session`, `cookies`, `request`, `params`
|
|
356
|
+
and the halt methods. Resolve the user from `session` so the same hook works in both
|
|
357
|
+
contexts:
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
Mbeditor.configure do |c|
|
|
361
|
+
c.authenticate_with = proc do
|
|
362
|
+
user = Current.user || User.find_by(id: session[:user_credentials_id])
|
|
363
|
+
head :forbidden unless user&.super_admin_access?
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
When one proc genuinely cannot serve both, give the cable its own:
|
|
369
|
+
|
|
342
370
|
```ruby
|
|
343
371
|
Mbeditor.configure do |c|
|
|
344
|
-
|
|
345
|
-
c.
|
|
372
|
+
c.authenticate_with = proc { head :forbidden unless Current.user&.super_admin_access? }
|
|
373
|
+
c.cable_authenticate_with = proc do
|
|
374
|
+
head :forbidden unless User.find_by(id: session[:user_credentials_id])&.super_admin_access?
|
|
375
|
+
end
|
|
346
376
|
end
|
|
347
377
|
```
|
|
348
378
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
379
|
+
Every rejection is logged as `[mbeditor] WebSocket subscription rejected: …` with the
|
|
380
|
+
reason, so a denial is visible rather than silent. The editor also shows a **Pairing off**
|
|
381
|
+
chip when collaboration cannot work, listing each failing condition. For defence in depth, also authenticate at your host app's
|
|
352
382
|
`ApplicationCable::Connection` (the standard `identified_by` / `reject_unauthorized_connection`
|
|
353
383
|
pattern) — mbeditor's hook is an additional gate, not a replacement for securing the cable
|
|
354
384
|
connection itself.
|
|
@@ -120,6 +120,67 @@ var CollaborationService = (function () {
|
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// Every condition collaboration depends on, reported separately.
|
|
124
|
+
//
|
|
125
|
+
// isAvailable() collapses five things into one boolean, which is useless when
|
|
126
|
+
// pairing does not work: "false" could be a missing vendored library, a cable
|
|
127
|
+
// the server never advertised, a rejected handshake, or simply nobody else
|
|
128
|
+
// being here. Someone debugging this on another machine cannot paste a console
|
|
129
|
+
// snippet back, so the editor has to be able to say which.
|
|
130
|
+
function diagnostics() {
|
|
131
|
+
var cableStatus = (typeof WebSocketService !== 'undefined' &&
|
|
132
|
+
typeof WebSocketService.cableStatus === 'function')
|
|
133
|
+
? WebSocketService.cableStatus() : 'unknown';
|
|
134
|
+
|
|
135
|
+
var checks = [
|
|
136
|
+
{
|
|
137
|
+
key: 'libraries',
|
|
138
|
+
label: 'Collaboration libraries loaded',
|
|
139
|
+
ok: typeof window.Y !== 'undefined' && typeof window.MonacoBinding !== 'undefined',
|
|
140
|
+
detail: 'vendor/assets/javascripts/yjs-collab.js is served by the asset pipeline. ' +
|
|
141
|
+
'If this fails, the host app is not serving it — try rails assets:clobber, ' +
|
|
142
|
+
'or check config.assets.precompile.'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: 'actioncable',
|
|
146
|
+
label: 'Action Cable JavaScript present',
|
|
147
|
+
ok: typeof window.ActionCable !== 'undefined',
|
|
148
|
+
detail: 'The host app must make actioncable.js available to the page.'
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
key: 'advertised',
|
|
152
|
+
label: 'Server advertises Action Cable',
|
|
153
|
+
ok: typeof WebSocketService !== 'undefined' && WebSocketService.isCableAvailable(),
|
|
154
|
+
detail: 'Action Cable must be enabled and mounted in the host app routes.'
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
key: 'connected',
|
|
158
|
+
label: 'WebSocket connected',
|
|
159
|
+
ok: cableStatus === 'connected',
|
|
160
|
+
detail: cableStatus === 'rejected'
|
|
161
|
+
? 'The server rejected the subscription. Usually action_cable.allowed_request_origins ' +
|
|
162
|
+
'not listing the origin you are reaching the editor through, or an authenticate_with ' +
|
|
163
|
+
'hook halting on the cable (it fails closed). Look for "Request origin not allowed" in the log.'
|
|
164
|
+
: 'Status: ' + cableStatus + '.'
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
key: 'peers',
|
|
168
|
+
label: 'Another participant connected',
|
|
169
|
+
ok: _peerPresent,
|
|
170
|
+
detail: 'Presence is held per web process. If two people are on different Puma workers ' +
|
|
171
|
+
'they never see each other — run a single worker (WEB_CONCURRENCY=0) while pairing.'
|
|
172
|
+
}
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
ok: checks.every(function (c) { return c.ok; }),
|
|
177
|
+
cableStatus: cableStatus,
|
|
178
|
+
checks: checks,
|
|
179
|
+
// The first failing check is the one worth acting on; the rest follow from it.
|
|
180
|
+
firstProblem: checks.filter(function (c) { return !c.ok; })[0] || null
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
123
184
|
// Subscribe to availability transitions. Returns an unsubscribe function.
|
|
124
185
|
function onAvailabilityChange(fn) {
|
|
125
186
|
_availabilityListeners.push(fn);
|
|
@@ -674,6 +735,7 @@ var CollaborationService = (function () {
|
|
|
674
735
|
return {
|
|
675
736
|
isAvailable: isAvailable,
|
|
676
737
|
setPeerPresent: setPeerPresent,
|
|
738
|
+
diagnostics: diagnostics,
|
|
677
739
|
onAvailabilityChange: onAvailabilityChange,
|
|
678
740
|
isEnabledFor: isEnabledFor,
|
|
679
741
|
ensureRoom: ensureRoom,
|
|
@@ -90,6 +90,7 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
90
90
|
|
|
91
91
|
var blameDecorationsRef = useRef([]);
|
|
92
92
|
var gitLineDecorationsRef = useRef([]);
|
|
93
|
+
var routeDecorationsRef = useRef([]);
|
|
93
94
|
// Latest git line-diff refresh, read by the poll effect so its interval does
|
|
94
95
|
// not have to be torn down whenever the active tab changes.
|
|
95
96
|
var gitLineRefreshRef = useRef(null);
|
|
@@ -1530,6 +1531,83 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1530
1531
|
}
|
|
1531
1532
|
}, [isBlameVisible, tab.path, blameData, isBlameLoading]);
|
|
1532
1533
|
|
|
1534
|
+
// Inline route hints for controllers.
|
|
1535
|
+
//
|
|
1536
|
+
// Every `def` in a controller gets the verb and path that reach it drawn after
|
|
1537
|
+
// the line, and a public action nothing routes to is called out — the question
|
|
1538
|
+
// "is this actually reachable?" otherwise means reading config/routes.rb and
|
|
1539
|
+
// mentally expanding `resources`. Routes come from the host app's own route
|
|
1540
|
+
// set, which is the only thing that knows how those expand.
|
|
1541
|
+
//
|
|
1542
|
+
// Decorations, not view zones: a hint belongs on the line, and a zone would
|
|
1543
|
+
// push the code around every time a file was opened.
|
|
1544
|
+
useEffect(function () {
|
|
1545
|
+
var editor = monacoRef.current;
|
|
1546
|
+
if (!editor || !window.monaco) return;
|
|
1547
|
+
|
|
1548
|
+
var clear = function () {
|
|
1549
|
+
if (!routeDecorationsRef.current.length) return;
|
|
1550
|
+
routeDecorationsRef.current = editor.deltaDecorations(routeDecorationsRef.current, []);
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
if (!tab.path || !/^app\/controllers\/.+_controller\.rb$/.test(tab.path)) {
|
|
1554
|
+
clear();
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
var cancelled = false;
|
|
1559
|
+
FileService.getRoutes(tab.path).then(function (data) {
|
|
1560
|
+
if (cancelled) return;
|
|
1561
|
+
var actions = (data && data.actions) || {};
|
|
1562
|
+
var model = editor.getModel();
|
|
1563
|
+
if (!model) return;
|
|
1564
|
+
|
|
1565
|
+
var decorations = [];
|
|
1566
|
+
var text = model.getValue().split('\n');
|
|
1567
|
+
// Only methods above the first `private`/`protected` are candidates for a
|
|
1568
|
+
// route; flagging a helper below it as unrouted would be noise.
|
|
1569
|
+
var visibilityEnded = false;
|
|
1570
|
+
|
|
1571
|
+
text.forEach(function (line, i) {
|
|
1572
|
+
if (/^\s*(private|protected)\s*$/.test(line)) { visibilityEnded = true; return; }
|
|
1573
|
+
var m = line.match(/^\s*def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_]*[?!=]?)/);
|
|
1574
|
+
if (!m) return;
|
|
1575
|
+
|
|
1576
|
+
var name = m[1];
|
|
1577
|
+
var routes = actions[name];
|
|
1578
|
+
var lineNo = i + 1;
|
|
1579
|
+
var col = line.length + 1;
|
|
1580
|
+
|
|
1581
|
+
if (routes && routes.length) {
|
|
1582
|
+
var label = routes.map(function (r) { return r.verb + ' ' + r.path; }).join(' · ');
|
|
1583
|
+
decorations.push({
|
|
1584
|
+
range: new window.monaco.Range(lineNo, col, lineNo, col),
|
|
1585
|
+
options: {
|
|
1586
|
+
after: { content: ' ' + label, inlineClassName: 'mbeditor-route-hint' },
|
|
1587
|
+
hoverMessage: routes.map(function (r) {
|
|
1588
|
+
return { value: '`' + r.verb + ' ' + r.path + '`' + (r.name ? ' — `' + r.name + '_path`' : '') };
|
|
1589
|
+
}),
|
|
1590
|
+
showIfCollapsed: true
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
} else if (!visibilityEnded && !/[?!=]$/.test(name) && name !== 'initialize') {
|
|
1594
|
+
decorations.push({
|
|
1595
|
+
range: new window.monaco.Range(lineNo, col, lineNo, col),
|
|
1596
|
+
options: {
|
|
1597
|
+
after: { content: ' no route', inlineClassName: 'mbeditor-route-hint-none' },
|
|
1598
|
+
hoverMessage: [{ value: 'No route in this application dispatches to this action.' }],
|
|
1599
|
+
showIfCollapsed: true
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
routeDecorationsRef.current = editor.deltaDecorations(routeDecorationsRef.current, decorations);
|
|
1606
|
+
}).catch(function () { /* hints are additive; a failure just means none */ });
|
|
1607
|
+
|
|
1608
|
+
return function () { cancelled = true; clear(); };
|
|
1609
|
+
}, [tab.path, tab.externalContentVersion, monacoReady]);
|
|
1610
|
+
|
|
1533
1611
|
// Render Blame block headers (author + summary) above contiguous commit regions.
|
|
1534
1612
|
useEffect(function () {
|
|
1535
1613
|
if (!monacoRef.current || !window.monaco || !isBlameVisible || !blameData) return;
|
|
@@ -2450,6 +2450,30 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2450
2450
|
// external-change detection for solo users. The service is told about the roster
|
|
2451
2451
|
// from the presence handler above, not from an effect here, so it hears about
|
|
2452
2452
|
// every message rather than only about a change in the participant count.
|
|
2453
|
+
var _collabDiag = useState(false);
|
|
2454
|
+
var collabDiagOpen = _collabDiag[0], setCollabDiagOpen = _collabDiag[1];
|
|
2455
|
+
var _collabTrouble = useState(null);
|
|
2456
|
+
var collabTrouble = _collabTrouble[0], setCollabTrouble = _collabTrouble[1];
|
|
2457
|
+
|
|
2458
|
+
// Re-checked on a slow interval because the failures worth reporting — a
|
|
2459
|
+
// rejected handshake, a dropped socket — happen asynchronously and nothing
|
|
2460
|
+
// else would notice. Only the first failing check's key is stored, so an
|
|
2461
|
+
// unchanged state is an identical string and React bails rather than
|
|
2462
|
+
// re-rendering the app every tick.
|
|
2463
|
+
useEffect(function () {
|
|
2464
|
+
function check() {
|
|
2465
|
+
if (typeof CollaborationService === 'undefined' ||
|
|
2466
|
+
typeof CollaborationService.diagnostics !== 'function') return;
|
|
2467
|
+
var d = CollaborationService.diagnostics();
|
|
2468
|
+
// "Nobody else is here" is not a fault; everything else is.
|
|
2469
|
+
var problem = d.firstProblem && d.firstProblem.key !== 'peers' ? d.firstProblem.key : null;
|
|
2470
|
+
setCollabTrouble(function (prev) { return prev === problem ? prev : problem; });
|
|
2471
|
+
}
|
|
2472
|
+
check();
|
|
2473
|
+
var id = setInterval(check, 10000);
|
|
2474
|
+
return function () { clearInterval(id); };
|
|
2475
|
+
}, []);
|
|
2476
|
+
|
|
2453
2477
|
var collabPeerIds = Object.keys(collabRoster);
|
|
2454
2478
|
|
|
2455
2479
|
// A labelled peer chip costs ~110px (name + filename), and the titlebar button
|
|
@@ -4217,6 +4241,28 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4217
4241
|
!toolbarIconOnly && " Git"
|
|
4218
4242
|
)
|
|
4219
4243
|
),
|
|
4244
|
+
// Collaboration trouble chip. Deliberately not shown when everything is
|
|
4245
|
+
// healthy and you are simply alone — that was the whole point of hiding
|
|
4246
|
+
// the solo chip. It appears only when a condition pairing needs has
|
|
4247
|
+
// actually failed, so silence means "nobody here" and never "quietly
|
|
4248
|
+
// broken", which is exactly the ambiguity that made a real pairing
|
|
4249
|
+
// failure impossible to diagnose from the other machine.
|
|
4250
|
+
collabTrouble && React.createElement(
|
|
4251
|
+
React.Fragment,
|
|
4252
|
+
null,
|
|
4253
|
+
React.createElement("div", { className: "statusbar-sep" }),
|
|
4254
|
+
React.createElement(
|
|
4255
|
+
"button",
|
|
4256
|
+
{
|
|
4257
|
+
type: "button",
|
|
4258
|
+
className: "statusbar-btn mbeditor-collab-trouble",
|
|
4259
|
+
title: "Pairing is not available — click for details",
|
|
4260
|
+
onClick: function () { setCollabDiagOpen(true); }
|
|
4261
|
+
},
|
|
4262
|
+
React.createElement("i", { className: "fas fa-user-slash" }),
|
|
4263
|
+
!toolbarIconOnly && " Pairing off"
|
|
4264
|
+
)
|
|
4265
|
+
),
|
|
4220
4266
|
// Your own chip appears only once someone else is actually connected.
|
|
4221
4267
|
// Alone it told you nothing — Action Cable is up in any normal dev setup,
|
|
4222
4268
|
// so it sat in the toolbar permanently announcing a session of one. While
|
|
@@ -4358,6 +4404,49 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
4358
4404
|
)
|
|
4359
4405
|
);
|
|
4360
4406
|
})(),
|
|
4407
|
+
// The diagnostics panel. Every condition listed with its own state, because
|
|
4408
|
+
// the person who needs this is usually on the other machine and cannot paste
|
|
4409
|
+
// a console snippet back.
|
|
4410
|
+
collabDiagOpen && (function () {
|
|
4411
|
+
var d = CollaborationService.diagnostics();
|
|
4412
|
+
return React.createElement(
|
|
4413
|
+
'div',
|
|
4414
|
+
{ className: 'mbeditor-modal-backdrop', onClick: function () { setCollabDiagOpen(false); } },
|
|
4415
|
+
React.createElement(
|
|
4416
|
+
'div',
|
|
4417
|
+
{ className: 'mbeditor-collab-diag', onClick: function (e) { e.stopPropagation(); } },
|
|
4418
|
+
React.createElement('div', { className: 'mbeditor-collab-diag-title' }, 'Pair programming status'),
|
|
4419
|
+
React.createElement(
|
|
4420
|
+
'ul',
|
|
4421
|
+
{ className: 'mbeditor-collab-diag-list' },
|
|
4422
|
+
d.checks.map(function (c) {
|
|
4423
|
+
return React.createElement(
|
|
4424
|
+
'li',
|
|
4425
|
+
{ key: c.key, className: 'mbeditor-collab-diag-row' + (c.ok ? '' : ' is-bad') },
|
|
4426
|
+
React.createElement('i', { className: c.ok ? 'fas fa-check' : 'fas fa-times' }),
|
|
4427
|
+
React.createElement(
|
|
4428
|
+
'div',
|
|
4429
|
+
null,
|
|
4430
|
+
React.createElement('div', { className: 'mbeditor-collab-diag-label' }, c.label),
|
|
4431
|
+
!c.ok && React.createElement('div', { className: 'mbeditor-collab-diag-detail' }, c.detail)
|
|
4432
|
+
)
|
|
4433
|
+
);
|
|
4434
|
+
})
|
|
4435
|
+
),
|
|
4436
|
+
React.createElement(
|
|
4437
|
+
'div',
|
|
4438
|
+
{ className: 'mbeditor-collab-diag-foot' },
|
|
4439
|
+
d.ok
|
|
4440
|
+
? 'Pairing is working.'
|
|
4441
|
+
: 'The first failing item above is the one to fix; the rest follow from it.'
|
|
4442
|
+
),
|
|
4443
|
+
React.createElement('button', {
|
|
4444
|
+
type: 'button', className: 'ide-model-graph-btn',
|
|
4445
|
+
onClick: function () { setCollabDiagOpen(false); }
|
|
4446
|
+
}, 'Close')
|
|
4447
|
+
)
|
|
4448
|
+
);
|
|
4449
|
+
})(),
|
|
4361
4450
|
showHelp && React.createElement(ShortcutHelp, { onClose: function () { return setShowHelp(false); } }),
|
|
4362
4451
|
React.createElement(
|
|
4363
4452
|
"div",
|