querysub 0.2.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.
Files changed (169) hide show
  1. package/.dependency-cruiser.js +304 -0
  2. package/.eslintrc.js +51 -0
  3. package/.github/copilot-instructions.md +1 -0
  4. package/.vscode/settings.json +25 -0
  5. package/bin/deploy.js +4 -0
  6. package/bin/function.js +4 -0
  7. package/bin/server.js +4 -0
  8. package/costsBenefits.txt +112 -0
  9. package/deploy.ts +3 -0
  10. package/inject.ts +1 -0
  11. package/package.json +60 -0
  12. package/prompts.txt +54 -0
  13. package/spec.txt +820 -0
  14. package/src/-a-archives/archiveCache.ts +913 -0
  15. package/src/-a-archives/archives.ts +148 -0
  16. package/src/-a-archives/archivesBackBlaze.ts +792 -0
  17. package/src/-a-archives/archivesDisk.ts +418 -0
  18. package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
  19. package/src/-a-auth/certs.ts +517 -0
  20. package/src/-a-auth/der.ts +122 -0
  21. package/src/-a-auth/ed25519.ts +1015 -0
  22. package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
  23. package/src/-b-authorities/dnsAuthority.ts +203 -0
  24. package/src/-b-authorities/emailAuthority.ts +57 -0
  25. package/src/-c-identity/IdentityController.ts +200 -0
  26. package/src/-d-trust/NetworkTrust2.ts +150 -0
  27. package/src/-e-certs/EdgeCertController.ts +288 -0
  28. package/src/-e-certs/certAuthority.ts +192 -0
  29. package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
  30. package/src/-g-core-values/NodeCapabilities.ts +134 -0
  31. package/src/-g-core-values/oneTimeForward.ts +91 -0
  32. package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
  33. package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
  34. package/src/0-path-value-core/LoggingClient.tsx +24 -0
  35. package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
  36. package/src/0-path-value-core/PathController.ts +1 -0
  37. package/src/0-path-value-core/PathValueCommitter.ts +565 -0
  38. package/src/0-path-value-core/PathValueController.ts +231 -0
  39. package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
  40. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
  41. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
  42. package/src/0-path-value-core/debugLogs.ts +90 -0
  43. package/src/0-path-value-core/pathValueArchives.ts +483 -0
  44. package/src/0-path-value-core/pathValueCore.ts +2217 -0
  45. package/src/1-path-client/RemoteWatcher.ts +558 -0
  46. package/src/1-path-client/pathValueClientWatcher.ts +702 -0
  47. package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
  48. package/src/2-proxy/archiveMoveHarness.ts +376 -0
  49. package/src/2-proxy/garbageCollection.ts +753 -0
  50. package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
  51. package/src/2-proxy/pathValueProxy.ts +139 -0
  52. package/src/2-proxy/schema2.ts +518 -0
  53. package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
  54. package/src/3-path-functions/PathFunctionRunner.ts +619 -0
  55. package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
  56. package/src/3-path-functions/deployBlock.ts +10 -0
  57. package/src/3-path-functions/deployCheck.ts +7 -0
  58. package/src/3-path-functions/deployMain.ts +160 -0
  59. package/src/3-path-functions/pathFunctionLoader.ts +282 -0
  60. package/src/3-path-functions/syncSchema.ts +475 -0
  61. package/src/3-path-functions/tests/functionsTest.ts +135 -0
  62. package/src/3-path-functions/tests/rejectTest.ts +77 -0
  63. package/src/4-dom/css.tsx +29 -0
  64. package/src/4-dom/cssTypes.d.ts +212 -0
  65. package/src/4-dom/qreact.tsx +2322 -0
  66. package/src/4-dom/qreactTest.tsx +417 -0
  67. package/src/4-querysub/Querysub.ts +877 -0
  68. package/src/4-querysub/QuerysubController.ts +620 -0
  69. package/src/4-querysub/copyEvent.ts +0 -0
  70. package/src/4-querysub/permissions.ts +289 -0
  71. package/src/4-querysub/permissionsShared.ts +1 -0
  72. package/src/4-querysub/querysubPrediction.ts +525 -0
  73. package/src/5-diagnostics/FullscreenModal.tsx +67 -0
  74. package/src/5-diagnostics/GenericFormat.tsx +165 -0
  75. package/src/5-diagnostics/Modal.tsx +79 -0
  76. package/src/5-diagnostics/Table.tsx +183 -0
  77. package/src/5-diagnostics/TimeGrouper.tsx +114 -0
  78. package/src/5-diagnostics/diskValueAudit.ts +216 -0
  79. package/src/5-diagnostics/memoryValueAudit.ts +442 -0
  80. package/src/5-diagnostics/nodeMetadata.ts +135 -0
  81. package/src/5-diagnostics/qreactDebug.tsx +309 -0
  82. package/src/5-diagnostics/shared.ts +26 -0
  83. package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
  84. package/src/TestController.ts +35 -0
  85. package/src/allowclient.flag +0 -0
  86. package/src/bits.ts +86 -0
  87. package/src/buffers.ts +69 -0
  88. package/src/config.ts +53 -0
  89. package/src/config2.ts +48 -0
  90. package/src/diagnostics/ActionsHistory.ts +56 -0
  91. package/src/diagnostics/NodeViewer.tsx +503 -0
  92. package/src/diagnostics/SizeLimiter.ts +62 -0
  93. package/src/diagnostics/TimeDebug.tsx +18 -0
  94. package/src/diagnostics/benchmark.ts +139 -0
  95. package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
  96. package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
  97. package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
  98. package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
  99. package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
  100. package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
  101. package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
  102. package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
  103. package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
  104. package/src/diagnostics/heapTag.ts +13 -0
  105. package/src/diagnostics/listenOnDebugger.ts +77 -0
  106. package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
  107. package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
  108. package/src/diagnostics/logs/ansiFormat.ts +108 -0
  109. package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
  110. package/src/diagnostics/logs/diskLogger.ts +305 -0
  111. package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
  112. package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
  113. package/src/diagnostics/logs/logGitHashes.ts +30 -0
  114. package/src/diagnostics/managementPages.tsx +289 -0
  115. package/src/diagnostics/periodic.ts +89 -0
  116. package/src/diagnostics/runSaturationTest.ts +416 -0
  117. package/src/diagnostics/satSchema.ts +64 -0
  118. package/src/diagnostics/trackResources.ts +82 -0
  119. package/src/diagnostics/watchdog.ts +55 -0
  120. package/src/errors.ts +132 -0
  121. package/src/forceProduction.ts +3 -0
  122. package/src/fs.ts +72 -0
  123. package/src/heapDumps.ts +666 -0
  124. package/src/https.ts +2 -0
  125. package/src/inject.ts +1 -0
  126. package/src/library-components/ATag.tsx +84 -0
  127. package/src/library-components/Button.tsx +344 -0
  128. package/src/library-components/ButtonSelector.tsx +64 -0
  129. package/src/library-components/DropdownCustom.tsx +151 -0
  130. package/src/library-components/DropdownSelector.tsx +32 -0
  131. package/src/library-components/Input.tsx +334 -0
  132. package/src/library-components/InputLabel.tsx +198 -0
  133. package/src/library-components/InputPicker.tsx +125 -0
  134. package/src/library-components/LazyComponent.tsx +62 -0
  135. package/src/library-components/MeasureHeightCSS.tsx +48 -0
  136. package/src/library-components/MeasuredDiv.tsx +47 -0
  137. package/src/library-components/ShowMore.tsx +51 -0
  138. package/src/library-components/SyncedController.ts +171 -0
  139. package/src/library-components/TimeRangeSelector.tsx +407 -0
  140. package/src/library-components/URLParam.ts +263 -0
  141. package/src/library-components/colors.tsx +14 -0
  142. package/src/library-components/drag.ts +114 -0
  143. package/src/library-components/icons.tsx +692 -0
  144. package/src/library-components/niceStringify.ts +50 -0
  145. package/src/library-components/renderToString.ts +52 -0
  146. package/src/misc/PromiseRace.ts +101 -0
  147. package/src/misc/color.ts +30 -0
  148. package/src/misc/getParentProcessId.cs +53 -0
  149. package/src/misc/getParentProcessId.ts +53 -0
  150. package/src/misc/hash.ts +83 -0
  151. package/src/misc/ipPong.js +13 -0
  152. package/src/misc/networking.ts +2 -0
  153. package/src/misc/random.ts +45 -0
  154. package/src/misc.ts +19 -0
  155. package/src/noserverhotreload.flag +0 -0
  156. package/src/path.ts +226 -0
  157. package/src/persistentLocalStore.ts +37 -0
  158. package/src/promise.ts +15 -0
  159. package/src/server.ts +73 -0
  160. package/src/src.d.ts +1 -0
  161. package/src/test/heapProcess.ts +36 -0
  162. package/src/test/mongoSatTest.tsx +55 -0
  163. package/src/test/satTest.ts +193 -0
  164. package/src/test/test.tsx +552 -0
  165. package/src/zip.ts +92 -0
  166. package/src/zipThreaded.ts +106 -0
  167. package/src/zipThreadedWorker.js +19 -0
  168. package/tsconfig.json +27 -0
  169. package/yarnSpec.txt +56 -0
@@ -0,0 +1,165 @@
1
+ import { formatDateTime, formatNiceDateTime, formatNumber, formatTime, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
2
+ import { css } from "typesafecss";
3
+ import { canHaveChildren } from "socket-function/src/types";
4
+ import { qreact } from "../4-dom/qreact";
5
+ import preact, { ContextType } from "preact";
6
+ import { errorMessage, warnMessage } from "../library-components/colors";
7
+
8
+ export { errorMessage, warnMessage };
9
+
10
+ export type RowType = { [columnName: string]: unknown };
11
+ export type FormatContext<RowT extends RowType = RowType> = {
12
+ row?: RowT;
13
+ columnName?: RowT extends undefined ? string : keyof RowT;
14
+ };
15
+ export type JSXFormatter<T = unknown, RowT extends RowType = RowType> = (
16
+ StringFormatters
17
+ | `varray:${StringFormatters}`
18
+ | `link:${string}`
19
+ | ((value: T, context?: FormatContext<RowT>) => preact.ComponentChild)
20
+ );
21
+
22
+ type StringFormatters = (
23
+ "guess"
24
+ | "string" | "number"
25
+ | "timeSpan" | "date"
26
+ | "error" | "link"
27
+ | "toSpaceCase"
28
+ );
29
+
30
+ function d(value: unknown, formattedValue: preact.ComponentChild) {
31
+ if (value === undefined || value === null) {
32
+ return "";
33
+ }
34
+ return formattedValue;
35
+ }
36
+ const startGuessDateRange = +new Date(2010, 0, 1).getTime();
37
+ const endGuessDateRange = +new Date(2040, 0, 1).getTime();
38
+ let formatters: { [formatter in StringFormatters]: (value: unknown) => preact.ComponentChild } = {
39
+ string: (value) => d(value, String(value || "")),
40
+ number: (value) => d(value, formatNumber(Number(value))),
41
+ timeSpan: (value) => d(value, formatTime(Number(value))),
42
+ date: (value) => d(value, formatVeryNiceDateTime(Number(value))),
43
+ error: (value) => d(value, <span class={errorMessage}>{String(value)}</span>),
44
+ toSpaceCase: (value) => d(value, toSpaceCase(String(value))),
45
+ link: (value) => {
46
+ if (value === undefined || value === null) {
47
+ return "";
48
+ }
49
+ let href = String(value);
50
+ let str = String(value);
51
+ // https://google.com<google> => href = https://google.com, str = google
52
+ let match = str.match(/<([^>]+)>/);
53
+ if (match) {
54
+ href = str.slice(0, match.index);
55
+ str = match[1];
56
+ }
57
+ return <a target="_blank" href={href}>{str}</a>;
58
+ },
59
+ guess: (value) => {
60
+ if (typeof value === "number") {
61
+ // NOTE: These special values don't represent real values, and if they are shown
62
+ // to the user, it is a mistake anyways. So instead of showing a large number
63
+ // that is not meaningful to the user, we show a string, so they the issue
64
+ // is not the system having a large number, but the system not changing
65
+ // the default value.
66
+ if (value === Number.MAX_SAFE_INTEGER) {
67
+ return "Number.MAX_SAFE_INTEGER";
68
+ }
69
+ if (value === Number.MIN_SAFE_INTEGER) {
70
+ return "Number.MIN_SAFE_INTEGER";
71
+ }
72
+ if (value === Number.MAX_VALUE) {
73
+ return "Number.MAX_VALUE";
74
+ }
75
+ if (value === Number.MIN_VALUE) {
76
+ return "Number.MIN_VALUE";
77
+ }
78
+ // Infinity should be somewhat understood by the user, if they are even a little
79
+ // bit literate. Of course, the value is likely a bug, but at least the consequences
80
+ // may be inferrable (the threshold is +Infinity, so it will never be reached, etc).
81
+ if (value === Number.POSITIVE_INFINITY) {
82
+ return "+Infinity";
83
+ }
84
+ if (value === Number.NEGATIVE_INFINITY) {
85
+ return "-Infinity";
86
+ }
87
+ // These are somewhat a mistake, and should almost always be displayed as ""
88
+ if (Number.isNaN(value)) {
89
+ return "";
90
+ }
91
+
92
+ if (startGuessDateRange < value && value < endGuessDateRange) {
93
+ return formatters.date(value);
94
+ }
95
+ return formatters.number(value);
96
+ }
97
+ if (typeof value === "string" && value.startsWith("Error:")) {
98
+ return formatters.error(value.slice("Error:".length));
99
+ }
100
+ if (typeof value === "string" && value.startsWith("https://")) {
101
+ return formatters.link(value);
102
+ }
103
+ // Don't render nested objects, etc, otherwise passing large arrays
104
+ // could just in us blowing the output up, when their intention
105
+ // is to just show "Array()"
106
+ if (Array.isArray(value) && !value.some(x => canHaveChildren(x))) {
107
+ return formatValue(value, "varray:guess");
108
+ }
109
+ return formatters.string(value);
110
+ },
111
+ };
112
+ function formatVArray(value: unknown[], formatter: StringFormatters) {
113
+ if (!Array.isArray(value)) {
114
+ return <span class={errorMessage}>Expected array, got {typeof value}</span>;
115
+ }
116
+ let values = value.map(v => {
117
+ let formatted = formatValue(v, formatter);
118
+ if (!canHaveChildren(formatted)) {
119
+ return <span>{formatted}</span>;
120
+ }
121
+ return formatted;
122
+ });
123
+ return (
124
+ <div class={css.vbox(4)}>
125
+ {values}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ export const genericFormat = formatValue;
131
+ export function formatValue(value: unknown, formatter: JSXFormatter = "guess", context?: FormatContext) {
132
+ if (typeof formatter === "function") {
133
+ return formatter(value, context);
134
+ }
135
+ let formatterT = formatter as StringFormatters;
136
+ if (formatterT.startsWith("varray:")) {
137
+ formatterT = formatterT.slice("varray:".length) as StringFormatters;
138
+ return formatVArray(value as unknown[], formatterT);
139
+ }
140
+ if (formatterT.startsWith("link:")) {
141
+ let href = formatterT.slice("link:".length);
142
+ href = href.replaceAll("$VALUE$", String(value));
143
+ return formatters.link(href);
144
+ }
145
+ if (!formatters[formatterT]) {
146
+ throw new Error(`Unknown formatter: ${formatter}`);
147
+ }
148
+ return formatters[formatterT](value);
149
+ }
150
+
151
+ export function toSpaceCase(text: string) {
152
+ return text
153
+ // "camelCase" => "camel Case"
154
+ // "URL" => "URL"
155
+ .replace(/([a-z][A-Z])/g, str => str[0] + " " + str[1])
156
+ // "snake_case" => "snake case"
157
+ .replace(/_([A-Za-z])/g, " $1")
158
+ // "firstletter" => "Firstletter"
159
+ .replace(/^./, str => str.toUpperCase())
160
+ // "first letter" => "first Letter"
161
+ .replace(/ ./, str => str.toUpperCase())
162
+ // Convert multiple spaces to a single space
163
+ .replace(/\s+/g, " ");
164
+ ;
165
+ }
@@ -0,0 +1,79 @@
1
+ import preact from "preact";
2
+ import { nextId } from "socket-function/src/misc";
3
+ import { lazy } from "socket-function/src/caching";
4
+ import { css } from "typesafecss";
5
+ import { Querysub } from "../4-querysub/Querysub";
6
+ import { atomicObjectWrite, atomicObjectWriteNoFreeze } from "../2-proxy/PathValueProxyWatcher";
7
+ import { qreact } from "../4-dom/qreact";
8
+
9
+ const data = Querysub.createLocalSchema<{
10
+ modals: {
11
+ [id: string]: {
12
+ value: preact.ComponentChild;
13
+ }
14
+ };
15
+ }>("modals");
16
+
17
+ export class Modal extends preact.Component {
18
+ id = nextId();
19
+ componentWillUnmount(): void {
20
+ delete data().modals[this.id];
21
+ }
22
+ render() {
23
+ ensureRendering();
24
+ data().modals[this.id] = atomicObjectWriteNoFreeze({ value: this.props.children });
25
+ return undefined;
26
+ }
27
+ }
28
+
29
+ export function showModal(config: {
30
+ content: preact.ComponentChild;
31
+ }): {
32
+ close: () => void;
33
+ } {
34
+ ensureRendering();
35
+ let id = nextId();
36
+ Querysub.commit(() => {
37
+ data().modals[id] = atomicObjectWriteNoFreeze({ value: config.content });
38
+ });
39
+ function close() {
40
+ Querysub.commit(() => {
41
+ delete data().modals[id];
42
+ });
43
+ }
44
+ return { close };
45
+ }
46
+
47
+ export function closeAllModals() {
48
+ Querysub.commit(() => {
49
+ for (let id in data().modals) {
50
+ delete data().modals[id];
51
+ }
52
+ });
53
+ }
54
+
55
+
56
+ export class ModalHolder extends preact.Component {
57
+ render() {
58
+ let modals = Object.values(data().modals);
59
+ return (
60
+ <div class={"ModalHolder" + css.position("fixed").pos(0, 0).size("100vw", "100vh").zIndex(20).pointerEvents("none")}>
61
+ {modals.map(x => x.value)}
62
+ <style>
63
+ {`
64
+ .ModalHolder > * {
65
+ pointer-events: all;
66
+ }
67
+ `}
68
+ </style>
69
+ </div>
70
+ );
71
+ }
72
+ }
73
+
74
+ const ensureRendering = lazy(() => {
75
+ let root = document.createElement("div");
76
+ root.id = "modals";
77
+ document.body.appendChild(root);
78
+ Querysub.render(<ModalHolder />, root);
79
+ });
@@ -0,0 +1,183 @@
1
+ import { qreact } from "../4-dom/qreact";
2
+ import { JSXFormatter, formatValue, toSpaceCase } from "./GenericFormat";
3
+ import { css } from "typesafecss";
4
+ import preact from "preact";
5
+ import { showModal } from "./Modal";
6
+ import { FullscreenModal } from "./FullscreenModal";
7
+ import { canHaveChildren } from "socket-function/src/types";
8
+
9
+ // Undefined means we infer the column
10
+ // Null means the column is removed
11
+ export type ColumnType<T = unknown, Row extends RowType = RowType> = undefined | null | {
12
+ // Defaults to column name
13
+ title?: string;
14
+ formatter?: JSXFormatter<T, Row>;
15
+ };
16
+ export type RowType = {
17
+ [columnName: string]: unknown;
18
+ };
19
+ export type ColumnsType = { [columnName: string]: ColumnType };
20
+ export type TableType<RowT extends RowType = RowType> = {
21
+ columns: { [columnName in keyof RowT]?: ColumnType<RowT[columnName], RowT> };
22
+ rows: RowT[];
23
+ };
24
+
25
+ module.hotreload = true;
26
+
27
+ export class Table<RowT extends RowType> extends qreact.Component<TableType<RowT> & {
28
+ class?: string;
29
+ cellClass?: string;
30
+ initialLimit?: number;
31
+
32
+ // Line and character limits before we cut off the inner content
33
+ lineLimit?: number;
34
+ characterLimit?: number;
35
+ }> {
36
+ state = {
37
+ limit: this.props.initialLimit || 100,
38
+ };
39
+ render() {
40
+ let { columns, rows } = this.props;
41
+ let cellClass = " " + String(this.props.cellClass || "") + " ";
42
+ let allRows = rows;
43
+ rows = rows.slice(0, this.state.limit);
44
+
45
+ const lineLimit = this.props.lineLimit ?? 3;
46
+ const characterLimit = this.props.characterLimit ?? 300;
47
+
48
+ return (
49
+ <table class={css.borderCollapse("collapse") + this.props.class}>
50
+ <tr class={css.position("sticky").zIndex(1).top(0).hsla(0, 0, 95, 0.9)}>
51
+ <th class={css.whiteSpace("nowrap")}>⧉ {allRows.length}</th>
52
+ {Object.entries(columns).filter(x => x[1] !== null).map(([columnName, column]) =>
53
+ <th class={css.pad2(8, 4) + cellClass}>{column?.title || toSpaceCase(columnName)}</th>
54
+ )}
55
+ </tr>
56
+ {rows.map((row, index) => (
57
+ <tr
58
+ class={
59
+ (index % 2 === 1 && css.hsla(0, 0, 100, 0.25) || "")
60
+ + css.relative.zIndex(0)
61
+ }
62
+ >
63
+ <td class={css.center}>{index + 1}</td>
64
+ {Object.entries(columns).filter(x => x[1] !== null).map(([columnName, column]) => {
65
+ let value = row[columnName];
66
+ let formatter = column?.formatter || "guess";
67
+ let result = formatValue(value, formatter, { row, columnName });
68
+ let renderedObj = renderTrimmed({
69
+ content: result,
70
+ lineLimit,
71
+ characterLimit,
72
+ });
73
+ let attributes = { ...renderedObj.outerAttributes };
74
+ attributes.class = attributes.class || "";
75
+ attributes.class += " " + css.whiteSpace("pre-wrap").pad2(8, 4);
76
+ attributes.class += cellClass;
77
+ // If the inner content looks like a VNode, take it's attributes and unwrap it,
78
+ // so it can fill the entire cell.
79
+ let innerContent = renderedObj.innerContent;
80
+ if (
81
+ canHaveChildren(innerContent) && "props" in innerContent
82
+ && canHaveChildren(innerContent.props)
83
+ && "children" in innerContent.props
84
+ && (
85
+ Array.isArray(innerContent.props.children) && innerContent.props.children.length === 1
86
+ || !Array.isArray(innerContent.props.children)
87
+ )
88
+ // AND, it is a div or span (a tags shouldn't be unwrapped)
89
+ && (innerContent.type === "div")
90
+ ) {
91
+ attributes.class += " " + innerContent.props.class;
92
+ let baseOnClick = attributes.onClick;
93
+ let props = innerContent.props;
94
+ attributes.onClick = (e) => {
95
+ if (baseOnClick) baseOnClick(e);
96
+ (props as any).onClick?.(e);
97
+ };
98
+ for (let key in props) {
99
+ if (key === "class") continue;
100
+ if (key === "onClick") continue;
101
+ (attributes as any)[key] = props[key];
102
+ }
103
+ innerContent = props.children as any;
104
+ }
105
+ return <td {...attributes}>
106
+ {innerContent}
107
+ </td>;
108
+ })}
109
+ </tr>
110
+ ))}
111
+ {allRows.length > rows.length && <tr>
112
+ <td
113
+ colSpan={1 + Object.keys(columns).length}
114
+ class={css.pad2(8).textAlign("center")}
115
+ >
116
+ <button onClick={() => this.state.limit += 100}>
117
+ {/* TODO: Load more as soon as they get close to the end.
118
+ - It doesn't really matter, as there is little reason for them to scroll far
119
+ (they should just filter/search instead).
120
+ */}
121
+ Load more
122
+ </button>
123
+ </td>
124
+ </tr>}
125
+ </table>
126
+ );
127
+ }
128
+ }
129
+
130
+ function renderTrimmed(config: {
131
+ content: preact.ComponentChild;
132
+ lineLimit: number;
133
+ characterLimit: number;
134
+ }): {
135
+ outerAttributes: preact.JSX.HTMLAttributes<HTMLTableCellElement>;
136
+ innerContent: preact.ComponentChild;
137
+ } {
138
+ let { content, lineLimit, characterLimit } = config;
139
+ if (typeof content !== "string" && typeof content !== "number") {
140
+ return {
141
+ outerAttributes: {},
142
+ innerContent: content as any,
143
+ };
144
+ }
145
+ let trimmed = false;
146
+ let contentStr = String(content);
147
+ if (contentStr.length > characterLimit) {
148
+ contentStr = contentStr.slice(0, characterLimit - 3) + "...";
149
+ trimmed = true;
150
+ }
151
+ let lines = contentStr.split("\n");
152
+ if (lines.length > lineLimit) {
153
+ lines = lines.slice(0, lineLimit);
154
+ lines[lines.length - 1] += "...";
155
+ contentStr = lines.join("\n");
156
+ trimmed = true;
157
+ }
158
+
159
+ if (!trimmed) {
160
+ return {
161
+ outerAttributes: {},
162
+ innerContent: contentStr,
163
+ };
164
+ }
165
+
166
+ return {
167
+ outerAttributes: {
168
+ class: css.opacity(0.5, "hover").button,
169
+ onClick: () => {
170
+ let close = showModal({
171
+ content: <FullscreenModal onCancel={() => {
172
+ close.close();
173
+ }}>
174
+ <div class={css.whiteSpace("pre-wrap")}>
175
+ {content}
176
+ </div>
177
+ </FullscreenModal>
178
+ });
179
+ }
180
+ },
181
+ innerContent: contentStr
182
+ };
183
+ }
@@ -0,0 +1,114 @@
1
+ import debugbreak from "debugbreak";
2
+ import { formatTime } from "socket-function/src/formatting/format";
3
+ import { isDefined } from "../misc";
4
+
5
+ const SUB_BUCKET_COUNT = 100;
6
+
7
+ export class TimeGrouper {
8
+ constructor(private timeBuckets = [
9
+ { name: "Last", duration: 0 },
10
+ { name: "Hour", duration: 60 * 60 * 1000 },
11
+ { name: "Day", duration: 24 * 60 * 60 * 1000 },
12
+ { name: "Week", duration: 7 * 24 * 60 * 60 * 100 },
13
+ ]) { }
14
+ private buckets = this.timeBuckets.map(x => ({
15
+ name: x.name,
16
+ duration: x.duration,
17
+ subBucketSize: x.duration / SUB_BUCKET_COUNT,
18
+ subBuckets: [] as {
19
+ min: number;
20
+ max: number;
21
+ sum: number;
22
+ last: number;
23
+ startTime: number;
24
+ endTime: number;
25
+ lastUpdateTime: number;
26
+ }[],
27
+ }));
28
+ public onValueChanged(value: number, time = Date.now()) {
29
+ for (let bucket of this.buckets) {
30
+ if (bucket.duration === 0) {
31
+ if (bucket.subBuckets.length === 0) {
32
+ let newBucket = {
33
+ min: 0, sum: 0, max: 0, last: 0, startTime: 0, endTime: 0, lastUpdateTime: 0,
34
+ };
35
+ bucket.subBuckets.push(newBucket);
36
+ }
37
+ let latest = bucket.subBuckets.at(-1)!;
38
+ latest.max = value;
39
+ latest.min = value;
40
+ latest.sum = value;
41
+ latest.last = value;
42
+ latest.startTime = time;
43
+ latest.endTime = time;
44
+ latest.lastUpdateTime = time;
45
+ } else {
46
+ // Add a new subbucket if the latest is too old
47
+ if (bucket.subBuckets.length === 0 || time > bucket.subBuckets.at(-1)!.endTime) {
48
+ let newBucket = {
49
+ min: value,
50
+ max: value,
51
+ sum: 0,
52
+ last: value,
53
+ startTime: time,
54
+ endTime: time + bucket.subBucketSize,
55
+ lastUpdateTime: 0,
56
+ };
57
+ bucket.subBuckets.push(newBucket);
58
+ // Remove buckets that are too old
59
+ let threshold = time - bucket.duration;
60
+ while (bucket.subBuckets.length > 0 && bucket.subBuckets[0].endTime < threshold) {
61
+ bucket.subBuckets.shift();
62
+ }
63
+ }
64
+ // Update the latest subbucket
65
+ let latest = bucket.subBuckets.at(-1)!;
66
+ latest.max = Math.max(latest.max, value);
67
+ latest.sum += value;
68
+ latest.last = value;
69
+ latest.lastUpdateTime = time;
70
+ }
71
+ }
72
+ }
73
+ public getSummary() {
74
+ let buckets = this.buckets.map(bucket => {
75
+ // Reduce subBuckets within each bucket
76
+ let sub = bucket.subBuckets;
77
+ return {
78
+ name: bucket.name,
79
+ duration: bucket.duration,
80
+ recordedDuration: sub.at(-1)!.startTime - sub[0].endTime,
81
+ min: sub.map(x => x.min).reduce((a, b) => Math.min(a, b), Number.POSITIVE_INFINITY),
82
+ max: sub.map(x => x.max).reduce((a, b) => Math.max(a, b), Number.NEGATIVE_INFINITY),
83
+ sum: sub.map(x => x.sum).reduce((a, b) => a + b, 0),
84
+ lastUpdateTime: sub.map(x => x.lastUpdateTime).reduce((a, b) => Math.max(a, b), 0),
85
+ };
86
+ });
87
+ // We have to have at least have more data than the smaller bucket, otherwise this bucket
88
+ // is too empty. This prevents us from confusingly displaying "1 week of data",
89
+ // when we only have 1 day.
90
+ return buckets.filter((bucket, index) => {
91
+ if (index === 0) return true;
92
+ return bucket.recordedDuration > 0 && (bucket.recordedDuration > buckets[index - 1].recordedDuration * 1.1);
93
+ });
94
+ }
95
+
96
+ public getStateSummary(format: (value: number) => string): string {
97
+ return this.getSummary().map(bucket => {
98
+ if (bucket.duration === 0 || bucket.min === bucket.max) {
99
+ if (bucket.min === 0) return undefined;
100
+ return `(${bucket.name}) ${format(bucket.min)}`;
101
+ }
102
+ return `(${bucket.name}) ${format(bucket.min)} to ${format(bucket.max)}`;
103
+ }).filter(isDefined).join("\n");
104
+ }
105
+ public getEventSummary(format: (value: number) => string): string {
106
+ return this.getSummary().map(bucket => {
107
+ if (bucket.duration === 0) {
108
+ if (bucket.sum === 0) return undefined;
109
+ return `(${bucket.name}) ${format(bucket.sum)}`;
110
+ }
111
+ return `(${bucket.name}) ${format(bucket.sum)}`;
112
+ }).filter(isDefined).join("\n");
113
+ }
114
+ }