websocket-text-relay 1.0.0 → 1.1.1
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/README.md +10 -13
- package/changelog.md +25 -0
- package/docs/code-structure.md +47 -0
- package/docs/dev-getting-started.md +62 -6
- package/index.js +5 -0
- package/package.json +1 -1
- package/src/ui/js/components/ActivityTimeseriesGraph.js +5 -11
- package/src/ui/js/components/HeaderSummary.js +0 -7
- package/src/ui/js/components/ServerStatus.js +1 -1
- package/src/ui/js/components/SessionLabels.js +13 -13
- package/src/ui/js/components/SessionWedges.js +1 -1
- package/src/ui/js/index.js +4 -4
- package/src/ui/js/main.js +1 -2
- package/src/ui/js/util/drawing.js +36 -49
- package/src/websocket-interface/WebsocketInterface.js +14 -5
- package/src/websocket-interface/httpServer.js +12 -4
- package/src/websocket-interface/util.js +16 -0
- package/src/websocket-interface/websocketServer.js +8 -3
package/README.md
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# websocket-text-relay (WTR)
|
|
2
2
|
|
|
3
|
-
## Alpha
|
|
4
|
-
|
|
5
|
-
Note: This tool is still in alpha state. Some features like the VsCode extension and vite plugin have not been published yet. Documentation is still a work in progress
|
|
6
|
-
|
|
7
3
|
This application connects to your text editor using the Language Server Protocol (LSP) and then starts a websocket
|
|
8
4
|
server that front end clients can connect to and subscribe to file change events. This allows users to see their
|
|
9
5
|
front end changes evaluated immediately as they type, without having to first save the file to disk or reload the browser.
|
|
@@ -15,26 +11,27 @@ of your personalized plugins instead of having to use an in browser code editor.
|
|
|
15
11
|
### 1. Install the extension for your text editor
|
|
16
12
|
|
|
17
13
|
WTR currently has plugins for Neovim and VSCode.
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
- [websocket-text-relay.nvim](https://github.com/niels4/websocket-text-relay.nvim)
|
|
15
|
+
- [websocket-text-relay-vscode](https://github.com/niels4/websocket-text-relay-vscode)
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
LSP support. See the [developer guide to creating a WTR text editor plugin](./docs/creating-text-editor-plugin.md)
|
|
17
|
+
### 2. Verify the webserver is running with the status UI
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
The websocket server hosts its own status UI on the same port as the websocket server. You can view
|
|
20
|
+
the status UI and verify everything is running by first starting up your text editor and then opening your browser to [http://localhost:38378](http://localhost:38378)
|
|
21
|
+
|
|
22
|
+
### 3. Connect to the websocket server from the front end application
|
|
25
23
|
|
|
26
24
|
To use this on a professional level project that gives you the option to use modules, typescript, and react, I recommend using vite along with
|
|
27
|
-
the plugin
|
|
25
|
+
the plugin [vite-plugin-websocket-text-relay](https://github.com/niels4/vite-plugin-websocket-text-relay). This plugin gives you all the power of vite when developing while also hooking
|
|
28
26
|
the live text updates into Vite's hot module reload system.
|
|
29
27
|
|
|
30
28
|
If you want to use this as a learning tool to play around with UI concepts using simple projects involving 1 html, css, and javascript file,
|
|
31
|
-
then check out this
|
|
29
|
+
then check out this [simple reference project](https://github.com/niels4/wtr-simple-example). This is a great setup for following along with any short and focused web development tutorials.
|
|
32
30
|
|
|
33
|
-
And finally, the
|
|
31
|
+
And finally, the status UI for this project was also created using live updates from websocket-text-relay.
|
|
34
32
|
In addition to giving you live feedback on the status and activity of the application, it is also meant to serve as a
|
|
35
33
|
reference UI project that is more complicated than a single javascript file, but still doesn't use any external dependencies.
|
|
36
34
|
|
|
37
|
-
|
|
38
35
|
## Developing
|
|
39
36
|
|
|
40
37
|
If you are a developer looking to run websocket-text-relay from source and make modifications, follow the [Developer Getting Started Guide](./docs/dev-getting-started.md)
|
package/changelog.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## 1.1.0 - 2024/04/07
|
|
2
|
+
|
|
3
|
+
Increased default security.
|
|
4
|
+
|
|
5
|
+
By default, the http and websocket server will only accept incoming connections from your local machine. If you
|
|
6
|
+
wish to allow network access you must set the `allow_network_access` option to true when configuring your editor plugin.
|
|
7
|
+
|
|
8
|
+
### For Neovim clients
|
|
9
|
+
|
|
10
|
+
```lua
|
|
11
|
+
{ 'niels4/websocket-text-relay.nvim', opts = {
|
|
12
|
+
allow_network_access = true
|
|
13
|
+
}},
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
By default, the http and websocket server will only accept connections where the hostname is `localhost`. If you wish
|
|
17
|
+
to allow other hosts to connect to the websocket server, you must explicitly allow them using the `allowed_hosts` option when configuring your editor plugin.
|
|
18
|
+
|
|
19
|
+
### For Neovim clients
|
|
20
|
+
|
|
21
|
+
```lua
|
|
22
|
+
{ 'niels4/websocket-text-relay.nvim', opts = {
|
|
23
|
+
allowed_hosts = { "some-allowed-host.test", "some-other-host.test" },
|
|
24
|
+
}},
|
|
25
|
+
```
|
package/docs/code-structure.md
CHANGED
|
@@ -75,3 +75,50 @@ to start the http server will be the new main server, the rest of the instances
|
|
|
75
75
|
proceed to connect to the new server in client mode again.
|
|
76
76
|
|
|
77
77
|
All init messages are resent when a websocket client reconnects.
|
|
78
|
+
|
|
79
|
+
## Status UI
|
|
80
|
+
|
|
81
|
+
The HTTP server created in `src/websocket-interface/httpServer.js` is also used to host the static files that make up the status UI.
|
|
82
|
+
|
|
83
|
+
The root of the static site is the `src/ui` directory.
|
|
84
|
+
|
|
85
|
+
### index.html
|
|
86
|
+
|
|
87
|
+
The UI is entirely made up of SVG elements, so the only thing the index.html file has to do is set up the root
|
|
88
|
+
SVG element with some groups to act as containers for the different components to use.
|
|
89
|
+
|
|
90
|
+
### js/index.js
|
|
91
|
+
|
|
92
|
+
- Sets up the websocket-text-relay client with handlers for css and javascript files.
|
|
93
|
+
- initializes the simple dependency management system that allows the UI to be split into several javascript files.
|
|
94
|
+
- Hooks up events to handle resizing the SVG element on window resize.
|
|
95
|
+
- emit data and activity events that the UI components can hook into
|
|
96
|
+
|
|
97
|
+
Whenever a javascript file is edited, its exports are updated and the main.js file is rerun.
|
|
98
|
+
|
|
99
|
+
### js/util/DependencyManager.js
|
|
100
|
+
|
|
101
|
+
This is a very quick and simple dependency management system. The dependency container is just an object in the global scope.
|
|
102
|
+
An exportDeps function is created to make it easy to specify which objects in scope are to be exported.
|
|
103
|
+
|
|
104
|
+
An onEvent function is also created and exported here, it prevents event leaks by cleaning up any registered event handlers whenever the javascript is reevaluated.
|
|
105
|
+
|
|
106
|
+
### js/util/drawing.js
|
|
107
|
+
|
|
108
|
+
This is the base utility class for drawing different shapes in SVG. It contains functions to help with drawing
|
|
109
|
+
wedges and other shapes using polar coordinates.
|
|
110
|
+
|
|
111
|
+
When dealing with polar coordinates, instead of angles being from 0 to 2 * PI,
|
|
112
|
+
the angle is scaled from 0 to 1, angles 0 and 1 pointing down the positive direciton of the x axis. 0.25 points straight up, 0.5 straight left and 0.75 straight down.
|
|
113
|
+
|
|
114
|
+
The center of the UI is at (0, 0) with a minimum height and width of 2. Having a good understanding of the unit circle makes dealing with polar coordinates fairly simple.
|
|
115
|
+
|
|
116
|
+
### js/components/
|
|
117
|
+
|
|
118
|
+
The components directory contains the javascript classes that render the different elements on the screen.
|
|
119
|
+
|
|
120
|
+
Each class handles the state for its component and has a draw function that renders it to the screen.
|
|
121
|
+
|
|
122
|
+
Any components affected by data updates will also have an update function. When called the component will update the elements it manages with the new data.
|
|
123
|
+
|
|
124
|
+
Some components also respond to text update activity, these components will have a triggerActivity function as well.
|
|
@@ -1,8 +1,64 @@
|
|
|
1
1
|
# Developers Guide: Getting Started
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
In this tutorial you will learn how to:
|
|
4
|
+
- Check out the repo and run unit tests
|
|
5
|
+
- Configure your editor to run the language server from local source
|
|
6
|
+
- Start in debugging mode and use the chrome inspector to set breakpoints and step through code
|
|
7
|
+
- Live edit the status UI
|
|
8
|
+
|
|
9
|
+
## Check out the repo
|
|
10
|
+
|
|
11
|
+
In this example we will be checking out the repo into the `~/dev/src` directory.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
mkdir -p ~/dev/src
|
|
15
|
+
cd ~/dev/src
|
|
16
|
+
git clone https://github.com/niels4/websocket-text-relay.git
|
|
17
|
+
cd websocket-text-relay
|
|
18
|
+
npm install
|
|
19
|
+
npm test
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Configure your editor
|
|
23
|
+
|
|
24
|
+
### Neovim
|
|
25
|
+
|
|
26
|
+
You can override the command to start up the language server using the setup options.
|
|
27
|
+
|
|
28
|
+
```lua
|
|
29
|
+
|
|
30
|
+
local home_dir = vim.fn.resolve(os.getenv("HOME"))
|
|
31
|
+
|
|
32
|
+
require('lazy').setup {
|
|
33
|
+
|
|
34
|
+
{ 'niels4/websocket-text-relay.nvim', opts = {
|
|
35
|
+
cmd = { "node", "--inspect", home_dir .. "/dev/src/websocket-text-relay/start.js" }
|
|
36
|
+
}}
|
|
37
|
+
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If you wish to pause the language server on startup so you can set break points on the init functions,
|
|
43
|
+
you should use the `--inspect-brk` option instead. You will need to open the chrome debugger and choose
|
|
44
|
+
to continue the execution before the language server will start up if you choose this option.
|
|
45
|
+
|
|
46
|
+
```lua
|
|
47
|
+
cmd = { "node", "--inspect-brk", home_dir .. "/dev/src/websocket-text-relay/start.js" }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Edit Status UI
|
|
51
|
+
|
|
52
|
+
With the language server running, you can connect to the status UI by opening your browser to [http://localhost:38378](http://localhost:38378)
|
|
53
|
+
|
|
54
|
+
You should be able to see at least 1 editor and 1 client (the status UI itself)
|
|
55
|
+
|
|
56
|
+
The UI was built using websocket-text-relay, so we can live edit the UI as its running.
|
|
57
|
+
|
|
58
|
+
Open up the file `src/ui/js/util/constants.js`. Make changes to the outer and inner ring radius variables. You should see the UI update as you make changes.
|
|
59
|
+
|
|
60
|
+
You can do the same with the main.css file. Try changing around some of the colors.
|
|
61
|
+
|
|
62
|
+
## Understanding the code structure
|
|
63
|
+
|
|
64
|
+
To effectively make any changes to the application, you will need to understand the [overall code structure](./code-structure.md)
|
package/index.js
CHANGED
|
@@ -34,6 +34,7 @@ const lspInitializeResponse = {
|
|
|
34
34
|
|
|
35
35
|
export const startLanguageServer = (options = {}) => {
|
|
36
36
|
const {inputStream: inputStreamParam, outputStream: outputStreamParam, port: portParam} = options
|
|
37
|
+
|
|
37
38
|
const jsonRpc = new JsonRpcInterface(resolveIoStreams({inputStreamParam, outputStreamParam}))
|
|
38
39
|
|
|
39
40
|
const wsInterface = new WebsocketInterface({port: resolvePort(portParam)})
|
|
@@ -52,6 +53,10 @@ export const startLanguageServer = (options = {}) => {
|
|
|
52
53
|
lsPid: process.pid
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
const initOptions = params.initializationOptions || {}
|
|
57
|
+
wsInterface.setAllowedHosts(initOptions.allowedHosts)
|
|
58
|
+
wsInterface.setAllowNetworkAccess(initOptions.allowNetworkAccess)
|
|
59
|
+
wsInterface.startInterface()
|
|
55
60
|
wsInterface.sendInitMessage(wsInitMessage)
|
|
56
61
|
|
|
57
62
|
return lspInitializeResponse
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { exportDeps, drawSvgElement, drawCircle, drawLinearPath, coordsToPathData, drawText, drawToolTip } = window.__WTR__
|
|
2
2
|
|
|
3
3
|
const drawValueWithTooltip = ({x, y, label, direction, parentNode}) => {
|
|
4
|
-
const tooltipWrapperGroup = drawSvgElement("g",
|
|
4
|
+
const tooltipWrapperGroup = drawSvgElement({tag: "g", className: "tooltip_wrapper_group", parent: parentNode})
|
|
5
5
|
drawToolTip({x: x, y: y - 0.0032, text: label, direction, parentNode: tooltipWrapperGroup})
|
|
6
6
|
return drawText({x: x, y: y, dominantBaseline: "middle", text: "0", className: "timeseries_value", parentNode: tooltipWrapperGroup})
|
|
7
7
|
}
|
|
@@ -67,9 +67,9 @@ class ActivityTimeseriesGraph {
|
|
|
67
67
|
const minY = -height / 2
|
|
68
68
|
const maxY = minY + height
|
|
69
69
|
|
|
70
|
-
const clipPath = drawSvgElement("clipPath", {id: innerCircleClipPathId},
|
|
70
|
+
const clipPath = drawSvgElement({tag: "clipPath", attributes: {id: innerCircleClipPathId}, parent: this.parentNode})
|
|
71
71
|
drawCircle({cx: 0, cy: 0, r: this.innerRingRadius - 0.005, parentNode: clipPath})
|
|
72
|
-
drawSvgElement("rect", {"clip-path": `url(#${innerCircleClipPathId})`, x: minX, y: minY, height, width}, "timeseries_bg", this.parentNode)
|
|
72
|
+
drawSvgElement({tag: "rect", attributes: {"clip-path": `url(#${innerCircleClipPathId})`, x: minX, y: minY, height, width}, className: "timeseries_bg", parent: this.parentNode})
|
|
73
73
|
|
|
74
74
|
const series = this.dataWindow
|
|
75
75
|
const {startTime, endTime, maxValue} = getSeriesWindowInfo(series)
|
|
@@ -90,13 +90,12 @@ class ActivityTimeseriesGraph {
|
|
|
90
90
|
const onTickUpdate = () => {
|
|
91
91
|
scheduleNextTick()
|
|
92
92
|
|
|
93
|
-
console.log("data tick", Date.now())
|
|
94
93
|
const series = this.dataWindow
|
|
95
94
|
const prevEndTime = series.at(-1).time
|
|
96
95
|
const newTime = prevEndTime + dataWindowInterval
|
|
97
96
|
series.shift()
|
|
98
97
|
series.push({time: newTime, value: window.currentActivityCount})
|
|
99
|
-
currentValueElement.
|
|
98
|
+
currentValueElement.textContent = window.currentActivityCount
|
|
100
99
|
window.currentActivityCount = 0
|
|
101
100
|
|
|
102
101
|
const pathCoords = series.map(({time, value}) => {
|
|
@@ -110,7 +109,7 @@ class ActivityTimeseriesGraph {
|
|
|
110
109
|
requestAnimationFrame(() => {
|
|
111
110
|
requestAnimationFrame(() => {
|
|
112
111
|
const {startTime, endTime, maxValue} = getSeriesWindowInfo(series)
|
|
113
|
-
maxValueElement.
|
|
112
|
+
maxValueElement.textContent = maxValue
|
|
114
113
|
|
|
115
114
|
valueScale = createLinearScale(0, maxValue, maxY, minY) // in svg, y increases as it goes down, so we need to flip max and min in the range
|
|
116
115
|
timeScale = createLinearScale(startTime, endTime, minX, maxX)
|
|
@@ -138,12 +137,7 @@ class ActivityTimeseriesGraph {
|
|
|
138
137
|
|
|
139
138
|
triggerActivity () {
|
|
140
139
|
window.currentActivityCount++
|
|
141
|
-
console.log("time series graph activity")
|
|
142
140
|
}
|
|
143
141
|
}
|
|
144
142
|
|
|
145
143
|
exportDeps({ActivityTimeseriesGraph})
|
|
146
|
-
|
|
147
|
-
/*
|
|
148
|
-
*
|
|
149
|
-
*/
|
|
@@ -9,14 +9,7 @@ class HeaderSummary {
|
|
|
9
9
|
draw () {
|
|
10
10
|
this.parentNode.innerHTML = ""
|
|
11
11
|
drawText({x: -.86, y: -.73, text: "editors", parentNode: this.parentNode})
|
|
12
|
-
// this.editorCountNode = drawText({x: -.96, y: -.62, text: "0", className: "header_number", parentNode: this.parentNode})
|
|
13
12
|
drawText({x: .86, y: -.73, text: "clients", className: "right_header", parentNode: this.parentNode})
|
|
14
|
-
// this.clientCountNode = drawText({x: .96, y: -.62, text: "0", className: ["right_header", "header_number"], parentNode: this.parentNode})
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
update (/* data */) {
|
|
18
|
-
// this.editorCountNode.innerHTML = data.editors.length
|
|
19
|
-
// this.clientCountNode.innerHTML = data.clients.length
|
|
20
13
|
}
|
|
21
14
|
}
|
|
22
15
|
|
|
@@ -21,7 +21,7 @@ class ServerStatus {
|
|
|
21
21
|
this.valueElement.classList.remove(valueTextClass)
|
|
22
22
|
this.offlineElement.classList.add(offlineTextClass)
|
|
23
23
|
} else {
|
|
24
|
-
this.valueElement.
|
|
24
|
+
this.valueElement.textContent = pid
|
|
25
25
|
this.valueElement.classList.add(valueTextClass)
|
|
26
26
|
this.offlineElement.classList.remove(offlineTextClass)
|
|
27
27
|
}
|
|
@@ -2,7 +2,7 @@ const { exportDeps, polarToCartesian, coordsToPathData, drawLinearPath, drawCirc
|
|
|
2
2
|
|
|
3
3
|
// a value with a colored circle and tooltip
|
|
4
4
|
const drawSummaryValue = ({x, y, label, circleClass, parentNode}) => {
|
|
5
|
-
const tooltipWrapperGroup = drawSvgElement("g",
|
|
5
|
+
const tooltipWrapperGroup = drawSvgElement({tag: "g", className: "tooltip_wrapper_group", parent: parentNode})
|
|
6
6
|
drawCircle({cx: x, cy: y - 0.0032, r: summaryCircleRadius, className: circleClass, parentNode: tooltipWrapperGroup})
|
|
7
7
|
drawToolTip({x: x, y: y - 0.0032, text: label, parentNode: tooltipWrapperGroup})
|
|
8
8
|
return drawText({x: x + summaryCircleRadius * 2, y: y, dominantBaseline: "middle", text: "0", className: "summary_text_value", parentNode: tooltipWrapperGroup})
|
|
@@ -10,13 +10,13 @@ const drawSummaryValue = ({x, y, label, circleClass, parentNode}) => {
|
|
|
10
10
|
|
|
11
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
12
|
const drawRightAlignedSummaryValue = ({x, y, label, circleClass, parentNode}) => {
|
|
13
|
-
const tooltipWrapperGroup = drawSvgElement("g",
|
|
13
|
+
const tooltipWrapperGroup = drawSvgElement({tag: "g", className: "tooltip_wrapper_group", parent: parentNode})
|
|
14
14
|
drawToolTip({x: x, y: y - 0.0032, text: label, parentNode: tooltipWrapperGroup})
|
|
15
15
|
const textElement = drawText({x: x + summaryCircleRadius * 2, y: y, dominantBaseline: "middle", text: "0", textAnchor: "end", className: "summary_text_value", parentNode: tooltipWrapperGroup})
|
|
16
16
|
const xDiff = textElement.getBBox().width
|
|
17
17
|
const labelCircle = drawCircle({cx: x - xDiff, cy: y - 0.0032, r: summaryCircleRadius, className: circleClass, parentNode: tooltipWrapperGroup})
|
|
18
18
|
const update = (value) => {
|
|
19
|
-
textElement.
|
|
19
|
+
textElement.textContent = value
|
|
20
20
|
const xDiff = textElement.getBBox().width
|
|
21
21
|
labelCircle.setAttribute("cx", x - xDiff)
|
|
22
22
|
}
|
|
@@ -67,7 +67,7 @@ class ClientLabel {
|
|
|
67
67
|
const watchedCountBbox = this.watchedCountElement.getBBox()
|
|
68
68
|
|
|
69
69
|
const xDiff = summaryValueSpacing + watchedCountBbox.width
|
|
70
|
-
this.activeCountTranslateWrapper = drawSvgElement("g", {transform: `translate(${xDiff}, 0)`},
|
|
70
|
+
this.activeCountTranslateWrapper = drawSvgElement({tag: "g", attributes: {transform: `translate(${xDiff}, 0)`}, parent: this.parentNode})
|
|
71
71
|
this.activeCountElement = drawSummaryValue({x: summaryStartX, y: summaryMidY, label: "Active Files", circleClass: "summary_active_circle", parentNode: this.activeCountTranslateWrapper})
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -81,13 +81,13 @@ class ClientLabel {
|
|
|
81
81
|
const halfIndex = Math.floor(name.length / 2)
|
|
82
82
|
const nameFirstHalf = name.substring(0, halfIndex)
|
|
83
83
|
const nameSecondHalf = name.substring(halfIndex, name.length)
|
|
84
|
-
this.topNameElement.
|
|
85
|
-
this.nameTextElement.
|
|
84
|
+
this.topNameElement.textContent = nameFirstHalf
|
|
85
|
+
this.nameTextElement.textContent = nameSecondHalf
|
|
86
86
|
this.nameTextElement.classList.add("small_text")
|
|
87
87
|
} else {
|
|
88
88
|
this.topNameElement.innerHTML = ""
|
|
89
89
|
this.nameTextElement.classList.remove("small_text")
|
|
90
|
-
this.nameTextElement.
|
|
90
|
+
this.nameTextElement.textContent = name
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
const textBbox = addPadding(this.nameTextElement.getBBox(), 0.00, underlinePadding)
|
|
@@ -95,8 +95,8 @@ class ClientLabel {
|
|
|
95
95
|
const newUnderlinePathData = coordsToPathData(underlineCoords)
|
|
96
96
|
this.underlinePath.setAttribute("d", newUnderlinePathData)
|
|
97
97
|
|
|
98
|
-
this.watchedCountElement.
|
|
99
|
-
this.activeCountElement.
|
|
98
|
+
this.watchedCountElement.textContent = client.watchCount
|
|
99
|
+
this.activeCountElement.textContent = client.activeWatchCount
|
|
100
100
|
|
|
101
101
|
const xDiff = summaryValueSpacing + this.watchedCountElement.getBBox().width
|
|
102
102
|
this.activeCountTranslateWrapper.setAttribute("transform", `translate(${xDiff}, 0)`)
|
|
@@ -129,7 +129,7 @@ class EditorLabel {
|
|
|
129
129
|
|
|
130
130
|
this.activeCountSummaryValue = drawRightAlignedSummaryValue({x: summaryStartX, y: summaryMidY, label: "Active Files", circleClass: "summary_active_circle", parentNode: this.parentNode})
|
|
131
131
|
const xDiff = this.activeCountSummaryValue.textElement.getBBox().width + summaryCircleRadius + editorSummaryPadding * 2
|
|
132
|
-
this.openFilesTransformGroup = drawSvgElement('g', {transform: `translate(${-xDiff})`},
|
|
132
|
+
this.openFilesTransformGroup = drawSvgElement({tag: 'g', attributes: {transform: `translate(${-xDiff})`}, parent: this.parentNode})
|
|
133
133
|
this.openCountSummaryValue = drawRightAlignedSummaryValue({x: summaryStartX, y: summaryMidY, label: "Open Files", circleClass: "summary_watched_circle", parentNode: this.openFilesTransformGroup})
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -147,13 +147,13 @@ class EditorLabel {
|
|
|
147
147
|
const halfIndex = Math.floor(name.length / 2)
|
|
148
148
|
const nameFirstHalf = name.substring(0, halfIndex)
|
|
149
149
|
const nameSecondHalf = name.substring(halfIndex, name.length)
|
|
150
|
-
this.topNameElement.
|
|
151
|
-
this.nameTextElement.
|
|
150
|
+
this.topNameElement.textContent = nameFirstHalf
|
|
151
|
+
this.nameTextElement.textContent = nameSecondHalf
|
|
152
152
|
this.nameTextElement.classList.add("small_text")
|
|
153
153
|
} else {
|
|
154
154
|
this.topNameElement.innerHTML = ""
|
|
155
155
|
this.nameTextElement.classList.remove("small_text")
|
|
156
|
-
this.nameTextElement.
|
|
156
|
+
this.nameTextElement.textContent = name
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
const textBbox = addPadding(this.nameTextElement.getBBox(), 0.00, underlinePadding)
|
|
@@ -46,7 +46,7 @@ class SessionWedges {
|
|
|
46
46
|
const wedgeAngleDelta = (totalAngleDelta / numWedges) - wedgeSpacing
|
|
47
47
|
const innerRadius = this.outerRingRadius - wedgeWidth / 2
|
|
48
48
|
for (let i = 0; i < numWedges; i++) {
|
|
49
|
-
const group = drawSvgElement("g",
|
|
49
|
+
const group = drawSvgElement({tag: "g", className: "single_wedge_group", parent: this.parentNode})
|
|
50
50
|
|
|
51
51
|
let startAngle = totalStartAngle + this.direction * (i + 1) * wedgeSpacing + this.direction * i * wedgeAngleDelta
|
|
52
52
|
if (this.direction === -1) {
|
package/src/ui/js/index.js
CHANGED
|
@@ -3,8 +3,6 @@ import "./util/DependencyManager.js"
|
|
|
3
3
|
|
|
4
4
|
const { cleanupEventHandlers, statusDataEmitter } = window.__WTR__
|
|
5
5
|
|
|
6
|
-
const PORT = 38378
|
|
7
|
-
|
|
8
6
|
const FILE_PREFIX = "websocket-text-relay/src/ui/"
|
|
9
7
|
const CSS_FILE = "css/main.css"
|
|
10
8
|
const cssEndsWith = FILE_PREFIX + CSS_FILE
|
|
@@ -28,7 +26,9 @@ const jsFiles = [
|
|
|
28
26
|
const svgRoot = document.getElementById('svg_root')
|
|
29
27
|
const cssElement = document.getElementById('main_style')
|
|
30
28
|
|
|
31
|
-
const
|
|
29
|
+
const {hostname, port, protocol} = window.location
|
|
30
|
+
const wsProtocol = protocol === "http:" ? "ws" : "wss"
|
|
31
|
+
const ws = new WebsocketClient({port: port, host: hostname, protocol: wsProtocol})
|
|
32
32
|
|
|
33
33
|
const handleCss = (contents) => {
|
|
34
34
|
cssElement.innerText = contents
|
|
@@ -94,7 +94,7 @@ const subscribeWatchers = () => {
|
|
|
94
94
|
ws.sendMessage({method: "watch-log-messages"})
|
|
95
95
|
ws.sendMessage({method: "watch-wtr-status"})
|
|
96
96
|
ws.sendMessage({method: "watch-wtr-activity"})
|
|
97
|
-
ws.sendMessage({ method: "init", name: "
|
|
97
|
+
ws.sendMessage({ method: "init", name: "status-ui" })
|
|
98
98
|
ws.sendMessage({ method: "watch-file", endsWith: cssEndsWith })
|
|
99
99
|
jsFiles.forEach((jsFile) => {
|
|
100
100
|
const jsEndsWith = FILE_PREFIX + jsFile
|
package/src/ui/js/main.js
CHANGED
|
@@ -9,7 +9,7 @@ gridGroup.innerHTML = ""
|
|
|
9
9
|
// drawGrid(gridGroup)
|
|
10
10
|
|
|
11
11
|
const headerSummaryNode = document.getElementById('header_summary_group')
|
|
12
|
-
|
|
12
|
+
new HeaderSummary({parentNode: headerSummaryNode})
|
|
13
13
|
|
|
14
14
|
const statusRingNode = document.getElementById('status_ring_group')
|
|
15
15
|
const statusRing = new StatusRing({innerRingRadius, outerRingRadius, outerArcSize, parentNode: statusRingNode})
|
|
@@ -70,7 +70,6 @@ const handleStatusData = (rawData) => {
|
|
|
70
70
|
window.__WTR__.lastStatusData = rawData
|
|
71
71
|
const data = statusDataTranform(rawData)
|
|
72
72
|
console.log("status data updated", data)
|
|
73
|
-
headerSummary.update(data)
|
|
74
73
|
statusRing.update(data)
|
|
75
74
|
clientWedges.update(data.clients)
|
|
76
75
|
editorWedges.update(data.editors)
|
|
@@ -3,8 +3,8 @@ const { exportDeps } = window.__WTR__
|
|
|
3
3
|
const TWO_PI = 2 * Math.PI
|
|
4
4
|
const MAX_ANGLE_DELTA = .99999
|
|
5
5
|
|
|
6
|
-
const drawSvgElement = (
|
|
7
|
-
const element = document.createElementNS("http://www.w3.org/2000/svg",
|
|
6
|
+
const drawSvgElement = ({tag, attributes = {}, className, parent}) => {
|
|
7
|
+
const element = document.createElementNS("http://www.w3.org/2000/svg", tag)
|
|
8
8
|
|
|
9
9
|
if (className && className.length > 0) {
|
|
10
10
|
if (Array.isArray(className)) {
|
|
@@ -20,51 +20,32 @@ const drawSvgElement = (tagName, attributes = {}, className, parentNode) => {
|
|
|
20
20
|
}
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
if (
|
|
24
|
-
|
|
23
|
+
if (parent) {
|
|
24
|
+
parent.append(element)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
return element
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const drawText = ({x, y, text, dominantBaseline, textAnchor, className, parentNode}) => {
|
|
31
|
-
const textElement = drawSvgElement("text", {x, y, "dominant-baseline": dominantBaseline, "text-anchor": textAnchor}, className,
|
|
32
|
-
textElement.
|
|
30
|
+
const drawText = ({x, y, text, dominantBaseline, textAnchor, className, parentNode: parent}) => {
|
|
31
|
+
const textElement = drawSvgElement({tag: "text", attributes: {x, y, "dominant-baseline": dominantBaseline, "text-anchor": textAnchor}, className, parent})
|
|
32
|
+
textElement.textContent = text
|
|
33
33
|
return textElement
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const drawLine = ({x1, y1, x2, y2, className, parentNode}) => {
|
|
37
|
-
return drawSvgElement("line", {x1, y1, x2, y2}, className,
|
|
36
|
+
const drawLine = ({x1, y1, x2, y2, className, parentNode: parent}) => {
|
|
37
|
+
return drawSvgElement({tag: "line", attributes: {x1, y1, x2, y2}, className, parent})
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const drawCircle = ({cx, cy, r, className, parentNode}) => {
|
|
41
|
-
return drawSvgElement("circle", {cx, cy, r}, className,
|
|
40
|
+
const drawCircle = ({cx, cy, r, className, parentNode: parent}) => {
|
|
41
|
+
return drawSvgElement({tag: "circle", attributes: {cx, cy, r}, className, parent})
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const coordsToPathData = (coords) => "M " + coords.map(coord => coord.join(',')).join(" L ")
|
|
45
45
|
|
|
46
|
-
const drawLinearPath = ({coords, className, parentNode}) => {
|
|
46
|
+
const drawLinearPath = ({coords, className, parentNode: parent}) => {
|
|
47
47
|
const d = coordsToPathData(coords)
|
|
48
|
-
return drawSvgElement("path", {d}, className,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const drawPolarLine = ({startAngle, startRadius, endAngle, endRadius, className, parentNode}) => {
|
|
52
|
-
const startAngleRadians = (startAngle % 1) * TWO_PI
|
|
53
|
-
const endAngleRadians = (endAngle % 1) * TWO_PI
|
|
54
|
-
const x1 = Math.cos(startAngleRadians) * startRadius
|
|
55
|
-
const y1 = -Math.sin(startAngleRadians) * startRadius
|
|
56
|
-
const x2 = Math.cos(endAngleRadians) * endRadius
|
|
57
|
-
const y2 = -Math.sin(endAngleRadians) * endRadius
|
|
58
|
-
|
|
59
|
-
return drawSvgElement("line", {x1, y1, x2, y2}, className, parentNode)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const drawPolarCircle = ({angle, radius, r, className, parentNode}) => {
|
|
63
|
-
const angleRadians = (angle % 1) * TWO_PI
|
|
64
|
-
const cx = Math.cos(angleRadians) * radius
|
|
65
|
-
const cy = -Math.sin(angleRadians) * radius
|
|
66
|
-
|
|
67
|
-
return drawSvgElement("circle", {cx, cy, r}, className, parentNode)
|
|
48
|
+
return drawSvgElement({tag: "path", attributes: {d}, className, parent})
|
|
68
49
|
}
|
|
69
50
|
|
|
70
51
|
const polarToCartesian = (angle, radius) => {
|
|
@@ -74,23 +55,29 @@ const polarToCartesian = (angle, radius) => {
|
|
|
74
55
|
return [x, y]
|
|
75
56
|
}
|
|
76
57
|
|
|
77
|
-
const
|
|
58
|
+
const drawPolarLine = ({startAngle, startRadius, endAngle, endRadius, className, parentNode: parent}) => {
|
|
59
|
+
const [x1, y1] = polarToCartesian(startAngle, startRadius)
|
|
60
|
+
const [x2, y2] = polarToCartesian(endAngle, endRadius)
|
|
61
|
+
|
|
62
|
+
return drawSvgElement({tag: "line", attributes: {x1, y1, x2, y2}, className, parent})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const drawPolarCircle = ({angle, radius, r, className, parentNode: parent}) => {
|
|
66
|
+
const [cx, cy] = polarToCartesian(angle, radius)
|
|
67
|
+
return drawSvgElement({tag: "circle", attributes: {cx, cy, r}, className, parent})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const drawWedge = ({startAngle, angleDelta, innerRadius, radiusDelta, className, parentNode: parent}) => {
|
|
78
71
|
if (angleDelta < 0) { angleDelta = 0 }
|
|
79
72
|
if (angleDelta > MAX_ANGLE_DELTA) {angleDelta = MAX_ANGLE_DELTA }
|
|
80
|
-
|
|
81
|
-
const startAngleRadians = (startAngle % 1) * TWO_PI
|
|
82
|
-
const endAngleRadians = ((startAngle + angleDelta) % 1) * TWO_PI
|
|
73
|
+
const endAngle = startAngle + angleDelta
|
|
83
74
|
const outerRadius = innerRadius + radiusDelta
|
|
84
75
|
const largeArcFlag = (angleDelta % 1) > .5 ? "1" : "0"
|
|
85
76
|
|
|
86
|
-
const startX1 =
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const endX1 = Math.cos(endAngleRadians) * innerRadius
|
|
91
|
-
const endY1 = -Math.sin(endAngleRadians) * innerRadius
|
|
92
|
-
const endX2 = Math.cos(endAngleRadians) * outerRadius
|
|
93
|
-
const endY2 = -Math.sin(endAngleRadians) * outerRadius
|
|
77
|
+
const [startX1, startY1] = polarToCartesian(startAngle, innerRadius)
|
|
78
|
+
const [startX2, startY2] = polarToCartesian(startAngle, outerRadius)
|
|
79
|
+
const [endX1, endY1] = polarToCartesian(endAngle, innerRadius)
|
|
80
|
+
const [endX2, endY2] = polarToCartesian(endAngle, outerRadius)
|
|
94
81
|
|
|
95
82
|
const d = `
|
|
96
83
|
M ${startX1} ${startY1},
|
|
@@ -100,28 +87,28 @@ A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${startX2} ${startY2},
|
|
|
100
87
|
Z
|
|
101
88
|
`
|
|
102
89
|
|
|
103
|
-
return drawSvgElement("path", {d}, className,
|
|
90
|
+
return drawSvgElement({tag: "path", attributes: {d}, className, parent})
|
|
104
91
|
}
|
|
105
92
|
|
|
106
93
|
const triangleHeight = 0.06
|
|
107
94
|
const verticalPadding = 0.01
|
|
108
95
|
const horizontalPadding = 0.04
|
|
109
96
|
|
|
110
|
-
const drawToolTip = ({x, y, text, direction = "below", parentNode}) => {
|
|
97
|
+
const drawToolTip = ({x, y, text, direction = "below", parentNode: parent}) => {
|
|
111
98
|
const directionMultiplier = direction === "above" ? -1 : 1
|
|
112
|
-
const tooltipDisplayGroup = drawSvgElement("g", undefined, "tooltip_display_group",
|
|
99
|
+
const tooltipDisplayGroup = drawSvgElement("g", undefined, "tooltip_display_group", parent)
|
|
113
100
|
const bgPlaceholder = drawSvgElement("g", undefined, undefined, tooltipDisplayGroup)
|
|
114
101
|
const textY = y + (triangleHeight + verticalPadding) * directionMultiplier
|
|
115
102
|
const textElement = drawText({x, y: textY, text, className: "tooltip_text", parentNode: tooltipDisplayGroup})
|
|
116
103
|
const textBbox = textElement.getBBox()
|
|
117
|
-
const
|
|
104
|
+
const attributes = {
|
|
118
105
|
x: textBbox.x - horizontalPadding,
|
|
119
106
|
y: textBbox.y - verticalPadding,
|
|
120
107
|
width: textBbox.width + horizontalPadding * 2,
|
|
121
108
|
height: textBbox.height + verticalPadding * 2,
|
|
122
109
|
rx: 0.015
|
|
123
110
|
}
|
|
124
|
-
drawSvgElement("rect",
|
|
111
|
+
drawSvgElement({tag: "rect", attributes, className: "tooltip_outline", parent: bgPlaceholder})
|
|
125
112
|
}
|
|
126
113
|
|
|
127
114
|
exportDeps({drawSvgElement, drawLine, drawCircle, drawLinearPath, drawPolarLine, drawPolarCircle, drawWedge,
|
|
@@ -14,11 +14,10 @@ export class WebsocketInterface {
|
|
|
14
14
|
this.openFileListMessage = null
|
|
15
15
|
this.serverSession = null
|
|
16
16
|
this.wsClient = null
|
|
17
|
+
this.allowNetworkAccess = false
|
|
17
18
|
|
|
18
19
|
this._onSocketClose = this._onSocketClose.bind(this)
|
|
19
20
|
this._sendQueuedMessages = this._sendQueuedMessages.bind(this)
|
|
20
|
-
|
|
21
|
-
this._startInterface()
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
sendInitMessage (initMessage) {
|
|
@@ -36,9 +35,19 @@ export class WebsocketInterface {
|
|
|
36
35
|
this._sendMessageToServer(sendTextMessage)
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
setAllowedHosts (allowedHostsList) {
|
|
39
|
+
this.allowedHosts = new Set(allowedHostsList)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setAllowNetworkAccess (allowNetworkAccessParam) {
|
|
43
|
+
if (allowNetworkAccessParam != null) {
|
|
44
|
+
this.allowNetworkAccess = allowNetworkAccessParam
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async startInterface () {
|
|
40
49
|
try {
|
|
41
|
-
await createWebsocketServer(this.port)
|
|
50
|
+
await createWebsocketServer({port: this.port, allowedHosts: this.allowedHosts, allowNetworkAccess: this.allowNetworkAccess })
|
|
42
51
|
this.serverSession = new WtrSession({apiMethods, wsInterfaceEmitter: this.emitter})
|
|
43
52
|
this._sendQueuedMessages()
|
|
44
53
|
} catch (e) {
|
|
@@ -52,7 +61,7 @@ export class WebsocketInterface {
|
|
|
52
61
|
this.wsClient.socket.removeEventListener("close", this._onSocketClose)
|
|
53
62
|
this.wsClient.socket.removeEventListener("open", this._sendQueuedMessages)
|
|
54
63
|
this.wsClient = null
|
|
55
|
-
this.
|
|
64
|
+
this.startInterface()
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
_sendQueuedMessages () {
|
|
@@ -2,6 +2,7 @@ import { createServer } from 'node:http'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import fs from 'node:fs'
|
|
4
4
|
import * as url from 'node:url'
|
|
5
|
+
import { isValidOrigin } from './util.js'
|
|
5
6
|
const parentDir = url.fileURLToPath(new URL('..', import.meta.url))
|
|
6
7
|
|
|
7
8
|
const uiDirName = "ui"
|
|
@@ -27,7 +28,12 @@ const getFileType = (fileUrl) => {
|
|
|
27
28
|
return fileUrl.substring(lastDotIndex + 1)
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const requestHandler = (req, res) => {
|
|
31
|
+
const requestHandler = (allowedHosts) => (req, res) => {
|
|
32
|
+
if (!isValidOrigin(allowedHosts, req)) {
|
|
33
|
+
res.writeHead(403)
|
|
34
|
+
res.end("FORBIDDEN!")
|
|
35
|
+
return
|
|
36
|
+
}
|
|
31
37
|
const filePath = getFilePath(req.url)
|
|
32
38
|
const fileType = getFileType(filePath)
|
|
33
39
|
const contentType = allowedFileTypes.get(fileType)
|
|
@@ -42,16 +48,18 @@ const requestHandler = (req, res) => {
|
|
|
42
48
|
fileStream.pipe(res)
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
export const createHttpServer = (port) => {
|
|
51
|
+
export const createHttpServer = ({port, allowedHosts, allowNetworkAccess}) => {
|
|
46
52
|
return new Promise((resolve, reject) => {
|
|
47
53
|
|
|
48
|
-
const server = createServer(requestHandler)
|
|
54
|
+
const server = createServer(requestHandler(allowedHosts))
|
|
49
55
|
|
|
50
56
|
server.on("error", (e) => {
|
|
51
57
|
reject(e)
|
|
52
58
|
})
|
|
53
59
|
|
|
54
|
-
|
|
60
|
+
const listenAddress = allowNetworkAccess ? "0.0.0.0" : "127.0.0.1"
|
|
61
|
+
|
|
62
|
+
server.listen(port, listenAddress, () => {
|
|
55
63
|
resolve(server)
|
|
56
64
|
})
|
|
57
65
|
})
|
|
@@ -10,3 +10,19 @@ export const debounce = (timeout, func) => {
|
|
|
10
10
|
// first ID returned is 0 and increases every time function is called
|
|
11
11
|
let currentId = 0
|
|
12
12
|
export const getNextId = () => currentId++
|
|
13
|
+
|
|
14
|
+
const defaultAllowedHost = "localhost"
|
|
15
|
+
|
|
16
|
+
export const isValidOrigin = (allowedHosts, req) => {
|
|
17
|
+
const {host, origin} = req.headers
|
|
18
|
+
let hostname
|
|
19
|
+
|
|
20
|
+
if (origin == null || origin.length === 0) {
|
|
21
|
+
hostname = host.split(":")[0]
|
|
22
|
+
} else {
|
|
23
|
+
const url = new URL(origin)
|
|
24
|
+
hostname = url.hostname
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return hostname === defaultAllowedHost || allowedHosts.has(hostname)
|
|
28
|
+
}
|
|
@@ -2,15 +2,20 @@ import { WebSocketServer } from 'ws'
|
|
|
2
2
|
import { createHttpServer } from "./httpServer.js"
|
|
3
3
|
import { apiMethods } from "./websocketApi.js"
|
|
4
4
|
import { WtrSession } from './WtrSession.js'
|
|
5
|
+
import { isValidOrigin } from './util.js'
|
|
5
6
|
|
|
6
|
-
export const createWebsocketServer = async (port) => {
|
|
7
|
-
const httpServer = await createHttpServer(port) // promise will reject if can't start HTTP server on specified port
|
|
7
|
+
export const createWebsocketServer = async ({port, allowedHosts, allowNetworkAccess}) => {
|
|
8
|
+
const httpServer = await createHttpServer({port, allowedHosts, allowNetworkAccess}) // promise will reject if can't start HTTP server on specified port
|
|
8
9
|
|
|
9
10
|
const websocketServer = new WebSocketServer({ server: httpServer })
|
|
10
11
|
|
|
11
12
|
websocketServer.on('error', () => { })
|
|
12
13
|
|
|
13
|
-
websocketServer.on('connection', (wsConnection) => {
|
|
14
|
+
websocketServer.on('connection', (wsConnection, request) => {
|
|
15
|
+
if (!isValidOrigin(allowedHosts, request)) {
|
|
16
|
+
wsConnection.close()
|
|
17
|
+
return
|
|
18
|
+
}
|
|
14
19
|
new WtrSession({apiMethods, wsConnection})
|
|
15
20
|
})
|
|
16
21
|
|