querysub 0.378.0 → 0.379.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/.cursorrules +2 -0
- package/package.json +1 -1
- package/src/diagnostics/MachineThreadInfo.tsx +33 -10
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +5 -1
- package/src/diagnostics/logs/errorNotifications2/ErrorNotificationPage.tsx +430 -201
- package/src/diagnostics/logs/errorNotifications2/ErrorWarning.tsx +32 -0
- package/src/diagnostics/logs/errorNotifications2/errorNotifications.ts +414 -54
- package/src/diagnostics/logs/errorNotifications2/errorWatcher.ts +168 -0
- package/src/diagnostics/logs/errorNotifications2/openRouterHelper.ts +77 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +1 -30
- package/src/diagnostics/managementPages.tsx +6 -2
- package/src/user-implementation/SecurityPage.tsx +6 -0
- package/src/user-implementation/userData.ts +6 -1
- /package/src/library-components/{errorNotifications.tsx → uncaughtToast.tsx} +0 -0
|
@@ -1,276 +1,505 @@
|
|
|
1
1
|
import { qreact } from "../../../4-dom/qreact";
|
|
2
2
|
import { t } from "../../../2-proxy/schema2";
|
|
3
3
|
import { css } from "typesafecss";
|
|
4
|
-
import { ErrorNotificationsController, watchUnmatchedErrors } from "./errorNotifications";
|
|
5
4
|
import { LogDatum } from "../diskLogger";
|
|
6
|
-
import { sort,
|
|
7
|
-
import {
|
|
5
|
+
import { sort, timeInDay } from "socket-function/src/misc";
|
|
6
|
+
import { formatDateTime, formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
8
7
|
import { Querysub } from "../../../4-querysub/QuerysubController";
|
|
9
8
|
import { InputLabel } from "../../../library-components/InputLabel";
|
|
10
|
-
import { SocketFunction } from "socket-function/SocketFunction";
|
|
11
9
|
import { isPublic } from "../../../config";
|
|
10
|
+
import { MachineThreadInfo } from "../../MachineThreadInfo";
|
|
11
|
+
import { ErrorWarning } from "./ErrorWarning";
|
|
12
|
+
import { getErrorNotificationsManager } from "./errorWatcher";
|
|
13
|
+
import { createMatchesPattern } from "../IndexedLogs/bufferSearchFindMatcher";
|
|
14
|
+
import { cacheLimited } from "socket-function/src/caching";
|
|
15
|
+
import { managementPageURL } from "../../managementPages";
|
|
16
|
+
import { searchTextURL, readProductionLogsURL } from "../IndexedLogs/LogViewerParams";
|
|
17
|
+
import { startTimeParam, endTimeParam } from "../TimeRangeSelector";
|
|
18
|
+
import { timeInHour } from "socket-function/src/misc";
|
|
19
|
+
import { ATag, URLOverride } from "../../../library-components/ATag";
|
|
20
|
+
import { Button } from "../../../library-components/Button";
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
let getMatcher = cacheLimited(100, (pattern: string) =>
|
|
23
|
+
createMatchesPattern(Buffer.from(pattern), false)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export class LogDatumRenderer extends qreact.Component<{
|
|
27
|
+
datum: LogDatum;
|
|
28
|
+
inlineMode?: boolean;
|
|
29
|
+
hideSuppression?: boolean;
|
|
30
|
+
pattern?: string;
|
|
31
|
+
}> {
|
|
14
32
|
state = t.state({
|
|
15
|
-
|
|
16
|
-
suppressionMatches: t.atomic<Map<string, {
|
|
17
|
-
history: Map<number, { count: number }>;
|
|
18
|
-
exampleIndex: number;
|
|
19
|
-
examples: LogDatum[];
|
|
20
|
-
}>>(new Map()),
|
|
21
|
-
suppressionEntries: t.atomic<Array<{
|
|
22
|
-
id: string;
|
|
23
|
-
description: string;
|
|
24
|
-
pattern: string;
|
|
25
|
-
createdTime: number;
|
|
26
|
-
lastUpdatedTime: number;
|
|
27
|
-
}>>([]),
|
|
28
|
-
addingNewSuppression: t.atomic<boolean>(false),
|
|
29
|
-
newSuppressionDescription: t.atomic<string>(""),
|
|
30
|
-
newSuppressionPattern: t.atomic<string>(""),
|
|
31
|
-
initError: t.atomic<string | undefined>(undefined),
|
|
32
|
-
isLoading: t.atomic<boolean>(true),
|
|
33
|
-
testErrorMessage: t.atomic<string>("example error"),
|
|
33
|
+
expanded: t.atomic<boolean>(false),
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
manager = getErrorNotificationsManager();
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
render() {
|
|
39
|
+
let datum = this.props.datum;
|
|
40
|
+
let mainMessage = datum.param0 && String(datum.param0) || "(no message)";
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
let controller = ErrorNotificationsController(SocketFunction.browserNodeId());
|
|
51
|
-
let data = await controller.getData.promise();
|
|
52
|
-
let suppressionEntries = await controller.getSuppressionEntries.promise();
|
|
53
|
-
|
|
54
|
-
Querysub.commit(() => {
|
|
55
|
-
this.state.unmatchedErrors = data.unmatchedErrors;
|
|
56
|
-
this.state.suppressionMatches = data.suppressionMatches;
|
|
57
|
-
this.state.suppressionEntries = suppressionEntries;
|
|
58
|
-
this.state.initError = undefined;
|
|
59
|
-
this.state.isLoading = false;
|
|
60
|
-
});
|
|
61
|
-
} catch (error) {
|
|
62
|
-
Querysub.commit(() => {
|
|
63
|
-
this.state.initError = error instanceof Error ? error.message : String(error);
|
|
64
|
-
this.state.isLoading = false;
|
|
65
|
-
});
|
|
42
|
+
let matchingSuppressions = [];
|
|
43
|
+
if (!this.manager.state.isLoading && !this.props.hideSuppression) {
|
|
44
|
+
for (let suppression of this.manager.state.suppressionEntries) {
|
|
45
|
+
let matcher = getMatcher(suppression.pattern);
|
|
46
|
+
let errorBuffer = Buffer.from(JSON.stringify(datum));
|
|
47
|
+
if (matcher(errorBuffer)) {
|
|
48
|
+
matchingSuppressions.push(suppression);
|
|
49
|
+
}
|
|
66
50
|
}
|
|
67
|
-
}
|
|
68
|
-
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let timedOutMatches = matchingSuppressions.filter(s => datum.time > s.timeout);
|
|
54
|
+
let shouldBeSuppressed = matchingSuppressions.filter(s => datum.time <= s.timeout);
|
|
69
55
|
|
|
70
|
-
|
|
71
|
-
|
|
56
|
+
let logViewerValues: URLOverride[] | undefined;
|
|
57
|
+
{
|
|
58
|
+
logViewerValues = [
|
|
59
|
+
managementPageURL.getOverride("LogViewer3"),
|
|
60
|
+
readProductionLogsURL.getOverride(isPublic()),
|
|
61
|
+
startTimeParam.getOverride(datum.time - timeInHour),
|
|
62
|
+
endTimeParam.getOverride(datum.time + timeInHour),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
let searchText = this.props.pattern && this.props.pattern || timedOutMatches[0]?.pattern || "";
|
|
66
|
+
if (datum.__threadId) {
|
|
67
|
+
if (searchText) {
|
|
68
|
+
searchText += "&";
|
|
69
|
+
}
|
|
70
|
+
searchText += `"__threadId":"${datum.__threadId}"`;
|
|
71
|
+
}
|
|
72
|
+
if (searchText) {
|
|
73
|
+
logViewerValues.push(searchTextURL.getOverride(searchText));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return <div className={
|
|
78
|
+
css.vbox(4)
|
|
79
|
+
+ (!this.props.inlineMode && css.maxWidth("80vw"))
|
|
80
|
+
+ (this.props.inlineMode && css.maxWidth("calc(100vw - 90px)"))
|
|
81
|
+
}>
|
|
82
|
+
<div
|
|
83
|
+
className={
|
|
84
|
+
css.hbox(8).maxWidth("100%")
|
|
85
|
+
}
|
|
86
|
+
>
|
|
87
|
+
{logViewerValues && (
|
|
88
|
+
<ATag values={logViewerValues}>
|
|
89
|
+
View Logs
|
|
90
|
+
</ATag>
|
|
91
|
+
)}
|
|
92
|
+
<div
|
|
93
|
+
className={
|
|
94
|
+
css.hbox(8)
|
|
95
|
+
+ (!this.props.inlineMode && css.button)
|
|
96
|
+
}
|
|
97
|
+
onClick={!this.props.inlineMode && (() => {
|
|
98
|
+
this.state.expanded = !this.state.expanded;
|
|
99
|
+
}) || undefined}
|
|
100
|
+
>
|
|
101
|
+
{!this.props.inlineMode && <span>{this.state.expanded && "▼" || "▶"}</span>}
|
|
102
|
+
<div>{formatDateTime(datum.time)}</div>
|
|
103
|
+
</div>
|
|
104
|
+
{datum.__machineId && <MachineThreadInfo machineId={datum.__machineId} threadId={datum.__threadId} />}
|
|
105
|
+
<span className={css.ellipsis.flexFillWidth.colorhsl(0, 50, 50).boldStyle}>{mainMessage}</span>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{!this.props.inlineMode && this.state.expanded && (
|
|
109
|
+
<div className={css.vbox(4).pad2(8).hsl(0, 0, 98).bord2(0, 0, 85)}>
|
|
110
|
+
{Object.entries(datum).map(([key, value]) => (
|
|
111
|
+
<div key={key} className={css.hbox(8)}>
|
|
112
|
+
<strong className={css.minWidth(120)}>{key}:</strong>
|
|
113
|
+
<pre className={css.flexGrow(1).margin(0)}>{JSON.stringify(value, undefined, 2)}</pre>
|
|
114
|
+
</div>
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{timedOutMatches.length > 0 && (
|
|
120
|
+
<div className={css.hbox(8).pad2(8).bord2(200, 50, 60).hsl(200, 50, 95)}>
|
|
121
|
+
<span>
|
|
122
|
+
<span className={css.colorhsl(0, 0, 50)}>(expired {formatTime(Date.now() - timedOutMatches[0].timeout)} AGO)</span>
|
|
123
|
+
{" "}
|
|
124
|
+
<span className={css.colorhsl(0, 0, 0).boldStyle}>{timedOutMatches[0].pattern}</span>
|
|
125
|
+
{timedOutMatches[0].notes && <span className={css.colorhsl(210, 80, 40)}> ({timedOutMatches[0].notes})</span>}
|
|
126
|
+
</span>
|
|
127
|
+
<Button
|
|
128
|
+
hue={30}
|
|
129
|
+
onClick={(e) => {
|
|
130
|
+
if (this.props.inlineMode) {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
e.stopPropagation();
|
|
133
|
+
}
|
|
134
|
+
let timeout = Date.now() + timeInDay * 2;
|
|
135
|
+
Querysub.onCommitFinished(async () => {
|
|
136
|
+
await this.manager.updateTimeout(timedOutMatches[0].id, timeout);
|
|
137
|
+
});
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
Fixed
|
|
141
|
+
</Button>
|
|
142
|
+
<Button
|
|
143
|
+
hue={120}
|
|
144
|
+
onClick={(e) => {
|
|
145
|
+
if (this.props.inlineMode) {
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
e.stopPropagation();
|
|
148
|
+
}
|
|
149
|
+
Querysub.onCommitFinished(async () => {
|
|
150
|
+
await this.manager.updateTimeout(timedOutMatches[0].id, Infinity);
|
|
151
|
+
});
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
Not a Bug
|
|
155
|
+
</Button>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{shouldBeSuppressed.length > 0 && timedOutMatches.length === 0 && (
|
|
160
|
+
<div className={css.pad2(8).bord2(30, 50, 60).hsl(30, 50, 95)}>
|
|
161
|
+
<span>
|
|
162
|
+
<span className={css.colorhsl(0, 0, 50)}>(for {formatTime(Date.now() - shouldBeSuppressed[0].lastUpdatedTime)})</span>
|
|
163
|
+
{" "}
|
|
164
|
+
<span className={css.colorhsl(0, 80, 30)}>{shouldBeSuppressed[0].pattern}</span>
|
|
165
|
+
{shouldBeSuppressed[0].notes && <span className={css.colorhsl(210, 80, 40)}> ({shouldBeSuppressed[0].notes})</span>}
|
|
166
|
+
</span>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</div>;
|
|
72
170
|
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export class ErrorNotificationPage extends qreact.Component {
|
|
174
|
+
state = t.state({
|
|
175
|
+
newFixedPattern: t.atomic<string>(""),
|
|
176
|
+
newNotABugPattern: t.atomic<string>(""),
|
|
177
|
+
testErrorMessage: t.atomic<string>("example error"),
|
|
178
|
+
showingAllErrors: t.atomic<boolean>(false),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
manager = getErrorNotificationsManager();
|
|
73
182
|
|
|
74
183
|
render() {
|
|
75
|
-
if (this.state.isLoading) {
|
|
184
|
+
if (this.manager.state.isLoading) {
|
|
76
185
|
return <div className={css.vbox(16).pad2(16)}>
|
|
77
186
|
<h2>Error Notifications</h2>
|
|
78
187
|
<div>Loading error notifications...</div>
|
|
79
188
|
</div>;
|
|
80
189
|
}
|
|
81
190
|
|
|
82
|
-
if (this.state.initError) {
|
|
191
|
+
if (this.manager.state.initError) {
|
|
83
192
|
return <div className={css.vbox(16).pad2(16)}>
|
|
84
193
|
<h2>Error Notifications</h2>
|
|
194
|
+
{!isPublic() && <h1>
|
|
195
|
+
IMPORTANT! You MUST run `yarn error-watch` to get errors notifications in the dev environment.
|
|
196
|
+
</h1>}
|
|
85
197
|
<div className={css.pad2(12).bord2(0, 80, 50).hsl(0, 80, 95).colorhsl(0, 80, 30)}>
|
|
86
198
|
<strong>Error loading error notifications:</strong>
|
|
87
199
|
<pre className={css.pad2(8).hsl(0, 0, 98).bord2(0, 0, 85)}>
|
|
88
|
-
{this.state.initError}
|
|
200
|
+
{this.manager.state.initError}
|
|
89
201
|
</pre>
|
|
90
202
|
</div>
|
|
91
203
|
</div>;
|
|
92
204
|
}
|
|
93
205
|
|
|
94
|
-
let sortedErrors = [...this.state.unmatchedErrors];
|
|
95
|
-
sort(sortedErrors, x =>
|
|
96
|
-
let
|
|
206
|
+
let sortedErrors = [...this.manager.state.unmatchedErrors];
|
|
207
|
+
sort(sortedErrors, x => x.time);
|
|
208
|
+
let displayedErrors = this.state.showingAllErrors && sortedErrors || sortedErrors.slice(0, 5);
|
|
97
209
|
|
|
98
|
-
let sortedSuppressions = [...this.state.suppressionEntries];
|
|
210
|
+
let sortedSuppressions = [...this.manager.state.suppressionEntries];
|
|
99
211
|
sort(sortedSuppressions, x => -x.lastUpdatedTime);
|
|
100
212
|
|
|
101
|
-
return <div className={css.vbox(16).pad2(16)}>
|
|
213
|
+
return <div className={css.vbox(16).pad2(16).fillWidth}>
|
|
102
214
|
<h2>Error Notifications</h2>
|
|
103
215
|
|
|
104
|
-
{!isPublic() && <h1>
|
|
105
|
-
IMPORTANT! You MUST run `yarn error-watch` to get errors notifications in the dev environment.
|
|
106
|
-
</h1>}
|
|
107
|
-
|
|
108
216
|
<div className={css.hbox(12).pad2(12).bord2(30, 50, 60).hsl(30, 50, 95)}>
|
|
109
217
|
<InputLabel
|
|
110
218
|
label="Test Error Message"
|
|
219
|
+
className={css.width(600)}
|
|
111
220
|
value={this.state.testErrorMessage}
|
|
112
221
|
onChangeValue={(value) => {
|
|
113
222
|
this.state.testErrorMessage = value;
|
|
114
223
|
}}
|
|
115
|
-
className={css.width(400)}
|
|
116
224
|
/>
|
|
117
|
-
<
|
|
118
|
-
|
|
225
|
+
<Button
|
|
226
|
+
hue={30}
|
|
119
227
|
onClick={() => {
|
|
120
228
|
let message = this.state.testErrorMessage;
|
|
121
229
|
Querysub.onCommitFinished(async () => {
|
|
122
|
-
|
|
123
|
-
await controller.logTestError.promise(message);
|
|
230
|
+
await this.manager.logTestError(message);
|
|
124
231
|
});
|
|
125
232
|
}}
|
|
126
233
|
>
|
|
127
234
|
Log Test Error
|
|
128
|
-
</
|
|
235
|
+
</Button>
|
|
129
236
|
</div>
|
|
130
237
|
|
|
131
238
|
<div className={css.vbox(12)}>
|
|
132
|
-
<h3>Unmatched Errors ({this.state.unmatchedErrors.length})</h3>
|
|
133
|
-
{
|
|
134
|
-
<div className={css.vbox(
|
|
135
|
-
{
|
|
136
|
-
<
|
|
137
|
-
<div className={css.hbox(8)}>
|
|
138
|
-
<strong>{formatVeryNiceDateTime(error.time)}</strong>
|
|
139
|
-
{error.machineId && <span>Machine: {error.machineId}</span>}
|
|
140
|
-
</div>
|
|
141
|
-
<pre className={css.overflowAuto.pad2(8).hsl(0, 0, 98).bord2(0, 0, 85)}>
|
|
142
|
-
{JSON.stringify(error)}
|
|
143
|
-
</pre>
|
|
144
|
-
</div>
|
|
239
|
+
<h3>Unmatched Errors ({this.manager.state.unmatchedErrors.length})</h3>
|
|
240
|
+
{displayedErrors.length === 0 && <div>No unmatched errors</div>}
|
|
241
|
+
<div className={css.vbox(0)}>
|
|
242
|
+
{displayedErrors.map((error, idx) => (
|
|
243
|
+
<LogDatumRenderer key={idx} datum={error} />
|
|
145
244
|
))}
|
|
146
245
|
</div>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<div className={css.hbox(12)}>
|
|
151
|
-
<h3>Suppressions ({sortedSuppressions.length})</h3>
|
|
152
|
-
<button
|
|
153
|
-
className={css.pad2(12, 8).button.bord2(120, 80, 50).hsl(120, 80, 90)}
|
|
246
|
+
{sortedErrors.length > 5 && (
|
|
247
|
+
<Button
|
|
248
|
+
hue={200}
|
|
154
249
|
onClick={() => {
|
|
155
|
-
this.state.
|
|
250
|
+
this.state.showingAllErrors = !this.state.showingAllErrors;
|
|
156
251
|
}}
|
|
157
252
|
>
|
|
158
|
-
{this.state.
|
|
159
|
-
</
|
|
160
|
-
|
|
253
|
+
{this.state.showingAllErrors && "Show Less" || `Show More (${sortedErrors.length - 5} more)`}
|
|
254
|
+
</Button>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
161
257
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<InputLabel
|
|
165
|
-
label="Description"
|
|
166
|
-
value={this.state.newSuppressionDescription}
|
|
167
|
-
onChangeValue={(value) => {
|
|
168
|
-
this.state.newSuppressionDescription = value;
|
|
169
|
-
}}
|
|
170
|
-
className={css.width(500)}
|
|
171
|
-
/>
|
|
172
|
-
<InputLabel
|
|
173
|
-
label="Pattern (regex)"
|
|
174
|
-
value={this.state.newSuppressionPattern}
|
|
175
|
-
onChangeValue={(value) => {
|
|
176
|
-
this.state.newSuppressionPattern = value;
|
|
177
|
-
}}
|
|
178
|
-
className={css.width(500)}
|
|
179
|
-
/>
|
|
180
|
-
<button
|
|
181
|
-
className={css.pad2(12, 8).button.bord2(120, 80, 50).hsl(120, 80, 85)}
|
|
182
|
-
onClick={() => {
|
|
183
|
-
let description = this.state.newSuppressionDescription;
|
|
184
|
-
let pattern = this.state.newSuppressionPattern;
|
|
185
|
-
if (!description || !pattern) {
|
|
186
|
-
alert("Description and pattern are required");
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
258
|
+
<div className={css.vbox(12)}>
|
|
259
|
+
<h3>Suppressions ({sortedSuppressions.length})</h3>
|
|
189
260
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
let
|
|
203
|
-
Querysub.
|
|
204
|
-
this.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
261
|
+
<div className={css.vbox(8).pad2(12).bord2(120, 50, 60).hsl(120, 50, 95)}>
|
|
262
|
+
<InputLabel
|
|
263
|
+
label="Fixed"
|
|
264
|
+
value={this.state.newFixedPattern}
|
|
265
|
+
onChangeValue={(value) => {
|
|
266
|
+
this.state.newFixedPattern = value;
|
|
267
|
+
}}
|
|
268
|
+
onKeyDown={e => {
|
|
269
|
+
if (e.key === "Enter") {
|
|
270
|
+
let pattern = e.currentTarget.value.trim();
|
|
271
|
+
if (pattern) {
|
|
272
|
+
this.state.newFixedPattern = pattern;
|
|
273
|
+
let timeout = Date.now() + timeInDay * 2;
|
|
274
|
+
Querysub.onCommitFinished(async () => {
|
|
275
|
+
await this.manager.addSuppression(pattern, timeout);
|
|
276
|
+
Querysub.commit(() => {
|
|
277
|
+
this.state.newFixedPattern = "";
|
|
278
|
+
});
|
|
208
279
|
});
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}}
|
|
283
|
+
className={css.width(500)}
|
|
284
|
+
/>
|
|
285
|
+
<InputLabel
|
|
286
|
+
label="Not a Bug"
|
|
287
|
+
value={this.state.newNotABugPattern}
|
|
288
|
+
onChangeValue={(value) => {
|
|
289
|
+
this.state.newNotABugPattern = value;
|
|
290
|
+
}}
|
|
291
|
+
onKeyDown={e => {
|
|
292
|
+
if (e.key === "Enter") {
|
|
293
|
+
let pattern = e.currentTarget.value.trim();
|
|
294
|
+
if (pattern) {
|
|
295
|
+
this.state.newNotABugPattern = pattern;
|
|
296
|
+
Querysub.onCommitFinished(async () => {
|
|
297
|
+
await this.manager.addSuppression(pattern, Infinity);
|
|
298
|
+
Querysub.commit(() => {
|
|
299
|
+
this.state.newNotABugPattern = "";
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}}
|
|
305
|
+
className={css.width(500)}
|
|
306
|
+
/>
|
|
307
|
+
</div>
|
|
216
308
|
|
|
217
309
|
<div className={css.vbox(12)}>
|
|
218
310
|
{sortedSuppressions.map((suppression) => {
|
|
219
|
-
let matchData = this.state.suppressionMatches.get(suppression.id);
|
|
220
|
-
let totalCount = 0;
|
|
221
|
-
if (matchData) {
|
|
222
|
-
for (let [chunk, data] of matchData.history) {
|
|
223
|
-
totalCount += data.count;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
311
|
+
let matchData = this.manager.state.suppressionMatches.get(suppression.id);
|
|
226
312
|
|
|
227
|
-
return <
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
<span>Pattern: <code className={css.pad2(4, 2).hsl(0, 0, 90)}>{suppression.pattern}</code></span>
|
|
233
|
-
<span>Total Matches: {totalCount}</span>
|
|
234
|
-
</div>
|
|
235
|
-
<div className={css.hbox(12)}>
|
|
236
|
-
<span>Created: {formatVeryNiceDateTime(suppression.createdTime)}</span>
|
|
237
|
-
<span>Updated: {formatVeryNiceDateTime(suppression.lastUpdatedTime)}</span>
|
|
238
|
-
</div>
|
|
239
|
-
</div>
|
|
240
|
-
<button
|
|
241
|
-
className={css.pad2(8, 6).button.bord2(0, 80, 50).hsl(0, 80, 90)}
|
|
242
|
-
onClick={() => {
|
|
243
|
-
if (!confirm(`Delete suppression "${suppression.description}"?`)) return;
|
|
244
|
-
|
|
245
|
-
Querysub.onCommitFinished(async () => {
|
|
246
|
-
let controller = ErrorNotificationsController(SocketFunction.browserNodeId());
|
|
247
|
-
await controller.deleteSuppressionEntry.promise(suppression.id);
|
|
248
|
-
|
|
249
|
-
let suppressionEntries = await controller.getSuppressionEntries.promise();
|
|
250
|
-
Querysub.commit(() => {
|
|
251
|
-
this.state.suppressionEntries = suppressionEntries;
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
}}
|
|
255
|
-
>
|
|
256
|
-
Delete
|
|
257
|
-
</button>
|
|
258
|
-
</div>
|
|
259
|
-
|
|
260
|
-
{matchData && matchData.examples.length > 0 && (
|
|
261
|
-
<div className={css.vbox(8)}>
|
|
262
|
-
<strong>Recent Examples ({matchData.examples.length}):</strong>
|
|
263
|
-
{matchData.examples.slice(0, 3).map((example, idx) => (
|
|
264
|
-
<pre key={idx} className={css.overflowAuto.pad2(8).hsl(0, 0, 98).bord2(0, 0, 85).fontSize(12)}>
|
|
265
|
-
{JSON.stringify(example, null, 2)}
|
|
266
|
-
</pre>
|
|
267
|
-
))}
|
|
268
|
-
</div>
|
|
269
|
-
)}
|
|
270
|
-
</div>;
|
|
313
|
+
return <SuppressionItem
|
|
314
|
+
key={suppression.id}
|
|
315
|
+
suppression={suppression}
|
|
316
|
+
matchData={matchData}
|
|
317
|
+
/>;
|
|
271
318
|
})}
|
|
272
319
|
</div>
|
|
273
320
|
</div>
|
|
274
321
|
</div>;
|
|
275
322
|
}
|
|
276
323
|
}
|
|
324
|
+
|
|
325
|
+
class SuppressionItem extends qreact.Component<{
|
|
326
|
+
suppression: {
|
|
327
|
+
id: string;
|
|
328
|
+
notes?: string;
|
|
329
|
+
pattern: string;
|
|
330
|
+
timeout: number;
|
|
331
|
+
createdTime: number;
|
|
332
|
+
lastUpdatedTime: number;
|
|
333
|
+
};
|
|
334
|
+
matchData: {
|
|
335
|
+
history: Map<number, { count: number }>;
|
|
336
|
+
exampleIndex: number;
|
|
337
|
+
examples: LogDatum[];
|
|
338
|
+
} | undefined;
|
|
339
|
+
}> {
|
|
340
|
+
state = t.state({
|
|
341
|
+
showingAllExamples: t.atomic<boolean>(false),
|
|
342
|
+
patternValue: t.atomic<string>(""),
|
|
343
|
+
notesValue: t.atomic<string>(""),
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
manager = getErrorNotificationsManager();
|
|
347
|
+
|
|
348
|
+
componentDidMount() {
|
|
349
|
+
this.state.patternValue = this.props.suppression.pattern;
|
|
350
|
+
this.state.notesValue = this.props.suppression.notes ?? "";
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
componentDidUpdate(prevProps: any) {
|
|
354
|
+
if (prevProps.suppression.pattern !== this.props.suppression.pattern) {
|
|
355
|
+
this.state.patternValue = this.props.suppression.pattern;
|
|
356
|
+
}
|
|
357
|
+
if (prevProps.suppression.notes !== this.props.suppression.notes) {
|
|
358
|
+
this.state.notesValue = this.props.suppression.notes ?? "";
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
render() {
|
|
363
|
+
let suppression = this.props.suppression;
|
|
364
|
+
let matchData = this.props.matchData;
|
|
365
|
+
let totalCount = 0;
|
|
366
|
+
if (matchData) {
|
|
367
|
+
for (let [chunk, data] of matchData.history) {
|
|
368
|
+
totalCount += data.count;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let timeoutText: preact.ComponentChild;
|
|
373
|
+
if (suppression.timeout === Infinity) {
|
|
374
|
+
timeoutText = "Never expires";
|
|
375
|
+
} else if (suppression.timeout > Date.now()) {
|
|
376
|
+
timeoutText = <span>Expires <b>{formatTime(suppression.timeout - Date.now())}</b></span>;
|
|
377
|
+
} else {
|
|
378
|
+
timeoutText = <span>Expired <b>{formatTime(Date.now() - suppression.timeout)}</b> ago</span>;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return <div key={suppression.id} className={css.pad2(12).bord2(200, 30, 70).hsl(200, 30, 95).vbox(8)}>
|
|
382
|
+
<div className={css.hbox(12)}>
|
|
383
|
+
<ATag
|
|
384
|
+
values={[
|
|
385
|
+
managementPageURL.getOverride("LogViewer3"),
|
|
386
|
+
searchTextURL.getOverride(suppression.pattern),
|
|
387
|
+
readProductionLogsURL.getOverride(true),
|
|
388
|
+
]}
|
|
389
|
+
>
|
|
390
|
+
View Logs
|
|
391
|
+
</ATag>
|
|
392
|
+
<Button
|
|
393
|
+
hue={30}
|
|
394
|
+
onClick={() => {
|
|
395
|
+
let timeout = Date.now() + timeInDay * 2;
|
|
396
|
+
Querysub.onCommitFinished(async () => {
|
|
397
|
+
await this.manager.updateTimeout(suppression.id, timeout);
|
|
398
|
+
});
|
|
399
|
+
}}
|
|
400
|
+
>
|
|
401
|
+
Fixed
|
|
402
|
+
</Button>
|
|
403
|
+
<Button
|
|
404
|
+
hue={120}
|
|
405
|
+
onClick={() => {
|
|
406
|
+
Querysub.onCommitFinished(async () => {
|
|
407
|
+
await this.manager.updateTimeout(suppression.id, Infinity);
|
|
408
|
+
});
|
|
409
|
+
}}
|
|
410
|
+
>
|
|
411
|
+
Not a Bug
|
|
412
|
+
</Button>
|
|
413
|
+
<Button
|
|
414
|
+
hue={30}
|
|
415
|
+
onClick={() => {
|
|
416
|
+
let timeout = Date.now();
|
|
417
|
+
Querysub.onCommitFinished(async () => {
|
|
418
|
+
await this.manager.updateTimeout(suppression.id, timeout);
|
|
419
|
+
});
|
|
420
|
+
}}
|
|
421
|
+
>
|
|
422
|
+
Expire Now
|
|
423
|
+
</Button>
|
|
424
|
+
<Button
|
|
425
|
+
hue={0}
|
|
426
|
+
onClick={() => {
|
|
427
|
+
if (!confirm(`Delete this suppression?`)) return;
|
|
428
|
+
|
|
429
|
+
Querysub.onCommitFinished(async () => {
|
|
430
|
+
await this.manager.deleteSuppression(suppression.id);
|
|
431
|
+
});
|
|
432
|
+
}}
|
|
433
|
+
>
|
|
434
|
+
Delete
|
|
435
|
+
</Button>
|
|
436
|
+
<div className={css.hbox(20)}>
|
|
437
|
+
<span><b>{formatNumber(totalCount)}</b> Matches</span>
|
|
438
|
+
<span>Updated <b>{formatTime(Date.now() - suppression.lastUpdatedTime)}</b> AGO</span>
|
|
439
|
+
<span>{timeoutText}</span>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
<div className={css.hbox(20)}>
|
|
444
|
+
<InputLabel
|
|
445
|
+
label="Pattern"
|
|
446
|
+
value={this.state.patternValue}
|
|
447
|
+
onChangeValue={(value) => {
|
|
448
|
+
this.state.patternValue = value;
|
|
449
|
+
}}
|
|
450
|
+
onKeyDown={e => {
|
|
451
|
+
if (e.key === "Enter") {
|
|
452
|
+
let pattern = e.currentTarget.value.trim();
|
|
453
|
+
if (!pattern) {
|
|
454
|
+
alert("Pattern cannot be empty");
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
this.state.patternValue = pattern;
|
|
458
|
+
Querysub.onCommitFinished(async () => {
|
|
459
|
+
await this.manager.updateSuppression(suppression.id, pattern);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}}
|
|
463
|
+
className={css.width(500)}
|
|
464
|
+
/>
|
|
465
|
+
<InputLabel
|
|
466
|
+
label="Reason"
|
|
467
|
+
value={this.state.notesValue}
|
|
468
|
+
onChangeValue={(value) => {
|
|
469
|
+
this.state.notesValue = value;
|
|
470
|
+
}}
|
|
471
|
+
onKeyDown={e => {
|
|
472
|
+
if (e.key === "Enter") {
|
|
473
|
+
let notes = e.currentTarget.value.trim();
|
|
474
|
+
this.state.notesValue = notes;
|
|
475
|
+
Querysub.onCommitFinished(async () => {
|
|
476
|
+
await this.manager.updateNotes(suppression.id, notes && notes || undefined);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}}
|
|
480
|
+
className={css.width(500)}
|
|
481
|
+
/>
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
{matchData && matchData.examples.length > 0 && (
|
|
485
|
+
<div className={css.hbox(8)}>
|
|
486
|
+
{matchData.examples.length > 1 && (
|
|
487
|
+
<Button
|
|
488
|
+
hue={200}
|
|
489
|
+
onClick={() => {
|
|
490
|
+
this.state.showingAllExamples = !this.state.showingAllExamples;
|
|
491
|
+
}}
|
|
492
|
+
>
|
|
493
|
+
{this.state.showingAllExamples && "Show Less" || `Show More (${matchData.examples.length - 1} more)`}
|
|
494
|
+
</Button>
|
|
495
|
+
)}
|
|
496
|
+
<div className={css.vbox(4)}>
|
|
497
|
+
{(this.state.showingAllExamples && matchData.examples || matchData.examples.slice(0, 1)).map((example, idx) => (
|
|
498
|
+
<LogDatumRenderer key={idx} hideSuppression datum={example} pattern={suppression.pattern} />
|
|
499
|
+
))}
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
)}
|
|
503
|
+
</div>;
|
|
504
|
+
}
|
|
505
|
+
}
|