@juice-js/observability 0.0.0-stage → 1.0.0-alpha.1

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.
Files changed (34) hide show
  1. package/README.md +31 -2
  2. package/fesm2022/juice-js-observability-alert.page-D2OL0zTT.mjs +405 -0
  3. package/fesm2022/juice-js-observability-alert.page-D2OL0zTT.mjs.map +1 -0
  4. package/fesm2022/juice-js-observability-alerts.page-BLJvD_op.mjs +208 -0
  5. package/fesm2022/juice-js-observability-alerts.page-BLJvD_op.mjs.map +1 -0
  6. package/fesm2022/juice-js-observability-logs.page-BWFLleyX.mjs +414 -0
  7. package/fesm2022/juice-js-observability-logs.page-BWFLleyX.mjs.map +1 -0
  8. package/fesm2022/juice-js-observability-metrics.page-D1caPbQq.mjs +268 -0
  9. package/fesm2022/juice-js-observability-metrics.page-D1caPbQq.mjs.map +1 -0
  10. package/fesm2022/juice-js-observability-overview.page-UBwHjlyq.mjs +213 -0
  11. package/fesm2022/juice-js-observability-overview.page-UBwHjlyq.mjs.map +1 -0
  12. package/fesm2022/juice-js-observability-resource.page-BZwLK_vU.mjs +275 -0
  13. package/fesm2022/juice-js-observability-resource.page-BZwLK_vU.mjs.map +1 -0
  14. package/fesm2022/juice-js-observability-resources.page-C0xWOb5I.mjs +210 -0
  15. package/fesm2022/juice-js-observability-resources.page-C0xWOb5I.mjs.map +1 -0
  16. package/fesm2022/juice-js-observability-rules.page-Db_U5rRu.mjs +176 -0
  17. package/fesm2022/juice-js-observability-rules.page-Db_U5rRu.mjs.map +1 -0
  18. package/fesm2022/juice-js-observability-service.page-B-SWJ9I5.mjs +727 -0
  19. package/fesm2022/juice-js-observability-service.page-B-SWJ9I5.mjs.map +1 -0
  20. package/fesm2022/juice-js-observability-services.page-BmWgFnC8.mjs +252 -0
  21. package/fesm2022/juice-js-observability-services.page-BmWgFnC8.mjs.map +1 -0
  22. package/fesm2022/juice-js-observability-telemetry-VA4_3nzr.mjs +1748 -0
  23. package/fesm2022/juice-js-observability-telemetry-VA4_3nzr.mjs.map +1 -0
  24. package/fesm2022/juice-js-observability-topology.page-DEUNp10U.mjs +458 -0
  25. package/fesm2022/juice-js-observability-topology.page-DEUNp10U.mjs.map +1 -0
  26. package/fesm2022/juice-js-observability-trace.page-gBhMG2eQ.mjs +530 -0
  27. package/fesm2022/juice-js-observability-trace.page-gBhMG2eQ.mjs.map +1 -0
  28. package/fesm2022/juice-js-observability-traces.page-BCQg4V4o.mjs +521 -0
  29. package/fesm2022/juice-js-observability-traces.page-BCQg4V4o.mjs.map +1 -0
  30. package/fesm2022/juice-js-observability.mjs +396 -0
  31. package/fesm2022/juice-js-observability.mjs.map +1 -0
  32. package/package.json +48 -4
  33. package/styles/_observability.scss +133 -0
  34. package/types/juice-js-observability.d.ts +387 -0
package/README.md CHANGED
@@ -1,3 +1,32 @@
1
- # Temporary Holding Version
1
+ # @juice-js/observability
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Juice Console pages for an Angular app built on `@juice-js/layout`: services, traces, dependencies, logs, metrics,
4
+ resources, alerts and rules. The pages read only the Juice Observability API (the `Juice.Observability.Api` NuGet
5
+ package, or the platform image).
6
+
7
+ ## Use
8
+
9
+ ```ts
10
+ // app module or app config
11
+ providers: [
12
+ provideHttpClient(withInterceptorsFromDi()),
13
+ provideJuiceObservability({ apiBase: '', basePath: '/observability' }),
14
+ ],
15
+
16
+ // routes: mount the pages under basePath, behind your own sign-in guard
17
+ { path: 'observability', children: OBSERVABILITY_ROUTES.map(r => ({ ...r, canActivate: [AuthGuard] })) },
18
+
19
+ // menu (juice-layout)
20
+ MenuModule.forRoot([...yourMenus, ...observabilityMenus('/observability', 100)]),
21
+ ```
22
+
23
+ ```scss
24
+ // styles.scss: the design tokens (light and dark) and jc-* classes the pages use
25
+ @use '@juice-js/observability';
26
+ ```
27
+
28
+ - `apiBase`: the API origin; empty when the API serves the app.
29
+ - `basePath`: where the routes are mounted; every link between pages goes under it.
30
+ - The router needs `bindToComponentInputs: true`. The pages take their route and query parameters as inputs.
31
+ - The host's bearer-token interceptor (`@juice-js/auth`) must cover `apiBase`. The token carries the `platform_tenant`
32
+ claim and a platform role (`viewer`, `operator` or `admin`).
@@ -0,0 +1,405 @@
1
+ import { ApiService, ContextService, LinkPipe } from "./juice-js-observability.mjs";
2
+ import { a as problemText, d as KeyValueTable, f as AgoPipe, h as ReasonPipe, l as ContextBar, u as HealthBadge } from "./juice-js-observability-telemetry-VA4_3nzr.mjs";
3
+ import * as i0 from "@angular/core";
4
+ import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from "@angular/core";
5
+ import { rxResource } from "@angular/core/rxjs-interop";
6
+ import { RouterLink } from "@angular/router";
7
+ import { DatePipe } from "@angular/common";
8
+ import * as i1 from "@angular/forms";
9
+ import { FormsModule } from "@angular/forms";
10
+ import * as i2 from "@angular/material/button";
11
+ import { MatButtonModule } from "@angular/material/button";
12
+ import { MatFormFieldModule } from "@angular/material/form-field";
13
+ import * as i4 from "@angular/material/input";
14
+ import { MatInputModule } from "@angular/material/input";
15
+ import * as i5 from "@angular/material/progress-bar";
16
+ import { MatProgressBarModule } from "@angular/material/progress-bar";
17
+ import * as i3 from "@angular/material/select";
18
+ var AlertPage = class AlertPage {
19
+ ctx = inject(ContextService);
20
+ api = inject(ApiService);
21
+ id = input.required(...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
22
+ comment = "";
23
+ sending = signal(false, ...ngDevMode ? [{ debugName: "sending" }] : /* istanbul ignore next */ []);
24
+ ackError = signal("", ...ngDevMode ? [{ debugName: "ackError" }] : /* istanbul ignore next */ []);
25
+ detail = rxResource({
26
+ params: () => this.id(),
27
+ stream: ({ params }) => this.api.alert(params)
28
+ });
29
+ error = computed(() => problemText(this.detail.error()), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
30
+ investigate(environment, extra = {}) {
31
+ const alert = this.detail.value()?.alert;
32
+ const start = alert?.breachStartedAt ? new Date(alert.breachStartedAt).getTime() - 9e5 : null;
33
+ const end = alert?.resolvedAt ? new Date(alert.resolvedAt).getTime() + 3e5 : Date.now();
34
+ const window = start && end - start <= 864e5 ? {
35
+ from: new Date(start).toISOString(),
36
+ to: new Date(end).toISOString(),
37
+ range: null
38
+ } : {};
39
+ return this.ctx.keep({
40
+ env: environment,
41
+ ...window,
42
+ ...extra
43
+ });
44
+ }
45
+ acknowledge() {
46
+ this.sending.set(true);
47
+ this.ackError.set("");
48
+ this.api.acknowledge(this.id(), this.comment).subscribe({
49
+ next: () => {
50
+ this.sending.set(false);
51
+ this.detail.reload();
52
+ },
53
+ error: (e) => {
54
+ this.sending.set(false);
55
+ this.ackError.set(e.status === 403 ? "Acknowledging needs the operator or admin role." : problemText(e));
56
+ }
57
+ });
58
+ }
59
+ static ɵfac = i0.ɵɵngDeclareFactory({
60
+ minVersion: "12.0.0",
61
+ version: "22.2.4",
62
+ ngImport: i0,
63
+ type: AlertPage,
64
+ deps: [],
65
+ target: i0.ɵɵFactoryTarget.Component
66
+ });
67
+ static ɵcmp = i0.ɵɵngDeclareComponent({
68
+ minVersion: "17.0.0",
69
+ version: "22.2.4",
70
+ type: AlertPage,
71
+ isStandalone: true,
72
+ selector: "ng-component",
73
+ inputs: { id: {
74
+ classPropertyName: "id",
75
+ publicName: "id",
76
+ isSignal: true,
77
+ isRequired: true,
78
+ transformFunction: null
79
+ } },
80
+ ngImport: i0,
81
+ template: `
82
+ <jc-context-bar>
83
+ <a [routerLink]="['alerts'] | jcLink" [queryParams]="ctx.keep()" class="jc-crumb">Alerts</a><span class="jc-crumb-sep">›</span>
84
+ <h1>{{ detail.value()?.alert?.rule ?? '…' }}</h1>
85
+ </jc-context-bar>
86
+ @if (detail.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
87
+ @if (detail.error()) { <p class="jc-error">{{ error() }}</p> }
88
+ @if (detail.value(); as d) {
89
+ <div class="jc-summary">
90
+ <span class="jc-badge" [class.jc-health-unhealthy]="d.alert.severity === 'Critical'" [class.jc-health-degraded]="d.alert.severity === 'Warning'">{{ d.alert.severity }}</span>
91
+ <strong>{{ d.alert.state }}</strong>
92
+ <span>{{ d.alert.subjectKey }}</span>
93
+ @if (d.alert.noData) { <span class="jc-muted">no data at the last evaluation: state kept</span> }
94
+ @if (d.alert.subjectType === 'service' && d.alert.subjectId) {
95
+ <a mat-flat-button [routerLink]="['services', d.alert.subjectId] | jcLink" [queryParams]="investigate(d.alert.environment)">Investigate</a>
96
+ <a mat-stroked-button [routerLink]="['services', d.alert.subjectId] | jcLink" [queryParams]="investigate(d.alert.environment, { tab: 'traces', status: 'error' })">Error traces</a>
97
+ }
98
+ @for (l of d.links; track l.url) { <a mat-button [href]="l.url" target="_blank" rel="noopener">{{ l.title }} ↗</a> }
99
+ </div>
100
+ <div class="jc-grid-2">
101
+ <section class="jc-card"><h2>Evidence</h2><jc-kv [values]="d.evidence" /></section>
102
+ <section class="jc-card">
103
+ <h2>Subject health</h2>
104
+ @for (h of d.subjectHealth; track h.subjectId + h.environment) {
105
+ <p><jc-health [state]="h.state" [stale]="h.stale" /> {{ h.environment }}</p>
106
+ <ul class="jc-reasons">@for (r of h.reasons; track r) { <li>{{ r | reason }}</li> }</ul>
107
+ } @empty { <p class="jc-muted">No health for this subject.</p> }
108
+ </section>
109
+ </div>
110
+ <section class="jc-card">
111
+ <h2>History</h2>
112
+ <table class="jc-table"><tbody>
113
+ @for (t of d.history; track t.at + t.to) {
114
+ <tr><td>{{ t.at | date: 'MMM d, HH:mm:ss' }}</td><td>{{ t.from ?? '—' }} → <strong>{{ t.to }}</strong></td><td>{{ t.reason | reason }}</td><td>{{ t.value ?? '—' }}</td></tr>
115
+ }
116
+ </tbody></table>
117
+ </section>
118
+ <section class="jc-card">
119
+ <h2>Acknowledgement</h2>
120
+ @if (d.alert.acknowledgedAt) {
121
+ <p>By <strong>{{ d.alert.acknowledgedBy }}</strong> {{ d.alert.acknowledgedAt | ago }}@if (d.acknowledgeComment) {: “{{ d.acknowledgeComment }}”}</p>
122
+ } @else if (d.alert.state !== 'Resolved') {
123
+ <form (ngSubmit)="acknowledge()" class="jc-filter-form">
124
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>Comment</mat-label><input matInput name="comment" [(ngModel)]="comment" maxlength="1000"></mat-form-field>
125
+ <button mat-flat-button type="submit" [disabled]="sending()">Acknowledge</button>
126
+ </form>
127
+ @if (ackError()) { <p class="jc-error">{{ ackError() }}</p> }
128
+ } @else { <p class="jc-muted">Resolved without acknowledgement.</p> }
129
+ </section>
130
+ }`,
131
+ isInline: true,
132
+ dependencies: [
133
+ {
134
+ kind: "directive",
135
+ type: RouterLink,
136
+ selector: "[routerLink]",
137
+ inputs: [
138
+ "target",
139
+ "queryParams",
140
+ "fragment",
141
+ "queryParamsHandling",
142
+ "state",
143
+ "info",
144
+ "relativeTo",
145
+ "preserveFragment",
146
+ "skipLocationChange",
147
+ "replaceUrl",
148
+ "browserUrl",
149
+ "routerLink"
150
+ ]
151
+ },
152
+ {
153
+ kind: "ngmodule",
154
+ type: FormsModule
155
+ },
156
+ {
157
+ kind: "directive",
158
+ type: i1.ɵNgNoValidate,
159
+ selector: "form:not([ngNoForm]):not([ngNativeValidate])"
160
+ },
161
+ {
162
+ kind: "directive",
163
+ type: i1.DefaultValueAccessor,
164
+ selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]"
165
+ },
166
+ {
167
+ kind: "directive",
168
+ type: i1.NgControlStatus,
169
+ selector: "[formControlName],[ngModel],[formControl]"
170
+ },
171
+ {
172
+ kind: "directive",
173
+ type: i1.NgControlStatusGroup,
174
+ selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]"
175
+ },
176
+ {
177
+ kind: "directive",
178
+ type: i1.MaxLengthValidator,
179
+ selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]",
180
+ inputs: ["maxlength"]
181
+ },
182
+ {
183
+ kind: "directive",
184
+ type: i1.NgModel,
185
+ selector: "[ngModel]:not([formControlName]):not([formControl])",
186
+ inputs: [
187
+ "name",
188
+ "disabled",
189
+ "ngModel",
190
+ "ngModelOptions"
191
+ ],
192
+ outputs: ["ngModelChange"],
193
+ exportAs: ["ngModel"]
194
+ },
195
+ {
196
+ kind: "directive",
197
+ type: i1.NgForm,
198
+ selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]",
199
+ inputs: ["ngFormOptions"],
200
+ outputs: ["ngSubmit"],
201
+ exportAs: ["ngForm"]
202
+ },
203
+ {
204
+ kind: "ngmodule",
205
+ type: MatButtonModule
206
+ },
207
+ {
208
+ kind: "component",
209
+ type: i2.MatButton,
210
+ selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ",
211
+ inputs: ["matButton"],
212
+ exportAs: ["matButton", "matAnchor"]
213
+ },
214
+ {
215
+ kind: "ngmodule",
216
+ type: MatFormFieldModule
217
+ },
218
+ {
219
+ kind: "component",
220
+ type: i3.MatFormField,
221
+ selector: "mat-form-field",
222
+ inputs: [
223
+ "hideRequiredMarker",
224
+ "color",
225
+ "floatLabel",
226
+ "appearance",
227
+ "subscriptSizing",
228
+ "hintLabel"
229
+ ],
230
+ exportAs: ["matFormField"]
231
+ },
232
+ {
233
+ kind: "directive",
234
+ type: i3.MatLabel,
235
+ selector: "mat-label"
236
+ },
237
+ {
238
+ kind: "ngmodule",
239
+ type: MatInputModule
240
+ },
241
+ {
242
+ kind: "directive",
243
+ type: i4.MatInput,
244
+ selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",
245
+ inputs: [
246
+ "disabled",
247
+ "id",
248
+ "placeholder",
249
+ "name",
250
+ "required",
251
+ "type",
252
+ "errorStateMatcher",
253
+ "aria-describedby",
254
+ "value",
255
+ "readonly",
256
+ "disabledInteractive"
257
+ ],
258
+ exportAs: ["matInput"]
259
+ },
260
+ {
261
+ kind: "ngmodule",
262
+ type: MatProgressBarModule
263
+ },
264
+ {
265
+ kind: "component",
266
+ type: i5.MatProgressBar,
267
+ selector: "mat-progress-bar",
268
+ inputs: [
269
+ "color",
270
+ "value",
271
+ "bufferValue",
272
+ "mode"
273
+ ],
274
+ outputs: ["animationEnd"],
275
+ exportAs: ["matProgressBar"]
276
+ },
277
+ {
278
+ kind: "component",
279
+ type: ContextBar,
280
+ selector: "jc-context-bar"
281
+ },
282
+ {
283
+ kind: "component",
284
+ type: HealthBadge,
285
+ selector: "jc-health",
286
+ inputs: ["state", "stale"]
287
+ },
288
+ {
289
+ kind: "component",
290
+ type: KeyValueTable,
291
+ selector: "jc-kv",
292
+ inputs: ["values", "empty"]
293
+ },
294
+ {
295
+ kind: "pipe",
296
+ type: LinkPipe,
297
+ name: "jcLink"
298
+ },
299
+ {
300
+ kind: "pipe",
301
+ type: DatePipe,
302
+ name: "date"
303
+ },
304
+ {
305
+ kind: "pipe",
306
+ type: AgoPipe,
307
+ name: "ago"
308
+ },
309
+ {
310
+ kind: "pipe",
311
+ type: ReasonPipe,
312
+ name: "reason"
313
+ }
314
+ ],
315
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
316
+ });
317
+ };
318
+ i0.ɵɵngDeclareClassMetadata({
319
+ minVersion: "12.0.0",
320
+ version: "22.2.4",
321
+ ngImport: i0,
322
+ type: AlertPage,
323
+ decorators: [{
324
+ type: Component,
325
+ args: [{
326
+ changeDetection: ChangeDetectionStrategy.OnPush,
327
+ imports: [
328
+ LinkPipe,
329
+ RouterLink,
330
+ DatePipe,
331
+ FormsModule,
332
+ MatButtonModule,
333
+ MatFormFieldModule,
334
+ MatInputModule,
335
+ MatProgressBarModule,
336
+ ContextBar,
337
+ HealthBadge,
338
+ KeyValueTable,
339
+ AgoPipe,
340
+ ReasonPipe
341
+ ],
342
+ template: `
343
+ <jc-context-bar>
344
+ <a [routerLink]="['alerts'] | jcLink" [queryParams]="ctx.keep()" class="jc-crumb">Alerts</a><span class="jc-crumb-sep">›</span>
345
+ <h1>{{ detail.value()?.alert?.rule ?? '…' }}</h1>
346
+ </jc-context-bar>
347
+ @if (detail.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
348
+ @if (detail.error()) { <p class="jc-error">{{ error() }}</p> }
349
+ @if (detail.value(); as d) {
350
+ <div class="jc-summary">
351
+ <span class="jc-badge" [class.jc-health-unhealthy]="d.alert.severity === 'Critical'" [class.jc-health-degraded]="d.alert.severity === 'Warning'">{{ d.alert.severity }}</span>
352
+ <strong>{{ d.alert.state }}</strong>
353
+ <span>{{ d.alert.subjectKey }}</span>
354
+ @if (d.alert.noData) { <span class="jc-muted">no data at the last evaluation: state kept</span> }
355
+ @if (d.alert.subjectType === 'service' && d.alert.subjectId) {
356
+ <a mat-flat-button [routerLink]="['services', d.alert.subjectId] | jcLink" [queryParams]="investigate(d.alert.environment)">Investigate</a>
357
+ <a mat-stroked-button [routerLink]="['services', d.alert.subjectId] | jcLink" [queryParams]="investigate(d.alert.environment, { tab: 'traces', status: 'error' })">Error traces</a>
358
+ }
359
+ @for (l of d.links; track l.url) { <a mat-button [href]="l.url" target="_blank" rel="noopener">{{ l.title }} ↗</a> }
360
+ </div>
361
+ <div class="jc-grid-2">
362
+ <section class="jc-card"><h2>Evidence</h2><jc-kv [values]="d.evidence" /></section>
363
+ <section class="jc-card">
364
+ <h2>Subject health</h2>
365
+ @for (h of d.subjectHealth; track h.subjectId + h.environment) {
366
+ <p><jc-health [state]="h.state" [stale]="h.stale" /> {{ h.environment }}</p>
367
+ <ul class="jc-reasons">@for (r of h.reasons; track r) { <li>{{ r | reason }}</li> }</ul>
368
+ } @empty { <p class="jc-muted">No health for this subject.</p> }
369
+ </section>
370
+ </div>
371
+ <section class="jc-card">
372
+ <h2>History</h2>
373
+ <table class="jc-table"><tbody>
374
+ @for (t of d.history; track t.at + t.to) {
375
+ <tr><td>{{ t.at | date: 'MMM d, HH:mm:ss' }}</td><td>{{ t.from ?? '—' }} → <strong>{{ t.to }}</strong></td><td>{{ t.reason | reason }}</td><td>{{ t.value ?? '—' }}</td></tr>
376
+ }
377
+ </tbody></table>
378
+ </section>
379
+ <section class="jc-card">
380
+ <h2>Acknowledgement</h2>
381
+ @if (d.alert.acknowledgedAt) {
382
+ <p>By <strong>{{ d.alert.acknowledgedBy }}</strong> {{ d.alert.acknowledgedAt | ago }}@if (d.acknowledgeComment) {: “{{ d.acknowledgeComment }}”}</p>
383
+ } @else if (d.alert.state !== 'Resolved') {
384
+ <form (ngSubmit)="acknowledge()" class="jc-filter-form">
385
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>Comment</mat-label><input matInput name="comment" [(ngModel)]="comment" maxlength="1000"></mat-form-field>
386
+ <button mat-flat-button type="submit" [disabled]="sending()">Acknowledge</button>
387
+ </form>
388
+ @if (ackError()) { <p class="jc-error">{{ ackError() }}</p> }
389
+ } @else { <p class="jc-muted">Resolved without acknowledgement.</p> }
390
+ </section>
391
+ }`
392
+ }]
393
+ }],
394
+ propDecorators: { id: [{
395
+ type: i0.Input,
396
+ args: [{
397
+ isSignal: true,
398
+ alias: "id",
399
+ required: true
400
+ }]
401
+ }] }
402
+ });
403
+ export { AlertPage };
404
+
405
+ //# sourceMappingURL=juice-js-observability-alert.page-D2OL0zTT.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"juice-js-observability-alert.page-D2OL0zTT.mjs","names":[],"sources":["../../../../projects/juice-js/observability/src/lib/pages/alert.page.ts"],"sourcesContent":["import { LinkPipe } from '../core/options';\nimport { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core';\nimport { rxResource } from '@angular/core/rxjs-interop';\nimport { DatePipe } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { RouterLink } from '@angular/router';\nimport { ApiService } from '../core/api.service';\nimport { ContextService } from '../core/context.service';\nimport { AgoPipe, ReasonPipe } from '../shared/format';\nimport { ContextBar, HealthBadge, KeyValueTable } from '../shared/ui';\nimport { problemText } from '../shared/telemetry';\n\n/**\n * One alert (Console requirements §14): evidence, lifecycle history, the subject's health and reasons, the way into\n * the investigation, and acknowledgement for operators.\n */\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [LinkPipe, RouterLink, DatePipe, FormsModule, MatButtonModule, MatFormFieldModule, MatInputModule, MatProgressBarModule,\n ContextBar, HealthBadge, KeyValueTable, AgoPipe, ReasonPipe],\n template: `\n <jc-context-bar>\n <a [routerLink]=\"['alerts'] | jcLink\" [queryParams]=\"ctx.keep()\" class=\"jc-crumb\">Alerts</a><span class=\"jc-crumb-sep\">›</span>\n <h1>{{ detail.value()?.alert?.rule ?? '…' }}</h1>\n </jc-context-bar>\n @if (detail.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (detail.error()) { <p class=\"jc-error\">{{ error() }}</p> }\n @if (detail.value(); as d) {\n <div class=\"jc-summary\">\n <span class=\"jc-badge\" [class.jc-health-unhealthy]=\"d.alert.severity === 'Critical'\" [class.jc-health-degraded]=\"d.alert.severity === 'Warning'\">{{ d.alert.severity }}</span>\n <strong>{{ d.alert.state }}</strong>\n <span>{{ d.alert.subjectKey }}</span>\n @if (d.alert.noData) { <span class=\"jc-muted\">no data at the last evaluation: state kept</span> }\n @if (d.alert.subjectType === 'service' && d.alert.subjectId) {\n <a mat-flat-button [routerLink]=\"['services', d.alert.subjectId] | jcLink\" [queryParams]=\"investigate(d.alert.environment)\">Investigate</a>\n <a mat-stroked-button [routerLink]=\"['services', d.alert.subjectId] | jcLink\" [queryParams]=\"investigate(d.alert.environment, { tab: 'traces', status: 'error' })\">Error traces</a>\n }\n @for (l of d.links; track l.url) { <a mat-button [href]=\"l.url\" target=\"_blank\" rel=\"noopener\">{{ l.title }} ↗</a> }\n </div>\n <div class=\"jc-grid-2\">\n <section class=\"jc-card\"><h2>Evidence</h2><jc-kv [values]=\"d.evidence\" /></section>\n <section class=\"jc-card\">\n <h2>Subject health</h2>\n @for (h of d.subjectHealth; track h.subjectId + h.environment) {\n <p><jc-health [state]=\"h.state\" [stale]=\"h.stale\" /> {{ h.environment }}</p>\n <ul class=\"jc-reasons\">@for (r of h.reasons; track r) { <li>{{ r | reason }}</li> }</ul>\n } @empty { <p class=\"jc-muted\">No health for this subject.</p> }\n </section>\n </div>\n <section class=\"jc-card\">\n <h2>History</h2>\n <table class=\"jc-table\"><tbody>\n @for (t of d.history; track t.at + t.to) {\n <tr><td>{{ t.at | date: 'MMM d, HH:mm:ss' }}</td><td>{{ t.from ?? '—' }} → <strong>{{ t.to }}</strong></td><td>{{ t.reason | reason }}</td><td>{{ t.value ?? '—' }}</td></tr>\n }\n </tbody></table>\n </section>\n <section class=\"jc-card\">\n <h2>Acknowledgement</h2>\n @if (d.alert.acknowledgedAt) {\n <p>By <strong>{{ d.alert.acknowledgedBy }}</strong> {{ d.alert.acknowledgedAt | ago }}@if (d.acknowledgeComment) {: “{{ d.acknowledgeComment }}”}</p>\n } @else if (d.alert.state !== 'Resolved') {\n <form (ngSubmit)=\"acknowledge()\" class=\"jc-filter-form\">\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-wide\"><mat-label>Comment</mat-label><input matInput name=\"comment\" [(ngModel)]=\"comment\" maxlength=\"1000\"></mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"sending()\">Acknowledge</button>\n </form>\n @if (ackError()) { <p class=\"jc-error\">{{ ackError() }}</p> }\n } @else { <p class=\"jc-muted\">Resolved without acknowledgement.</p> }\n </section>\n }`,\n})\nexport class AlertPage {\n protected readonly ctx = inject(ContextService);\n private readonly api = inject(ApiService);\n readonly id = input.required<string>();\n protected comment = '';\n protected readonly sending = signal(false);\n protected readonly ackError = signal('');\n\n protected readonly detail = rxResource({ params: () => this.id(), stream: ({ params }) => this.api.alert(params) });\n protected readonly error = computed(() => problemText(this.detail.error()));\n\n /** The service page over the alert's environment, from shortly before the breach. */\n investigate(environment: string | null, extra: Record<string, string> = {}) {\n const alert = this.detail.value()?.alert;\n const start = alert?.breachStartedAt ? new Date(alert.breachStartedAt).getTime() - 15 * 60_000 : null;\n const end = alert?.resolvedAt ? new Date(alert.resolvedAt).getTime() + 5 * 60_000 : Date.now();\n const window = start && end - start <= 24 * 3_600_000\n ? { from: new Date(start).toISOString(), to: new Date(end).toISOString(), range: null } : {};\n return this.ctx.keep({ env: environment, ...window, ...extra });\n }\n\n acknowledge() {\n this.sending.set(true);\n this.ackError.set('');\n this.api.acknowledge(this.id(), this.comment).subscribe({\n next: () => { this.sending.set(false); this.detail.reload(); },\n error: e => {\n this.sending.set(false);\n this.ackError.set((e as { status?: number }).status === 403 ? 'Acknowledging needs the operator or admin role.' : problemText(e));\n },\n });\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,208 @@
1
+ import { ApiService, ContextService, LinkPipe } from "./juice-js-observability.mjs";
2
+ import { a as problemText, f as AgoPipe, l as ContextBar, o as cursorList } from "./juice-js-observability-telemetry-VA4_3nzr.mjs";
3
+ import * as i0 from "@angular/core";
4
+ import { ChangeDetectionStrategy, Component, computed, inject, input } from "@angular/core";
5
+ import { RouterLink } from "@angular/router";
6
+ import * as i1 from "@angular/material/button";
7
+ import { MatButtonModule } from "@angular/material/button";
8
+ import * as i2 from "@angular/material/progress-bar";
9
+ import { MatProgressBarModule } from "@angular/material/progress-bar";
10
+ var AlertsPage = class AlertsPage {
11
+ ctx = inject(ContextService);
12
+ api = inject(ApiService);
13
+ states = [
14
+ "firing",
15
+ "pending",
16
+ "resolved"
17
+ ];
18
+ state = input(...ngDevMode ? [void 0, { debugName: "state" }] : /* istanbul ignore next */ []);
19
+ alerts = cursorList(() => this.state() ?? "firing", (state) => ({ state }), (q) => this.api.alerts(q));
20
+ error = computed(() => problemText(this.alerts.first.error()), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
21
+ moreError = computed(() => problemText(this.alerts.moreError()), ...ngDevMode ? [{ debugName: "moreError" }] : /* istanbul ignore next */ []);
22
+ static ɵfac = i0.ɵɵngDeclareFactory({
23
+ minVersion: "12.0.0",
24
+ version: "22.2.4",
25
+ ngImport: i0,
26
+ type: AlertsPage,
27
+ deps: [],
28
+ target: i0.ɵɵFactoryTarget.Component
29
+ });
30
+ static ɵcmp = i0.ɵɵngDeclareComponent({
31
+ minVersion: "17.0.0",
32
+ version: "22.2.4",
33
+ type: AlertsPage,
34
+ isStandalone: true,
35
+ selector: "ng-component",
36
+ inputs: { state: {
37
+ classPropertyName: "state",
38
+ publicName: "state",
39
+ isSignal: true,
40
+ isRequired: false,
41
+ transformFunction: null
42
+ } },
43
+ ngImport: i0,
44
+ template: `
45
+ <jc-context-bar><h1>Alerts</h1>
46
+ @for (s of states; track s) {
47
+ <a class="jc-chip" [class.jc-selected]="s === (state() ?? 'firing')" [routerLink]="['alerts'] | jcLink" [queryParams]="ctx.keep({ state: s })">{{ s }}</a>
48
+ }
49
+ </jc-context-bar>
50
+ @if (alerts.first.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
51
+ @if (alerts.first.error()) { <p class="jc-error">{{ error() }}</p> }
52
+ <table class="jc-table">
53
+ <thead><tr><th>Severity</th><th>Rule</th><th>Subject</th><th>State</th><th>Value</th><th>Fired</th><th>Acknowledged</th></tr></thead>
54
+ <tbody>
55
+ @for (a of alerts.items(); track a.id) {
56
+ <tr>
57
+ <td><span class="jc-badge" [class.jc-health-unhealthy]="a.severity === 'Critical'" [class.jc-health-degraded]="a.severity === 'Warning'">{{ a.severity }}</span></td>
58
+ <td><a [routerLink]="['alerts', a.id] | jcLink" [queryParams]="ctx.keep()">{{ a.rule }}</a></td>
59
+ <td>{{ a.subjectKey }}</td>
60
+ <td>{{ a.state }}@if (a.noData) { <span class="jc-muted" title="The last evaluation had too few samples; the state is kept"> · no data</span> }</td>
61
+ <td>{{ a.lastValue ?? '—' }}</td>
62
+ <td>{{ a.firedAt | ago }}</td>
63
+ <td>{{ a.acknowledgedBy ?? '—' }}</td>
64
+ </tr>
65
+ } @empty { @if (alerts.first.hasValue()) { <tr><td colspan="7" class="jc-muted">No {{ state() ?? 'firing' }} alerts.</td></tr> } }
66
+ </tbody>
67
+ </table>
68
+ @if (alerts.items().length > 0) {
69
+ <div class="jc-more">
70
+ <span class="jc-muted">{{ alerts.items().length }} {{ state() ?? 'firing' }} alert{{ alerts.items().length === 1 ? '' : 's' }}{{ alerts.hasMore() ? ', newest first' : '' }}</span>
71
+ @if (alerts.hasMore()) {
72
+ <button mat-button (click)="alerts.more()" [disabled]="alerts.loadingMore()">{{ alerts.loadingMore() ? 'Loading…' : 'Load more' }}</button>
73
+ }
74
+ @if (alerts.moreError()) { <span class="jc-error">{{ moreError() }}</span> }
75
+ </div>
76
+ }`,
77
+ isInline: true,
78
+ dependencies: [
79
+ {
80
+ kind: "directive",
81
+ type: RouterLink,
82
+ selector: "[routerLink]",
83
+ inputs: [
84
+ "target",
85
+ "queryParams",
86
+ "fragment",
87
+ "queryParamsHandling",
88
+ "state",
89
+ "info",
90
+ "relativeTo",
91
+ "preserveFragment",
92
+ "skipLocationChange",
93
+ "replaceUrl",
94
+ "browserUrl",
95
+ "routerLink"
96
+ ]
97
+ },
98
+ {
99
+ kind: "ngmodule",
100
+ type: MatButtonModule
101
+ },
102
+ {
103
+ kind: "component",
104
+ type: i1.MatButton,
105
+ selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ",
106
+ inputs: ["matButton"],
107
+ exportAs: ["matButton", "matAnchor"]
108
+ },
109
+ {
110
+ kind: "ngmodule",
111
+ type: MatProgressBarModule
112
+ },
113
+ {
114
+ kind: "component",
115
+ type: i2.MatProgressBar,
116
+ selector: "mat-progress-bar",
117
+ inputs: [
118
+ "color",
119
+ "value",
120
+ "bufferValue",
121
+ "mode"
122
+ ],
123
+ outputs: ["animationEnd"],
124
+ exportAs: ["matProgressBar"]
125
+ },
126
+ {
127
+ kind: "component",
128
+ type: ContextBar,
129
+ selector: "jc-context-bar"
130
+ },
131
+ {
132
+ kind: "pipe",
133
+ type: LinkPipe,
134
+ name: "jcLink"
135
+ },
136
+ {
137
+ kind: "pipe",
138
+ type: AgoPipe,
139
+ name: "ago"
140
+ }
141
+ ],
142
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
143
+ });
144
+ };
145
+ i0.ɵɵngDeclareClassMetadata({
146
+ minVersion: "12.0.0",
147
+ version: "22.2.4",
148
+ ngImport: i0,
149
+ type: AlertsPage,
150
+ decorators: [{
151
+ type: Component,
152
+ args: [{
153
+ changeDetection: ChangeDetectionStrategy.OnPush,
154
+ imports: [
155
+ LinkPipe,
156
+ RouterLink,
157
+ MatButtonModule,
158
+ MatProgressBarModule,
159
+ ContextBar,
160
+ AgoPipe
161
+ ],
162
+ template: `
163
+ <jc-context-bar><h1>Alerts</h1>
164
+ @for (s of states; track s) {
165
+ <a class="jc-chip" [class.jc-selected]="s === (state() ?? 'firing')" [routerLink]="['alerts'] | jcLink" [queryParams]="ctx.keep({ state: s })">{{ s }}</a>
166
+ }
167
+ </jc-context-bar>
168
+ @if (alerts.first.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
169
+ @if (alerts.first.error()) { <p class="jc-error">{{ error() }}</p> }
170
+ <table class="jc-table">
171
+ <thead><tr><th>Severity</th><th>Rule</th><th>Subject</th><th>State</th><th>Value</th><th>Fired</th><th>Acknowledged</th></tr></thead>
172
+ <tbody>
173
+ @for (a of alerts.items(); track a.id) {
174
+ <tr>
175
+ <td><span class="jc-badge" [class.jc-health-unhealthy]="a.severity === 'Critical'" [class.jc-health-degraded]="a.severity === 'Warning'">{{ a.severity }}</span></td>
176
+ <td><a [routerLink]="['alerts', a.id] | jcLink" [queryParams]="ctx.keep()">{{ a.rule }}</a></td>
177
+ <td>{{ a.subjectKey }}</td>
178
+ <td>{{ a.state }}@if (a.noData) { <span class="jc-muted" title="The last evaluation had too few samples; the state is kept"> · no data</span> }</td>
179
+ <td>{{ a.lastValue ?? '—' }}</td>
180
+ <td>{{ a.firedAt | ago }}</td>
181
+ <td>{{ a.acknowledgedBy ?? '—' }}</td>
182
+ </tr>
183
+ } @empty { @if (alerts.first.hasValue()) { <tr><td colspan="7" class="jc-muted">No {{ state() ?? 'firing' }} alerts.</td></tr> } }
184
+ </tbody>
185
+ </table>
186
+ @if (alerts.items().length > 0) {
187
+ <div class="jc-more">
188
+ <span class="jc-muted">{{ alerts.items().length }} {{ state() ?? 'firing' }} alert{{ alerts.items().length === 1 ? '' : 's' }}{{ alerts.hasMore() ? ', newest first' : '' }}</span>
189
+ @if (alerts.hasMore()) {
190
+ <button mat-button (click)="alerts.more()" [disabled]="alerts.loadingMore()">{{ alerts.loadingMore() ? 'Loading…' : 'Load more' }}</button>
191
+ }
192
+ @if (alerts.moreError()) { <span class="jc-error">{{ moreError() }}</span> }
193
+ </div>
194
+ }`
195
+ }]
196
+ }],
197
+ propDecorators: { state: [{
198
+ type: i0.Input,
199
+ args: [{
200
+ isSignal: true,
201
+ alias: "state",
202
+ required: false
203
+ }]
204
+ }] }
205
+ });
206
+ export { AlertsPage };
207
+
208
+ //# sourceMappingURL=juice-js-observability-alerts.page-BLJvD_op.mjs.map