bugpatrol 0.1.3 → 0.1.4
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.
- package/LICENSE +202 -0
- package/NOTICE +7 -0
- package/dist/bin.js +10611 -10236
- package/package.json +4 -2
- package/src/ui/app.js +850 -356
- package/src/ui/graph-layout.js +101 -43
- package/src/ui/index.html +7 -7
- package/src/ui/style.css +1067 -262
package/src/ui/graph-layout.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// Pure layout for the agent app map. Coordinates are SVG user units.
|
|
2
2
|
export function layoutGraph(screens, edges, entryId) {
|
|
3
|
-
const w = 190,
|
|
4
|
-
|
|
3
|
+
const w = 190,
|
|
4
|
+
h = 150,
|
|
5
|
+
xGap = 130,
|
|
6
|
+
yGap = 55,
|
|
7
|
+
pad = 45;
|
|
8
|
+
const xStep = w + xGap,
|
|
9
|
+
yStep = h + yGap;
|
|
5
10
|
const ids = new Set(screens.map((screen) => screen.id));
|
|
6
11
|
const virtual = new Set(screens.filter((screen) => screen.virtual).map((screen) => screen.id));
|
|
7
12
|
const valid = edges.filter((edge) => ids.has(edge.from) && ids.has(edge.to) && edge.from !== edge.to);
|
|
@@ -27,10 +32,11 @@ export function layoutGraph(screens, edges, entryId) {
|
|
|
27
32
|
seen.add(id);
|
|
28
33
|
for (const current of queue) {
|
|
29
34
|
members.push(current);
|
|
30
|
-
for (const next of neighbors.get(current))
|
|
31
|
-
seen.
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
for (const next of neighbors.get(current))
|
|
36
|
+
if (!seen.has(next)) {
|
|
37
|
+
seen.add(next);
|
|
38
|
+
queue.push(next);
|
|
39
|
+
}
|
|
34
40
|
}
|
|
35
41
|
components.push(members.sort());
|
|
36
42
|
}
|
|
@@ -43,10 +49,12 @@ export function layoutGraph(screens, edges, entryId) {
|
|
|
43
49
|
if (memberSet.has(entryId)) {
|
|
44
50
|
const queue = [entryId];
|
|
45
51
|
reachedFromEntry.add(entryId);
|
|
46
|
-
for (const id of queue)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
for (const id of queue)
|
|
53
|
+
for (const to of outgoing.get(id))
|
|
54
|
+
if (!reachedFromEntry.has(to)) {
|
|
55
|
+
reachedFromEntry.add(to);
|
|
56
|
+
queue.push(to);
|
|
57
|
+
}
|
|
50
58
|
}
|
|
51
59
|
const roots = [
|
|
52
60
|
...(memberSet.has(entryId) ? [entryId] : []),
|
|
@@ -57,30 +65,42 @@ export function layoutGraph(screens, edges, entryId) {
|
|
|
57
65
|
const visit = () => {
|
|
58
66
|
for (let index = 0; index < queue.length; index++) {
|
|
59
67
|
const id = queue[index];
|
|
60
|
-
for (const to of outgoing.get(id))
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
for (const to of outgoing.get(id))
|
|
69
|
+
if (memberSet.has(to) && !layer.has(to)) {
|
|
70
|
+
layer.set(to, layer.get(id) + 1);
|
|
71
|
+
queue.push(to);
|
|
72
|
+
}
|
|
64
73
|
}
|
|
65
74
|
};
|
|
66
75
|
visit();
|
|
67
76
|
// A directed cycle can have no zero-incoming root.
|
|
68
|
-
for (const id of members)
|
|
77
|
+
for (const id of members)
|
|
78
|
+
if (!layer.has(id)) {
|
|
79
|
+
layer.set(id, 0);
|
|
80
|
+
queue.push(id);
|
|
81
|
+
visit();
|
|
82
|
+
}
|
|
69
83
|
|
|
70
84
|
const columns = Array.from({ length: Math.max(...layer.values()) + 1 }, () => []);
|
|
71
85
|
for (const id of members) columns[layer.get(id)].push(id);
|
|
72
86
|
const order = new Map();
|
|
73
|
-
|
|
87
|
+
for (const list of columns) for (const [index, id] of list.entries()) order.set(id, index);
|
|
74
88
|
const sweep = (column, neighbor) => {
|
|
75
89
|
const list = columns[column];
|
|
76
90
|
const center = (id) => {
|
|
77
|
-
const positions = valid
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
const positions = valid
|
|
92
|
+
.filter(
|
|
93
|
+
(edge) =>
|
|
94
|
+
edge.kind !== 'back' &&
|
|
95
|
+
((edge.from === id && layer.get(edge.to) === neighbor) ||
|
|
96
|
+
(edge.to === id && layer.get(edge.from) === neighbor)),
|
|
97
|
+
)
|
|
98
|
+
.map((edge) => order.get(edge.from === id ? edge.to : edge.from))
|
|
99
|
+
.filter((value) => value !== undefined);
|
|
80
100
|
return positions.length ? positions.reduce((sum, value) => sum + value, 0) / positions.length : order.get(id);
|
|
81
101
|
};
|
|
82
102
|
list.sort((a, b) => center(a) - center(b) || a.localeCompare(b));
|
|
83
|
-
list.
|
|
103
|
+
for (const [index, id] of list.entries()) order.set(id, index);
|
|
84
104
|
};
|
|
85
105
|
for (let pass = 0; pass < 4; pass++) {
|
|
86
106
|
for (let col = 1; col < columns.length; col++) sweep(col, col - 1);
|
|
@@ -91,23 +111,31 @@ export function layoutGraph(screens, edges, entryId) {
|
|
|
91
111
|
let xColumn = 0;
|
|
92
112
|
for (const list of columns) {
|
|
93
113
|
list.forEach((id, index) => {
|
|
94
|
-
const x = (xColumn + Math.floor(index / 10)) * xStep,
|
|
95
|
-
|
|
96
|
-
|
|
114
|
+
const x = (xColumn + Math.floor(index / 10)) * xStep,
|
|
115
|
+
y = (index % 10) * yStep;
|
|
116
|
+
nodes.push(virtual.has(id) ? { id, x: x + 35, y: y + 53, w: 120, h: 44 } : { id, x, y, w, h });
|
|
97
117
|
});
|
|
98
118
|
xColumn += Math.ceil(list.length / 10);
|
|
99
119
|
}
|
|
100
|
-
blocks.push({
|
|
101
|
-
|
|
120
|
+
blocks.push({
|
|
121
|
+
nodes,
|
|
122
|
+
width: xColumn * xStep - xGap,
|
|
123
|
+
height: Math.max(...nodes.map((node) => node.y + node.h)),
|
|
124
|
+
isolated: false,
|
|
125
|
+
});
|
|
102
126
|
}
|
|
103
127
|
|
|
104
128
|
if (isolated.length) {
|
|
105
|
-
blocks.push({
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
129
|
+
blocks.push({
|
|
130
|
+
nodes: isolated.map((id, index) => {
|
|
131
|
+
const x = Math.floor(index / 6) * xStep,
|
|
132
|
+
y = 35 + (index % 6) * yStep;
|
|
133
|
+
return virtual.has(id) ? { id, x: x + 35, y: y + 53, w: 120, h: 44 } : { id, x, y, w, h };
|
|
134
|
+
}),
|
|
135
|
+
width: Math.ceil(isolated.length / 6) * xStep - xGap,
|
|
136
|
+
height: 35 + (Math.min(6, isolated.length) - 1) * yStep + h,
|
|
137
|
+
isolated: true,
|
|
138
|
+
});
|
|
111
139
|
}
|
|
112
140
|
|
|
113
141
|
const gap = 100;
|
|
@@ -119,9 +147,13 @@ export function layoutGraph(screens, edges, entryId) {
|
|
|
119
147
|
widths.add(width);
|
|
120
148
|
}
|
|
121
149
|
}
|
|
122
|
-
let targetWidth = 0,
|
|
150
|
+
let targetWidth = 0,
|
|
151
|
+
bestScore = Infinity;
|
|
123
152
|
for (const width of widths) {
|
|
124
|
-
let rowWidth = 0,
|
|
153
|
+
let rowWidth = 0,
|
|
154
|
+
rowHeight = 0,
|
|
155
|
+
usedWidth = 0,
|
|
156
|
+
usedHeight = 0;
|
|
125
157
|
for (const block of blocks) {
|
|
126
158
|
if (rowWidth && rowWidth + gap + block.width > width) {
|
|
127
159
|
usedHeight += rowHeight + gap;
|
|
@@ -134,15 +166,30 @@ export function layoutGraph(screens, edges, entryId) {
|
|
|
134
166
|
}
|
|
135
167
|
const ratio = (usedWidth + pad * 2) / (usedHeight + rowHeight + pad * 2);
|
|
136
168
|
const score = Math.abs(Math.log(ratio / 1.6));
|
|
137
|
-
if (score < bestScore) {
|
|
169
|
+
if (score < bestScore) {
|
|
170
|
+
bestScore = score;
|
|
171
|
+
targetWidth = width;
|
|
172
|
+
}
|
|
138
173
|
}
|
|
139
174
|
const nodes = [];
|
|
140
|
-
let x = pad,
|
|
141
|
-
|
|
175
|
+
let x = pad,
|
|
176
|
+
y = pad,
|
|
177
|
+
rowHeight = 0,
|
|
178
|
+
maxRight = 0,
|
|
179
|
+
maxBottom = 0;
|
|
180
|
+
let unlinkedX = null,
|
|
181
|
+
unlinkedY = null;
|
|
142
182
|
for (const block of blocks) {
|
|
143
|
-
if (x > pad && x - pad + block.width > targetWidth) {
|
|
183
|
+
if (x > pad && x - pad + block.width > targetWidth) {
|
|
184
|
+
x = pad;
|
|
185
|
+
y += rowHeight + gap;
|
|
186
|
+
rowHeight = 0;
|
|
187
|
+
}
|
|
144
188
|
nodes.push(...block.nodes.map((node) => ({ ...node, x: node.x + x, y: node.y + y })));
|
|
145
|
-
if (block.isolated) {
|
|
189
|
+
if (block.isolated) {
|
|
190
|
+
unlinkedX = x;
|
|
191
|
+
unlinkedY = y;
|
|
192
|
+
}
|
|
146
193
|
maxRight = Math.max(maxRight, x + block.width);
|
|
147
194
|
maxBottom = Math.max(maxBottom, y + block.height);
|
|
148
195
|
rowHeight = Math.max(rowHeight, block.height);
|
|
@@ -151,16 +198,27 @@ export function layoutGraph(screens, edges, entryId) {
|
|
|
151
198
|
|
|
152
199
|
const positions = new Map(nodes.map((node) => [node.id, node]));
|
|
153
200
|
const paths = valid.map((edge, index) => {
|
|
154
|
-
const from = positions.get(edge.from),
|
|
201
|
+
const from = positions.get(edge.from),
|
|
202
|
+
to = positions.get(edge.to);
|
|
155
203
|
if (to.x > from.x) {
|
|
156
|
-
const x1 = from.x + from.w,
|
|
204
|
+
const x1 = from.x + from.w,
|
|
205
|
+
x2 = to.x,
|
|
206
|
+
y1 = from.y + from.h / 2,
|
|
207
|
+
y2 = to.y + to.h / 2;
|
|
157
208
|
const bend = Math.max(65, (x2 - x1) / 2);
|
|
158
209
|
return { ...edge, path: `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}` };
|
|
159
210
|
}
|
|
160
211
|
const y = Math.min(from.y, to.y) - 25 - (index % 5) * 12;
|
|
161
|
-
const x1 = from.x + from.w / 2,
|
|
212
|
+
const x1 = from.x + from.w / 2,
|
|
213
|
+
x2 = to.x + to.w / 2;
|
|
162
214
|
return { ...edge, path: `M ${x1} ${from.y} C ${x1} ${y}, ${x2} ${y}, ${x2} ${to.y}` };
|
|
163
215
|
});
|
|
164
|
-
return {
|
|
165
|
-
|
|
216
|
+
return {
|
|
217
|
+
nodes,
|
|
218
|
+
edges: paths,
|
|
219
|
+
width: Math.max(320, maxRight + pad),
|
|
220
|
+
height: Math.max(320, maxBottom + pad),
|
|
221
|
+
unlinkedX,
|
|
222
|
+
unlinkedY,
|
|
223
|
+
};
|
|
166
224
|
}
|
package/src/ui/index.html
CHANGED
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
<strong id="project-name">Bugpatrol</strong>
|
|
14
14
|
</div>
|
|
15
15
|
<nav class="tabs" id="tabs">
|
|
16
|
-
<button data-view="overview" class="active">Overview</button>
|
|
17
|
-
<button data-view="issues">Issues</button>
|
|
18
|
-
<button data-view="activity">Activity</button>
|
|
19
|
-
<button data-view="screens">Screens</button>
|
|
20
|
-
<button data-view="memory">Memory</button>
|
|
21
|
-
<button data-view="checks">Checks</button>
|
|
16
|
+
<button type="button" data-view="overview" class="active">Overview</button>
|
|
17
|
+
<button type="button" data-view="issues">Issues</button>
|
|
18
|
+
<button type="button" data-view="activity">Activity</button>
|
|
19
|
+
<button type="button" data-view="screens">Screens</button>
|
|
20
|
+
<button type="button" data-view="memory">Memory</button>
|
|
21
|
+
<button type="button" data-view="checks">Checks</button>
|
|
22
22
|
</nav>
|
|
23
23
|
<div class="status">
|
|
24
24
|
<span id="live" class="live" title="Live connection to the local watcher">live</span>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<main id="view"></main>
|
|
31
31
|
|
|
32
32
|
<div id="lightbox" class="lightbox" hidden>
|
|
33
|
-
<button class="lightbox-close" aria-label="Close">×</button>
|
|
33
|
+
<button type="button" class="lightbox-close" aria-label="Close">×</button>
|
|
34
34
|
<img id="lightbox-img" alt="">
|
|
35
35
|
<p id="lightbox-caption"></p>
|
|
36
36
|
</div>
|