websocket-text-relay 1.0.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.
- package/.eslintrc +29 -0
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/docs/code-structure.md +77 -0
- package/docs/creating-text-editor-plugin.md +10 -0
- package/docs/dev-getting-started.md +8 -0
- package/index.js +87 -0
- package/package.json +45 -0
- package/src/language-server/JsonRpcInterface.js +126 -0
- package/src/language-server/JsonRpcInterface.test.js +496 -0
- package/src/language-server/LspReader.js +119 -0
- package/src/language-server/LspReader.test.js +508 -0
- package/src/language-server/LspWriter.js +44 -0
- package/src/language-server/LspWriter.test.js +179 -0
- package/src/language-server/constants.js +23 -0
- package/src/ui/css/fonts/Montserrat-Black.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Bold.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-ExtraBold.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-ExtraLight.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Light.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Medium.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Regular.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-SemiBold.ttf +0 -0
- package/src/ui/css/fonts/Montserrat-Thin.ttf +0 -0
- package/src/ui/css/fonts.css +54 -0
- package/src/ui/css/main.css +198 -0
- package/src/ui/index.html +36 -0
- package/src/ui/js/components/ActivityTimeseriesGraph.js +149 -0
- package/src/ui/js/components/HeaderSummary.js +23 -0
- package/src/ui/js/components/ServerStatus.js +31 -0
- package/src/ui/js/components/SessionLabels.js +171 -0
- package/src/ui/js/components/SessionWedges.js +107 -0
- package/src/ui/js/components/StatusRing.js +42 -0
- package/src/ui/js/components/grids.js +29 -0
- package/src/ui/js/index.js +117 -0
- package/src/ui/js/main.js +97 -0
- package/src/ui/js/util/DependencyManager.js +31 -0
- package/src/ui/js/util/EventEmitter.js +40 -0
- package/src/ui/js/util/WebsocketClient.js +76 -0
- package/src/ui/js/util/constants.js +9 -0
- package/src/ui/js/util/drawing.js +128 -0
- package/src/websocket-interface/WebsocketClient.js +56 -0
- package/src/websocket-interface/WebsocketInterface.js +71 -0
- package/src/websocket-interface/WtrSession.js +122 -0
- package/src/websocket-interface/httpServer.js +58 -0
- package/src/websocket-interface/sessionManager.js +107 -0
- package/src/websocket-interface/util.js +12 -0
- package/src/websocket-interface/websocketApi.js +116 -0
- package/src/websocket-interface/websocketServer.js +18 -0
- package/start.js +4 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { exportDeps, drawText } = window.__WTR__
|
|
2
|
+
|
|
3
|
+
const valueTextClass = "server_status_value"
|
|
4
|
+
const offlineTextClass = "server_status_offline"
|
|
5
|
+
|
|
6
|
+
class ServerStatus {
|
|
7
|
+
constructor ({parentNode}) {
|
|
8
|
+
this.parentNode = parentNode
|
|
9
|
+
this.draw()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
draw () {
|
|
13
|
+
this.parentNode.innerHTML = ""
|
|
14
|
+
drawText({x: 0, y: .85, text: "WS Server PID", className: "server_status_label", parentNode: this.parentNode})
|
|
15
|
+
this.valueElement = drawText({x: 0, y: .748, text: "138324", parentNode: this.parentNode})
|
|
16
|
+
this.offlineElement = drawText({x: 0, y: .748, text: "OFFLINE", className:offlineTextClass, parentNode: this.parentNode})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
update (pid) {
|
|
20
|
+
if (pid == null) {
|
|
21
|
+
this.valueElement.classList.remove(valueTextClass)
|
|
22
|
+
this.offlineElement.classList.add(offlineTextClass)
|
|
23
|
+
} else {
|
|
24
|
+
this.valueElement.innerHTML = pid
|
|
25
|
+
this.valueElement.classList.add(valueTextClass)
|
|
26
|
+
this.offlineElement.classList.remove(offlineTextClass)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
exportDeps({ServerStatus})
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const { exportDeps, polarToCartesian, coordsToPathData, drawLinearPath, drawCircle, drawText, drawSvgElement, drawToolTip } = window.__WTR__
|
|
2
|
+
|
|
3
|
+
// a value with a colored circle and tooltip
|
|
4
|
+
const drawSummaryValue = ({x, y, label, circleClass, parentNode}) => {
|
|
5
|
+
const tooltipWrapperGroup = drawSvgElement("g", undefined, "tooltip_wrapper_group", parentNode)
|
|
6
|
+
drawCircle({cx: x, cy: y - 0.0032, r: summaryCircleRadius, className: circleClass, parentNode: tooltipWrapperGroup})
|
|
7
|
+
drawToolTip({x: x, y: y - 0.0032, text: label, parentNode: tooltipWrapperGroup})
|
|
8
|
+
return drawText({x: x + summaryCircleRadius * 2, y: y, dominantBaseline: "middle", text: "0", className: "summary_text_value", parentNode: tooltipWrapperGroup})
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// similar to summary value, but with an update function that automatically handles moving the circle on the left as the text size changes
|
|
12
|
+
const drawRightAlignedSummaryValue = ({x, y, label, circleClass, parentNode}) => {
|
|
13
|
+
const tooltipWrapperGroup = drawSvgElement("g", undefined, "tooltip_wrapper_group", parentNode)
|
|
14
|
+
drawToolTip({x: x, y: y - 0.0032, text: label, parentNode: tooltipWrapperGroup})
|
|
15
|
+
const textElement = drawText({x: x + summaryCircleRadius * 2, y: y, dominantBaseline: "middle", text: "0", textAnchor: "end", className: "summary_text_value", parentNode: tooltipWrapperGroup})
|
|
16
|
+
const xDiff = textElement.getBBox().width
|
|
17
|
+
const labelCircle = drawCircle({cx: x - xDiff, cy: y - 0.0032, r: summaryCircleRadius, className: circleClass, parentNode: tooltipWrapperGroup})
|
|
18
|
+
const update = (value) => {
|
|
19
|
+
textElement.innerHTML = value
|
|
20
|
+
const xDiff = textElement.getBBox().width
|
|
21
|
+
labelCircle.setAttribute("cx", x - xDiff)
|
|
22
|
+
}
|
|
23
|
+
return {textElement, update}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const underlinePadding = 0.01
|
|
27
|
+
const summaryCircleRadius = 0.0140
|
|
28
|
+
const summaryLeftPadding = 0.03
|
|
29
|
+
const summaryValueSpacing = 0.065
|
|
30
|
+
const editorSummaryPadding = 0.025
|
|
31
|
+
|
|
32
|
+
const addPadding = ({x, y, height, width}, horizontalPadding, verticalPadding) => {
|
|
33
|
+
if (verticalPadding == null) { verticalPadding = horizontalPadding }
|
|
34
|
+
x -= horizontalPadding
|
|
35
|
+
width += horizontalPadding * 2
|
|
36
|
+
y -= verticalPadding
|
|
37
|
+
height += verticalPadding * 2
|
|
38
|
+
return {x, y, height, width}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const labelLineDistance = 0.07
|
|
42
|
+
|
|
43
|
+
class ClientLabel {
|
|
44
|
+
constructor ({wedgeCenterAngle, wedgeCenterRadius, parentNode}) {
|
|
45
|
+
this.parentNode = parentNode
|
|
46
|
+
this.wedgeCenterAngle = wedgeCenterAngle
|
|
47
|
+
this.wedgeCenterRadius = wedgeCenterRadius
|
|
48
|
+
this.wedgeCenter = polarToCartesian(this.wedgeCenterAngle, this.wedgeCenterRadius)
|
|
49
|
+
this.draw()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
draw () {
|
|
53
|
+
const textStart = polarToCartesian(this.wedgeCenterAngle, this.wedgeCenterRadius + labelLineDistance)
|
|
54
|
+
|
|
55
|
+
this.topNameElement = drawText({x: textStart[0], y: textStart[1] - 0.06 - underlinePadding / 2, text: "", className: ["wedge_identifier", "small_text"], parentNode: this.parentNode})
|
|
56
|
+
this.nameTextElement = drawText({x: textStart[0], y: textStart[1] - 0.017 - underlinePadding / 2, text: ".", className: "wedge_identifier", parentNode: this.parentNode})
|
|
57
|
+
|
|
58
|
+
drawCircle({cx: this.wedgeCenter[0], cy: this.wedgeCenter[1], r: 0.01, className: "test_circle", parentNode: this.parentNode})
|
|
59
|
+
|
|
60
|
+
const textBbox = addPadding(this.nameTextElement.getBBox(), 0.00, underlinePadding)
|
|
61
|
+
const underlineCoords = [[this.wedgeCenter[0], this.wedgeCenter[1]], [textBbox.x, textBbox.y + textBbox.height], [textBbox.x + textBbox.width, textBbox.y + textBbox.height]]
|
|
62
|
+
this.underlinePath = drawLinearPath({coords: underlineCoords, className: "test_line", parentNode: this.parentNode})
|
|
63
|
+
|
|
64
|
+
const summaryMidY = textBbox.y + textBbox.height + underlinePadding + summaryCircleRadius / 2 + 0.02
|
|
65
|
+
const summaryStartX = textBbox.x + summaryLeftPadding
|
|
66
|
+
this.watchedCountElement = drawSummaryValue({x: summaryStartX, y: summaryMidY, label: "Watched Files", circleClass: "summary_watched_circle", parentNode: this.parentNode})
|
|
67
|
+
const watchedCountBbox = this.watchedCountElement.getBBox()
|
|
68
|
+
|
|
69
|
+
const xDiff = summaryValueSpacing + watchedCountBbox.width
|
|
70
|
+
this.activeCountTranslateWrapper = drawSvgElement("g", {transform: `translate(${xDiff}, 0)`}, undefined, this.parentNode)
|
|
71
|
+
this.activeCountElement = drawSummaryValue({x: summaryStartX, y: summaryMidY, label: "Active Files", circleClass: "summary_active_circle", parentNode: this.activeCountTranslateWrapper})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
update (client) {
|
|
75
|
+
let {name} = client
|
|
76
|
+
if (!name || name.length === 0) {
|
|
77
|
+
name = "."
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (name.length > 14) {
|
|
81
|
+
const halfIndex = Math.floor(name.length / 2)
|
|
82
|
+
const nameFirstHalf = name.substring(0, halfIndex)
|
|
83
|
+
const nameSecondHalf = name.substring(halfIndex, name.length)
|
|
84
|
+
this.topNameElement.innerHTML = nameFirstHalf
|
|
85
|
+
this.nameTextElement.innerHTML = nameSecondHalf
|
|
86
|
+
this.nameTextElement.classList.add("small_text")
|
|
87
|
+
} else {
|
|
88
|
+
this.topNameElement.innerHTML = ""
|
|
89
|
+
this.nameTextElement.classList.remove("small_text")
|
|
90
|
+
this.nameTextElement.innerHTML = name
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const textBbox = addPadding(this.nameTextElement.getBBox(), 0.00, underlinePadding)
|
|
94
|
+
const underlineCoords = [[this.wedgeCenter[0], this.wedgeCenter[1]], [textBbox.x, textBbox.y + textBbox.height], [textBbox.x + textBbox.width, textBbox.y + textBbox.height]]
|
|
95
|
+
const newUnderlinePathData = coordsToPathData(underlineCoords)
|
|
96
|
+
this.underlinePath.setAttribute("d", newUnderlinePathData)
|
|
97
|
+
|
|
98
|
+
this.watchedCountElement.innerHTML = client.watchCount
|
|
99
|
+
this.activeCountElement.innerHTML = client.activeWatchCount
|
|
100
|
+
|
|
101
|
+
const xDiff = summaryValueSpacing + this.watchedCountElement.getBBox().width
|
|
102
|
+
this.activeCountTranslateWrapper.setAttribute("transform", `translate(${xDiff}, 0)`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class EditorLabel {
|
|
107
|
+
constructor ({wedgeCenterAngle, wedgeCenterRadius, parentNode}) {
|
|
108
|
+
this.parentNode = parentNode
|
|
109
|
+
this.wedgeCenterAngle = wedgeCenterAngle
|
|
110
|
+
this.wedgeCenterRadius = wedgeCenterRadius
|
|
111
|
+
this.wedgeCenter = polarToCartesian(this.wedgeCenterAngle, this.wedgeCenterRadius)
|
|
112
|
+
this.draw()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
draw () {
|
|
116
|
+
const textStart = polarToCartesian(this.wedgeCenterAngle, this.wedgeCenterRadius + labelLineDistance)
|
|
117
|
+
|
|
118
|
+
this.topNameElement = drawText({x: textStart[0], y: textStart[1] - 0.06 - underlinePadding / 2, text: "", textAnchor: "end", className: ["wedge_identifier", "small_text"], parentNode: this.parentNode})
|
|
119
|
+
this.nameTextElement = drawText({x: textStart[0], y: textStart[1] - 0.017 - underlinePadding / 2, text: ".", textAnchor: "end", className: "wedge_identifier", parentNode: this.parentNode})
|
|
120
|
+
|
|
121
|
+
drawCircle({cx: this.wedgeCenter[0], cy: this.wedgeCenter[1], r: 0.01, className: "test_circle", parentNode: this.parentNode})
|
|
122
|
+
|
|
123
|
+
const textBbox = addPadding(this.nameTextElement.getBBox(), 0.00, underlinePadding)
|
|
124
|
+
const underlineCoords = [[this.wedgeCenter[0], this.wedgeCenter[1]], [textBbox.x, textBbox.y + textBbox.height], [textBbox.x + textBbox.width, textBbox.y + textBbox.height]]
|
|
125
|
+
this.underlinePath = drawLinearPath({coords: underlineCoords, className: "test_line", parentNode: this.parentNode})
|
|
126
|
+
|
|
127
|
+
const summaryStartX = textBbox.x + textBbox.width - summaryLeftPadding
|
|
128
|
+
const summaryMidY = textBbox.y + textBbox.height + underlinePadding * 3 + summaryCircleRadius / 2
|
|
129
|
+
|
|
130
|
+
this.activeCountSummaryValue = drawRightAlignedSummaryValue({x: summaryStartX, y: summaryMidY, label: "Active Files", circleClass: "summary_active_circle", parentNode: this.parentNode})
|
|
131
|
+
const xDiff = this.activeCountSummaryValue.textElement.getBBox().width + summaryCircleRadius + editorSummaryPadding * 2
|
|
132
|
+
this.openFilesTransformGroup = drawSvgElement('g', {transform: `translate(${-xDiff})`}, undefined, this.parentNode)
|
|
133
|
+
this.openCountSummaryValue = drawRightAlignedSummaryValue({x: summaryStartX, y: summaryMidY, label: "Open Files", circleClass: "summary_watched_circle", parentNode: this.openFilesTransformGroup})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
update (editor) {
|
|
137
|
+
let {name} = editor
|
|
138
|
+
if (!name || name.length === 0) {
|
|
139
|
+
name = "."
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (editor.isServer) {
|
|
143
|
+
name = "* " + name
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (name.length > 14) {
|
|
147
|
+
const halfIndex = Math.floor(name.length / 2)
|
|
148
|
+
const nameFirstHalf = name.substring(0, halfIndex)
|
|
149
|
+
const nameSecondHalf = name.substring(halfIndex, name.length)
|
|
150
|
+
this.topNameElement.innerHTML = nameFirstHalf
|
|
151
|
+
this.nameTextElement.innerHTML = nameSecondHalf
|
|
152
|
+
this.nameTextElement.classList.add("small_text")
|
|
153
|
+
} else {
|
|
154
|
+
this.topNameElement.innerHTML = ""
|
|
155
|
+
this.nameTextElement.classList.remove("small_text")
|
|
156
|
+
this.nameTextElement.innerHTML = name
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const textBbox = addPadding(this.nameTextElement.getBBox(), 0.00, underlinePadding)
|
|
160
|
+
const underlineCoords = [[this.wedgeCenter[0], this.wedgeCenter[1]], [textBbox.x + textBbox.width, textBbox.y + textBbox.height], [textBbox.x, textBbox.y + textBbox.height]]
|
|
161
|
+
const newUnderlinePathData = coordsToPathData(underlineCoords)
|
|
162
|
+
this.underlinePath.setAttribute("d", newUnderlinePathData)
|
|
163
|
+
|
|
164
|
+
this.activeCountSummaryValue.update(editor.activeOpenCount)
|
|
165
|
+
this.openCountSummaryValue.update(editor.openCount)
|
|
166
|
+
const xDiff = this.activeCountSummaryValue.textElement.getBBox().width + summaryCircleRadius + editorSummaryPadding * 2
|
|
167
|
+
this.openFilesTransformGroup.setAttribute("transform", `translate(${-xDiff})`)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
exportDeps({ClientLabel, EditorLabel})
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const { exportDeps, drawSvgElement, drawWedge } = window.__WTR__
|
|
2
|
+
|
|
3
|
+
const numWedges = 5
|
|
4
|
+
const wedgeSpacing = 0.01
|
|
5
|
+
const wedgeWidth = 0.08
|
|
6
|
+
|
|
7
|
+
const flashAnimationKeyframes = [{fill: "#fff"}, {fill: "var(--wedge-active-color)"}]
|
|
8
|
+
const flashAnimationProps = {duration: 250, easing: "ease-out", iterations: 1}
|
|
9
|
+
|
|
10
|
+
const createSessionSummary = (sessions) => {
|
|
11
|
+
const summary = {
|
|
12
|
+
name: `${sessions.length - numWedges + 1} others...`,
|
|
13
|
+
watchCount: 0,
|
|
14
|
+
activeWatchCount: 0,
|
|
15
|
+
openCount: 0,
|
|
16
|
+
activeOpenCount: 0
|
|
17
|
+
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (let i = numWedges - 1; i < sessions.length; i++) {
|
|
21
|
+
const session = sessions[i]
|
|
22
|
+
summary.watchCount += session.watchCount
|
|
23
|
+
summary.activeWatchCount += session.activeWatchCount
|
|
24
|
+
summary.openCount += session.openCount
|
|
25
|
+
summary.activeOpenCount += session.activeOpenCount
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return summary
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class SessionWedges {
|
|
32
|
+
constructor ({outerRingRadius, outerArcSize, direction = 1, Label, parentNode}) {
|
|
33
|
+
this.outerRingRadius = outerRingRadius
|
|
34
|
+
this.outerArcSize = outerArcSize
|
|
35
|
+
this.parentNode = parentNode
|
|
36
|
+
this.direction = direction
|
|
37
|
+
this.Label = Label
|
|
38
|
+
this.draw()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
draw () {
|
|
42
|
+
this.parentNode.innerHTML = ""
|
|
43
|
+
this.wedgeNodes = []
|
|
44
|
+
const totalStartAngle = 0.25 + this.direction * this.outerArcSize / 2
|
|
45
|
+
const totalAngleDelta = 0.5 - this.outerArcSize - wedgeSpacing
|
|
46
|
+
const wedgeAngleDelta = (totalAngleDelta / numWedges) - wedgeSpacing
|
|
47
|
+
const innerRadius = this.outerRingRadius - wedgeWidth / 2
|
|
48
|
+
for (let i = 0; i < numWedges; i++) {
|
|
49
|
+
const group = drawSvgElement("g", undefined, "single_wedge_group", this.parentNode)
|
|
50
|
+
|
|
51
|
+
let startAngle = totalStartAngle + this.direction * (i + 1) * wedgeSpacing + this.direction * i * wedgeAngleDelta
|
|
52
|
+
if (this.direction === -1) {
|
|
53
|
+
startAngle -= wedgeAngleDelta
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const wedge = drawWedge({startAngle, angleDelta: wedgeAngleDelta, innerRadius, radiusDelta: wedgeWidth, className: "wedge_node", parentNode: group})
|
|
57
|
+
|
|
58
|
+
const wedgeCenterAngle = startAngle + wedgeAngleDelta / 2
|
|
59
|
+
const label = new this.Label({wedgeCenterAngle, wedgeCenterRadius: this.outerRingRadius, parentNode: group})
|
|
60
|
+
|
|
61
|
+
this.wedgeNodes.push({group, label, wedge})
|
|
62
|
+
}
|
|
63
|
+
this.drawCalled = true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
update (sessions) {
|
|
67
|
+
this.sessionsData = sessions
|
|
68
|
+
for (let i = 0; i < numWedges; i++) {
|
|
69
|
+
const {group, label} = this.wedgeNodes[i]
|
|
70
|
+
group.classList.remove('active', 'online')
|
|
71
|
+
let session = sessions[i]
|
|
72
|
+
if (!session) {
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (i === numWedges - 1 && sessions.length > numWedges) {
|
|
77
|
+
session = createSessionSummary(sessions)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (session.activeWatchCount > 0 || session.activeOpenCount > 0) {
|
|
81
|
+
group.classList.add('active')
|
|
82
|
+
} else {
|
|
83
|
+
group.classList.add('online')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
label.update(session)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
triggerActivity (sessionIds) {
|
|
91
|
+
const sessions = this.sessionsData
|
|
92
|
+
if (!sessions) { return }
|
|
93
|
+
for (let i = 0; i < numWedges; i++) {
|
|
94
|
+
const {wedge} = this.wedgeNodes[i]
|
|
95
|
+
const session = sessions[i]
|
|
96
|
+
if (!session) {
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if ((typeof sessionIds === 'number' && session.id === sessionIds) || sessionIds.has && sessionIds.has(session.id)) {
|
|
101
|
+
wedge.animate(flashAnimationKeyframes, flashAnimationProps)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
exportDeps({SessionWedges})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const { exportDeps, drawCircle, drawWedge } = window.__WTR__
|
|
2
|
+
|
|
3
|
+
class StatusRing {
|
|
4
|
+
constructor ({innerRingRadius, outerRingRadius, outerArcSize, parentNode}) {
|
|
5
|
+
this.innerRingRadius = innerRingRadius
|
|
6
|
+
this.outerRingRadius = outerRingRadius
|
|
7
|
+
this.outerArcSize = outerArcSize
|
|
8
|
+
this.parentNode = parentNode
|
|
9
|
+
this.draw()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
draw () {
|
|
13
|
+
this.parentNode.innerHTML = ""
|
|
14
|
+
this.parentNode.classList.remove(...this.parentNode.classList)
|
|
15
|
+
this.currentClassName = 'offline'
|
|
16
|
+
this.parentNode.classList.add(this.currentClassName)
|
|
17
|
+
|
|
18
|
+
drawCircle({ cx: 0, cy: 0, r: this.innerRingRadius, parentNode: this.parentNode })
|
|
19
|
+
drawWedge({startAngle: 0.25 - this.outerArcSize / 2, angleDelta: this.outerArcSize, innerRadius: this.outerRingRadius, radiusDelta: 0, parentNode: this.parentNode})
|
|
20
|
+
drawWedge({startAngle: 0.75 - this.outerArcSize / 2, angleDelta: this.outerArcSize, innerRadius: this.outerRingRadius, radiusDelta: 0, parentNode: this.parentNode})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
update (data) {
|
|
24
|
+
const hasActiveClient = data.clients.some(client => client.activeWatchCount > 0)
|
|
25
|
+
let newClassName
|
|
26
|
+
if (hasActiveClient) {
|
|
27
|
+
newClassName = "active"
|
|
28
|
+
} else if (data.clients.length > 0) {
|
|
29
|
+
newClassName = "online"
|
|
30
|
+
} else {
|
|
31
|
+
newClassName = "offline"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (newClassName != this.currentClassName) {
|
|
35
|
+
this.parentNode.classList.remove(this.currentClassName)
|
|
36
|
+
this.parentNode.classList.add(newClassName)
|
|
37
|
+
this.currentClassName = newClassName
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
exportDeps({StatusRing})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { exportDeps, drawLine, drawCircle, drawPolarLine } = window.__WTR__
|
|
2
|
+
|
|
3
|
+
const gridCellWidth = 0.08
|
|
4
|
+
const maxGrid = 1
|
|
5
|
+
|
|
6
|
+
const drawGrid = (parentNode) => {
|
|
7
|
+
for (let i = 0; i < maxGrid; i += gridCellWidth) {
|
|
8
|
+
const className = i === 0 ? "grid_axis" : "grid_line"
|
|
9
|
+
drawLine({x1: -1, y1: i, x2: 1, y2: i, className, parentNode})
|
|
10
|
+
drawLine({x1: -1, y1: -i, x2: 1, y2: -i, className, parentNode})
|
|
11
|
+
drawLine({x1: i, y1: -1, x2: i, y2: 1, className, parentNode})
|
|
12
|
+
drawLine({x1: -i, y1: -1, x2: -i, y2: 1, className, parentNode})
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const radiusDiff = 0.04
|
|
17
|
+
const angleDiff = 0.025
|
|
18
|
+
|
|
19
|
+
const drawPolarGrid = (parentNode) => {
|
|
20
|
+
for (let r = 0; r <= 1.0001; r += radiusDiff) {
|
|
21
|
+
drawCircle({cx: 0, cy: 0, r, className: "grid_line", parentNode})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (let angle = 0; angle < 1; angle += angleDiff) {
|
|
25
|
+
drawPolarLine({startAngle: 0, startRadius: 0, endAngle: angle, endRadius: 1, className: "grid_line", parentNode})
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
exportDeps({drawGrid, drawPolarGrid})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { WebsocketClient } from "./util/WebsocketClient.js"
|
|
2
|
+
import "./util/DependencyManager.js"
|
|
3
|
+
|
|
4
|
+
const { cleanupEventHandlers, statusDataEmitter } = window.__WTR__
|
|
5
|
+
|
|
6
|
+
const PORT = 38378
|
|
7
|
+
|
|
8
|
+
const FILE_PREFIX = "websocket-text-relay/src/ui/"
|
|
9
|
+
const CSS_FILE = "css/main.css"
|
|
10
|
+
const cssEndsWith = FILE_PREFIX + CSS_FILE
|
|
11
|
+
|
|
12
|
+
const MAIN_JS_FILE = "js/main.js"
|
|
13
|
+
const mainJsEndsWith = FILE_PREFIX + MAIN_JS_FILE
|
|
14
|
+
|
|
15
|
+
const jsFiles = [
|
|
16
|
+
"js/util/constants.js",
|
|
17
|
+
"js/util/drawing.js",
|
|
18
|
+
"js/components/grids.js",
|
|
19
|
+
"js/components/HeaderSummary.js",
|
|
20
|
+
"js/components/StatusRing.js",
|
|
21
|
+
"js/components/SessionWedges.js",
|
|
22
|
+
"js/components/SessionLabels.js",
|
|
23
|
+
"js/components/ActivityTimeseriesGraph.js",
|
|
24
|
+
"js/components/ServerStatus.js",
|
|
25
|
+
MAIN_JS_FILE
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const svgRoot = document.getElementById('svg_root')
|
|
29
|
+
const cssElement = document.getElementById('main_style')
|
|
30
|
+
|
|
31
|
+
const ws = new WebsocketClient({port: PORT})
|
|
32
|
+
|
|
33
|
+
const handleCss = (contents) => {
|
|
34
|
+
cssElement.innerText = contents
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let lastMainContents = null
|
|
38
|
+
let initFinished = false
|
|
39
|
+
const handleJs = (contents) => {
|
|
40
|
+
try {
|
|
41
|
+
eval(contents)
|
|
42
|
+
} catch (e) {
|
|
43
|
+
window._lastEvalError = e
|
|
44
|
+
console.log(e)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ws.emitter.on('message', (message) => {
|
|
49
|
+
console.log("got emitter message", message)
|
|
50
|
+
if (message.endsWith === cssEndsWith) {
|
|
51
|
+
handleCss(message.contents)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
if (message.method === "watch-file" && message.endsWith.endsWith('.js')) {
|
|
55
|
+
if (!initFinished) { return }
|
|
56
|
+
cleanupEventHandlers()
|
|
57
|
+
handleJs(message.contents)
|
|
58
|
+
if (message.endsWith === mainJsEndsWith) {
|
|
59
|
+
lastMainContents = message.contents
|
|
60
|
+
} else if (lastMainContents != null) {
|
|
61
|
+
handleJs(lastMainContents)
|
|
62
|
+
}
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
if (message.method === "watch-wtr-status") {
|
|
66
|
+
statusDataEmitter.emit('data', message.data)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
if (message.method === "watch-wtr-activity") {
|
|
70
|
+
statusDataEmitter.emit('activity', message.data)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
ws.emitter.on('socket-open', () => statusDataEmitter.emit('socket-open'))
|
|
76
|
+
ws.emitter.on('socket-close', () => statusDataEmitter.emit('socket-close'))
|
|
77
|
+
|
|
78
|
+
const initFiles = async () => {
|
|
79
|
+
await fetch(CSS_FILE).then(r => r.text()).then(handleCss)
|
|
80
|
+
const jsFetches = jsFiles.map(async (fileName) => {
|
|
81
|
+
return fetch(fileName).then(r => r.text())
|
|
82
|
+
})
|
|
83
|
+
const jsResults = await Promise.all(jsFetches)
|
|
84
|
+
lastMainContents = jsResults.at(-1)
|
|
85
|
+
requestAnimationFrame(() => { // make sure css is applied first
|
|
86
|
+
jsResults.forEach((contents) => {
|
|
87
|
+
handleJs(contents)
|
|
88
|
+
})
|
|
89
|
+
initFinished = true
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const subscribeWatchers = () => {
|
|
94
|
+
ws.sendMessage({method: "watch-log-messages"})
|
|
95
|
+
ws.sendMessage({method: "watch-wtr-status"})
|
|
96
|
+
ws.sendMessage({method: "watch-wtr-activity"})
|
|
97
|
+
ws.sendMessage({ method: "init", name: "beta-status-ui" })
|
|
98
|
+
ws.sendMessage({ method: "watch-file", endsWith: cssEndsWith })
|
|
99
|
+
jsFiles.forEach((jsFile) => {
|
|
100
|
+
const jsEndsWith = FILE_PREFIX + jsFile
|
|
101
|
+
ws.sendMessage({ method: "watch-file", endsWith: jsEndsWith })
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await initFiles()
|
|
107
|
+
subscribeWatchers()
|
|
108
|
+
|
|
109
|
+
const updateSvgDimensions = () => {
|
|
110
|
+
svgRoot.setAttribute("height", window.innerHeight - 4)
|
|
111
|
+
svgRoot.setAttribute("width", window.innerWidth)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
updateSvgDimensions()
|
|
115
|
+
window.addEventListener("resize", () => {
|
|
116
|
+
updateSvgDimensions()
|
|
117
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* eslint no-unused-vars: 0 */
|
|
2
|
+
const { drawGrid, drawPolarGrid, HeaderSummary, StatusRing, onEvent, statusDataEmitter, SessionWedges, ClientLabel,
|
|
3
|
+
ActivityTimeseriesGraph, EditorLabel, ServerStatus, constants } = window.__WTR__
|
|
4
|
+
const { outerRingRadius, innerRingRadius, outerArcSize } = constants
|
|
5
|
+
|
|
6
|
+
const gridGroup = document.getElementById("grid_group")
|
|
7
|
+
gridGroup.innerHTML = ""
|
|
8
|
+
// drawPolarGrid(gridGroup)
|
|
9
|
+
// drawGrid(gridGroup)
|
|
10
|
+
|
|
11
|
+
const headerSummaryNode = document.getElementById('header_summary_group')
|
|
12
|
+
const headerSummary = new HeaderSummary({parentNode: headerSummaryNode})
|
|
13
|
+
|
|
14
|
+
const statusRingNode = document.getElementById('status_ring_group')
|
|
15
|
+
const statusRing = new StatusRing({innerRingRadius, outerRingRadius, outerArcSize, parentNode: statusRingNode})
|
|
16
|
+
|
|
17
|
+
const clientWedgesNode = document.getElementById('client_wedges_group')
|
|
18
|
+
const clientWedges = new SessionWedges({outerRingRadius, outerArcSize, direction: -1, Label: ClientLabel, parentNode: clientWedgesNode})
|
|
19
|
+
|
|
20
|
+
const editorWedgesNode = document.getElementById('editor_wedges_group')
|
|
21
|
+
const editorWedges = new SessionWedges({outerRingRadius, outerArcSize, Label: EditorLabel, parentNode: editorWedgesNode})
|
|
22
|
+
|
|
23
|
+
const activityGraphNode = document.getElementById('activity_timeseries_graph')
|
|
24
|
+
const activityGraph = new ActivityTimeseriesGraph({innerRingRadius, parentNode: activityGraphNode})
|
|
25
|
+
|
|
26
|
+
const serverStatusNode = document.getElementById('server_status_group')
|
|
27
|
+
const serverStatus = new ServerStatus({parentNode: serverStatusNode})
|
|
28
|
+
|
|
29
|
+
const statusDataTranform = (rawData) => {
|
|
30
|
+
|
|
31
|
+
const editors = []
|
|
32
|
+
const clients = []
|
|
33
|
+
const allSessions = new Map()
|
|
34
|
+
|
|
35
|
+
rawData.sessions.forEach((session) => {
|
|
36
|
+
allSessions.set(session.id, session)
|
|
37
|
+
session.activeWatchCount = 0
|
|
38
|
+
session.activeOpenCount = 0
|
|
39
|
+
const isEditor = session.editorPid != null || session.lsPid != null
|
|
40
|
+
if (isEditor) {
|
|
41
|
+
editors.push(session)
|
|
42
|
+
} else {
|
|
43
|
+
clients.push(session)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
for (const session of allSessions.values()) {
|
|
48
|
+
const openFileLinks = Object.values(session.openFileLinks)
|
|
49
|
+
session.activeOpenCount = openFileLinks.length
|
|
50
|
+
openFileLinks.forEach((links) => {
|
|
51
|
+
links.forEach(({clientId}) => {
|
|
52
|
+
const clientSession = allSessions.get(clientId)
|
|
53
|
+
if (!clientSession) { return }
|
|
54
|
+
clientSession.activeWatchCount++
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { editors, clients }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const getServerPid = (editors) => {
|
|
63
|
+
for (const editor of editors) {
|
|
64
|
+
if (editor.isServer) { return editor.lsPid }
|
|
65
|
+
}
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const handleStatusData = (rawData) => {
|
|
70
|
+
window.__WTR__.lastStatusData = rawData
|
|
71
|
+
const data = statusDataTranform(rawData)
|
|
72
|
+
console.log("status data updated", data)
|
|
73
|
+
headerSummary.update(data)
|
|
74
|
+
statusRing.update(data)
|
|
75
|
+
clientWedges.update(data.clients)
|
|
76
|
+
editorWedges.update(data.editors)
|
|
77
|
+
serverStatus.update(getServerPid(data.editors))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
onEvent(statusDataEmitter, 'data', handleStatusData)
|
|
81
|
+
|
|
82
|
+
if (window.__WTR__.lastStatusData) {
|
|
83
|
+
handleStatusData(window.__WTR__.lastStatusData)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const handleActivity = (data) => {
|
|
87
|
+
editorWedges.triggerActivity(data.relayer)
|
|
88
|
+
clientWedges.triggerActivity(new Set(data.watchers))
|
|
89
|
+
activityGraph.triggerActivity()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
onEvent(statusDataEmitter, 'activity', handleActivity)
|
|
93
|
+
|
|
94
|
+
onEvent(statusDataEmitter, 'socket-close', () => {
|
|
95
|
+
console.log("socket closed!")
|
|
96
|
+
serverStatus.update(null)
|
|
97
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { EventEmitter } from './EventEmitter.js'
|
|
2
|
+
|
|
3
|
+
const dependencies = {}
|
|
4
|
+
window.__WTR__ = dependencies
|
|
5
|
+
|
|
6
|
+
const exportDeps = (exportObj) => Object.assign(dependencies, exportObj)
|
|
7
|
+
|
|
8
|
+
const statusDataEmitter = new EventEmitter()
|
|
9
|
+
|
|
10
|
+
const cleanupFuncs = []
|
|
11
|
+
|
|
12
|
+
const onEvent = (emitter, event, func) => {
|
|
13
|
+
if (typeof emitter.on === 'function') {
|
|
14
|
+
emitter.on(event, func)
|
|
15
|
+
cleanupFuncs.push(() => emitter.removeListener(event, func))
|
|
16
|
+
} else if (typeof emitter.addEventListener === 'function') {
|
|
17
|
+
emitter.addEventListener(event, func)
|
|
18
|
+
cleanupFuncs.push(() => emitter.removeEventListener(event, func))
|
|
19
|
+
} else {
|
|
20
|
+
throw new Error("target is not an event emitter")
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const cleanupEventHandlers = () => {
|
|
25
|
+
cleanupFuncs.forEach(f => f())
|
|
26
|
+
cleanupFuncs.length = 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
exportDeps({exportDeps, statusDataEmitter, onEvent, cleanupEventHandlers})
|
|
30
|
+
|
|
31
|
+
export default dependencies
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class EventEmitter {
|
|
2
|
+
constructor () {
|
|
3
|
+
this.events = new Map()
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
on (event, listener) {
|
|
7
|
+
let eventSubscribers = this.events.get(event)
|
|
8
|
+
if (!eventSubscribers) {
|
|
9
|
+
eventSubscribers = new Set()
|
|
10
|
+
this.events.set(event, eventSubscribers)
|
|
11
|
+
}
|
|
12
|
+
eventSubscribers.add(listener)
|
|
13
|
+
return () => this.removeListener(event, listener)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
removeListener (event, listener) {
|
|
17
|
+
const eventSubscribers = this.events.get(event)
|
|
18
|
+
if (!eventSubscribers) { return }
|
|
19
|
+
if (!listener) {
|
|
20
|
+
this.events.delete(event)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
eventSubscribers.delete(listener)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
emit (event, ...args) {
|
|
27
|
+
const eventSubscribers = this.events.get(event)
|
|
28
|
+
if (!eventSubscribers) { return }
|
|
29
|
+
for (const listener of eventSubscribers) {
|
|
30
|
+
listener.apply(this, args)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
once (event, listener) {
|
|
35
|
+
const remove = this.on(event, (...args) => {
|
|
36
|
+
remove()
|
|
37
|
+
listener.apply(this, args)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
}
|