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.
- package/.dependency-cruiser.js +304 -0
- package/.eslintrc.js +51 -0
- package/.github/copilot-instructions.md +1 -0
- package/.vscode/settings.json +25 -0
- package/bin/deploy.js +4 -0
- package/bin/function.js +4 -0
- package/bin/server.js +4 -0
- package/costsBenefits.txt +112 -0
- package/deploy.ts +3 -0
- package/inject.ts +1 -0
- package/package.json +60 -0
- package/prompts.txt +54 -0
- package/spec.txt +820 -0
- package/src/-a-archives/archiveCache.ts +913 -0
- package/src/-a-archives/archives.ts +148 -0
- package/src/-a-archives/archivesBackBlaze.ts +792 -0
- package/src/-a-archives/archivesDisk.ts +418 -0
- package/src/-a-archives/copyLocalToBackblaze.ts +24 -0
- package/src/-a-auth/certs.ts +517 -0
- package/src/-a-auth/der.ts +122 -0
- package/src/-a-auth/ed25519.ts +1015 -0
- package/src/-a-auth/node-forge-ed25519.d.ts +17 -0
- package/src/-b-authorities/dnsAuthority.ts +203 -0
- package/src/-b-authorities/emailAuthority.ts +57 -0
- package/src/-c-identity/IdentityController.ts +200 -0
- package/src/-d-trust/NetworkTrust2.ts +150 -0
- package/src/-e-certs/EdgeCertController.ts +288 -0
- package/src/-e-certs/certAuthority.ts +192 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +543 -0
- package/src/-g-core-values/NodeCapabilities.ts +134 -0
- package/src/-g-core-values/oneTimeForward.ts +91 -0
- package/src/-h-path-value-serialize/PathValueSerializer.ts +769 -0
- package/src/-h-path-value-serialize/stringSerializer.ts +176 -0
- package/src/0-path-value-core/LoggingClient.tsx +24 -0
- package/src/0-path-value-core/NodePathAuthorities.ts +978 -0
- package/src/0-path-value-core/PathController.ts +1 -0
- package/src/0-path-value-core/PathValueCommitter.ts +565 -0
- package/src/0-path-value-core/PathValueController.ts +231 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +154 -0
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +820 -0
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +180 -0
- package/src/0-path-value-core/debugLogs.ts +90 -0
- package/src/0-path-value-core/pathValueArchives.ts +483 -0
- package/src/0-path-value-core/pathValueCore.ts +2217 -0
- package/src/1-path-client/RemoteWatcher.ts +558 -0
- package/src/1-path-client/pathValueClientWatcher.ts +702 -0
- package/src/2-proxy/PathValueProxyWatcher.ts +1857 -0
- package/src/2-proxy/archiveMoveHarness.ts +376 -0
- package/src/2-proxy/garbageCollection.ts +753 -0
- package/src/2-proxy/pathDatabaseProxyBase.ts +37 -0
- package/src/2-proxy/pathValueProxy.ts +139 -0
- package/src/2-proxy/schema2.ts +518 -0
- package/src/3-path-functions/PathFunctionHelpers.ts +129 -0
- package/src/3-path-functions/PathFunctionRunner.ts +619 -0
- package/src/3-path-functions/PathFunctionRunnerMain.ts +67 -0
- package/src/3-path-functions/deployBlock.ts +10 -0
- package/src/3-path-functions/deployCheck.ts +7 -0
- package/src/3-path-functions/deployMain.ts +160 -0
- package/src/3-path-functions/pathFunctionLoader.ts +282 -0
- package/src/3-path-functions/syncSchema.ts +475 -0
- package/src/3-path-functions/tests/functionsTest.ts +135 -0
- package/src/3-path-functions/tests/rejectTest.ts +77 -0
- package/src/4-dom/css.tsx +29 -0
- package/src/4-dom/cssTypes.d.ts +212 -0
- package/src/4-dom/qreact.tsx +2322 -0
- package/src/4-dom/qreactTest.tsx +417 -0
- package/src/4-querysub/Querysub.ts +877 -0
- package/src/4-querysub/QuerysubController.ts +620 -0
- package/src/4-querysub/copyEvent.ts +0 -0
- package/src/4-querysub/permissions.ts +289 -0
- package/src/4-querysub/permissionsShared.ts +1 -0
- package/src/4-querysub/querysubPrediction.ts +525 -0
- package/src/5-diagnostics/FullscreenModal.tsx +67 -0
- package/src/5-diagnostics/GenericFormat.tsx +165 -0
- package/src/5-diagnostics/Modal.tsx +79 -0
- package/src/5-diagnostics/Table.tsx +183 -0
- package/src/5-diagnostics/TimeGrouper.tsx +114 -0
- package/src/5-diagnostics/diskValueAudit.ts +216 -0
- package/src/5-diagnostics/memoryValueAudit.ts +442 -0
- package/src/5-diagnostics/nodeMetadata.ts +135 -0
- package/src/5-diagnostics/qreactDebug.tsx +309 -0
- package/src/5-diagnostics/shared.ts +26 -0
- package/src/5-diagnostics/synchronousLagTracking.ts +47 -0
- package/src/TestController.ts +35 -0
- package/src/allowclient.flag +0 -0
- package/src/bits.ts +86 -0
- package/src/buffers.ts +69 -0
- package/src/config.ts +53 -0
- package/src/config2.ts +48 -0
- package/src/diagnostics/ActionsHistory.ts +56 -0
- package/src/diagnostics/NodeViewer.tsx +503 -0
- package/src/diagnostics/SizeLimiter.ts +62 -0
- package/src/diagnostics/TimeDebug.tsx +18 -0
- package/src/diagnostics/benchmark.ts +139 -0
- package/src/diagnostics/errorLogs/ErrorLogController.ts +515 -0
- package/src/diagnostics/errorLogs/ErrorLogCore.ts +274 -0
- package/src/diagnostics/errorLogs/LogClassifiers.tsx +302 -0
- package/src/diagnostics/errorLogs/LogFilterUI.tsx +84 -0
- package/src/diagnostics/errorLogs/LogNotify.tsx +101 -0
- package/src/diagnostics/errorLogs/LogTimeSelector.tsx +724 -0
- package/src/diagnostics/errorLogs/LogViewer.tsx +757 -0
- package/src/diagnostics/errorLogs/hookErrors.ts +60 -0
- package/src/diagnostics/errorLogs/logFiltering.tsx +149 -0
- package/src/diagnostics/heapTag.ts +13 -0
- package/src/diagnostics/listenOnDebugger.ts +77 -0
- package/src/diagnostics/logs/DiskLoggerPage.tsx +572 -0
- package/src/diagnostics/logs/ObjectDisplay.tsx +165 -0
- package/src/diagnostics/logs/ansiFormat.ts +108 -0
- package/src/diagnostics/logs/diskLogGlobalContext.ts +38 -0
- package/src/diagnostics/logs/diskLogger.ts +305 -0
- package/src/diagnostics/logs/diskShimConsoleLogs.ts +32 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +50 -0
- package/src/diagnostics/logs/logGitHashes.ts +30 -0
- package/src/diagnostics/managementPages.tsx +289 -0
- package/src/diagnostics/periodic.ts +89 -0
- package/src/diagnostics/runSaturationTest.ts +416 -0
- package/src/diagnostics/satSchema.ts +64 -0
- package/src/diagnostics/trackResources.ts +82 -0
- package/src/diagnostics/watchdog.ts +55 -0
- package/src/errors.ts +132 -0
- package/src/forceProduction.ts +3 -0
- package/src/fs.ts +72 -0
- package/src/heapDumps.ts +666 -0
- package/src/https.ts +2 -0
- package/src/inject.ts +1 -0
- package/src/library-components/ATag.tsx +84 -0
- package/src/library-components/Button.tsx +344 -0
- package/src/library-components/ButtonSelector.tsx +64 -0
- package/src/library-components/DropdownCustom.tsx +151 -0
- package/src/library-components/DropdownSelector.tsx +32 -0
- package/src/library-components/Input.tsx +334 -0
- package/src/library-components/InputLabel.tsx +198 -0
- package/src/library-components/InputPicker.tsx +125 -0
- package/src/library-components/LazyComponent.tsx +62 -0
- package/src/library-components/MeasureHeightCSS.tsx +48 -0
- package/src/library-components/MeasuredDiv.tsx +47 -0
- package/src/library-components/ShowMore.tsx +51 -0
- package/src/library-components/SyncedController.ts +171 -0
- package/src/library-components/TimeRangeSelector.tsx +407 -0
- package/src/library-components/URLParam.ts +263 -0
- package/src/library-components/colors.tsx +14 -0
- package/src/library-components/drag.ts +114 -0
- package/src/library-components/icons.tsx +692 -0
- package/src/library-components/niceStringify.ts +50 -0
- package/src/library-components/renderToString.ts +52 -0
- package/src/misc/PromiseRace.ts +101 -0
- package/src/misc/color.ts +30 -0
- package/src/misc/getParentProcessId.cs +53 -0
- package/src/misc/getParentProcessId.ts +53 -0
- package/src/misc/hash.ts +83 -0
- package/src/misc/ipPong.js +13 -0
- package/src/misc/networking.ts +2 -0
- package/src/misc/random.ts +45 -0
- package/src/misc.ts +19 -0
- package/src/noserverhotreload.flag +0 -0
- package/src/path.ts +226 -0
- package/src/persistentLocalStore.ts +37 -0
- package/src/promise.ts +15 -0
- package/src/server.ts +73 -0
- package/src/src.d.ts +1 -0
- package/src/test/heapProcess.ts +36 -0
- package/src/test/mongoSatTest.tsx +55 -0
- package/src/test/satTest.ts +193 -0
- package/src/test/test.tsx +552 -0
- package/src/zip.ts +92 -0
- package/src/zipThreaded.ts +106 -0
- package/src/zipThreadedWorker.js +19 -0
- package/tsconfig.json +27 -0
- 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
|
+
}
|