querysub 0.152.0 → 0.154.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/package.json +6 -6
- package/src/-b-authorities/cloudflareHelpers.ts +11 -2
- package/src/3-path-functions/PathFunctionRunner.ts +168 -97
- package/src/3-path-functions/PathFunctionRunnerMain.ts +8 -2
- package/src/3-path-functions/pathFunctionLoader.ts +11 -6
- package/src/3-path-functions/syncSchema.ts +10 -1
- package/src/4-deploy/edgeBootstrap.ts +10 -1
- package/src/4-querysub/Querysub.ts +77 -3
- package/src/4-querysub/QuerysubController.ts +22 -2
- package/src/4-querysub/permissions.ts +33 -2
- package/src/4-querysub/querysubPrediction.ts +52 -18
- package/src/archiveapps/archiveGCEntry.tsx +38 -0
- package/src/archiveapps/archiveJoinEntry.ts +121 -0
- package/src/archiveapps/archiveMergeEntry.tsx +47 -0
- package/src/archiveapps/compressTest.tsx +59 -0
- package/src/archiveapps/lockTest.ts +127 -0
- package/src/config.ts +5 -0
- package/src/diagnostics/managementPages.tsx +55 -0
- package/src/diagnostics/misc-pages/ArchiveInspect.tsx +325 -0
- package/src/diagnostics/misc-pages/ArchiveViewer.tsx +781 -0
- package/src/diagnostics/misc-pages/ArchiveViewerTable.tsx +156 -0
- package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +573 -0
- package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +129 -0
- package/src/diagnostics/misc-pages/LocalWatchViewer.tsx +431 -0
- package/src/diagnostics/misc-pages/RequireAuditPage.tsx +218 -0
- package/src/diagnostics/misc-pages/SnapshotViewer.tsx +206 -0
- package/src/diagnostics/misc-pages/TimeRangeView.tsx +648 -0
- package/src/diagnostics/misc-pages/archiveViewerFilter.tsx +221 -0
- package/src/diagnostics/misc-pages/archiveViewerShared.tsx +76 -0
- package/src/email/postmark.tsx +40 -0
- package/src/email/sendgrid.tsx +44 -0
- package/src/functional/UndoWatch.tsx +133 -0
- package/src/functional/diff.ts +858 -0
- package/src/functional/promiseCache.ts +67 -0
- package/src/functional/random.ts +9 -0
- package/src/functional/runCommand.ts +42 -0
- package/src/functional/runOnce.ts +7 -0
- package/src/functional/stats.ts +61 -0
- package/src/functional/throttleRerender.tsx +80 -0
- package/src/library-components/AspectSizedComponent.tsx +88 -0
- package/src/library-components/Histogram.tsx +338 -0
- package/src/library-components/InlinePopup.tsx +67 -0
- package/src/library-components/Notifications.tsx +153 -0
- package/src/library-components/RenderIfVisible.tsx +80 -0
- package/src/library-components/SimpleNotification.tsx +133 -0
- package/src/library-components/TabbedUI.tsx +39 -0
- package/src/library-components/animateAnyElement.tsx +65 -0
- package/src/library-components/errorNotifications.tsx +81 -0
- package/src/library-components/placeholder.ts +18 -0
- package/src/misc/format2.ts +48 -0
- package/src/misc.ts +33 -0
- package/src/misc2.ts +5 -0
- package/src/server.ts +2 -1
- package/src/storage/diskCache.ts +227 -0
- package/src/storage/diskCache2.ts +122 -0
- package/src/storage/fileSystemPointer.ts +72 -0
- package/src/user-implementation/LoginPage.tsx +78 -0
- package/src/user-implementation/RequireAuditPage.tsx +219 -0
- package/src/user-implementation/SecurityPage.tsx +212 -0
- package/src/user-implementation/UserPage.tsx +320 -0
- package/src/user-implementation/addSuperUser.ts +21 -0
- package/src/user-implementation/canSeeSource.ts +41 -0
- package/src/user-implementation/loginEmail.tsx +159 -0
- package/src/user-implementation/setEmailKey.ts +20 -0
- package/src/user-implementation/userData.ts +974 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import preact from "preact"; import { qreact } from "../4-dom/qreact";
|
|
2
|
+
import { formatNumber } from "socket-function/src/formatting/format";
|
|
3
|
+
import { css } from "typesafecss";
|
|
4
|
+
import { URLParam } from "../library-components/URLParam";
|
|
5
|
+
import { Anchor } from "../library-components/ATag";
|
|
6
|
+
import { performDrag2 } from "../library-components/drag";
|
|
7
|
+
import { atomic, atomicObjectSymbol, atomicObjectWrite } from "../2-proxy/PathValueProxyWatcher";
|
|
8
|
+
import { getProxyPath } from "../2-proxy/pathValueProxy";
|
|
9
|
+
import { authorityStorage } from "../0-path-value-core/pathValueCore";
|
|
10
|
+
import { addEpsilons } from "../bits";
|
|
11
|
+
import { Querysub } from "../4-querysub/QuerysubController";
|
|
12
|
+
import { MeasuredDiv } from "../library-components/MeasuredDiv";
|
|
13
|
+
|
|
14
|
+
export type ColumnDesc = {
|
|
15
|
+
title?: string;
|
|
16
|
+
format?: (value: number, context: { maxAbsoluteValue: number; long?: boolean }) => preact.ComponentChild;
|
|
17
|
+
link?: (range: { xStart: number; xEnd: number; }) => { param: URLParam; value: unknown; }[];
|
|
18
|
+
};
|
|
19
|
+
export type XYValue = { x: number; y: number };
|
|
20
|
+
export class Histogram extends qreact.Component<{
|
|
21
|
+
title?: string;
|
|
22
|
+
className?: string;
|
|
23
|
+
values: XYValue[];
|
|
24
|
+
xColumn?: ColumnDesc;
|
|
25
|
+
yColumn?: ColumnDesc;
|
|
26
|
+
noXAxisRounding?: boolean;
|
|
27
|
+
|
|
28
|
+
barWidth?: number;
|
|
29
|
+
barGap?: number;
|
|
30
|
+
yLabelCount?: number;
|
|
31
|
+
pixelsPerLabel?: number;
|
|
32
|
+
}> {
|
|
33
|
+
state = {
|
|
34
|
+
width: window.innerWidth,
|
|
35
|
+
height: 500,
|
|
36
|
+
yAxisWidth: 100,
|
|
37
|
+
dragging: undefined as {
|
|
38
|
+
xStart: number;
|
|
39
|
+
xEnd: number;
|
|
40
|
+
} | undefined,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
render() {
|
|
44
|
+
let { className, values, xColumn, yColumn, noXAxisRounding } = this.props;
|
|
45
|
+
if (values.length === 0) return <div class={this.props.className + css.center}>No data</div>;
|
|
46
|
+
let width = Math.max(1, this.state.width);
|
|
47
|
+
let yAxisWidth = this.state.yAxisWidth;
|
|
48
|
+
const barSpacing = this.props.barGap ?? 2;
|
|
49
|
+
// In reality the width will be less, as some space will be taken up by the axis labels
|
|
50
|
+
let barWidth = this.props.barWidth ?? 20;
|
|
51
|
+
let barCount = Math.ceil((width - yAxisWidth) / (barWidth + barSpacing));
|
|
52
|
+
const pixelsPerLabel = this.props.pixelsPerLabel ?? 100;
|
|
53
|
+
let barsPerLabel = Math.ceil(pixelsPerLabel / (barWidth + barSpacing));
|
|
54
|
+
// The bars have to align with our labels, otherwise they can't be equally spaced,
|
|
55
|
+
// and that is the only spacing mode flex supports!
|
|
56
|
+
let barCountDelta = (barCount % barsPerLabel);
|
|
57
|
+
barCount -= barCountDelta;
|
|
58
|
+
barCount = Math.max(barsPerLabel, barCount);
|
|
59
|
+
// Recalculate the width
|
|
60
|
+
barWidth = (width - yAxisWidth) / barCount - barSpacing;
|
|
61
|
+
const yLabelCount = this.props.yLabelCount ?? 10;
|
|
62
|
+
|
|
63
|
+
// Values test code
|
|
64
|
+
// values = [];
|
|
65
|
+
// let rand = getSeededRandom(7);
|
|
66
|
+
// for (let i = 0; i < 1000; i++) {
|
|
67
|
+
// values.push({ x: i + 500, y: rand() * 1000 });
|
|
68
|
+
// }
|
|
69
|
+
// xColumn = { title: "X" };
|
|
70
|
+
// yColumn = { title: "Y" };
|
|
71
|
+
|
|
72
|
+
let minX = Math.min(...values.map((v) => v.x));
|
|
73
|
+
let maxX = Math.max(...values.map((v) => v.x));
|
|
74
|
+
if (maxX === minX) {
|
|
75
|
+
maxX = minX + 1;
|
|
76
|
+
}
|
|
77
|
+
// We often use maxX as the end, instead of the max, so... make it a bit bigger, so that works.
|
|
78
|
+
maxX = addEpsilons(maxX, 1);
|
|
79
|
+
let xIncrement = Math.ceil((maxX - minX) / barCount);
|
|
80
|
+
/** Get a nice number, https://stackoverflow.com/a/16363437/1117119, https://gist.github.com/igodorogea/4f42a95ea31414c3a755a8b202676dfd
|
|
81
|
+
* - Modified to handle negative numbers
|
|
82
|
+
* - Always returns a value >= num, unless num is < 0, then returns a number <= num.
|
|
83
|
+
*/
|
|
84
|
+
function getNiceNumber(num: number): number {
|
|
85
|
+
if (num < 0) return -getNiceNumber(-num);
|
|
86
|
+
let zeros = Math.floor(Math.log10(num));
|
|
87
|
+
if (zeros === Number.NEGATIVE_INFINITY) return 1;
|
|
88
|
+
let fraction = num / 10 ** zeros;
|
|
89
|
+
if (fraction <= 1) fraction = 1;
|
|
90
|
+
else if (fraction <= 2) fraction = 2;
|
|
91
|
+
else if (fraction <= 5) fraction = 5;
|
|
92
|
+
else fraction = 10;
|
|
93
|
+
return fraction * 10 ** zeros;
|
|
94
|
+
}
|
|
95
|
+
if (!noXAxisRounding) {
|
|
96
|
+
xIncrement = getNiceNumber(xIncrement);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Round to xIncrement
|
|
100
|
+
minX = Math.floor(minX / xIncrement) * xIncrement;
|
|
101
|
+
maxX = Math.ceil(maxX / xIncrement) * xIncrement;
|
|
102
|
+
|
|
103
|
+
// xIncrement MIGHT not cover the range we need (as we extended the range).
|
|
104
|
+
// So just add bars in increments of barsParLabel. This shrinks the bar pixel width, but... it should be fine.
|
|
105
|
+
while (barCount * xIncrement <= (maxX - minX)) {
|
|
106
|
+
barCount += barsPerLabel;
|
|
107
|
+
}
|
|
108
|
+
if (noXAxisRounding) {
|
|
109
|
+
xIncrement = (maxX - minX) / barCount;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let xBuckets: {
|
|
113
|
+
xStart: number;
|
|
114
|
+
xEnd: number;
|
|
115
|
+
sum: number;
|
|
116
|
+
}[] = [];
|
|
117
|
+
for (let i = 0; i < barCount; i++) {
|
|
118
|
+
xBuckets.push({
|
|
119
|
+
xStart: minX + i * xIncrement,
|
|
120
|
+
xEnd: minX + (i + 1) * xIncrement,
|
|
121
|
+
sum: 0,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function getBucketIndex(xValue: number) {
|
|
125
|
+
return Math.floor((xValue - minX) / xIncrement);
|
|
126
|
+
}
|
|
127
|
+
for (let value of values) {
|
|
128
|
+
let bucketIndex = getBucketIndex(value.x);
|
|
129
|
+
xBuckets[bucketIndex].sum += value.y;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let minY = Math.min(...xBuckets.map((v) => v.sum));
|
|
133
|
+
let maxY = Math.max(...xBuckets.map((v) => v.sum));
|
|
134
|
+
maxY = getNiceNumber(maxY);
|
|
135
|
+
// Always try to start at zero
|
|
136
|
+
if (minY / (maxY - minY) < 0.35) {
|
|
137
|
+
minY = 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let yIncrement = getNiceNumber((maxY - minY) / yLabelCount);
|
|
141
|
+
let yLabelValues: number[] = [];
|
|
142
|
+
for (let i = 0; i < yLabelCount; i++) {
|
|
143
|
+
yLabelValues.push(minY + i * yIncrement);
|
|
144
|
+
}
|
|
145
|
+
yLabelValues.reverse();
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
let maxAbsoluteY = Math.max(Math.abs(minY), Math.abs(maxY));
|
|
149
|
+
let maxAbsoluteX = Math.max(Math.abs(minX), Math.abs(maxX));
|
|
150
|
+
|
|
151
|
+
function formatX(value: number) {
|
|
152
|
+
return xColumn?.format?.(value, { maxAbsoluteValue: maxAbsoluteX }) ?? formatNumber(value);
|
|
153
|
+
}
|
|
154
|
+
function formatXLong(value: number) {
|
|
155
|
+
return xColumn?.format?.(value, { maxAbsoluteValue: maxAbsoluteX, long: true }) ?? formatNumber(value);
|
|
156
|
+
}
|
|
157
|
+
function formatY(value: number) {
|
|
158
|
+
return yColumn?.format?.(value, { maxAbsoluteValue: maxAbsoluteY }) ?? formatNumber(value);
|
|
159
|
+
}
|
|
160
|
+
function formatYLong(value: number) {
|
|
161
|
+
return yColumn?.format?.(value, { maxAbsoluteValue: maxAbsoluteY, long: true }) ?? formatNumber(value);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// NOTE: Y Axis labels are less accurately positioned (especially if some have wrapping text, and others don't)
|
|
165
|
+
// but... it should be fine. The x axis labels are the ones that really need to be accurate, because the users will
|
|
166
|
+
// be looking at specific bars.
|
|
167
|
+
let yAxisLabels = yLabelValues.map((yValue, i) => {
|
|
168
|
+
i = yLabelCount - i - 1;
|
|
169
|
+
let frac = 1 - i / (yLabelCount - 1);
|
|
170
|
+
let pos = frac === 0 ? "0%" as const : `calc(${frac * 100}% - 2px)` as const;
|
|
171
|
+
return (
|
|
172
|
+
<div class={
|
|
173
|
+
css.maxWidth("30vw").height("100%").hbox0.relative
|
|
174
|
+
}>
|
|
175
|
+
<div class={css.relative.top(`${80 * (frac - 0.5)}%`)}>
|
|
176
|
+
{formatY(yValue)}
|
|
177
|
+
</div>
|
|
178
|
+
{/* <div class={css.fillHeight.relative.marginLeft(4)}>
|
|
179
|
+
<div class={css.relative.pos(0, pos).size(4, 1).hsla(0, 0, 7, 0.4).display("block").marginTop(1)} />
|
|
180
|
+
</div> */}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
let xAxisLabels: preact.ComponentChild[] = [];
|
|
185
|
+
for (let i = 0; i < barCount; i += barsPerLabel) {
|
|
186
|
+
let firstBucket = xBuckets[i];
|
|
187
|
+
let lastBucket = xBuckets[i + barsPerLabel - 1];
|
|
188
|
+
let middleXValue = (firstBucket.xStart + lastBucket.xEnd) / 2;
|
|
189
|
+
if (i === 0) {
|
|
190
|
+
middleXValue = firstBucket.xStart;
|
|
191
|
+
} else if (i === barCount - barsPerLabel) {
|
|
192
|
+
middleXValue = lastBucket.xEnd;
|
|
193
|
+
}
|
|
194
|
+
let link = xColumn?.link?.({ xStart: firstBucket.xStart, xEnd: lastBucket.xEnd }) || [];
|
|
195
|
+
xAxisLabels.push(
|
|
196
|
+
<div class={
|
|
197
|
+
css.relative
|
|
198
|
+
.width("100%")
|
|
199
|
+
.vbox0
|
|
200
|
+
.alignCenter
|
|
201
|
+
.offsetx(`calc(${barWidth * 0.5}px - 50%)`)
|
|
202
|
+
}>
|
|
203
|
+
{/* NOTE: Our ticks are off again, so... just don't show them. */}
|
|
204
|
+
{/* <div class={css.offsetx("-50%").size(1, 4).hsla(0, 0, 7, 0.4).display("block").marginTop(1)} /> */}
|
|
205
|
+
<Anchor
|
|
206
|
+
class={
|
|
207
|
+
css
|
|
208
|
+
.pad2(10, 0)
|
|
209
|
+
.boxSizing("content-box")
|
|
210
|
+
.textAlign("center")
|
|
211
|
+
}
|
|
212
|
+
values={link}
|
|
213
|
+
>
|
|
214
|
+
{formatX(middleXValue)}
|
|
215
|
+
</Anchor>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let totalY = xBuckets.reduce((sum, v) => sum + v.sum, 0);
|
|
221
|
+
const renderBucket = (bucket: { xStart: number; xEnd: number; sum: number }) => {
|
|
222
|
+
let barEnd = (bucket.sum - minY) / (maxY - minY);
|
|
223
|
+
let barStart = 0;
|
|
224
|
+
// If we have a value, but it's very small, increase it, so the bar is visible
|
|
225
|
+
if (0 < barEnd && barEnd < 0.01) {
|
|
226
|
+
// We want it to be 1 pixel high. Our bar height is less than this.state.height,
|
|
227
|
+
// but... as long as this is > 0.5 pixels, it should be visible.
|
|
228
|
+
barEnd = 1 / this.state.height;
|
|
229
|
+
}
|
|
230
|
+
if (minY < 0) {
|
|
231
|
+
barStart = (0 - minY) / (maxY - minY);
|
|
232
|
+
}
|
|
233
|
+
if (barEnd < barStart) {
|
|
234
|
+
[barStart, barEnd] = [barEnd, barStart];
|
|
235
|
+
}
|
|
236
|
+
let link = xColumn?.link?.({ xStart: bucket.xStart, xEnd: bucket.xEnd }) || [];
|
|
237
|
+
let drag = atomic(this.state.dragging);
|
|
238
|
+
let center = (bucket.xStart + bucket.xEnd) / 2;
|
|
239
|
+
let draggedOver = drag && drag.xStart <= center && center <= drag.xEnd;
|
|
240
|
+
return (
|
|
241
|
+
<Anchor
|
|
242
|
+
class={
|
|
243
|
+
css
|
|
244
|
+
.size(barWidth, "100%")
|
|
245
|
+
.relative
|
|
246
|
+
.background(`hsla(0, 0%, 100%, 0.2)`, "hover")
|
|
247
|
+
.userSelect("none").filter("brightness(1.1)", "hover")
|
|
248
|
+
.fillWidth
|
|
249
|
+
.flexShrink(100000000)
|
|
250
|
+
+ (draggedOver && css.background("hsla(0, 0%, 100%, 0.4)"))
|
|
251
|
+
}
|
|
252
|
+
title={`${formatYLong(bucket.sum)} at ${formatXLong(bucket.xStart)} to ${formatXLong(bucket.xEnd)}`}
|
|
253
|
+
values={link}
|
|
254
|
+
onMouseDown={(e) => {
|
|
255
|
+
e.preventDefault();
|
|
256
|
+
const self = this;
|
|
257
|
+
self.state.dragging = atomicObjectWrite({ xStart: bucket.xStart, xEnd: bucket.xEnd });
|
|
258
|
+
performDrag2({
|
|
259
|
+
e,
|
|
260
|
+
slop: 5,
|
|
261
|
+
onFinally(apply) {
|
|
262
|
+
let dragState = atomic(self.state.dragging);
|
|
263
|
+
self.state.dragging = undefined;
|
|
264
|
+
if (dragState && apply) {
|
|
265
|
+
let xStart = dragState.xStart;
|
|
266
|
+
let xEnd = dragState.xEnd;
|
|
267
|
+
let link = xColumn?.link?.({ xStart, xEnd }) || [];
|
|
268
|
+
for (let value of link) {
|
|
269
|
+
value.param.value = value.value;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}}
|
|
275
|
+
onMouseOver={e => {
|
|
276
|
+
let dragging = atomic(this.state.dragging);
|
|
277
|
+
if (dragging) {
|
|
278
|
+
let xStart = Math.min(dragging.xStart, bucket.xStart);
|
|
279
|
+
let xEnd = Math.max(dragging.xEnd, bucket.xEnd);
|
|
280
|
+
this.state.dragging = atomicObjectWrite({ xStart, xEnd });
|
|
281
|
+
}
|
|
282
|
+
}}
|
|
283
|
+
>
|
|
284
|
+
<div class={
|
|
285
|
+
css
|
|
286
|
+
.absolute
|
|
287
|
+
.size("100%", `${(barEnd - barStart) * 100}%`)
|
|
288
|
+
.pos(0, `${(1 - barEnd) * 100}%`)
|
|
289
|
+
.hsl(240, 50, 75).vbox0.justifyContent("flex-end")
|
|
290
|
+
}>
|
|
291
|
+
</div>
|
|
292
|
+
</Anchor>
|
|
293
|
+
);
|
|
294
|
+
};
|
|
295
|
+
return (
|
|
296
|
+
<MeasuredDiv
|
|
297
|
+
onNewSize={(width, height, elem) => {
|
|
298
|
+
this.state.width = width;
|
|
299
|
+
this.state.height = height;
|
|
300
|
+
}}
|
|
301
|
+
class={
|
|
302
|
+
(className || css.size("100%", "100%"))
|
|
303
|
+
+ css.hsl(75, 50, 75)
|
|
304
|
+
// NOTE: We might need scroll bars if the axis labels are too big (too much text).
|
|
305
|
+
+ css.overflowAuto.vbox0
|
|
306
|
+
}
|
|
307
|
+
>
|
|
308
|
+
<div class={css.alignSelf("center").margins2(10, 4).fontSize(16)}>
|
|
309
|
+
{this.props.title}
|
|
310
|
+
{this.props.title && " ("}
|
|
311
|
+
{
|
|
312
|
+
yColumn && yColumn && `${formatY(totalY)} ${yColumn.title} vs ${xColumn?.title}`
|
|
313
|
+
|| yColumn?.title && `${formatY(totalY)} ${yColumn.title}`
|
|
314
|
+
|| xColumn?.title
|
|
315
|
+
}
|
|
316
|
+
{this.props.title && ")"}
|
|
317
|
+
</div>
|
|
318
|
+
<div class={
|
|
319
|
+
css
|
|
320
|
+
.display("grid").gridTemplateColumns("auto 1fr").gridTemplateRows("1fr auto")
|
|
321
|
+
.fillBoth
|
|
322
|
+
.relative
|
|
323
|
+
}>
|
|
324
|
+
<div className={css.vbox0.justifyContent("space-between").flexShrink0.fillHeight.alignEnd.margins2(4, 0)}>
|
|
325
|
+
{yAxisLabels}
|
|
326
|
+
</div>
|
|
327
|
+
<div class={css.hbox(barSpacing).fillBoth.overflowHidden.flexShrink(10000000)}>
|
|
328
|
+
{xBuckets.map(renderBucket)}
|
|
329
|
+
</div>
|
|
330
|
+
<div />
|
|
331
|
+
<div class={css.hbox(barSpacing).alignItems("start").margins2(0, 2)}>
|
|
332
|
+
{xAxisLabels}
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
</MeasuredDiv>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { Querysub } from "../4-querysub/QuerysubController";
|
|
3
|
+
import { qreact } from "../4-dom/qreact";
|
|
4
|
+
import { Button } from "../library-components/Button";
|
|
5
|
+
import { Icon } from "../library-components/icons";
|
|
6
|
+
import { createURLSync } from "./URLParam";
|
|
7
|
+
|
|
8
|
+
const popupURL = createURLSync<string>("popup", "");
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export class InlinePopup extends qreact.Component<{
|
|
12
|
+
type: string;
|
|
13
|
+
margin?: number;
|
|
14
|
+
}> {
|
|
15
|
+
render() {
|
|
16
|
+
if (popupURL.value !== this.props.type) return undefined;
|
|
17
|
+
let margin = this.props.margin ?? 100;
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
style={{
|
|
21
|
+
position: "fixed",
|
|
22
|
+
left: 0, top: 0,
|
|
23
|
+
width: "100vw", height: "100vh",
|
|
24
|
+
zIndex: 1000,
|
|
25
|
+
}}
|
|
26
|
+
onClick={e => qreact.isTarget(e) && (popupURL.value = "")}
|
|
27
|
+
>
|
|
28
|
+
<div style={{
|
|
29
|
+
margin,
|
|
30
|
+
position: "relative",
|
|
31
|
+
zIndex: 1,
|
|
32
|
+
}}>
|
|
33
|
+
<Button
|
|
34
|
+
hotkeys={["Escape"]}
|
|
35
|
+
showHotkeys
|
|
36
|
+
onClick={() => popupURL.value = ""}
|
|
37
|
+
style={{
|
|
38
|
+
position: "absolute",
|
|
39
|
+
right: 0, top: -10,
|
|
40
|
+
transform: "translate(0%, -100%)",
|
|
41
|
+
border: "1px solid hsl(0, 0%, 7%)",
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
{Icon.close()}
|
|
45
|
+
</Button>
|
|
46
|
+
<div style={{
|
|
47
|
+
maxHeight: `calc(100vh - ${margin * 2}px)`,
|
|
48
|
+
overflow: "auto",
|
|
49
|
+
}}>
|
|
50
|
+
{this.props.children}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div
|
|
54
|
+
className="button"
|
|
55
|
+
style={{
|
|
56
|
+
position: "absolute",
|
|
57
|
+
left: 0, top: 0,
|
|
58
|
+
width: "100%", height: "100%",
|
|
59
|
+
background: "hsl(0, 0%, 50%, 0.5)",
|
|
60
|
+
zIndex: 0,
|
|
61
|
+
}}
|
|
62
|
+
onClick={e => popupURL.value = ""}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import preact from "preact";
|
|
2
|
+
import { showModal } from "../5-diagnostics/Modal";
|
|
3
|
+
import { lazy } from "socket-function/src/caching";
|
|
4
|
+
import { Querysub } from "../4-querysub/Querysub";
|
|
5
|
+
import { nextId, sort } from "socket-function/src/misc";
|
|
6
|
+
import { atomicObjectWrite } from "../2-proxy/PathValueProxyWatcher";
|
|
7
|
+
import { css } from "../4-dom/css";
|
|
8
|
+
import { qreact } from "../4-dom/qreact";
|
|
9
|
+
import { yellow } from "socket-function/src/formatting/logColors";
|
|
10
|
+
|
|
11
|
+
export type NotifyPos = "topLeft" | "topRight" | "bottomLeft" | "bottomRight";
|
|
12
|
+
|
|
13
|
+
let nextSeqNum = 1;
|
|
14
|
+
interface NotifySchema {
|
|
15
|
+
id?: string;
|
|
16
|
+
seqNum: number;
|
|
17
|
+
notification: preact.ComponentChild;
|
|
18
|
+
pos: NotifyPos;
|
|
19
|
+
}
|
|
20
|
+
let notifySchema = Querysub.createLocalSchema<{
|
|
21
|
+
notifications: {
|
|
22
|
+
[seqNum: number]: NotifySchema;
|
|
23
|
+
};
|
|
24
|
+
}>("notifications");
|
|
25
|
+
|
|
26
|
+
class Notifications extends qreact.Component {
|
|
27
|
+
render() {
|
|
28
|
+
let notes = Object.values(notifySchema().notifications);
|
|
29
|
+
let byPos = new Map<NotifyPos, NotifySchema[]>();
|
|
30
|
+
for (let note of notes) {
|
|
31
|
+
let pos = note.pos;
|
|
32
|
+
if (!byPos.has(pos)) byPos.set(pos, []);
|
|
33
|
+
byPos.get(pos)!.push(note);
|
|
34
|
+
}
|
|
35
|
+
return <>
|
|
36
|
+
{Array.from(byPos.entries()).map(([pos, notes]) => {
|
|
37
|
+
sort(notes, note => note.seqNum);
|
|
38
|
+
let style: preact.JSX.CSSProperties = {};
|
|
39
|
+
if (pos === "topLeft") {
|
|
40
|
+
style.top = 0;
|
|
41
|
+
style.left = 0;
|
|
42
|
+
}
|
|
43
|
+
if (pos === "topRight") {
|
|
44
|
+
style.top = 0;
|
|
45
|
+
style.right = 0;
|
|
46
|
+
}
|
|
47
|
+
if (pos === "bottomLeft") {
|
|
48
|
+
style.bottom = 0;
|
|
49
|
+
style.left = 0;
|
|
50
|
+
}
|
|
51
|
+
if (pos === "bottomRight") {
|
|
52
|
+
style.bottom = 0;
|
|
53
|
+
style.right = 0;
|
|
54
|
+
}
|
|
55
|
+
let usedIds = new Set<string>();
|
|
56
|
+
notes = notes.filter(note => {
|
|
57
|
+
if (!note.id) return true;
|
|
58
|
+
if (usedIds.has(note.id)) return false;
|
|
59
|
+
usedIds.add(note.id);
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
return <div
|
|
63
|
+
class={
|
|
64
|
+
css.position("fixed").vbox(10).pad(20)
|
|
65
|
+
.zIndex(1000000)
|
|
66
|
+
}
|
|
67
|
+
style={style}
|
|
68
|
+
>
|
|
69
|
+
{notes.slice(0, 10).map(note =>
|
|
70
|
+
<CloseWrapper key={note.seqNum} seqNum={note.seqNum}>
|
|
71
|
+
{note.notification}
|
|
72
|
+
</CloseWrapper>
|
|
73
|
+
)}
|
|
74
|
+
{notes.length > 10 && <div class={css.hsl(0, 0, 50).pad2(4, 2)}>
|
|
75
|
+
<div>And {notes.length - 10} more notifications</div>
|
|
76
|
+
</div>}
|
|
77
|
+
</div>;
|
|
78
|
+
})}
|
|
79
|
+
</>;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
class CloseWrapper extends qreact.Component<{ seqNum: number }> {
|
|
83
|
+
render() {
|
|
84
|
+
return this.props.children;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
let ensureNotifications = lazy(() => {
|
|
88
|
+
showModal({ content: <Notifications /> });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** Allows capturing the current state so the notification can be closed later on */
|
|
92
|
+
export function createCloseCurrentNotification() {
|
|
93
|
+
const ancestor = qreact.getAncestor(CloseWrapper);
|
|
94
|
+
if (!ancestor) {
|
|
95
|
+
console.warn(yellow(`Cannot close current Notification as we are not presently inside of a Notification.`));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
let seqNum = ancestor.data().props.seqNum as number;
|
|
99
|
+
return () => {
|
|
100
|
+
Querysub.commit(() => {
|
|
101
|
+
// Close all, as we are getting too much notify spam
|
|
102
|
+
for (let key in notifySchema().notifications) {
|
|
103
|
+
delete notifySchema().notifications[key];
|
|
104
|
+
}
|
|
105
|
+
// delete notifySchema().notifications[seqNum];
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function closeCurrentNotification() {
|
|
111
|
+
const ancestor = qreact.getAncestor(CloseWrapper);
|
|
112
|
+
if (!ancestor) {
|
|
113
|
+
console.warn(yellow(`Cannot close current Notification as we are not presently inside of a Notification.`));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
Querysub.commit(() => {
|
|
117
|
+
// Close all, as we are getting too much notify spam
|
|
118
|
+
for (let key in notifySchema().notifications) {
|
|
119
|
+
delete notifySchema().notifications[key];
|
|
120
|
+
}
|
|
121
|
+
//delete notifySchema().notifications[ancestor.data().props.seqNum as number];
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function showNotification(config: {
|
|
126
|
+
notification: preact.ComponentChild;
|
|
127
|
+
pos: NotifyPos;
|
|
128
|
+
timeout?: number;
|
|
129
|
+
id?: string;
|
|
130
|
+
}): {
|
|
131
|
+
close: () => void;
|
|
132
|
+
} {
|
|
133
|
+
let { notification, pos, timeout, id } = config;
|
|
134
|
+
ensureNotifications();
|
|
135
|
+
let seqNum = nextSeqNum++;
|
|
136
|
+
Querysub.commit(() => {
|
|
137
|
+
notifySchema().notifications[seqNum] = atomicObjectWrite({
|
|
138
|
+
notification,
|
|
139
|
+
pos,
|
|
140
|
+
seqNum,
|
|
141
|
+
id,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
function close() {
|
|
145
|
+
Querysub.commit(() => {
|
|
146
|
+
delete notifySchema().notifications[seqNum];
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (timeout) {
|
|
150
|
+
setTimeout(close, timeout);
|
|
151
|
+
}
|
|
152
|
+
return { close };
|
|
153
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
|
|
2
|
+
import preact from "preact";
|
|
3
|
+
import { VirtualDOM, qreact } from "../4-dom/qreact";
|
|
4
|
+
import { Querysub } from "../4-querysub/Querysub";
|
|
5
|
+
import { cacheLimited, cacheWeak, lazy } from "socket-function/src/caching";
|
|
6
|
+
import { nextId, throttleFunction } from "socket-function/src/misc";
|
|
7
|
+
|
|
8
|
+
let data = Querysub.createLocalSchema<{
|
|
9
|
+
visibleIds: {
|
|
10
|
+
[key: string]: boolean;
|
|
11
|
+
}
|
|
12
|
+
}>("RenderIfVisible");
|
|
13
|
+
|
|
14
|
+
let getIntersectionObserver = cacheWeak((root: HTMLElement) => {
|
|
15
|
+
return new IntersectionObserver(entries => {
|
|
16
|
+
let visibleCount = 0;
|
|
17
|
+
Querysub.localCommit(() => {
|
|
18
|
+
for (let entry of entries) {
|
|
19
|
+
let id = entry.target.getAttribute("data-intersection-id")!;
|
|
20
|
+
let isVisible = !!entry.isIntersecting;
|
|
21
|
+
data().visibleIds[id] = isVisible;
|
|
22
|
+
if (isVisible) {
|
|
23
|
+
visibleCount++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
console.log("Visible count", visibleCount);
|
|
28
|
+
}, {
|
|
29
|
+
root,
|
|
30
|
+
rootMargin: "2000px",
|
|
31
|
+
// TODO: Scrolling breaks this slightly. We need to add a fixed position
|
|
32
|
+
// div inside of the scroll div, and use that as the root. Maybe?
|
|
33
|
+
// - See https://stackoverflow.com/questions/58622664/intersectionobserver-how-rootmargin-work/58625634#58625634
|
|
34
|
+
// - Basically... we can't know the position of the images outside of the scrolled div,
|
|
35
|
+
// because they are cutoff by the scrolling, which hides their true position
|
|
36
|
+
// from the IntersectionObserver. Or something...
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** If not visible, wipes out the children. We still need to render something,
|
|
41
|
+
* otherwise we can't know if it would be visible or not!
|
|
42
|
+
* The size of the element shouldn't depend on it's children, otherwise it will move
|
|
43
|
+
* around, and possibly get an infinite loop of re-renders.
|
|
44
|
+
*/
|
|
45
|
+
export class RenderIfVisible extends qreact.Component<preact.JSX.HTMLAttributes<HTMLDivElement> & {
|
|
46
|
+
uniqueKey: string;
|
|
47
|
+
overrideType?: string;
|
|
48
|
+
root?: () => HTMLElement | undefined | null;
|
|
49
|
+
replacement?: VirtualDOM;
|
|
50
|
+
forceShow?: boolean;
|
|
51
|
+
}> {
|
|
52
|
+
id = nextId();
|
|
53
|
+
lastElement: HTMLElement | null = null;
|
|
54
|
+
observer() {
|
|
55
|
+
return getIntersectionObserver(this.props.root?.() ?? document.body);
|
|
56
|
+
}
|
|
57
|
+
componentWillUnmount(): void {
|
|
58
|
+
if (this.lastElement) {
|
|
59
|
+
this.observer().unobserve(this.lastElement);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
render() {
|
|
63
|
+
let props = { ...this.props };
|
|
64
|
+
if (!this.props.forceShow && !data().visibleIds[this.id]) {
|
|
65
|
+
props.children = undefined;
|
|
66
|
+
}
|
|
67
|
+
return qreact.createElement(this.props.overrideType ?? "div", {
|
|
68
|
+
key: this.props.uniqueKey,
|
|
69
|
+
...props,
|
|
70
|
+
"data-intersection-id": this.id,
|
|
71
|
+
ref2: (elem: HTMLElement) => {
|
|
72
|
+
if (this.lastElement) {
|
|
73
|
+
this.observer().unobserve(this.lastElement);
|
|
74
|
+
}
|
|
75
|
+
this.lastElement = elem;
|
|
76
|
+
this.observer().observe(elem);
|
|
77
|
+
}
|
|
78
|
+
} as any);
|
|
79
|
+
}
|
|
80
|
+
}
|