querysub 0.349.0 → 0.350.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 +1 -1
- package/src/diagnostics/logs/FastArchiveAppendable.ts +6 -1
- package/src/diagnostics/logs/FastArchiveViewer.tsx +33 -13
- package/src/diagnostics/logs/LogViewer2.tsx +91 -2
- package/src/diagnostics/logs/errorNotifications/errorWatchEntry.tsx +6 -1
- package/src/diagnostics/logs/logViewerExtractField.ts +37 -0
- package/src/library-components/InputPicker.tsx +15 -2
package/package.json
CHANGED
|
@@ -35,6 +35,7 @@ import { assertIsNetworkTrusted } from "../../-d-trust/NetworkTrust2";
|
|
|
35
35
|
import { blue, magenta } from "socket-function/src/formatting/logColors";
|
|
36
36
|
import { FileMetadata, FastArchiveAppendableControllerBase, FastArchiveAppendableController, getFileMetadataHash } from "./FastArchiveController";
|
|
37
37
|
import { fsExistsAsync } from "../../fs";
|
|
38
|
+
import { ScanFnc } from "./FastArchiveViewer";
|
|
38
39
|
|
|
39
40
|
// NOTE: In a single command line micro-test it looks like we can write about 40K writes of 500 per once, when using 10X parallel, on a fairly potato server. We should probably batch though, and only do 1X parallel.
|
|
40
41
|
/*
|
|
@@ -360,6 +361,7 @@ export class FastArchiveAppendable<Datum> {
|
|
|
360
361
|
endTime: number;
|
|
361
362
|
};
|
|
362
363
|
cacheBust: number;
|
|
364
|
+
scanFnc?: ScanFnc;
|
|
363
365
|
getWantData?: (file: FileMetadata) => Promise<((posStart: number, posEnd: number, data: Buffer) => boolean) | undefined>;
|
|
364
366
|
onData: (datum: Datum[], file: FileMetadata) => void;
|
|
365
367
|
// Called after onData
|
|
@@ -519,7 +521,7 @@ export class FastArchiveAppendable<Datum> {
|
|
|
519
521
|
let processProgress = await createProgress("Processing (datums)", 0);
|
|
520
522
|
let corruptDatumsProgress = await createProgress("Corrupt Datums (datums)", 0);
|
|
521
523
|
|
|
522
|
-
|
|
524
|
+
let scanFnc = config.scanFnc;
|
|
523
525
|
|
|
524
526
|
|
|
525
527
|
async function downloadAndParseFile(file: FileMetadata, runInner: (code: () => Promise<void>) => Promise<void>) {
|
|
@@ -546,6 +548,9 @@ export class FastArchiveAppendable<Datum> {
|
|
|
546
548
|
scanProgressCount++;
|
|
547
549
|
}
|
|
548
550
|
if (buffer !== "done") {
|
|
551
|
+
if (scanFnc) {
|
|
552
|
+
scanFnc(posStart, posEnd, buffer);
|
|
553
|
+
}
|
|
549
554
|
if (wantData && !wantData(posStart, posEnd, buffer)) {
|
|
550
555
|
notMatchedSize += (posEnd - posStart);
|
|
551
556
|
notMatchedCount++;
|
|
@@ -25,15 +25,22 @@ const RENDER_INTERVAL = 1000;
|
|
|
25
25
|
const HISTOGRAM_RERENDER_INTERVAL = 10000;
|
|
26
26
|
|
|
27
27
|
export const filterParam = new URLParam("filter", "");
|
|
28
|
+
// If filter2 is set, we also need it. Not great (why not support arbitrary counts of these), but for now... this should be fine (and support arbitrary counts might slow down our incredibly optimized scan function, where most of our time is spent).
|
|
29
|
+
// NOTE: This supports less than filterParam (no object parsing, it only does text contains)
|
|
30
|
+
export const filterParam2 = new URLParam("filter2", "");
|
|
31
|
+
|
|
28
32
|
export const cacheBustParam = new URLParam("cacheBust", 0);
|
|
29
33
|
const caseInsensitiveParam = new URLParam("caseInsensitive", false);
|
|
30
34
|
const hideAllDataParam = new URLParam("hideAllData", false);
|
|
31
35
|
|
|
36
|
+
export type ScanFnc = (posStart: number, posEnd: number, data: Buffer) => boolean;
|
|
37
|
+
|
|
32
38
|
export class FastArchiveViewer<T> extends qreact.Component<{
|
|
33
39
|
fastArchives: FastArchiveAppendable<T>[];
|
|
34
40
|
runOnLoad?: boolean;
|
|
35
41
|
onStart: () => MaybePromise<void>;
|
|
36
|
-
|
|
42
|
+
getScanFnc?: () => ScanFnc;
|
|
43
|
+
getWantData?: (file: FileMetadata) => Promise<ScanFnc | undefined>;
|
|
37
44
|
onDatums: (source: FastArchiveAppendable<T>, datums: T[], metadata: FileMetadata) => void;
|
|
38
45
|
// Called after onData
|
|
39
46
|
onStats?: (source: FastArchiveAppendable<T>, stats: DatumStats, metadata: FileMetadata) => void;
|
|
@@ -108,6 +115,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
|
|
|
108
115
|
|
|
109
116
|
@measureFnc
|
|
110
117
|
private async synchronizeData() {
|
|
118
|
+
console.log("synchronizeData");
|
|
111
119
|
let onStart = Querysub.fastRead(() => this.props.onStart);
|
|
112
120
|
let onDatums = Querysub.fastRead(() => this.props.onDatums);
|
|
113
121
|
let onStats = Querysub.fastRead(() => this.props.onStats);
|
|
@@ -144,7 +152,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
|
|
|
144
152
|
|
|
145
153
|
const timeRange = getTimeRange();
|
|
146
154
|
let filterString = filterParam.value;
|
|
147
|
-
|
|
155
|
+
let filterString2 = filterParam2.value;
|
|
148
156
|
let scannedValueCount = 0;
|
|
149
157
|
|
|
150
158
|
// Store current sync parameters BEFORE calling synchronizeData
|
|
@@ -310,6 +318,15 @@ export class FastArchiveViewer<T> extends qreact.Component<{
|
|
|
310
318
|
});
|
|
311
319
|
let { scanMatch, datumMatch } = joinMatches(andMatches, "||");
|
|
312
320
|
|
|
321
|
+
let andMatches2 = filterString2.split("|").map(orParts => {
|
|
322
|
+
let andParts = orParts.split("&");
|
|
323
|
+
let andMatches = andParts.map(part => parsePart(part.trim()));
|
|
324
|
+
return joinMatches(andMatches, "&&");
|
|
325
|
+
});
|
|
326
|
+
let scanObj2 = joinMatches(andMatches2, "||");
|
|
327
|
+
let scanMatch2 = scanObj2.scanMatch;
|
|
328
|
+
let datumMatch2 = scanObj2.datumMatch;
|
|
329
|
+
let useScan2 = !!filterString2.trim();
|
|
313
330
|
|
|
314
331
|
const limitedBuffer = Buffer.from(LOG_LIMIT_FLAG);
|
|
315
332
|
|
|
@@ -318,6 +335,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
|
|
|
318
335
|
const result = await fastArchive.synchronizeData({
|
|
319
336
|
range: timeRange,
|
|
320
337
|
cacheBust: Querysub.fastRead(() => cacheBustParam.value),
|
|
338
|
+
scanFnc: Querysub.fastRead(() => this.props.getScanFnc?.()),
|
|
321
339
|
getWantData: async (file) => {
|
|
322
340
|
let wantData = await getWantData?.(file);
|
|
323
341
|
return (posStart: number, posEnd: number, data: Buffer) => {
|
|
@@ -341,6 +359,9 @@ export class FastArchiveViewer<T> extends qreact.Component<{
|
|
|
341
359
|
|
|
342
360
|
// scanMatch is faster than wantData (generally), so run it first
|
|
343
361
|
let matched = scanMatch(posStart, posEnd, data);
|
|
362
|
+
if (matched && useScan2) {
|
|
363
|
+
matched = scanMatch2(posStart, posEnd, data);
|
|
364
|
+
}
|
|
344
365
|
if (matched && wantData) {
|
|
345
366
|
matched = wantData(posStart, posEnd, data);
|
|
346
367
|
}
|
|
@@ -635,8 +656,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
|
|
|
635
656
|
return warnings;
|
|
636
657
|
}
|
|
637
658
|
|
|
638
|
-
public
|
|
639
|
-
console.log("handleDownload");
|
|
659
|
+
public synchronizeDataThrottled = throttleFunction(500, () => {
|
|
640
660
|
Querysub.onCommitFinished(() => {
|
|
641
661
|
logErrors(this.synchronizeData());
|
|
642
662
|
});
|
|
@@ -683,7 +703,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
|
|
|
683
703
|
label={
|
|
684
704
|
<div className={css.hbox(10)}>
|
|
685
705
|
<Button onClick={() => {
|
|
686
|
-
void this.
|
|
706
|
+
void this.synchronizeDataThrottled();
|
|
687
707
|
}}>
|
|
688
708
|
Run
|
|
689
709
|
</Button>
|
|
@@ -694,10 +714,10 @@ export class FastArchiveViewer<T> extends qreact.Component<{
|
|
|
694
714
|
hot
|
|
695
715
|
flavor="large"
|
|
696
716
|
fillWidth
|
|
697
|
-
onKeyUp={this.
|
|
717
|
+
onKeyUp={this.synchronizeDataThrottled}
|
|
698
718
|
ref2={() => {
|
|
699
719
|
if (this.props.runOnLoad) {
|
|
700
|
-
void this.
|
|
720
|
+
void this.synchronizeDataThrottled();
|
|
701
721
|
}
|
|
702
722
|
}}
|
|
703
723
|
noEnterKeyBlur
|
|
@@ -708,7 +728,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
|
|
|
708
728
|
checkbox
|
|
709
729
|
url={caseInsensitiveParam}
|
|
710
730
|
onChange={() => {
|
|
711
|
-
void this.
|
|
731
|
+
void this.synchronizeDataThrottled();
|
|
712
732
|
}}
|
|
713
733
|
flavor="small"
|
|
714
734
|
/>
|
|
@@ -729,7 +749,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
|
|
|
729
749
|
<div
|
|
730
750
|
className={infoDisplay(30).button}
|
|
731
751
|
onClick={() => {
|
|
732
|
-
void this.
|
|
752
|
+
void this.synchronizeDataThrottled();
|
|
733
753
|
}}
|
|
734
754
|
title={outdatedWarnings.join(", ")}
|
|
735
755
|
>
|
|
@@ -746,7 +766,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
|
|
|
746
766
|
})()}
|
|
747
767
|
{this.state.runCount === 0 && (
|
|
748
768
|
<div className={infoDisplay(200).button} onClick={() => {
|
|
749
|
-
void this.
|
|
769
|
+
void this.synchronizeDataThrottled();
|
|
750
770
|
}}>
|
|
751
771
|
No data downloaded yet. Click here to download data.
|
|
752
772
|
</div>
|
|
@@ -766,7 +786,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
|
|
|
766
786
|
Querysub.commit(() => {
|
|
767
787
|
cacheBustParam.value = Date.now();
|
|
768
788
|
});
|
|
769
|
-
void this.
|
|
789
|
+
void this.synchronizeDataThrottled();
|
|
770
790
|
}}
|
|
771
791
|
>
|
|
772
792
|
<div className={css.hbox(8).alignItems("center").wrap}>
|
|
@@ -783,7 +803,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
|
|
|
783
803
|
<div className={infoDisplay(60).boldStyle.button}
|
|
784
804
|
onClick={() => {
|
|
785
805
|
filterParam.value = LOG_LIMIT_FLAG;
|
|
786
|
-
void this.
|
|
806
|
+
void this.synchronizeDataThrottled();
|
|
787
807
|
}}
|
|
788
808
|
>
|
|
789
809
|
Click here to see {formatNumber(this.limitedScanCount)} scanned logs were rate limited at a file level. This means lines might be missing. If this happens a lot, use pass {`{ [LOG_LINE_LIMIT_ID]: "INSERT RANDOM GUID HERE" }`} in the error/warn message to limit only the spamming line.
|
|
@@ -793,7 +813,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
|
|
|
793
813
|
<div className={infoDisplay(0).boldStyle.button}
|
|
794
814
|
onClick={() => {
|
|
795
815
|
filterParam.value += " & " + LOG_LIMIT_FLAG;
|
|
796
|
-
void this.
|
|
816
|
+
void this.synchronizeDataThrottled();
|
|
797
817
|
}}
|
|
798
818
|
>
|
|
799
819
|
Click here to see {formatNumber(this.limitedMatchCount)} matched logs were rate limited at a file level. This means lines might be missing. If this happens a lot, use pass {`{ [LOG_LINE_LIMIT_ID]: "INSERT RANDOM GUID HERE" }`} in the error/warn message to limit only the spamming line.
|
|
@@ -13,7 +13,7 @@ import { logErrors } from "../../errors";
|
|
|
13
13
|
import { batchFunction, runInSerial } from "socket-function/src/batching";
|
|
14
14
|
import { Querysub } from "../../4-querysub/QuerysubController";
|
|
15
15
|
import { sort, timeInDay, timeInHour } from "socket-function/src/misc";
|
|
16
|
-
import { FastArchiveViewer, cacheBustParam, filterParam } from "./FastArchiveViewer";
|
|
16
|
+
import { FastArchiveViewer, cacheBustParam, filterParam, filterParam2 } from "./FastArchiveViewer";
|
|
17
17
|
import { LogDatum, getLoggers, LOG_LIMIT_FLAG, LOG_LINE_LIMIT_FLAG } from "./diskLogger";
|
|
18
18
|
import { ColumnType, Table, TableType } from "../../5-diagnostics/Table";
|
|
19
19
|
import { formatDateJSX } from "../../misc/formatJSX";
|
|
@@ -28,6 +28,7 @@ import { RecentErrorsController, SuppressionListController, getSuppressEntryChec
|
|
|
28
28
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
29
29
|
import { throttleRender } from "../../functional/throttleRender";
|
|
30
30
|
import { isNode } from "typesafecss";
|
|
31
|
+
import { createLogViewerExtractField } from "./logViewerExtractField";
|
|
31
32
|
|
|
32
33
|
const RENDER_INTERVAL = 1000;
|
|
33
34
|
|
|
@@ -56,6 +57,7 @@ const defaultSelectedFields = {
|
|
|
56
57
|
export class LogViewer2 extends qreact.Component {
|
|
57
58
|
state = t.state({
|
|
58
59
|
datumsSeqNum: t.atomic<number>(0),
|
|
60
|
+
showCountByFile: t.boolean,
|
|
59
61
|
});
|
|
60
62
|
|
|
61
63
|
private example: string | undefined = undefined;
|
|
@@ -66,6 +68,7 @@ export class LogViewer2 extends qreact.Component {
|
|
|
66
68
|
private errors = 0;
|
|
67
69
|
private notMatchedCount = 0;
|
|
68
70
|
private datums: LogDatum[] = [];
|
|
71
|
+
private countsPerName = new Map<string, number>();
|
|
69
72
|
|
|
70
73
|
private lastRenderTime = 0;
|
|
71
74
|
private suppressionCounts = new Map<string, number>();
|
|
@@ -74,7 +77,7 @@ export class LogViewer2 extends qreact.Component {
|
|
|
74
77
|
|
|
75
78
|
rerun() {
|
|
76
79
|
cacheBustParam.value = Date.now();
|
|
77
|
-
void this.fastArchiveViewer?.
|
|
80
|
+
void this.fastArchiveViewer?.synchronizeDataThrottled();
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
render() {
|
|
@@ -166,6 +169,7 @@ export class LogViewer2 extends qreact.Component {
|
|
|
166
169
|
this.datums = [];
|
|
167
170
|
this.suppressionCounts = new Map();
|
|
168
171
|
this.expiredSuppressionCounts = new Map();
|
|
172
|
+
this.countsPerName = new Map();
|
|
169
173
|
this.operationSequenceNum++;
|
|
170
174
|
void (async () => {
|
|
171
175
|
const currentSequenceNum = this.operationSequenceNum;
|
|
@@ -183,10 +187,21 @@ export class LogViewer2 extends qreact.Component {
|
|
|
183
187
|
suppressionList = await suppressionController.getSuppressionList.promise();
|
|
184
188
|
|
|
185
189
|
}}
|
|
190
|
+
getScanFnc={() => {
|
|
191
|
+
if (!this.state.showCountByFile) {
|
|
192
|
+
return () => false;
|
|
193
|
+
}
|
|
194
|
+
return createLogViewerExtractField("name", (value) => {
|
|
195
|
+
let prevCount = this.countsPerName.get(value) ?? 0;
|
|
196
|
+
prevCount++;
|
|
197
|
+
this.countsPerName.set(value, prevCount);
|
|
198
|
+
});
|
|
199
|
+
}}
|
|
186
200
|
getWantData={async (file) => {
|
|
187
201
|
if (!hasErrorNotifyToggle) return undefined;
|
|
188
202
|
// By defaulting to the synchronous one, the list will be updated if there's any changes. However, we will also just get it asynchronously if the list hasn't been updated by the time we call get1data. And because we assign it back to the variable, it'll be cached.
|
|
189
203
|
suppressionList = suppressionList || await suppressionController.getSuppressionList.promise();
|
|
204
|
+
|
|
190
205
|
let suppressionFull = getSuppressionFull({
|
|
191
206
|
entries: suppressionList,
|
|
192
207
|
blockTimeRange: {
|
|
@@ -329,6 +344,7 @@ export class LogViewer2 extends qreact.Component {
|
|
|
329
344
|
</div>;
|
|
330
345
|
}
|
|
331
346
|
};
|
|
347
|
+
let includedFiles = filterParam2.value.split("|").map(x => x.trim());
|
|
332
348
|
return <>
|
|
333
349
|
<div className={css.hbox(10)}>
|
|
334
350
|
<InputPicker
|
|
@@ -373,6 +389,79 @@ export class LogViewer2 extends qreact.Component {
|
|
|
373
389
|
{this.datums.length === 0 && <div className={css.hsl(40, 50, 50).colorhsl(60, 50, 100).boldStyle.pad2(10).ellipsis}>
|
|
374
390
|
No logs matched, either increase the time range or decrease the filter specificity.
|
|
375
391
|
</div>}
|
|
392
|
+
{includedFiles.length > 0 && includedFiles[0] && <div className={css.hbox(20)}>
|
|
393
|
+
<div className={css.hbox(10)}>
|
|
394
|
+
<div className={css.fontSize(18).hsl(0, 50, 50).colorhsl(0, 50, 95).pad2(8, 4)}>
|
|
395
|
+
Only showing {includedFiles.length} files:
|
|
396
|
+
</div>
|
|
397
|
+
<Button onClick={() => {
|
|
398
|
+
filterParam2.value = "";
|
|
399
|
+
this.rerun();
|
|
400
|
+
}}>
|
|
401
|
+
Clear All Filters
|
|
402
|
+
</Button>
|
|
403
|
+
</div>
|
|
404
|
+
<div className={css.hbox(10, 10).wrap}>
|
|
405
|
+
{includedFiles.filter(x => x).map((fileName) => (
|
|
406
|
+
<div
|
|
407
|
+
key={fileName}
|
|
408
|
+
className={css.pad2(8, 4).hsl(120, 40, 80).bord2(120, 40, 60, 1).button}
|
|
409
|
+
onClick={() => {
|
|
410
|
+
let newFiles = includedFiles.filter(x => x !== fileName);
|
|
411
|
+
filterParam2.value = newFiles.join("|");
|
|
412
|
+
}}
|
|
413
|
+
title={`Click to remove ${fileName} from filter`}
|
|
414
|
+
>
|
|
415
|
+
{fileName} ✕
|
|
416
|
+
</div>
|
|
417
|
+
))}
|
|
418
|
+
</div>
|
|
419
|
+
</div>}
|
|
420
|
+
|
|
421
|
+
{this.state.showCountByFile && (() => {
|
|
422
|
+
let counts = Array.from(this.countsPerName.entries());
|
|
423
|
+
sort(counts, x => -x[1]);
|
|
424
|
+
let topCounts = counts.slice(0, 20);
|
|
425
|
+
let totalCount = counts.reduce((sum, [, count]) => sum + count, 0);
|
|
426
|
+
|
|
427
|
+
return <div className={css.vbox(10)}>
|
|
428
|
+
<div className={css.hbox(10, 10).wrap}>
|
|
429
|
+
{topCounts.map(([name, count]) => {
|
|
430
|
+
let percentage = totalCount > 0 ? (count / totalCount) * 100 : 0;
|
|
431
|
+
let isFiltered = includedFiles.includes(name);
|
|
432
|
+
|
|
433
|
+
return <div
|
|
434
|
+
key={name}
|
|
435
|
+
className={css.relative.pad2(8, 4).bord2(210, 30, 60, 1).ellipsis.button}
|
|
436
|
+
title={isFiltered ? `Click to remove ${name} from filter` : `Click to add ${name} to filter`}
|
|
437
|
+
onClick={() => {
|
|
438
|
+
if (isFiltered) {
|
|
439
|
+
// Remove from filter
|
|
440
|
+
let newFiles = includedFiles.filter(x => x !== name);
|
|
441
|
+
filterParam2.value = newFiles.join("|");
|
|
442
|
+
} else {
|
|
443
|
+
// Add to filter
|
|
444
|
+
let newFiles = [...includedFiles.filter(x => x), name];
|
|
445
|
+
filterParam2.value = newFiles.join("|");
|
|
446
|
+
}
|
|
447
|
+
}}
|
|
448
|
+
>
|
|
449
|
+
<div
|
|
450
|
+
className={css.absolute.pos(0, 0).height("100%").width(`${percentage}%`).hsl(isFiltered ? 120 : 210, 40, 80)}
|
|
451
|
+
/>
|
|
452
|
+
<div className={css.relative.maxWidth(200).ellipsis}>
|
|
453
|
+
{formatNumber(count)} | {name || "(no name present)"} {isFiltered && "✓" || ""}
|
|
454
|
+
</div>
|
|
455
|
+
</div>;
|
|
456
|
+
})}
|
|
457
|
+
</div>
|
|
458
|
+
</div>;
|
|
459
|
+
})() || <Button onClick={() => {
|
|
460
|
+
this.state.showCountByFile = true;
|
|
461
|
+
this.rerun();
|
|
462
|
+
}}>
|
|
463
|
+
Show Count by File
|
|
464
|
+
</Button>}
|
|
376
465
|
<Table
|
|
377
466
|
rows={this.datums}
|
|
378
467
|
columns={columns}
|
|
@@ -121,7 +121,12 @@ const sendIMs = batchFunction(({ delay: BATCH_TIME }), async (logsAll: LogDatum[
|
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
123
|
let url = createLink(getErrorLogsLink());
|
|
124
|
-
let
|
|
124
|
+
let flat = Object.values(info.perFile).flat();
|
|
125
|
+
if (flat.length === 0) {
|
|
126
|
+
console.log(`All messages have hit their IM limit, so not sending any message at all.`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let message = flat.map(
|
|
125
130
|
x => `[${formatDateTime(x.time)}](${url}) | ${ellipsisMiddle(String(x.param0), 100)} (${x.__NAME__})`
|
|
126
131
|
).join("\n");
|
|
127
132
|
message = ellipsisMiddle(message, 900);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ScanFnc } from "./FastArchiveViewer";
|
|
2
|
+
|
|
3
|
+
export function createLogViewerExtractField(
|
|
4
|
+
fieldName: string,
|
|
5
|
+
onFieldValue: (value: string) => void,
|
|
6
|
+
): ScanFnc {
|
|
7
|
+
let fieldNameBuffer = Buffer.from(`${JSON.stringify(fieldName)}:"`);
|
|
8
|
+
let quoteChar = Buffer.from(`"`)[0];
|
|
9
|
+
let escapeChar = Buffer.from(`\\`)[0];
|
|
10
|
+
return (posStart: number, posEnd: number, data: Buffer) => {
|
|
11
|
+
for (let i = posStart; i < posEnd; i++) {
|
|
12
|
+
if (data[i] === fieldNameBuffer[0]) {
|
|
13
|
+
for (let j = 1; j < fieldNameBuffer.length; j++) {
|
|
14
|
+
if (data[i + j] !== fieldNameBuffer[j]) {
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
if (j === fieldNameBuffer.length - 1) {
|
|
18
|
+
// Parse until the string ends, ignoring escaped characters
|
|
19
|
+
let start = i + j + 1;
|
|
20
|
+
for (let k = start; k < posEnd; k++) {
|
|
21
|
+
if (data[k] === quoteChar) {
|
|
22
|
+
onFieldValue(data.slice(start, k).toString());
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
if (data[k] === escapeChar) {
|
|
26
|
+
k++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
onFieldValue("");
|
|
35
|
+
return false;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -35,7 +35,9 @@ export class InputPicker<T> extends qreact.Component<{
|
|
|
35
35
|
state = {
|
|
36
36
|
pendingText: "",
|
|
37
37
|
focused: false,
|
|
38
|
+
showLimit: this.props.matchLimit ?? 10,
|
|
38
39
|
};
|
|
40
|
+
inputElem: HTMLInputElement | null = null;
|
|
39
41
|
render() {
|
|
40
42
|
const { singleOption, fillWidth } = this.props;
|
|
41
43
|
// Input, and beside it the picked values
|
|
@@ -63,7 +65,7 @@ export class InputPicker<T> extends qreact.Component<{
|
|
|
63
65
|
pendingMatches = resolvedOptions;
|
|
64
66
|
}
|
|
65
67
|
let extra = pendingMatches.length;
|
|
66
|
-
pendingMatches = pendingMatches.slice(0, this.
|
|
68
|
+
pendingMatches = pendingMatches.slice(0, this.state.showLimit);
|
|
67
69
|
extra -= pendingMatches.length;
|
|
68
70
|
return (
|
|
69
71
|
<div class={
|
|
@@ -78,6 +80,7 @@ export class InputPicker<T> extends qreact.Component<{
|
|
|
78
80
|
{this.props.label}
|
|
79
81
|
</div>
|
|
80
82
|
<Input
|
|
83
|
+
inputRef={x => this.inputElem = x}
|
|
81
84
|
value={this.state.pendingText}
|
|
82
85
|
hot
|
|
83
86
|
alwaysUseLatestValueWhenFocused
|
|
@@ -119,7 +122,17 @@ export class InputPicker<T> extends qreact.Component<{
|
|
|
119
122
|
</Button>
|
|
120
123
|
))}
|
|
121
124
|
{extra > 0 && (
|
|
122
|
-
<Button class={css.hbox(5).button}
|
|
125
|
+
<Button class={css.hbox(5).button + " keepModalsOpen"}
|
|
126
|
+
onMouseDown={e => {
|
|
127
|
+
this.state.showLimit *= 2;
|
|
128
|
+
// HACK: Trying to prevent the event from causing a blur doesn't seem to work, so we'll just refocus it. And also waiting for 0 milliseconds doesn't work, so we'll wait for 100 milliseconds.
|
|
129
|
+
// - If the user clicks too fast this doesn't work, but the on-mouse out handler also won't work, Because we'll blur before on mouse up and then will be removed.
|
|
130
|
+
// TODO: Instead of keeping track of the focus state, we should make it a palette and when the palette goes away we should hide the open state.
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
this.inputElem?.focus();
|
|
133
|
+
}, 1000);
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
123
136
|
+ {extra} more...
|
|
124
137
|
</Button>
|
|
125
138
|
)}
|