@design.estate/dees-catalog 3.35.0 → 3.36.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/dist_bundle/bundle.js +817 -204
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-chart/dees-chart-log/dees-chart-log.d.ts +104 -6
- package/dist_ts_web/elements/00group-chart/dees-chart-log/dees-chart-log.demo.js +153 -54
- package/dist_ts_web/elements/00group-chart/dees-chart-log/dees-chart-log.js +716 -153
- package/dist_ts_web/elements/dees-statsgrid/dees-statsgrid.js +18 -5
- package/dist_ts_web/services/DeesServiceLibLoader.d.ts +34 -1
- package/dist_ts_web/services/DeesServiceLibLoader.js +27 -1
- package/dist_ts_web/services/index.d.ts +1 -1
- package/dist_ts_web/services/versions.d.ts +1 -0
- package/dist_ts_web/services/versions.js +2 -1
- package/dist_watch/bundle.js +815 -202
- package/dist_watch/bundle.js.map +3 -3
- package/package.json +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00group-chart/dees-chart-log/dees-chart-log.demo.ts +163 -56
- package/ts_web/elements/00group-chart/dees-chart-log/dees-chart-log.ts +756 -161
- package/ts_web/elements/dees-statsgrid/dees-statsgrid.ts +18 -4
- package/ts_web/services/DeesServiceLibLoader.ts +50 -1
- package/ts_web/services/index.ts +1 -1
- package/ts_web/services/versions.ts +1 -0
|
@@ -5,13 +5,18 @@ import {
|
|
|
5
5
|
customElement,
|
|
6
6
|
html,
|
|
7
7
|
property,
|
|
8
|
+
state,
|
|
8
9
|
type TemplateResult,
|
|
9
10
|
} from '@design.estate/dees-element';
|
|
10
11
|
|
|
11
12
|
import * as domtools from '@design.estate/dees-domtools';
|
|
12
13
|
import { demoFunc } from './dees-chart-log.demo.js';
|
|
13
14
|
import { themeDefaultStyles } from '../../00theme.js';
|
|
15
|
+
import { DeesServiceLibLoader, type IXtermSearchAddon, CDN_BASE, CDN_VERSIONS } from '../../../services/index.js';
|
|
14
16
|
|
|
17
|
+
// Type imports (no runtime overhead)
|
|
18
|
+
import type { Terminal } from 'xterm';
|
|
19
|
+
import type { FitAddon } from 'xterm-addon-fit';
|
|
15
20
|
|
|
16
21
|
declare global {
|
|
17
22
|
interface HTMLElementTagNameMap {
|
|
@@ -26,6 +31,16 @@ export interface ILogEntry {
|
|
|
26
31
|
source?: string;
|
|
27
32
|
}
|
|
28
33
|
|
|
34
|
+
export interface ILogMetrics {
|
|
35
|
+
debug: number;
|
|
36
|
+
info: number;
|
|
37
|
+
warn: number;
|
|
38
|
+
error: number;
|
|
39
|
+
success: number;
|
|
40
|
+
total: number;
|
|
41
|
+
rate: number; // logs per second (rolling average)
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
@customElement('dees-chart-log')
|
|
30
45
|
export class DeesChartLog extends DeesElement {
|
|
31
46
|
public static demo = demoFunc;
|
|
@@ -34,6 +49,9 @@ export class DeesChartLog extends DeesElement {
|
|
|
34
49
|
@property()
|
|
35
50
|
accessor label: string = 'Server Logs';
|
|
36
51
|
|
|
52
|
+
@property({ type: String })
|
|
53
|
+
accessor mode: 'structured' | 'raw' = 'structured';
|
|
54
|
+
|
|
37
55
|
@property({ type: Array })
|
|
38
56
|
accessor logEntries: ILogEntry[] = [];
|
|
39
57
|
|
|
@@ -41,27 +59,54 @@ export class DeesChartLog extends DeesElement {
|
|
|
41
59
|
accessor autoScroll: boolean = true;
|
|
42
60
|
|
|
43
61
|
@property({ type: Number })
|
|
44
|
-
accessor maxEntries: number =
|
|
62
|
+
accessor maxEntries: number = 10000;
|
|
45
63
|
|
|
46
|
-
|
|
64
|
+
@property({ type: Array })
|
|
65
|
+
accessor highlightKeywords: string[] = [];
|
|
47
66
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
@property({ type: Boolean })
|
|
68
|
+
accessor showMetrics: boolean = true;
|
|
69
|
+
|
|
70
|
+
@state()
|
|
71
|
+
accessor searchQuery: string = '';
|
|
72
|
+
|
|
73
|
+
@state()
|
|
74
|
+
accessor filterMode: boolean = false;
|
|
75
|
+
|
|
76
|
+
@state()
|
|
77
|
+
accessor metrics: ILogMetrics = { debug: 0, info: 0, warn: 0, error: 0, success: 0, total: 0, rate: 0 };
|
|
78
|
+
|
|
79
|
+
@state()
|
|
80
|
+
accessor terminalReady: boolean = false;
|
|
81
|
+
|
|
82
|
+
// Buffer of all log entries for filter mode
|
|
83
|
+
private logBuffer: ILogEntry[] = [];
|
|
84
|
+
|
|
85
|
+
// Track trailing hidden entries count for live updates in filter mode
|
|
86
|
+
private trailingHiddenCount: number = 0;
|
|
87
|
+
|
|
88
|
+
// xterm instances
|
|
89
|
+
private terminal: Terminal | null = null;
|
|
90
|
+
private fitAddon: FitAddon | null = null;
|
|
91
|
+
private searchAddon: IXtermSearchAddon | null = null;
|
|
92
|
+
private resizeObserver: ResizeObserver | null = null;
|
|
93
|
+
private terminalThemeSubscription: any = null;
|
|
94
|
+
private domtoolsInstance: any = null;
|
|
95
|
+
|
|
96
|
+
// Rate calculation
|
|
97
|
+
private rateBuffer: number[] = [];
|
|
98
|
+
private rateInterval: ReturnType<typeof setInterval> | null = null;
|
|
53
99
|
|
|
54
100
|
public static styles = [
|
|
55
101
|
themeDefaultStyles,
|
|
56
102
|
cssManager.defaultStyles,
|
|
57
103
|
css`
|
|
58
|
-
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
|
59
104
|
:host {
|
|
60
|
-
|
|
105
|
+
display: block;
|
|
106
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
61
107
|
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
|
62
|
-
font-size: 12px;
|
|
63
|
-
line-height: 1.5;
|
|
64
108
|
}
|
|
109
|
+
|
|
65
110
|
.mainbox {
|
|
66
111
|
position: relative;
|
|
67
112
|
width: 100%;
|
|
@@ -76,255 +121,805 @@ export class DeesChartLog extends DeesElement {
|
|
|
76
121
|
|
|
77
122
|
.header {
|
|
78
123
|
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
|
|
79
|
-
padding: 12px
|
|
124
|
+
padding: 8px 12px;
|
|
80
125
|
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
|
81
126
|
display: flex;
|
|
82
|
-
justify-content: space-between;
|
|
83
127
|
align-items: center;
|
|
128
|
+
gap: 12px;
|
|
84
129
|
flex-shrink: 0;
|
|
130
|
+
flex-wrap: wrap;
|
|
85
131
|
}
|
|
86
132
|
|
|
87
133
|
.title {
|
|
88
134
|
font-weight: 500;
|
|
89
135
|
font-size: 14px;
|
|
90
136
|
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
|
91
|
-
|
|
137
|
+
white-space: nowrap;
|
|
92
138
|
}
|
|
93
139
|
|
|
94
|
-
.
|
|
140
|
+
.search-box {
|
|
95
141
|
display: flex;
|
|
96
|
-
|
|
142
|
+
align-items: center;
|
|
143
|
+
gap: 4px;
|
|
144
|
+
flex: 1;
|
|
145
|
+
min-width: 150px;
|
|
146
|
+
max-width: 300px;
|
|
97
147
|
}
|
|
98
148
|
|
|
99
|
-
.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
border-radius: 6px;
|
|
103
|
-
padding: 6px 12px;
|
|
104
|
-
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
|
105
|
-
cursor: pointer;
|
|
149
|
+
.search-box input {
|
|
150
|
+
flex: 1;
|
|
151
|
+
padding: 4px 8px;
|
|
106
152
|
font-size: 12px;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.control-button:hover {
|
|
113
|
-
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
|
114
|
-
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
|
115
|
-
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
|
153
|
+
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
|
154
|
+
border-radius: 4px;
|
|
155
|
+
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
|
156
|
+
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
|
157
|
+
outline: none;
|
|
116
158
|
}
|
|
117
159
|
|
|
118
|
-
.
|
|
119
|
-
|
|
120
|
-
color: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(0 0% 3.9%)')};
|
|
160
|
+
.search-box input:focus {
|
|
161
|
+
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
|
121
162
|
}
|
|
122
163
|
|
|
123
|
-
.
|
|
124
|
-
|
|
125
|
-
overflow-y: auto;
|
|
126
|
-
overflow-x: hidden;
|
|
127
|
-
padding: 16px;
|
|
128
|
-
font-size: 12px;
|
|
164
|
+
.search-box input::placeholder {
|
|
165
|
+
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
|
|
129
166
|
}
|
|
130
167
|
|
|
131
|
-
.
|
|
132
|
-
margin-bottom: 4px;
|
|
168
|
+
.search-nav {
|
|
133
169
|
display: flex;
|
|
134
|
-
|
|
135
|
-
word-break: break-all;
|
|
136
|
-
font-variant-numeric: tabular-nums;
|
|
170
|
+
gap: 2px;
|
|
137
171
|
}
|
|
138
172
|
|
|
139
|
-
.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
173
|
+
.search-nav button {
|
|
174
|
+
padding: 4px 6px;
|
|
175
|
+
font-size: 11px;
|
|
176
|
+
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')};
|
|
177
|
+
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
|
178
|
+
border-radius: 3px;
|
|
179
|
+
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
line-height: 1;
|
|
143
182
|
}
|
|
144
183
|
|
|
145
|
-
.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
border-radius: 3px;
|
|
149
|
-
font-weight: 600;
|
|
150
|
-
text-transform: uppercase;
|
|
151
|
-
font-size: 10px;
|
|
152
|
-
flex-shrink: 0;
|
|
184
|
+
.search-nav button:hover {
|
|
185
|
+
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 20%)')};
|
|
186
|
+
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
|
153
187
|
}
|
|
154
188
|
|
|
155
|
-
.
|
|
189
|
+
.filter-toggle {
|
|
190
|
+
padding: 4px 8px;
|
|
191
|
+
font-size: 11px;
|
|
192
|
+
font-weight: 500;
|
|
193
|
+
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')};
|
|
194
|
+
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
|
195
|
+
border-radius: 4px;
|
|
156
196
|
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
|
157
|
-
|
|
197
|
+
cursor: pointer;
|
|
198
|
+
transition: all 0.15s;
|
|
199
|
+
white-space: nowrap;
|
|
158
200
|
}
|
|
159
201
|
|
|
160
|
-
.
|
|
161
|
-
|
|
162
|
-
|
|
202
|
+
.filter-toggle:hover {
|
|
203
|
+
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 20%)')};
|
|
204
|
+
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
|
163
205
|
}
|
|
164
206
|
|
|
165
|
-
.
|
|
166
|
-
|
|
167
|
-
|
|
207
|
+
.filter-toggle.active {
|
|
208
|
+
background: ${cssManager.bdTheme('hsl(45 93% 47%)', 'hsl(45 93% 47%)')};
|
|
209
|
+
border-color: ${cssManager.bdTheme('hsl(45 93% 47%)', 'hsl(45 93% 47%)')};
|
|
210
|
+
color: hsl(0 0% 9%);
|
|
168
211
|
}
|
|
169
212
|
|
|
170
|
-
.
|
|
171
|
-
|
|
172
|
-
|
|
213
|
+
.controls {
|
|
214
|
+
display: flex;
|
|
215
|
+
gap: 6px;
|
|
216
|
+
margin-left: auto;
|
|
173
217
|
}
|
|
174
218
|
|
|
175
|
-
.
|
|
176
|
-
|
|
177
|
-
|
|
219
|
+
.control-button {
|
|
220
|
+
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')};
|
|
221
|
+
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
|
222
|
+
border-radius: 4px;
|
|
223
|
+
padding: 4px 10px;
|
|
224
|
+
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
font-size: 12px;
|
|
227
|
+
font-weight: 500;
|
|
228
|
+
transition: all 0.15s;
|
|
178
229
|
}
|
|
179
230
|
|
|
180
|
-
.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
231
|
+
.control-button:hover {
|
|
232
|
+
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 20%)')};
|
|
233
|
+
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 25%)')};
|
|
234
|
+
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
|
184
235
|
}
|
|
185
236
|
|
|
186
|
-
.
|
|
187
|
-
|
|
237
|
+
.control-button.active {
|
|
238
|
+
background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
|
239
|
+
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
|
240
|
+
color: white;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.terminal-container {
|
|
188
244
|
flex: 1;
|
|
245
|
+
overflow: hidden;
|
|
246
|
+
padding: 8px;
|
|
247
|
+
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.terminal-container .xterm {
|
|
251
|
+
height: 100%;
|
|
189
252
|
}
|
|
190
253
|
|
|
191
|
-
.
|
|
254
|
+
.loading-state {
|
|
192
255
|
display: flex;
|
|
193
256
|
align-items: center;
|
|
194
257
|
justify-content: center;
|
|
195
258
|
height: 100%;
|
|
196
259
|
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
|
197
260
|
font-style: italic;
|
|
261
|
+
font-size: 13px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.metrics-bar {
|
|
265
|
+
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
|
|
266
|
+
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
|
267
|
+
padding: 6px 12px;
|
|
268
|
+
display: flex;
|
|
269
|
+
gap: 16px;
|
|
270
|
+
font-size: 11px;
|
|
271
|
+
font-weight: 500;
|
|
272
|
+
flex-shrink: 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.metric {
|
|
276
|
+
display: flex;
|
|
277
|
+
align-items: center;
|
|
278
|
+
gap: 4px;
|
|
198
279
|
}
|
|
199
280
|
|
|
200
|
-
|
|
201
|
-
|
|
281
|
+
.metric::before {
|
|
282
|
+
content: '';
|
|
202
283
|
width: 8px;
|
|
284
|
+
height: 8px;
|
|
285
|
+
border-radius: 50%;
|
|
203
286
|
}
|
|
204
287
|
|
|
205
|
-
.
|
|
206
|
-
background:
|
|
288
|
+
.metric.error::before {
|
|
289
|
+
background: hsl(0 84.2% 60.2%);
|
|
207
290
|
}
|
|
208
291
|
|
|
209
|
-
.
|
|
210
|
-
background:
|
|
211
|
-
border-radius: 4px;
|
|
292
|
+
.metric.warn::before {
|
|
293
|
+
background: hsl(25 95% 53%);
|
|
212
294
|
}
|
|
213
295
|
|
|
214
|
-
.
|
|
215
|
-
background:
|
|
296
|
+
.metric.info::before {
|
|
297
|
+
background: hsl(222.2 47.4% 51.2%);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.metric.success::before {
|
|
301
|
+
background: hsl(142.1 76.2% 36.3%);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.metric.debug::before {
|
|
305
|
+
background: hsl(0 0% 63.9%);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.metric.rate {
|
|
309
|
+
margin-left: auto;
|
|
310
|
+
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.metric.rate::before {
|
|
314
|
+
display: none;
|
|
216
315
|
}
|
|
217
316
|
`,
|
|
218
317
|
];
|
|
219
318
|
|
|
319
|
+
constructor() {
|
|
320
|
+
super();
|
|
321
|
+
domtools.elementBasic.setup();
|
|
322
|
+
}
|
|
323
|
+
|
|
220
324
|
public render(): TemplateResult {
|
|
221
325
|
return html`
|
|
222
326
|
<div class="mainbox">
|
|
223
327
|
<div class="header">
|
|
224
328
|
<div class="title">${this.label}</div>
|
|
329
|
+
<div class="search-box">
|
|
330
|
+
<input
|
|
331
|
+
type="text"
|
|
332
|
+
placeholder="Search logs..."
|
|
333
|
+
.value=${this.searchQuery}
|
|
334
|
+
@input=${(e: InputEvent) => this.handleSearchInput(e)}
|
|
335
|
+
@keydown=${(e: KeyboardEvent) => this.handleSearchKeydown(e)}
|
|
336
|
+
/>
|
|
337
|
+
<div class="search-nav">
|
|
338
|
+
<button @click=${() => this.searchPrevious()} title="Previous match">↑</button>
|
|
339
|
+
<button @click=${() => this.searchNext()} title="Next match">↓</button>
|
|
340
|
+
</div>
|
|
341
|
+
<button
|
|
342
|
+
class="filter-toggle ${this.filterMode ? 'active' : ''}"
|
|
343
|
+
@click=${() => this.toggleFilterMode()}
|
|
344
|
+
title="${this.filterMode ? 'Switch to highlight mode' : 'Switch to filter mode'}"
|
|
345
|
+
>
|
|
346
|
+
${this.filterMode ? 'Filter' : 'Highlight'}
|
|
347
|
+
</button>
|
|
348
|
+
</div>
|
|
225
349
|
<div class="controls">
|
|
226
|
-
<button
|
|
350
|
+
<button
|
|
227
351
|
class="control-button ${this.autoScroll ? 'active' : ''}"
|
|
228
|
-
@click=${() =>
|
|
352
|
+
@click=${() => this.toggleAutoScroll()}
|
|
229
353
|
>
|
|
230
354
|
Auto Scroll
|
|
231
355
|
</button>
|
|
232
|
-
<button
|
|
233
|
-
class="control-button"
|
|
234
|
-
@click=${() => { this.clearLogs(); }}
|
|
235
|
-
>
|
|
356
|
+
<button class="control-button" @click=${() => this.clearLogs()}>
|
|
236
357
|
Clear
|
|
237
358
|
</button>
|
|
238
359
|
</div>
|
|
239
360
|
</div>
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
361
|
+
|
|
362
|
+
<div class="terminal-container">
|
|
363
|
+
${!this.terminalReady
|
|
364
|
+
? html`<div class="loading-state">Loading terminal...</div>`
|
|
365
|
+
: ''}
|
|
245
366
|
</div>
|
|
367
|
+
|
|
368
|
+
${this.showMetrics
|
|
369
|
+
? html`
|
|
370
|
+
<div class="metrics-bar">
|
|
371
|
+
<span class="metric error">errors: ${this.metrics.error}</span>
|
|
372
|
+
<span class="metric warn">warns: ${this.metrics.warn}</span>
|
|
373
|
+
<span class="metric info">info: ${this.metrics.info}</span>
|
|
374
|
+
<span class="metric success">success: ${this.metrics.success}</span>
|
|
375
|
+
<span class="metric debug">debug: ${this.metrics.debug}</span>
|
|
376
|
+
<span class="metric rate">${this.metrics.rate.toFixed(1)} logs/sec</span>
|
|
377
|
+
</div>
|
|
378
|
+
`
|
|
379
|
+
: ''}
|
|
246
380
|
</div>
|
|
247
381
|
`;
|
|
248
382
|
}
|
|
249
383
|
|
|
250
|
-
|
|
251
|
-
|
|
384
|
+
public async firstUpdated() {
|
|
385
|
+
this.domtoolsInstance = await this.domtoolsPromise;
|
|
386
|
+
await this.initializeTerminal();
|
|
387
|
+
|
|
388
|
+
// Process any initial log entries
|
|
389
|
+
if (this.logEntries.length > 0) {
|
|
390
|
+
for (const entry of this.logEntries) {
|
|
391
|
+
this.writeLogEntry(entry);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private async initializeTerminal() {
|
|
397
|
+
const libLoader = DeesServiceLibLoader.getInstance();
|
|
398
|
+
|
|
399
|
+
const [xtermBundle, fitBundle, searchBundle] = await Promise.all([
|
|
400
|
+
libLoader.loadXterm(),
|
|
401
|
+
libLoader.loadXtermFitAddon(),
|
|
402
|
+
libLoader.loadXtermSearchAddon(),
|
|
403
|
+
]);
|
|
404
|
+
|
|
405
|
+
// Inject xterm CSS into shadow root (needed because shadow DOM doesn't inherit from document.head)
|
|
406
|
+
await this.injectXtermStylesIntoShadow();
|
|
407
|
+
|
|
408
|
+
this.terminal = new xtermBundle.Terminal({
|
|
409
|
+
cursorBlink: false,
|
|
410
|
+
disableStdin: true,
|
|
411
|
+
fontSize: 12,
|
|
412
|
+
fontFamily: "'SF Mono', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', monospace",
|
|
413
|
+
theme: this.getTerminalTheme(),
|
|
414
|
+
scrollback: this.maxEntries,
|
|
415
|
+
convertEol: true,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
this.fitAddon = new fitBundle.FitAddon();
|
|
419
|
+
this.searchAddon = new searchBundle.SearchAddon();
|
|
420
|
+
|
|
421
|
+
this.terminal.loadAddon(this.fitAddon);
|
|
422
|
+
this.terminal.loadAddon(this.searchAddon);
|
|
423
|
+
|
|
424
|
+
const container = this.shadowRoot!.querySelector('.terminal-container') as HTMLElement;
|
|
425
|
+
this.terminal.open(container);
|
|
426
|
+
|
|
427
|
+
// Fit after a small delay to ensure proper sizing
|
|
428
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
429
|
+
this.fitAddon.fit();
|
|
430
|
+
|
|
431
|
+
// Set up resize observer
|
|
432
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
433
|
+
this.fitAddon?.fit();
|
|
434
|
+
});
|
|
435
|
+
this.resizeObserver.observe(container);
|
|
436
|
+
|
|
437
|
+
// Subscribe to theme changes
|
|
438
|
+
this.terminalThemeSubscription = this.domtoolsInstance.themeManager.themeObservable.subscribe(() => {
|
|
439
|
+
if (this.terminal) {
|
|
440
|
+
this.terminal.options.theme = this.getTerminalTheme();
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Start rate calculation interval
|
|
445
|
+
this.rateInterval = setInterval(() => this.calculateRate(), 1000);
|
|
446
|
+
|
|
447
|
+
this.terminalReady = true;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private getTerminalTheme() {
|
|
451
|
+
const isDark = this.domtoolsInstance?.themeManager?.isDarkMode ?? true;
|
|
452
|
+
return isDark
|
|
453
|
+
? {
|
|
454
|
+
background: '#0a0a0a',
|
|
455
|
+
foreground: '#e0e0e0',
|
|
456
|
+
cursor: '#e0e0e0',
|
|
457
|
+
selectionBackground: '#404040',
|
|
458
|
+
black: '#000000',
|
|
459
|
+
red: '#ff5555',
|
|
460
|
+
green: '#50fa7b',
|
|
461
|
+
yellow: '#f1fa8c',
|
|
462
|
+
blue: '#6272a4',
|
|
463
|
+
magenta: '#ff79c6',
|
|
464
|
+
cyan: '#8be9fd',
|
|
465
|
+
white: '#f8f8f2',
|
|
466
|
+
brightBlack: '#6272a4',
|
|
467
|
+
brightRed: '#ff6e6e',
|
|
468
|
+
brightGreen: '#69ff94',
|
|
469
|
+
brightYellow: '#ffffa5',
|
|
470
|
+
brightBlue: '#d6acff',
|
|
471
|
+
brightMagenta: '#ff92df',
|
|
472
|
+
brightCyan: '#a4ffff',
|
|
473
|
+
brightWhite: '#ffffff',
|
|
474
|
+
}
|
|
475
|
+
: {
|
|
476
|
+
background: '#ffffff',
|
|
477
|
+
foreground: '#333333',
|
|
478
|
+
cursor: '#333333',
|
|
479
|
+
selectionBackground: '#add6ff',
|
|
480
|
+
black: '#000000',
|
|
481
|
+
red: '#cd3131',
|
|
482
|
+
green: '#00bc00',
|
|
483
|
+
yellow: '#949800',
|
|
484
|
+
blue: '#0451a5',
|
|
485
|
+
magenta: '#bc05bc',
|
|
486
|
+
cyan: '#0598bc',
|
|
487
|
+
white: '#555555',
|
|
488
|
+
brightBlack: '#666666',
|
|
489
|
+
brightRed: '#cd3131',
|
|
490
|
+
brightGreen: '#14ce14',
|
|
491
|
+
brightYellow: '#b5ba00',
|
|
492
|
+
brightBlue: '#0451a5',
|
|
493
|
+
brightMagenta: '#bc05bc',
|
|
494
|
+
brightCyan: '#0598bc',
|
|
495
|
+
brightWhite: '#a5a5a5',
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Inject xterm CSS styles into shadow root
|
|
501
|
+
* This is needed because shadow DOM doesn't inherit styles from document.head
|
|
502
|
+
*/
|
|
503
|
+
private async injectXtermStylesIntoShadow(): Promise<void> {
|
|
504
|
+
const styleId = 'xterm-shadow-styles';
|
|
505
|
+
if (this.shadowRoot!.getElementById(styleId)) {
|
|
506
|
+
return; // Already injected
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const cssUrl = `${CDN_BASE}/xterm@${CDN_VERSIONS.xterm}/css/xterm.css`;
|
|
510
|
+
const response = await fetch(cssUrl);
|
|
511
|
+
const cssText = await response.text();
|
|
512
|
+
|
|
513
|
+
const style = document.createElement('style');
|
|
514
|
+
style.id = styleId;
|
|
515
|
+
style.textContent = cssText;
|
|
516
|
+
this.shadowRoot!.appendChild(style);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// =====================
|
|
520
|
+
// Structured Log Methods
|
|
521
|
+
// =====================
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Add a single structured log entry
|
|
525
|
+
*/
|
|
526
|
+
public addLog(level: ILogEntry['level'], message: string, source?: string) {
|
|
527
|
+
const entry: ILogEntry = {
|
|
528
|
+
timestamp: new Date().toISOString(),
|
|
529
|
+
level,
|
|
530
|
+
message,
|
|
531
|
+
source,
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// Add to buffer
|
|
535
|
+
this.logBuffer.push(entry);
|
|
536
|
+
if (this.logBuffer.length > this.maxEntries) {
|
|
537
|
+
this.logBuffer.shift();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Handle display based on filter mode
|
|
541
|
+
if (!this.filterMode || !this.searchQuery) {
|
|
542
|
+
// No filtering - show all entries
|
|
543
|
+
this.writeLogEntry(entry);
|
|
544
|
+
} else if (this.entryMatchesFilter(entry)) {
|
|
545
|
+
// Entry matches filter - reset trailing count and write entry
|
|
546
|
+
this.trailingHiddenCount = 0;
|
|
547
|
+
this.writeLogEntry(entry);
|
|
548
|
+
} else {
|
|
549
|
+
// Entry doesn't match - update trailing placeholder
|
|
550
|
+
this.updateTrailingPlaceholder();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
this.updateMetrics(entry.level);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Add multiple structured log entries
|
|
558
|
+
*/
|
|
559
|
+
public updateLog(entries?: ILogEntry[]) {
|
|
560
|
+
if (!entries) return;
|
|
561
|
+
for (const entry of entries) {
|
|
562
|
+
// Add to buffer
|
|
563
|
+
this.logBuffer.push(entry);
|
|
564
|
+
if (this.logBuffer.length > this.maxEntries) {
|
|
565
|
+
this.logBuffer.shift();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Handle display based on filter mode
|
|
569
|
+
if (!this.filterMode || !this.searchQuery) {
|
|
570
|
+
// No filtering - show all entries
|
|
571
|
+
this.writeLogEntry(entry);
|
|
572
|
+
} else if (this.entryMatchesFilter(entry)) {
|
|
573
|
+
// Entry matches filter - reset trailing count and write entry
|
|
574
|
+
this.trailingHiddenCount = 0;
|
|
575
|
+
this.writeLogEntry(entry);
|
|
576
|
+
} else {
|
|
577
|
+
// Entry doesn't match - update trailing placeholder
|
|
578
|
+
this.updateTrailingPlaceholder();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
this.updateMetrics(entry.level);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Update the trailing hidden placeholder in real-time
|
|
587
|
+
* Clears the last line if a placeholder already exists, then writes updated count
|
|
588
|
+
*/
|
|
589
|
+
private updateTrailingPlaceholder() {
|
|
590
|
+
if (!this.terminal) return;
|
|
591
|
+
|
|
592
|
+
if (this.trailingHiddenCount > 0) {
|
|
593
|
+
// Clear the previous placeholder line (move up, clear line, move to start)
|
|
594
|
+
this.terminal.write('\x1b[1A\x1b[2K\r');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
this.trailingHiddenCount++;
|
|
598
|
+
this.writeHiddenPlaceholder(this.trailingHiddenCount);
|
|
599
|
+
|
|
600
|
+
if (this.autoScroll) {
|
|
601
|
+
this.terminal.scrollToBottom();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Check if a log entry matches the current filter
|
|
607
|
+
*/
|
|
608
|
+
private entryMatchesFilter(entry: ILogEntry): boolean {
|
|
609
|
+
if (!this.searchQuery) return true;
|
|
610
|
+
const query = this.searchQuery.toLowerCase();
|
|
611
|
+
return (
|
|
612
|
+
entry.message.toLowerCase().includes(query) ||
|
|
613
|
+
entry.level.toLowerCase().includes(query) ||
|
|
614
|
+
(entry.source?.toLowerCase().includes(query) ?? false)
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private writeLogEntry(entry: ILogEntry) {
|
|
619
|
+
if (!this.terminal) return;
|
|
620
|
+
|
|
621
|
+
const formatted = this.formatLogEntry(entry);
|
|
622
|
+
this.terminal.writeln(formatted);
|
|
623
|
+
|
|
624
|
+
if (this.autoScroll) {
|
|
625
|
+
this.terminal.scrollToBottom();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private formatLogEntry(entry: ILogEntry): string {
|
|
630
|
+
const timestamp = this.formatTimestamp(entry.timestamp);
|
|
631
|
+
const levelColors: Record<ILogEntry['level'], string> = {
|
|
632
|
+
debug: '\x1b[90m', // Gray
|
|
633
|
+
info: '\x1b[36m', // Cyan
|
|
634
|
+
warn: '\x1b[33m', // Yellow
|
|
635
|
+
error: '\x1b[31m', // Red
|
|
636
|
+
success: '\x1b[32m', // Green
|
|
637
|
+
};
|
|
638
|
+
const reset = '\x1b[0m';
|
|
639
|
+
const dim = '\x1b[2m';
|
|
640
|
+
|
|
641
|
+
const levelStr = `${levelColors[entry.level]}[${entry.level.toUpperCase().padEnd(7)}]${reset}`;
|
|
642
|
+
const sourceStr = entry.source ? `${dim}[${entry.source}]${reset} ` : '';
|
|
643
|
+
const messageStr = this.applyHighlights(entry.message);
|
|
644
|
+
|
|
645
|
+
return `${dim}${timestamp}${reset} ${levelStr} ${sourceStr}${messageStr}`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private formatTimestamp(isoString: string): string {
|
|
649
|
+
const date = new Date(isoString);
|
|
650
|
+
return date.toLocaleTimeString('en-US', {
|
|
252
651
|
hour12: false,
|
|
253
652
|
hour: '2-digit',
|
|
254
653
|
minute: '2-digit',
|
|
255
654
|
second: '2-digit',
|
|
256
|
-
fractionalSecondDigits: 3
|
|
257
|
-
});
|
|
655
|
+
fractionalSecondDigits: 3,
|
|
656
|
+
} as Intl.DateTimeFormatOptions);
|
|
657
|
+
}
|
|
258
658
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
659
|
+
private applyHighlights(text: string): string {
|
|
660
|
+
// Collect all keywords to highlight
|
|
661
|
+
const keywords = [...this.highlightKeywords];
|
|
662
|
+
|
|
663
|
+
// In filter mode, also highlight the search query
|
|
664
|
+
if (this.filterMode && this.searchQuery) {
|
|
665
|
+
keywords.push(this.searchQuery);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (keywords.length === 0) return text;
|
|
669
|
+
|
|
670
|
+
let result = text;
|
|
671
|
+
for (const keyword of keywords) {
|
|
672
|
+
// Escape regex special characters
|
|
673
|
+
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
674
|
+
const regex = new RegExp(`(${escaped})`, 'gi');
|
|
675
|
+
// Yellow background, black text for highlights
|
|
676
|
+
result = result.replace(regex, '\x1b[43m\x1b[30m$1\x1b[0m');
|
|
677
|
+
}
|
|
678
|
+
return result;
|
|
267
679
|
}
|
|
268
680
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
public
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
681
|
+
// =====================
|
|
682
|
+
// Raw Log Methods
|
|
683
|
+
// =====================
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Write raw data to the terminal (for Docker logs, etc.)
|
|
687
|
+
*/
|
|
688
|
+
public writeRaw(data: string) {
|
|
689
|
+
if (!this.terminal) return;
|
|
690
|
+
this.terminal.write(data);
|
|
691
|
+
this.recordLogEvent();
|
|
692
|
+
|
|
693
|
+
if (this.autoScroll) {
|
|
694
|
+
this.terminal.scrollToBottom();
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Write a raw line to the terminal
|
|
700
|
+
*/
|
|
701
|
+
public writelnRaw(line: string) {
|
|
702
|
+
if (!this.terminal) return;
|
|
703
|
+
this.terminal.writeln(line);
|
|
704
|
+
this.recordLogEvent();
|
|
705
|
+
|
|
706
|
+
if (this.autoScroll) {
|
|
707
|
+
this.terminal.scrollToBottom();
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// =====================
|
|
712
|
+
// Search Methods
|
|
713
|
+
// =====================
|
|
714
|
+
|
|
715
|
+
private handleSearchInput(e: InputEvent) {
|
|
716
|
+
const input = e.target as HTMLInputElement;
|
|
717
|
+
const newQuery = input.value;
|
|
718
|
+
const queryChanged = this.searchQuery !== newQuery;
|
|
719
|
+
this.searchQuery = newQuery;
|
|
720
|
+
|
|
721
|
+
if (this.filterMode && queryChanged) {
|
|
722
|
+
// Re-render with filtered logs
|
|
723
|
+
this.reRenderFilteredLogs();
|
|
724
|
+
} else if (this.searchQuery) {
|
|
725
|
+
// Just highlight/search in current view
|
|
726
|
+
this.searchAddon?.findNext(this.searchQuery);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private handleSearchKeydown(e: KeyboardEvent) {
|
|
731
|
+
if (e.key === 'Enter') {
|
|
732
|
+
if (e.shiftKey) {
|
|
733
|
+
this.searchPrevious();
|
|
734
|
+
} else {
|
|
735
|
+
this.searchNext();
|
|
306
736
|
}
|
|
737
|
+
} else if (e.key === 'Escape') {
|
|
738
|
+
this.searchQuery = '';
|
|
739
|
+
(e.target as HTMLInputElement).value = '';
|
|
307
740
|
}
|
|
308
741
|
}
|
|
309
742
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
743
|
+
/**
|
|
744
|
+
* Search for a query in the terminal
|
|
745
|
+
*/
|
|
746
|
+
public search(query: string): void {
|
|
747
|
+
this.searchQuery = query;
|
|
748
|
+
this.searchAddon?.findNext(query);
|
|
313
749
|
}
|
|
314
750
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
751
|
+
/**
|
|
752
|
+
* Find next search match
|
|
753
|
+
*/
|
|
754
|
+
public searchNext(): void {
|
|
755
|
+
if (this.searchQuery) {
|
|
756
|
+
this.searchAddon?.findNext(this.searchQuery);
|
|
318
757
|
}
|
|
319
758
|
}
|
|
320
759
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
760
|
+
/**
|
|
761
|
+
* Find previous search match
|
|
762
|
+
*/
|
|
763
|
+
public searchPrevious(): void {
|
|
764
|
+
if (this.searchQuery) {
|
|
765
|
+
this.searchAddon?.findPrevious(this.searchQuery);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// =====================
|
|
770
|
+
// Control Methods
|
|
771
|
+
// =====================
|
|
772
|
+
|
|
773
|
+
private toggleAutoScroll() {
|
|
774
|
+
this.autoScroll = !this.autoScroll;
|
|
775
|
+
if (this.autoScroll && this.terminal) {
|
|
776
|
+
this.terminal.scrollToBottom();
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Toggle between filter mode and highlight mode
|
|
782
|
+
*/
|
|
783
|
+
private toggleFilterMode() {
|
|
784
|
+
this.filterMode = !this.filterMode;
|
|
785
|
+
this.reRenderFilteredLogs();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Re-render logs based on current filter state
|
|
790
|
+
* In filter mode: show matching logs with placeholders for hidden entries
|
|
791
|
+
* In highlight mode: show all logs
|
|
792
|
+
*/
|
|
793
|
+
private reRenderFilteredLogs() {
|
|
794
|
+
if (!this.terminal) return;
|
|
795
|
+
|
|
796
|
+
// Clear terminal and re-render
|
|
797
|
+
this.terminal.clear();
|
|
798
|
+
|
|
799
|
+
// Reset trailing count for fresh render
|
|
800
|
+
this.trailingHiddenCount = 0;
|
|
801
|
+
|
|
802
|
+
if (!this.filterMode || !this.searchQuery) {
|
|
803
|
+
// No filtering - show all entries
|
|
804
|
+
for (const entry of this.logBuffer) {
|
|
805
|
+
const formatted = this.formatLogEntry(entry);
|
|
806
|
+
this.terminal.writeln(formatted);
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
// Filter mode with placeholders for hidden entries
|
|
810
|
+
let hiddenCount = 0;
|
|
811
|
+
|
|
812
|
+
for (const entry of this.logBuffer) {
|
|
813
|
+
if (this.entryMatchesFilter(entry)) {
|
|
814
|
+
// Output placeholder for hidden entries if any
|
|
815
|
+
if (hiddenCount > 0) {
|
|
816
|
+
this.writeHiddenPlaceholder(hiddenCount);
|
|
817
|
+
hiddenCount = 0;
|
|
818
|
+
}
|
|
819
|
+
// Output the matching entry
|
|
820
|
+
const formatted = this.formatLogEntry(entry);
|
|
821
|
+
this.terminal.writeln(formatted);
|
|
822
|
+
} else {
|
|
823
|
+
hiddenCount++;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Handle trailing hidden entries
|
|
828
|
+
if (hiddenCount > 0) {
|
|
829
|
+
this.writeHiddenPlaceholder(hiddenCount);
|
|
830
|
+
// Store trailing count for live updates
|
|
831
|
+
this.trailingHiddenCount = hiddenCount;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (this.autoScroll) {
|
|
836
|
+
this.terminal.scrollToBottom();
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Write a placeholder line showing how many log entries are hidden by filter
|
|
842
|
+
*/
|
|
843
|
+
private writeHiddenPlaceholder(count: number) {
|
|
844
|
+
const dim = '\x1b[2m';
|
|
845
|
+
const reset = '\x1b[0m';
|
|
846
|
+
const text = count === 1
|
|
847
|
+
? `[1 log line hidden by filter ...]`
|
|
848
|
+
: `[${count} log lines hidden by filter ...]`;
|
|
849
|
+
this.terminal?.writeln(`${dim}${text}${reset}`);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Clear all logs and reset metrics
|
|
854
|
+
*/
|
|
855
|
+
public clearLogs() {
|
|
856
|
+
this.terminal?.clear();
|
|
857
|
+
this.logBuffer = [];
|
|
858
|
+
this.trailingHiddenCount = 0;
|
|
859
|
+
this.resetMetrics();
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Scroll to the bottom of the log
|
|
864
|
+
*/
|
|
865
|
+
public scrollToBottom() {
|
|
866
|
+
this.terminal?.scrollToBottom();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// =====================
|
|
870
|
+
// Metrics Methods
|
|
871
|
+
// =====================
|
|
872
|
+
|
|
873
|
+
private updateMetrics(level: ILogEntry['level']) {
|
|
874
|
+
this.metrics = {
|
|
875
|
+
...this.metrics,
|
|
876
|
+
[level]: this.metrics[level] + 1,
|
|
877
|
+
total: this.metrics.total + 1,
|
|
327
878
|
};
|
|
328
|
-
this.
|
|
879
|
+
this.recordLogEvent();
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
private recordLogEvent() {
|
|
883
|
+
this.rateBuffer.push(Date.now());
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
private calculateRate() {
|
|
887
|
+
const now = Date.now();
|
|
888
|
+
// Keep only events from the last 10 seconds
|
|
889
|
+
this.rateBuffer = this.rateBuffer.filter((t) => now - t < 10000);
|
|
890
|
+
const rate = this.rateBuffer.length / 10;
|
|
891
|
+
|
|
892
|
+
if (rate !== this.metrics.rate) {
|
|
893
|
+
this.metrics = { ...this.metrics, rate };
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
private resetMetrics() {
|
|
898
|
+
this.metrics = { debug: 0, info: 0, warn: 0, error: 0, success: 0, total: 0, rate: 0 };
|
|
899
|
+
this.rateBuffer = [];
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// =====================
|
|
903
|
+
// Lifecycle
|
|
904
|
+
// =====================
|
|
905
|
+
|
|
906
|
+
async disconnectedCallback() {
|
|
907
|
+
await super.disconnectedCallback();
|
|
908
|
+
|
|
909
|
+
if (this.resizeObserver) {
|
|
910
|
+
this.resizeObserver.disconnect();
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (this.terminalThemeSubscription) {
|
|
914
|
+
this.terminalThemeSubscription.unsubscribe();
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (this.rateInterval) {
|
|
918
|
+
clearInterval(this.rateInterval);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (this.terminal) {
|
|
922
|
+
this.terminal.dispose();
|
|
923
|
+
}
|
|
329
924
|
}
|
|
330
925
|
}
|