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,179 @@
|
|
|
1
|
+
import { Writable } from 'node:stream'
|
|
2
|
+
import { describe, it, expect, beforeAll } from "vitest"
|
|
3
|
+
import { TESTONLY_resetRequestId, createHeader, writeNotification, writeRequest, writeResponse, writeToOutput } from "./LspWriter.js"
|
|
4
|
+
|
|
5
|
+
describe("LspWriter", () => {
|
|
6
|
+
describe("createHeader", () => {
|
|
7
|
+
describe("Simple Case", () => {
|
|
8
|
+
const messageObj = {}
|
|
9
|
+
const expectedHeader = "Content-Length: 2\r\n\r\n"
|
|
10
|
+
|
|
11
|
+
it("Should create header with correct length", () => {
|
|
12
|
+
const actual = createHeader(JSON.stringify(messageObj))
|
|
13
|
+
expect(actual).toEqual(expectedHeader)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe("With some content", () => {
|
|
18
|
+
const messageObj = {"a": "1"}
|
|
19
|
+
const expectedHeader = "Content-Length: 9\r\n\r\n"
|
|
20
|
+
|
|
21
|
+
it("Should create header with correct length", () => {
|
|
22
|
+
const actual = createHeader(JSON.stringify(messageObj))
|
|
23
|
+
expect(actual).toEqual(expectedHeader)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe("With multi byte characters", () => {
|
|
28
|
+
const messageObj = {"a": "🌊"}
|
|
29
|
+
const expectedHeader = "Content-Length: 12\r\n\r\n"
|
|
30
|
+
|
|
31
|
+
it("Should create header with correct byte length, not string length", () => {
|
|
32
|
+
const actual = createHeader(JSON.stringify(messageObj))
|
|
33
|
+
expect(actual).toEqual(expectedHeader)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe("writeToOutput", () => {
|
|
39
|
+
describe("simple object", () => {
|
|
40
|
+
const messageObj = {"a": "1"}
|
|
41
|
+
const expectedOutput = `Content-Length: 9\r\n\r\n{"a":"1"}`
|
|
42
|
+
let actualOutput = ""
|
|
43
|
+
|
|
44
|
+
beforeAll(() => {
|
|
45
|
+
const outputStream = new Writable({
|
|
46
|
+
write (data, _enc, next) {
|
|
47
|
+
actualOutput += data.toString()
|
|
48
|
+
next()
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
writeToOutput(outputStream, messageObj)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("should stringify the json and add a Content-Length header, then write the head and json to the output stream", () => {
|
|
55
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe("writeResponse", () => {
|
|
61
|
+
describe("successful response", () => {
|
|
62
|
+
const result = {"a": "1"}
|
|
63
|
+
const error = null
|
|
64
|
+
const expectedOutput = `Content-Length: 45\r\n\r\n{"jsonrpc":"2.0","id":"0","result":{"a":"1"}}`
|
|
65
|
+
let actualOutput = ""
|
|
66
|
+
|
|
67
|
+
beforeAll(() => {
|
|
68
|
+
const outputStream = new Writable({
|
|
69
|
+
write (data, _enc, next) {
|
|
70
|
+
actualOutput += data.toString()
|
|
71
|
+
next()
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
writeResponse(outputStream, "0", error, result)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("should create a response object with a result property (and no error) and write it to the output stream", () => {
|
|
78
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe("error response", () => {
|
|
83
|
+
const result = null
|
|
84
|
+
const error = {code: 1, message: "test error", data: {t: 5}}
|
|
85
|
+
const expectedOutput = `Content-Length: 83\r\n\r\n{"jsonrpc":"2.0","id":"0","error":{"code":1,"message":"test error","data":{"t":5}}}`
|
|
86
|
+
let actualOutput = ""
|
|
87
|
+
|
|
88
|
+
beforeAll(() => {
|
|
89
|
+
const outputStream = new Writable({
|
|
90
|
+
write (data, _enc, next) {
|
|
91
|
+
actualOutput += data.toString()
|
|
92
|
+
next()
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
writeResponse(outputStream, "0", error, result)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("should create a response object with an error property (and no result) and write it to the output stream", () => {
|
|
99
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe("writeNotification", () => {
|
|
105
|
+
describe("simple parameters", () => {
|
|
106
|
+
const method = "test/test-method"
|
|
107
|
+
const parameters = {"a": "1"}
|
|
108
|
+
const expectedOutput = `Content-Length: 64\r\n\r\n{"jsonrpc":"2.0","method":"test/test-method","params":{"a":"1"}}`
|
|
109
|
+
let actualOutput = ""
|
|
110
|
+
|
|
111
|
+
beforeAll(() => {
|
|
112
|
+
const outputStream = new Writable({
|
|
113
|
+
write (data, _enc, next) {
|
|
114
|
+
actualOutput += data.toString()
|
|
115
|
+
next()
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
writeNotification(outputStream, method, parameters)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("should write a notification message without an ID property on the output stream", () => {
|
|
122
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe("writeReqest", () => {
|
|
128
|
+
describe("single request", () => {
|
|
129
|
+
const method = "test/test-method"
|
|
130
|
+
const parameters = {"a": "1"}
|
|
131
|
+
const expectedOutput = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"0","method":"test/test-method","params":{"a":"1"}}`
|
|
132
|
+
let actualOutput = ""
|
|
133
|
+
|
|
134
|
+
beforeAll(() => {
|
|
135
|
+
TESTONLY_resetRequestId()
|
|
136
|
+
const outputStream = new Writable({
|
|
137
|
+
write (data, _enc, next) {
|
|
138
|
+
actualOutput += data.toString()
|
|
139
|
+
next()
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
writeRequest(outputStream, method, parameters)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("should write a request message with an ID property on the output stream", () => {
|
|
146
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe("multiple requests", () => {
|
|
151
|
+
const method = "test/test-method"
|
|
152
|
+
const params1 = {"a": "1"}
|
|
153
|
+
const params2 = {"b": "2"}
|
|
154
|
+
const params3 = {"c": "3"}
|
|
155
|
+
const expectedOutput1 = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"0","method":"test/test-method","params":{"a":"1"}}`
|
|
156
|
+
const expectedOutput2 = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"1","method":"test/test-method","params":{"b":"2"}}`
|
|
157
|
+
const expectedOutput3 = `Content-Length: 73\r\n\r\n{"jsonrpc":"2.0","id":"2","method":"test/test-method","params":{"c":"3"}}`
|
|
158
|
+
const expectedOutput = expectedOutput1 + expectedOutput2 + expectedOutput3
|
|
159
|
+
let actualOutput = ""
|
|
160
|
+
|
|
161
|
+
beforeAll(() => {
|
|
162
|
+
TESTONLY_resetRequestId()
|
|
163
|
+
const outputStream = new Writable({
|
|
164
|
+
write (data, _enc, next) {
|
|
165
|
+
actualOutput += data.toString()
|
|
166
|
+
next()
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
writeRequest(outputStream, method, params1)
|
|
170
|
+
writeRequest(outputStream, method, params2)
|
|
171
|
+
writeRequest(outputStream, method, params3)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it("should write multiple request messages with increasing IDs", () => {
|
|
175
|
+
expect(actualOutput).toEqual(expectedOutput)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const headerKey = "Content-Length"
|
|
2
|
+
|
|
3
|
+
export const parseErrorCode = -32700
|
|
4
|
+
|
|
5
|
+
export const parseHeaderErrorMessage = "Could not read Content-Length from header"
|
|
6
|
+
|
|
7
|
+
export const parseJsonErrorMessage = "Could not parse JSON message"
|
|
8
|
+
|
|
9
|
+
export const methodNotFoundErrorCode = -32601
|
|
10
|
+
|
|
11
|
+
export const methodNotFoundErrorMessage = "Method not found"
|
|
12
|
+
|
|
13
|
+
export const invalidRequestErrorCode = -32600
|
|
14
|
+
|
|
15
|
+
export const invalidRequestErrorMessage = "Invalid Message. Missing id and/or method"
|
|
16
|
+
|
|
17
|
+
export const unexpectedNotificationErrorCode = -1
|
|
18
|
+
|
|
19
|
+
export const unexpectedNotificationErrorMessage = "Unexpected error in notification handler"
|
|
20
|
+
|
|
21
|
+
export const unexpectedRequestErrorCode = -2
|
|
22
|
+
|
|
23
|
+
export const unexpectedRequestErrorMessage = "Unexpected error in request handler"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
@font-face {
|
|
2
|
+
font-family: "Montserrat";
|
|
3
|
+
src: url("/css/fonts/Montserrat-Thin.ttf") format("truetype");
|
|
4
|
+
font-weight: 100;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
@font-face {
|
|
8
|
+
font-family: "Montserrat";
|
|
9
|
+
src: url("/css/fonts/Montserrat-ExtraLight.ttf") format("truetype");
|
|
10
|
+
font-weight: 200;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@font-face {
|
|
14
|
+
font-family: "Montserrat";
|
|
15
|
+
src: url("/css/fonts/Montserrat-Light.ttf") format("truetype");
|
|
16
|
+
font-weight: 300;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@font-face {
|
|
20
|
+
font-family: "Montserrat";
|
|
21
|
+
src: url("/css/fonts/Montserrat-Regular.ttf") format("truetype");
|
|
22
|
+
font-weight: 400;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@font-face {
|
|
26
|
+
font-family: "Montserrat";
|
|
27
|
+
src: url("/css/fonts/Montserrat-Medium.ttf") format("truetype");
|
|
28
|
+
font-weight: 500;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@font-face {
|
|
32
|
+
font-family: "Montserrat";
|
|
33
|
+
src: url("/css/fonts/Montserrat-SemiBold.ttf") format("truetype");
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@font-face {
|
|
38
|
+
font-family: "Montserrat";
|
|
39
|
+
src: url("/css/fonts/Montserrat-Bold.ttf") format("truetype");
|
|
40
|
+
font-weight: 700;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@font-face {
|
|
44
|
+
font-family: "Montserrat";
|
|
45
|
+
src: url("/css/fonts/Montserrat-ExtraBold.ttf") format("truetype");
|
|
46
|
+
font-weight: 800;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@font-face {
|
|
50
|
+
font-family: "Montserrat";
|
|
51
|
+
src: url("/css/fonts/Montserrat-Black.ttf") format("truetype");
|
|
52
|
+
font-weight: 900;
|
|
53
|
+
}
|
|
54
|
+
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
.base_colors {
|
|
2
|
+
--color-navy-nightfall: #0d151b;
|
|
3
|
+
--color-tangerine-sunset: #fb4b37;
|
|
4
|
+
--color-golden-sunset: #fbcc3b;
|
|
5
|
+
--color-amber-dawn: #ffd986;
|
|
6
|
+
--color-azure-afternoon: #629bc2;
|
|
7
|
+
--color-ivory-daybreak: #f0ebeb;
|
|
8
|
+
--color-light-sand-dune: #d6d2c2;
|
|
9
|
+
--color-evening-shadow: #514a45;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.color_palette {
|
|
13
|
+
--bg2: var(--color-navy-nightfall);
|
|
14
|
+
--accent-active: var(var(--color-golden-sunset))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.color_theme {
|
|
18
|
+
--main-background-color: var(--bg2);
|
|
19
|
+
--header-text-color: var(--color-ivory-daybreak);
|
|
20
|
+
--summary-value-text-color: hsl(from var(--header-text-color) h s l / 0.9);
|
|
21
|
+
--offline-color: oklch(from var(--color-golden-sunset) calc(l * .5) calc(c * .25) h);
|
|
22
|
+
--online-color: oklch(from var(--color-golden-sunset) calc(l * .80) calc(c * .25) h);
|
|
23
|
+
--active-color: var(--color-golden-sunset);
|
|
24
|
+
--wedge-online-color: oklch(from var(--color-azure-afternoon) calc(l * .5) calc(c * .5) h);
|
|
25
|
+
--wedge-active-color: var(--color-azure-afternoon);
|
|
26
|
+
--timeseries-line-color: var(--color-tangerine-sunset);
|
|
27
|
+
--timeseries-label-color: #7f8186;
|
|
28
|
+
--timeseries-value-color: oklch(from var(--timeseries-label-color) calc(l * 1.2) c h);
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
body {
|
|
33
|
+
background: var(--main-background-color);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#svg_root {
|
|
37
|
+
fill: none;
|
|
38
|
+
stroke: none;
|
|
39
|
+
stroke-width: 0.01;
|
|
40
|
+
font-family: Montserrat, sans-serif;
|
|
41
|
+
font-size: 0.1pt;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#grid_group {
|
|
45
|
+
stroke: #fff;
|
|
46
|
+
stroke-width: 0.001;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.grid_axis {
|
|
50
|
+
stroke-width: 0.003;
|
|
51
|
+
stroke: hsl(76, 90%, 80%);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#header_summary_group {
|
|
55
|
+
fill: var(--header-text-color);
|
|
56
|
+
font-weight: 300;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.right_header {
|
|
60
|
+
text-anchor: end;;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.header_number {
|
|
64
|
+
font-weight: 100;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#status_ring_group {
|
|
68
|
+
stroke: var(--offline-color);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#status_ring_group.online {
|
|
72
|
+
stroke: var(--online-color);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#status_ring_group.active {
|
|
76
|
+
stroke: var(--active-color);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.single_wedge_group {
|
|
80
|
+
display: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.single_wedge_group.online, .single_wedge_group.active {
|
|
84
|
+
display: inherit;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.online .wedge_node {
|
|
88
|
+
fill: var(--wedge-online-color);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.active .wedge_node {
|
|
92
|
+
fill: var(--wedge-active-color);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.wedge_identifier {
|
|
96
|
+
fill: var(--header-text-color);
|
|
97
|
+
font-size: 0.04pt;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.wedge_identifier.small_text {
|
|
101
|
+
font-size: 0.03pt;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.summary_text_value {
|
|
105
|
+
fill: var(--summary-value-text-color);
|
|
106
|
+
font-size: 0.03pt;
|
|
107
|
+
font-weight: 200;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.summary_watched_circle {
|
|
111
|
+
fill: var(--wedge-active-color);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.summary_active_circle {
|
|
115
|
+
fill: var(--active-color);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.timeseries_bg {
|
|
119
|
+
/* fill: #ffffff1f; */
|
|
120
|
+
fill: none;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.timeseries_path {
|
|
124
|
+
stroke: var(--timeseries-line-color);
|
|
125
|
+
clip-path: url(#inner_circle_clip);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.timeseries_big_label {
|
|
129
|
+
/* fill: var(--timeseries-label-color); */
|
|
130
|
+
font-size: 0.04pt;
|
|
131
|
+
font-weight: 400;
|
|
132
|
+
text-anchor: end;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.timeseries_small_label {
|
|
136
|
+
/* fill: var(--timeseries-label-color); */
|
|
137
|
+
font-size: 0.03pt;
|
|
138
|
+
font-weight: 400;
|
|
139
|
+
text-anchor: end;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.timeseries_value {
|
|
143
|
+
fill: var(--timeseries-value-color);
|
|
144
|
+
font-size: 0.05pt;
|
|
145
|
+
font-weight: 300;
|
|
146
|
+
text-anchor: middle;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.tooltip_text {
|
|
150
|
+
fill: #fff;
|
|
151
|
+
font-size: 0.025pt;
|
|
152
|
+
font-weight: 200;
|
|
153
|
+
text-anchor: middle;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.tooltip_outline {
|
|
157
|
+
stroke: #fff;
|
|
158
|
+
stroke-width: 0.002;
|
|
159
|
+
fill: #000;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.tooltip_display_group {
|
|
163
|
+
display: none;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.tooltip_wrapper_group:hover .tooltip_display_group {
|
|
167
|
+
display: initial;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.server_status_label {
|
|
171
|
+
fill: var(--timeseries-label-color);
|
|
172
|
+
font-weight: 300;
|
|
173
|
+
font-size: 0.06pt;
|
|
174
|
+
text-anchor: middle;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.server_status_value {
|
|
178
|
+
fill: var(--timeseries-value-color);
|
|
179
|
+
font-weight: 100;
|
|
180
|
+
font-size: 0.06pt;
|
|
181
|
+
text-anchor: middle;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.server_status_offline {
|
|
185
|
+
fill: var(--timeseries-line-color);
|
|
186
|
+
font-weight: 400;
|
|
187
|
+
font-size: 0.06pt;
|
|
188
|
+
text-anchor: middle;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.test_circle {
|
|
192
|
+
fill: #fff;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.test_line {
|
|
196
|
+
stroke: #fff;
|
|
197
|
+
stroke-width: 0.005;
|
|
198
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
|
|
7
|
+
<title>WTR Status</title>
|
|
8
|
+
|
|
9
|
+
<link rel="stylesheet" href="css/fonts.css">
|
|
10
|
+
<style id="main_style"></style>
|
|
11
|
+
<script type="module" src="js/index.js" defer></script>
|
|
12
|
+
|
|
13
|
+
<style>
|
|
14
|
+
body {
|
|
15
|
+
margin: 0;
|
|
16
|
+
overscroll-behavior: none;
|
|
17
|
+
display: grid;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
padding-top: 2px;
|
|
21
|
+
}
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
|
|
25
|
+
<body class="base_colors color_palette color_theme">
|
|
26
|
+
<svg id="svg_root" viewBox="-1 -1 2 2" xmlns="http://www.w3.org/2000/svg">
|
|
27
|
+
<g id="grid_group"></g>
|
|
28
|
+
<g id="header_summary_group"></g>
|
|
29
|
+
<g id="status_ring_group"></g>
|
|
30
|
+
<g id="client_wedges_group" class="wedge_group"></g>
|
|
31
|
+
<g id="editor_wedges_group" class="wedge_group"></g>
|
|
32
|
+
<g id="activity_timeseries_graph"></g>
|
|
33
|
+
<g id="server_status_group"></g>
|
|
34
|
+
</svg>
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const { exportDeps, drawSvgElement, drawCircle, drawLinearPath, coordsToPathData, drawText, drawToolTip } = window.__WTR__
|
|
2
|
+
|
|
3
|
+
const drawValueWithTooltip = ({x, y, label, direction, parentNode}) => {
|
|
4
|
+
const tooltipWrapperGroup = drawSvgElement("g", undefined, "tooltip_wrapper_group", parentNode)
|
|
5
|
+
drawToolTip({x: x, y: y - 0.0032, text: label, direction, parentNode: tooltipWrapperGroup})
|
|
6
|
+
return drawText({x: x, y: y, dominantBaseline: "middle", text: "0", className: "timeseries_value", parentNode: tooltipWrapperGroup})
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const dataWindowSize = 16
|
|
10
|
+
const dataWindowInterval = 1000
|
|
11
|
+
const graphHeight = 0.25
|
|
12
|
+
|
|
13
|
+
const innerCircleClipPathId = "inner_circle_clip"
|
|
14
|
+
|
|
15
|
+
const createLinearScale = (domainMin, domainMax, rangeMin, rangeMax) => {
|
|
16
|
+
const domainSize = domainMax - domainMin
|
|
17
|
+
if (domainSize === 0) { return () => rangeMin }
|
|
18
|
+
const rangeSize = rangeMax - rangeMin
|
|
19
|
+
const ratio = rangeSize / domainSize
|
|
20
|
+
|
|
21
|
+
return (domainValue) => (domainValue - domainMin) * ratio + rangeMin
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const createRandomDataWindow = () => {
|
|
25
|
+
const series = []
|
|
26
|
+
const endTime = new Date().setMilliseconds(0).valueOf()
|
|
27
|
+
const startTime = endTime - dataWindowInterval * dataWindowSize
|
|
28
|
+
let maxValue = 0
|
|
29
|
+
|
|
30
|
+
for (let time = startTime; time < endTime; time += dataWindowInterval) {
|
|
31
|
+
// const value = Math.floor(Math.random() * 101)
|
|
32
|
+
const value = 0
|
|
33
|
+
if (value > maxValue) { maxValue = value }
|
|
34
|
+
series.push({time, value})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return series
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const getSeriesWindowInfo = (series) => {
|
|
41
|
+
const startTime = series.at(1).time
|
|
42
|
+
const endTime = series.at(-1).time
|
|
43
|
+
let maxValue = 0
|
|
44
|
+
series.forEach(({value}) => {
|
|
45
|
+
if (value > maxValue) { maxValue = value }
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return {startTime, endTime, maxValue}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class ActivityTimeseriesGraph {
|
|
52
|
+
constructor ({innerRingRadius, parentNode}) {
|
|
53
|
+
this.innerRingRadius = innerRingRadius
|
|
54
|
+
this.parentNode = parentNode
|
|
55
|
+
this.parentNode.innerHTML = ""
|
|
56
|
+
this.dataWindow = window.activityDataWindow || createRandomDataWindow()
|
|
57
|
+
window.activityDataWindow = this.dataWindow
|
|
58
|
+
if (!window.currentActivityCount) { window.currentActivityCount = 0 }
|
|
59
|
+
this.draw()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
draw () {
|
|
63
|
+
const minX = -this.innerRingRadius
|
|
64
|
+
const maxX = this.innerRingRadius
|
|
65
|
+
const width = maxX - minX
|
|
66
|
+
const height = graphHeight
|
|
67
|
+
const minY = -height / 2
|
|
68
|
+
const maxY = minY + height
|
|
69
|
+
|
|
70
|
+
const clipPath = drawSvgElement("clipPath", {id: innerCircleClipPathId}, undefined, this.parentNode)
|
|
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)
|
|
73
|
+
|
|
74
|
+
const series = this.dataWindow
|
|
75
|
+
const {startTime, endTime, maxValue} = getSeriesWindowInfo(series)
|
|
76
|
+
|
|
77
|
+
const maxValueElement = drawValueWithTooltip({x: minX + width / 2, y: minY - 0.08, label: "Max updates in a 1 second window", direction: "above", parentNode: this.parentNode})
|
|
78
|
+
const currentValueElement = drawValueWithTooltip({x: minX + width / 2, y: maxY + 0.095, label: "Updates in last full second", parentNode: this.parentNode})
|
|
79
|
+
|
|
80
|
+
let 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
|
|
81
|
+
let timeScale = createLinearScale(startTime, endTime, minX, maxX)
|
|
82
|
+
|
|
83
|
+
const pathCoords = series.map(({time, value}) => {
|
|
84
|
+
return [timeScale(time), valueScale(value)]
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const graphPath = drawLinearPath({coords: pathCoords, className: "timeseries_path", parentNode: this.parentNode})
|
|
88
|
+
|
|
89
|
+
if (window.activityTimeout) { clearTimeout(window.activityTimeout) }
|
|
90
|
+
const onTickUpdate = () => {
|
|
91
|
+
scheduleNextTick()
|
|
92
|
+
|
|
93
|
+
console.log("data tick", Date.now())
|
|
94
|
+
const series = this.dataWindow
|
|
95
|
+
const prevEndTime = series.at(-1).time
|
|
96
|
+
const newTime = prevEndTime + dataWindowInterval
|
|
97
|
+
series.shift()
|
|
98
|
+
series.push({time: newTime, value: window.currentActivityCount})
|
|
99
|
+
currentValueElement.innerHTML = window.currentActivityCount
|
|
100
|
+
window.currentActivityCount = 0
|
|
101
|
+
|
|
102
|
+
const pathCoords = series.map(({time, value}) => {
|
|
103
|
+
return [timeScale(time), valueScale(value)]
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const pathData = coordsToPathData(pathCoords)
|
|
107
|
+
graphPath.style.transition = ""
|
|
108
|
+
graphPath.setAttribute("d", pathData)
|
|
109
|
+
|
|
110
|
+
requestAnimationFrame(() => {
|
|
111
|
+
requestAnimationFrame(() => {
|
|
112
|
+
const {startTime, endTime, maxValue} = getSeriesWindowInfo(series)
|
|
113
|
+
maxValueElement.innerHTML = maxValue
|
|
114
|
+
|
|
115
|
+
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
|
+
timeScale = createLinearScale(startTime, endTime, minX, maxX)
|
|
117
|
+
|
|
118
|
+
const pathCoords = series.map(({time, value}) => {
|
|
119
|
+
return [timeScale(time), valueScale(value)]
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const pathData = coordsToPathData(pathCoords)
|
|
123
|
+
graphPath.style.transition = "all 1s linear"
|
|
124
|
+
graphPath.setAttribute("d", pathData)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const scheduleNextTick = () => {
|
|
131
|
+
const nowMillis = new Date().getMilliseconds()
|
|
132
|
+
const millisUntilNextSecond = 1000 - nowMillis
|
|
133
|
+
window.activityTimeout = setTimeout(onTickUpdate, millisUntilNextSecond)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
scheduleNextTick()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
triggerActivity () {
|
|
140
|
+
window.currentActivityCount++
|
|
141
|
+
console.log("time series graph activity")
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
exportDeps({ActivityTimeseriesGraph})
|
|
146
|
+
|
|
147
|
+
/*
|
|
148
|
+
*
|
|
149
|
+
*/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { exportDeps, drawText } = window.__WTR__
|
|
2
|
+
|
|
3
|
+
class HeaderSummary {
|
|
4
|
+
constructor ({parentNode}) {
|
|
5
|
+
this.parentNode = parentNode
|
|
6
|
+
this.draw()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
draw () {
|
|
10
|
+
this.parentNode.innerHTML = ""
|
|
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
|
+
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
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
exportDeps({HeaderSummary})
|