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

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 +63 -2
  2. package/fesm2022/juice-js-observability-alert.page-TKwqJT2d.mjs +404 -0
  3. package/fesm2022/juice-js-observability-alert.page-TKwqJT2d.mjs.map +1 -0
  4. package/fesm2022/juice-js-observability-alerts.page-B-2uS9wr.mjs +211 -0
  5. package/fesm2022/juice-js-observability-alerts.page-B-2uS9wr.mjs.map +1 -0
  6. package/fesm2022/juice-js-observability-logs.page-CZNbTl2Q.mjs +413 -0
  7. package/fesm2022/juice-js-observability-logs.page-CZNbTl2Q.mjs.map +1 -0
  8. package/fesm2022/juice-js-observability-metrics.page-DoVED9MT.mjs +267 -0
  9. package/fesm2022/juice-js-observability-metrics.page-DoVED9MT.mjs.map +1 -0
  10. package/fesm2022/juice-js-observability-overview.page-Cod7-cIy.mjs +249 -0
  11. package/fesm2022/juice-js-observability-overview.page-Cod7-cIy.mjs.map +1 -0
  12. package/fesm2022/juice-js-observability-resource.page-C8RhUipF.mjs +274 -0
  13. package/fesm2022/juice-js-observability-resource.page-C8RhUipF.mjs.map +1 -0
  14. package/fesm2022/juice-js-observability-resources.page-KadGxEAh.mjs +209 -0
  15. package/fesm2022/juice-js-observability-resources.page-KadGxEAh.mjs.map +1 -0
  16. package/fesm2022/juice-js-observability-rules.page-DKWvNpu9.mjs +175 -0
  17. package/fesm2022/juice-js-observability-rules.page-DKWvNpu9.mjs.map +1 -0
  18. package/fesm2022/juice-js-observability-service.page-_yK0kVYS.mjs +726 -0
  19. package/fesm2022/juice-js-observability-service.page-_yK0kVYS.mjs.map +1 -0
  20. package/fesm2022/juice-js-observability-services.page-BtG8zyiI.mjs +251 -0
  21. package/fesm2022/juice-js-observability-services.page-BtG8zyiI.mjs.map +1 -0
  22. package/fesm2022/juice-js-observability-telemetry-B5jMJYfn.mjs +1801 -0
  23. package/fesm2022/juice-js-observability-telemetry-B5jMJYfn.mjs.map +1 -0
  24. package/fesm2022/juice-js-observability-topology.page-B7aXWOYV.mjs +486 -0
  25. package/fesm2022/juice-js-observability-topology.page-B7aXWOYV.mjs.map +1 -0
  26. package/fesm2022/juice-js-observability-trace.page-CkLNNnKf.mjs +528 -0
  27. package/fesm2022/juice-js-observability-trace.page-CkLNNnKf.mjs.map +1 -0
  28. package/fesm2022/juice-js-observability-traces.page-DgvNixIc.mjs +520 -0
  29. package/fesm2022/juice-js-observability-traces.page-DgvNixIc.mjs.map +1 -0
  30. package/fesm2022/juice-js-observability.mjs +405 -0
  31. package/fesm2022/juice-js-observability.mjs.map +1 -0
  32. package/package.json +48 -4
  33. package/styles/_observability.scss +220 -0
  34. package/types/juice-js-observability.d.ts +400 -0
package/README.md CHANGED
@@ -1,3 +1,64 @@
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 app's global styles, once): the pages' layout, in the Juice Console's teal theme, light and dark (.dark).
25
+ @use '@juice-js/observability';
26
+
27
+ // Optional: colours from your own Angular Material (M2) themes instead.
28
+ :root { @include observability.colors($light-theme); }
29
+ .dark { @include observability.colors($dark-theme); } // under your dark-mode selector
30
+ ```
31
+
32
+ The default colours sit under zero-specificity `:where()` selectors, so your `colors()` replaces them wherever it
33
+ comes in the file. `colors($theme, $status: (), $series: null)` sets the `--jc-*` custom properties the pages, charts
34
+ and dependency graph read:
35
+
36
+ | From your theme | Used for |
37
+ |---|---|
38
+ | background, card, divider, text, secondary text | page, cards and tables, borders, text, labels |
39
+ | primary | links, selection, the selected graph node or edge |
40
+ | warn | unhealthy and critical, errors |
41
+
42
+ A Material theme has no colour for healthy, degraded or offline, nor a categorical palette, so they have defaults
43
+ (Material green, deep orange and greys; ten Material hues for services in a trace and lines in a chart). Set your own:
44
+
45
+ ```scss
46
+ :root {
47
+ @include observability.colors($light-theme,
48
+ $status: (ok: #1b5e20, warn: #e65100, bad: #c62828, crit: #8e0000, off: #546e7a, none: #90a4ae),
49
+ $series: (#1565c0, #6a1b9a, #00838f, #ef6c00));
50
+ }
51
+ ```
52
+
53
+ The pages use the properties but don't style the page itself; give `body` your colours, for example
54
+ `background: var(--jc-bg); color: var(--jc-text);`. A theme switch at runtime (a class change on `<html>` or `<body>`,
55
+ like juice-layout's dark-mode toggle) repaints the charts and the graph.
56
+
57
+ - `apiBase`: the API origin; empty when the API serves the app.
58
+ - `basePath`: where the routes are mounted; every link between pages goes under it.
59
+ - `auditLink` (optional): a link from a trace to its audit records, shown on the trace page as "Audit records ↗".
60
+ With `@juice-js/audit` mounted at `/audit`: `auditLink: id => ['/audit/traces', id]`. A string is an external URL.
61
+ Without it, the trace page shows no link.
62
+ - The router needs `bindToComponentInputs: true`. The pages take their route and query parameters as inputs.
63
+ - The host's bearer-token interceptor (`@juice-js/auth`) must cover `apiBase`. The token carries the `platform_tenant`
64
+ claim and a platform role (`viewer`, `operator` or `admin`).
@@ -0,0 +1,404 @@
1
+ import { ApiService, ContextService, LinkPipe } from "./juice-js-observability.mjs";
2
+ import { _ as ReasonPipe, a as problemText, d as KeyValueTable, l as ContextBar, m as AgoPipe, u as HealthBadge, x as apiResource } from "./juice-js-observability-telemetry-B5jMJYfn.mjs";
3
+ import * as i0 from "@angular/core";
4
+ import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from "@angular/core";
5
+ import { RouterLink } from "@angular/router";
6
+ import { DatePipe } from "@angular/common";
7
+ import * as i1 from "@angular/forms";
8
+ import { FormsModule } from "@angular/forms";
9
+ import * as i2 from "@angular/material/button";
10
+ import { MatButtonModule } from "@angular/material/button";
11
+ import { MatFormFieldModule } from "@angular/material/form-field";
12
+ import * as i4 from "@angular/material/input";
13
+ import { MatInputModule } from "@angular/material/input";
14
+ import * as i5 from "@angular/material/progress-bar";
15
+ import { MatProgressBarModule } from "@angular/material/progress-bar";
16
+ import * as i3 from "@angular/material/select";
17
+ var AlertPage = class AlertPage {
18
+ ctx = inject(ContextService);
19
+ api = inject(ApiService);
20
+ id = input.required(...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
21
+ comment = "";
22
+ sending = signal(false, ...ngDevMode ? [{ debugName: "sending" }] : /* istanbul ignore next */ []);
23
+ ackError = signal("", ...ngDevMode ? [{ debugName: "ackError" }] : /* istanbul ignore next */ []);
24
+ detail = apiResource({
25
+ params: () => this.id(),
26
+ stream: ({ params }) => this.api.alert(params)
27
+ });
28
+ error = computed(() => problemText(this.detail.error()), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
29
+ investigate(environment, extra = {}) {
30
+ const alert = this.detail.value()?.alert;
31
+ const start = alert?.breachStartedAt ? new Date(alert.breachStartedAt).getTime() - 9e5 : null;
32
+ const end = alert?.resolvedAt ? new Date(alert.resolvedAt).getTime() + 3e5 : Date.now();
33
+ const window = start && end - start <= 864e5 ? {
34
+ from: new Date(start).toISOString(),
35
+ to: new Date(end).toISOString(),
36
+ range: null
37
+ } : {};
38
+ return this.ctx.keep({
39
+ env: environment,
40
+ ...window,
41
+ ...extra
42
+ });
43
+ }
44
+ acknowledge() {
45
+ this.sending.set(true);
46
+ this.ackError.set("");
47
+ this.api.acknowledge(this.id(), this.comment).subscribe({
48
+ next: () => {
49
+ this.sending.set(false);
50
+ this.detail.reload();
51
+ },
52
+ error: (e) => {
53
+ this.sending.set(false);
54
+ this.ackError.set(e.status === 403 ? "Acknowledging needs the operator or admin role." : problemText(e));
55
+ }
56
+ });
57
+ }
58
+ static ɵfac = i0.ɵɵngDeclareFactory({
59
+ minVersion: "12.0.0",
60
+ version: "22.2.4",
61
+ ngImport: i0,
62
+ type: AlertPage,
63
+ deps: [],
64
+ target: i0.ɵɵFactoryTarget.Component
65
+ });
66
+ static ɵcmp = i0.ɵɵngDeclareComponent({
67
+ minVersion: "17.0.0",
68
+ version: "22.2.4",
69
+ type: AlertPage,
70
+ isStandalone: true,
71
+ selector: "ng-component",
72
+ inputs: { id: {
73
+ classPropertyName: "id",
74
+ publicName: "id",
75
+ isSignal: true,
76
+ isRequired: true,
77
+ transformFunction: null
78
+ } },
79
+ ngImport: i0,
80
+ template: `
81
+ <jc-context-bar>
82
+ <a [routerLink]="['alerts'] | jcLink" [queryParams]="ctx.keep()" class="jc-crumb">Alerts</a><span class="jc-crumb-sep">›</span>
83
+ <h1>{{ detail.value()?.alert?.rule ?? '…' }}</h1>
84
+ </jc-context-bar>
85
+ @if (detail.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
86
+ @if (detail.error()) { <p class="jc-error">{{ error() }}</p> }
87
+ @if (detail.value(); as d) {
88
+ <div class="jc-summary">
89
+ <span class="jc-badge" [class.jc-health-unhealthy]="d.alert.severity === 'Critical'" [class.jc-health-degraded]="d.alert.severity === 'Warning'">{{ d.alert.severity }}</span>
90
+ <strong>{{ d.alert.state }}</strong>
91
+ <span>{{ d.alert.subjectKey }}</span>
92
+ @if (d.alert.noData) { <span class="jc-muted">no data at the last evaluation: state kept</span> }
93
+ @if (d.alert.subjectType === 'service' && d.alert.subjectId) {
94
+ <a mat-flat-button [routerLink]="['services', d.alert.subjectId] | jcLink" [queryParams]="investigate(d.alert.environment)">Investigate</a>
95
+ <a mat-stroked-button [routerLink]="['services', d.alert.subjectId] | jcLink" [queryParams]="investigate(d.alert.environment, { tab: 'traces', status: 'error' })">Error traces</a>
96
+ }
97
+ @for (l of d.links; track l.url) { <a mat-button [href]="l.url" target="_blank" rel="noopener">{{ l.title }} ↗</a> }
98
+ </div>
99
+ <div class="jc-grid-2">
100
+ <section class="jc-card"><h2>Evidence</h2><jc-kv [values]="d.evidence" /></section>
101
+ <section class="jc-card">
102
+ <h2>Subject health</h2>
103
+ @for (h of d.subjectHealth; track h.subjectId + h.environment) {
104
+ <p><jc-health [state]="h.state" [stale]="h.stale" /> {{ h.environment }}</p>
105
+ <ul class="jc-reasons">@for (r of h.reasons; track r) { <li>{{ r | reason }}</li> }</ul>
106
+ } @empty { <p class="jc-muted">No health for this subject.</p> }
107
+ </section>
108
+ </div>
109
+ <section class="jc-card">
110
+ <h2>History</h2>
111
+ <table class="jc-table"><tbody>
112
+ @for (t of d.history; track t.at + t.to) {
113
+ <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>
114
+ }
115
+ </tbody></table>
116
+ </section>
117
+ <section class="jc-card">
118
+ <h2>Acknowledgement</h2>
119
+ @if (d.alert.acknowledgedAt) {
120
+ <p>By <strong>{{ d.alert.acknowledgedBy }}</strong> {{ d.alert.acknowledgedAt | ago }}@if (d.acknowledgeComment) {: “{{ d.acknowledgeComment }}”}</p>
121
+ } @else if (d.alert.state !== 'Resolved') {
122
+ <form (ngSubmit)="acknowledge()" class="jc-filter-form">
123
+ <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>
124
+ <button mat-flat-button type="submit" [disabled]="sending()">Acknowledge</button>
125
+ </form>
126
+ @if (ackError()) { <p class="jc-error">{{ ackError() }}</p> }
127
+ } @else { <p class="jc-muted">Resolved without acknowledgement.</p> }
128
+ </section>
129
+ }`,
130
+ isInline: true,
131
+ dependencies: [
132
+ {
133
+ kind: "directive",
134
+ type: RouterLink,
135
+ selector: "[routerLink]",
136
+ inputs: [
137
+ "target",
138
+ "queryParams",
139
+ "fragment",
140
+ "queryParamsHandling",
141
+ "state",
142
+ "info",
143
+ "relativeTo",
144
+ "preserveFragment",
145
+ "skipLocationChange",
146
+ "replaceUrl",
147
+ "browserUrl",
148
+ "routerLink"
149
+ ]
150
+ },
151
+ {
152
+ kind: "ngmodule",
153
+ type: FormsModule
154
+ },
155
+ {
156
+ kind: "directive",
157
+ type: i1.ɵNgNoValidate,
158
+ selector: "form:not([ngNoForm]):not([ngNativeValidate])"
159
+ },
160
+ {
161
+ kind: "directive",
162
+ type: i1.DefaultValueAccessor,
163
+ 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]"
164
+ },
165
+ {
166
+ kind: "directive",
167
+ type: i1.NgControlStatus,
168
+ selector: "[formControlName],[ngModel],[formControl]"
169
+ },
170
+ {
171
+ kind: "directive",
172
+ type: i1.NgControlStatusGroup,
173
+ selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]"
174
+ },
175
+ {
176
+ kind: "directive",
177
+ type: i1.MaxLengthValidator,
178
+ selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]",
179
+ inputs: ["maxlength"]
180
+ },
181
+ {
182
+ kind: "directive",
183
+ type: i1.NgModel,
184
+ selector: "[ngModel]:not([formControlName]):not([formControl])",
185
+ inputs: [
186
+ "name",
187
+ "disabled",
188
+ "ngModel",
189
+ "ngModelOptions"
190
+ ],
191
+ outputs: ["ngModelChange"],
192
+ exportAs: ["ngModel"]
193
+ },
194
+ {
195
+ kind: "directive",
196
+ type: i1.NgForm,
197
+ selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]",
198
+ inputs: ["ngFormOptions"],
199
+ outputs: ["ngSubmit"],
200
+ exportAs: ["ngForm"]
201
+ },
202
+ {
203
+ kind: "ngmodule",
204
+ type: MatButtonModule
205
+ },
206
+ {
207
+ kind: "component",
208
+ type: i2.MatButton,
209
+ 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] ",
210
+ inputs: ["matButton"],
211
+ exportAs: ["matButton", "matAnchor"]
212
+ },
213
+ {
214
+ kind: "ngmodule",
215
+ type: MatFormFieldModule
216
+ },
217
+ {
218
+ kind: "component",
219
+ type: i3.MatFormField,
220
+ selector: "mat-form-field",
221
+ inputs: [
222
+ "hideRequiredMarker",
223
+ "color",
224
+ "floatLabel",
225
+ "appearance",
226
+ "subscriptSizing",
227
+ "hintLabel"
228
+ ],
229
+ exportAs: ["matFormField"]
230
+ },
231
+ {
232
+ kind: "directive",
233
+ type: i3.MatLabel,
234
+ selector: "mat-label"
235
+ },
236
+ {
237
+ kind: "ngmodule",
238
+ type: MatInputModule
239
+ },
240
+ {
241
+ kind: "directive",
242
+ type: i4.MatInput,
243
+ selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",
244
+ inputs: [
245
+ "disabled",
246
+ "id",
247
+ "placeholder",
248
+ "name",
249
+ "required",
250
+ "type",
251
+ "errorStateMatcher",
252
+ "aria-describedby",
253
+ "value",
254
+ "readonly",
255
+ "disabledInteractive"
256
+ ],
257
+ exportAs: ["matInput"]
258
+ },
259
+ {
260
+ kind: "ngmodule",
261
+ type: MatProgressBarModule
262
+ },
263
+ {
264
+ kind: "component",
265
+ type: i5.MatProgressBar,
266
+ selector: "mat-progress-bar",
267
+ inputs: [
268
+ "color",
269
+ "value",
270
+ "bufferValue",
271
+ "mode"
272
+ ],
273
+ outputs: ["animationEnd"],
274
+ exportAs: ["matProgressBar"]
275
+ },
276
+ {
277
+ kind: "component",
278
+ type: ContextBar,
279
+ selector: "jc-context-bar"
280
+ },
281
+ {
282
+ kind: "component",
283
+ type: HealthBadge,
284
+ selector: "jc-health",
285
+ inputs: ["state", "stale"]
286
+ },
287
+ {
288
+ kind: "component",
289
+ type: KeyValueTable,
290
+ selector: "jc-kv",
291
+ inputs: ["values", "empty"]
292
+ },
293
+ {
294
+ kind: "pipe",
295
+ type: LinkPipe,
296
+ name: "jcLink"
297
+ },
298
+ {
299
+ kind: "pipe",
300
+ type: DatePipe,
301
+ name: "date"
302
+ },
303
+ {
304
+ kind: "pipe",
305
+ type: AgoPipe,
306
+ name: "ago"
307
+ },
308
+ {
309
+ kind: "pipe",
310
+ type: ReasonPipe,
311
+ name: "reason"
312
+ }
313
+ ],
314
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
315
+ });
316
+ };
317
+ i0.ɵɵngDeclareClassMetadata({
318
+ minVersion: "12.0.0",
319
+ version: "22.2.4",
320
+ ngImport: i0,
321
+ type: AlertPage,
322
+ decorators: [{
323
+ type: Component,
324
+ args: [{
325
+ changeDetection: ChangeDetectionStrategy.OnPush,
326
+ imports: [
327
+ LinkPipe,
328
+ RouterLink,
329
+ DatePipe,
330
+ FormsModule,
331
+ MatButtonModule,
332
+ MatFormFieldModule,
333
+ MatInputModule,
334
+ MatProgressBarModule,
335
+ ContextBar,
336
+ HealthBadge,
337
+ KeyValueTable,
338
+ AgoPipe,
339
+ ReasonPipe
340
+ ],
341
+ template: `
342
+ <jc-context-bar>
343
+ <a [routerLink]="['alerts'] | jcLink" [queryParams]="ctx.keep()" class="jc-crumb">Alerts</a><span class="jc-crumb-sep">›</span>
344
+ <h1>{{ detail.value()?.alert?.rule ?? '…' }}</h1>
345
+ </jc-context-bar>
346
+ @if (detail.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
347
+ @if (detail.error()) { <p class="jc-error">{{ error() }}</p> }
348
+ @if (detail.value(); as d) {
349
+ <div class="jc-summary">
350
+ <span class="jc-badge" [class.jc-health-unhealthy]="d.alert.severity === 'Critical'" [class.jc-health-degraded]="d.alert.severity === 'Warning'">{{ d.alert.severity }}</span>
351
+ <strong>{{ d.alert.state }}</strong>
352
+ <span>{{ d.alert.subjectKey }}</span>
353
+ @if (d.alert.noData) { <span class="jc-muted">no data at the last evaluation: state kept</span> }
354
+ @if (d.alert.subjectType === 'service' && d.alert.subjectId) {
355
+ <a mat-flat-button [routerLink]="['services', d.alert.subjectId] | jcLink" [queryParams]="investigate(d.alert.environment)">Investigate</a>
356
+ <a mat-stroked-button [routerLink]="['services', d.alert.subjectId] | jcLink" [queryParams]="investigate(d.alert.environment, { tab: 'traces', status: 'error' })">Error traces</a>
357
+ }
358
+ @for (l of d.links; track l.url) { <a mat-button [href]="l.url" target="_blank" rel="noopener">{{ l.title }} ↗</a> }
359
+ </div>
360
+ <div class="jc-grid-2">
361
+ <section class="jc-card"><h2>Evidence</h2><jc-kv [values]="d.evidence" /></section>
362
+ <section class="jc-card">
363
+ <h2>Subject health</h2>
364
+ @for (h of d.subjectHealth; track h.subjectId + h.environment) {
365
+ <p><jc-health [state]="h.state" [stale]="h.stale" /> {{ h.environment }}</p>
366
+ <ul class="jc-reasons">@for (r of h.reasons; track r) { <li>{{ r | reason }}</li> }</ul>
367
+ } @empty { <p class="jc-muted">No health for this subject.</p> }
368
+ </section>
369
+ </div>
370
+ <section class="jc-card">
371
+ <h2>History</h2>
372
+ <table class="jc-table"><tbody>
373
+ @for (t of d.history; track t.at + t.to) {
374
+ <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>
375
+ }
376
+ </tbody></table>
377
+ </section>
378
+ <section class="jc-card">
379
+ <h2>Acknowledgement</h2>
380
+ @if (d.alert.acknowledgedAt) {
381
+ <p>By <strong>{{ d.alert.acknowledgedBy }}</strong> {{ d.alert.acknowledgedAt | ago }}@if (d.acknowledgeComment) {: “{{ d.acknowledgeComment }}”}</p>
382
+ } @else if (d.alert.state !== 'Resolved') {
383
+ <form (ngSubmit)="acknowledge()" class="jc-filter-form">
384
+ <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>
385
+ <button mat-flat-button type="submit" [disabled]="sending()">Acknowledge</button>
386
+ </form>
387
+ @if (ackError()) { <p class="jc-error">{{ ackError() }}</p> }
388
+ } @else { <p class="jc-muted">Resolved without acknowledgement.</p> }
389
+ </section>
390
+ }`
391
+ }]
392
+ }],
393
+ propDecorators: { id: [{
394
+ type: i0.Input,
395
+ args: [{
396
+ isSignal: true,
397
+ alias: "id",
398
+ required: true
399
+ }]
400
+ }] }
401
+ });
402
+ export { AlertPage };
403
+
404
+ //# sourceMappingURL=juice-js-observability-alert.page-TKwqJT2d.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"juice-js-observability-alert.page-TKwqJT2d.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 { apiResource } from '../shared/resource';\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 = apiResource({ 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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"}