@juice-js/observability 1.1.0-alpha.20 → 1.1.0-alpha.22

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 (28) hide show
  1. package/README.md +1 -0
  2. package/fesm2022/{juice-js-observability-alert.page-nEy5Gc6j.mjs → juice-js-observability-alert.page-DOAX9WWa.mjs} +154 -2
  3. package/fesm2022/juice-js-observability-alert.page-DOAX9WWa.mjs.map +1 -0
  4. package/fesm2022/juice-js-observability-incident.page-CCHEWXou.mjs +400 -0
  5. package/fesm2022/juice-js-observability-incident.page-CCHEWXou.mjs.map +1 -0
  6. package/fesm2022/juice-js-observability-incidents.page-CpNqYp2W.mjs +438 -0
  7. package/fesm2022/juice-js-observability-incidents.page-CpNqYp2W.mjs.map +1 -0
  8. package/fesm2022/juice-js-observability-load-runs.page-C2rk2bz9.mjs +349 -0
  9. package/fesm2022/juice-js-observability-load-runs.page-C2rk2bz9.mjs.map +1 -0
  10. package/fesm2022/juice-js-observability-notifications.page-BDqb10C6.mjs +725 -0
  11. package/fesm2022/juice-js-observability-notifications.page-BDqb10C6.mjs.map +1 -0
  12. package/fesm2022/juice-js-observability-rules.page-Df2uDVwz.mjs +580 -0
  13. package/fesm2022/juice-js-observability-rules.page-Df2uDVwz.mjs.map +1 -0
  14. package/fesm2022/{juice-js-observability-service.page-rGAvUK_k.mjs → juice-js-observability-service.page-BFXUe4mX.mjs} +38 -4
  15. package/fesm2022/juice-js-observability-service.page-BFXUe4mX.mjs.map +1 -0
  16. package/fesm2022/juice-js-observability-silences.page-Cg53dJAy.mjs +523 -0
  17. package/fesm2022/juice-js-observability-silences.page-Cg53dJAy.mjs.map +1 -0
  18. package/fesm2022/juice-js-observability-timeline.page-CtU2EoDN.mjs +213 -0
  19. package/fesm2022/juice-js-observability-timeline.page-CtU2EoDN.mjs.map +1 -0
  20. package/fesm2022/juice-js-observability.mjs +141 -3
  21. package/fesm2022/juice-js-observability.mjs.map +1 -1
  22. package/package.json +1 -1
  23. package/styles/_observability.scss +6 -0
  24. package/types/juice-js-observability.d.ts +242 -0
  25. package/fesm2022/juice-js-observability-alert.page-nEy5Gc6j.mjs.map +0 -1
  26. package/fesm2022/juice-js-observability-rules.page-Crb07uWm.mjs +0 -175
  27. package/fesm2022/juice-js-observability-rules.page-Crb07uWm.mjs.map +0 -1
  28. package/fesm2022/juice-js-observability-service.page-rGAvUK_k.mjs.map +0 -1
@@ -0,0 +1,400 @@
1
+ import { ApiService, ContextService, LinkPipe } from "./juice-js-observability.mjs";
2
+ import { S as apiResource, o as problemText, u as ContextBar } from "./juice-js-observability-telemetry-BftWSOaQ.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
+ const MOVES = {
18
+ Open: [
19
+ "Investigating",
20
+ "Mitigated",
21
+ "Resolved"
22
+ ],
23
+ Investigating: ["Mitigated", "Resolved"],
24
+ Mitigated: ["Investigating", "Resolved"],
25
+ Resolved: ["Investigating"]
26
+ };
27
+ var IncidentPage = class IncidentPage {
28
+ ctx = inject(ContextService);
29
+ api = inject(ApiService);
30
+ id = input.required(...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
31
+ text = "";
32
+ sending = signal(false, ...ngDevMode ? [{ debugName: "sending" }] : /* istanbul ignore next */ []);
33
+ actionError = signal("", ...ngDevMode ? [{ debugName: "actionError" }] : /* istanbul ignore next */ []);
34
+ detail = apiResource({
35
+ params: () => this.id(),
36
+ stream: ({ params }) => this.api.incident(params)
37
+ });
38
+ error = computed(() => problemText(this.detail.error()), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
39
+ moves(state) {
40
+ return MOVES[state] ?? [];
41
+ }
42
+ describe(e) {
43
+ switch (e.type) {
44
+ case "created": return "Opened";
45
+ case "state": return `${e.from} → ${e.to}`;
46
+ case "note": return "Note";
47
+ case "alert_added": return "Alert added";
48
+ case "alert_removed": return "Alert removed";
49
+ default: return e.type;
50
+ }
51
+ }
52
+ run(request) {
53
+ this.sending.set(true);
54
+ this.actionError.set("");
55
+ request.subscribe({
56
+ next: () => {
57
+ this.sending.set(false);
58
+ this.text = "";
59
+ this.detail.reload();
60
+ },
61
+ error: (e) => {
62
+ this.sending.set(false);
63
+ this.actionError.set(e.status === 403 ? "This needs the operator or admin role." : problemText(e));
64
+ }
65
+ });
66
+ }
67
+ move(to) {
68
+ this.run(this.api.moveIncident(this.id(), to, this.text));
69
+ }
70
+ note() {
71
+ this.run(this.api.addIncidentNote(this.id(), this.text));
72
+ }
73
+ removeAlert(alertId) {
74
+ this.run(this.api.removeIncidentAlert(this.id(), alertId));
75
+ }
76
+ static ɵfac = i0.ɵɵngDeclareFactory({
77
+ minVersion: "12.0.0",
78
+ version: "22.2.4",
79
+ ngImport: i0,
80
+ type: IncidentPage,
81
+ deps: [],
82
+ target: i0.ɵɵFactoryTarget.Component
83
+ });
84
+ static ɵcmp = i0.ɵɵngDeclareComponent({
85
+ minVersion: "17.0.0",
86
+ version: "22.2.4",
87
+ type: IncidentPage,
88
+ isStandalone: true,
89
+ selector: "ng-component",
90
+ inputs: { id: {
91
+ classPropertyName: "id",
92
+ publicName: "id",
93
+ isSignal: true,
94
+ isRequired: true,
95
+ transformFunction: null
96
+ } },
97
+ ngImport: i0,
98
+ template: `
99
+ <jc-context-bar>
100
+ <a [routerLink]="['incidents'] | jcLink" [queryParams]="ctx.keep()" class="jc-crumb">Incidents</a><span class="jc-crumb-sep">›</span>
101
+ <h1>{{ detail.value()?.incident?.title ?? '…' }}</h1>
102
+ </jc-context-bar>
103
+ @if (detail.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
104
+ @if (detail.error()) { <p class="jc-error">{{ error() }}</p> }
105
+ @if (detail.value(); as d) {
106
+ <div class="jc-summary">
107
+ <span class="jc-badge" [class.jc-health-unhealthy]="d.incident.severity === 'Critical'" [class.jc-health-degraded]="d.incident.severity === 'Warning'">{{ d.incident.severity }}</span>
108
+ <strong data-testid="incident-state">{{ d.incident.state }}</strong>
109
+ <span class="jc-muted">Opened by {{ d.incident.createdBy }} {{ d.incident.createdAt | date: 'MMM d, HH:mm' }}</span>
110
+ </div>
111
+ @if (d.incident.state !== 'Resolved' && d.openAlerts > 0) {
112
+ <p class="jc-notice" data-testid="incident-open-alerts">{{ d.openAlerts }} of its alerts {{ d.openAlerts === 1 ? 'is' : 'are' }} still open. Resolving the incident won't change them.</p>
113
+ }
114
+ <section class="jc-card">
115
+ <h2>Move or note</h2>
116
+ <form class="jc-form" (ngSubmit)="note()">
117
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>Comment or note</mat-label><input matInput name="text" data-testid="incident-text" [(ngModel)]="text" maxlength="2000"></mat-form-field>
118
+ <button mat-stroked-button type="submit" data-testid="incident-note" [disabled]="sending()">Add note</button>
119
+ @for (m of moves(d.incident.state); track m) {
120
+ <button mat-flat-button type="button" [attr.data-testid]="'incident-move-' + m" [disabled]="sending()" (click)="move(m)">{{ d.incident.state === 'Resolved' ? 'Reopen' : m }}</button>
121
+ }
122
+ @if (actionError()) { <span class="jc-error" data-testid="incident-action-error">{{ actionError() }}</span> }
123
+ </form>
124
+ </section>
125
+ <section class="jc-card" data-testid="incident-alerts">
126
+ <h2>Alerts</h2>
127
+ <table class="jc-table"><tbody>
128
+ @for (a of d.alerts; track a.id) {
129
+ <tr>
130
+ <td><a [routerLink]="['alerts', a.id] | jcLink" [queryParams]="ctx.keep()">{{ a.rule }}</a></td>
131
+ <td>{{ a.subjectKey }}</td><td>{{ a.severity }}</td><td>{{ a.state }}</td>
132
+ <td class="jc-row-actions"><button mat-button type="button" (click)="removeAlert(a.id)">Remove</button></td>
133
+ </tr>
134
+ } @empty { <tr><td class="jc-muted">None. Open an alert and use “Open an incident”, or add one through the API.</td></tr> }
135
+ </tbody></table>
136
+ </section>
137
+ <section class="jc-card" data-testid="incident-events">
138
+ <h2>What happened</h2>
139
+ <table class="jc-table"><tbody>
140
+ @for (e of d.events; track $index) {
141
+ <tr>
142
+ <td class="jc-nowrap">{{ e.at | date: 'MMM d, HH:mm:ss' }}</td>
143
+ <td>{{ e.actor }}</td>
144
+ <td><strong>{{ describe(e) }}</strong>@if (e.text) { <div>{{ e.text }}</div> }</td>
145
+ </tr>
146
+ }
147
+ </tbody></table>
148
+ </section>
149
+ }`,
150
+ isInline: true,
151
+ dependencies: [
152
+ {
153
+ kind: "directive",
154
+ type: RouterLink,
155
+ selector: "[routerLink]",
156
+ inputs: [
157
+ "target",
158
+ "queryParams",
159
+ "fragment",
160
+ "queryParamsHandling",
161
+ "state",
162
+ "info",
163
+ "relativeTo",
164
+ "preserveFragment",
165
+ "skipLocationChange",
166
+ "replaceUrl",
167
+ "browserUrl",
168
+ "routerLink"
169
+ ]
170
+ },
171
+ {
172
+ kind: "ngmodule",
173
+ type: FormsModule
174
+ },
175
+ {
176
+ kind: "directive",
177
+ type: i1.ɵNgNoValidate,
178
+ selector: "form:not([ngNoForm]):not([ngNativeValidate])"
179
+ },
180
+ {
181
+ kind: "directive",
182
+ type: i1.DefaultValueAccessor,
183
+ 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]"
184
+ },
185
+ {
186
+ kind: "directive",
187
+ type: i1.NgControlStatus,
188
+ selector: "[formControlName],[ngModel],[formControl]"
189
+ },
190
+ {
191
+ kind: "directive",
192
+ type: i1.NgControlStatusGroup,
193
+ selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]"
194
+ },
195
+ {
196
+ kind: "directive",
197
+ type: i1.MaxLengthValidator,
198
+ selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]",
199
+ inputs: ["maxlength"]
200
+ },
201
+ {
202
+ kind: "directive",
203
+ type: i1.NgModel,
204
+ selector: "[ngModel]:not([formControlName]):not([formControl])",
205
+ inputs: [
206
+ "name",
207
+ "disabled",
208
+ "ngModel",
209
+ "ngModelOptions"
210
+ ],
211
+ outputs: ["ngModelChange"],
212
+ exportAs: ["ngModel"]
213
+ },
214
+ {
215
+ kind: "directive",
216
+ type: i1.NgForm,
217
+ selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]",
218
+ inputs: ["ngFormOptions"],
219
+ outputs: ["ngSubmit"],
220
+ exportAs: ["ngForm"]
221
+ },
222
+ {
223
+ kind: "ngmodule",
224
+ type: MatButtonModule
225
+ },
226
+ {
227
+ kind: "component",
228
+ type: i2.MatButton,
229
+ 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] ",
230
+ inputs: ["matButton"],
231
+ exportAs: ["matButton", "matAnchor"]
232
+ },
233
+ {
234
+ kind: "ngmodule",
235
+ type: MatFormFieldModule
236
+ },
237
+ {
238
+ kind: "component",
239
+ type: i3.MatFormField,
240
+ selector: "mat-form-field",
241
+ inputs: [
242
+ "hideRequiredMarker",
243
+ "color",
244
+ "floatLabel",
245
+ "appearance",
246
+ "subscriptSizing",
247
+ "hintLabel"
248
+ ],
249
+ exportAs: ["matFormField"]
250
+ },
251
+ {
252
+ kind: "directive",
253
+ type: i3.MatLabel,
254
+ selector: "mat-label"
255
+ },
256
+ {
257
+ kind: "ngmodule",
258
+ type: MatInputModule
259
+ },
260
+ {
261
+ kind: "directive",
262
+ type: i4.MatInput,
263
+ selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",
264
+ inputs: [
265
+ "disabled",
266
+ "id",
267
+ "placeholder",
268
+ "name",
269
+ "required",
270
+ "type",
271
+ "errorStateMatcher",
272
+ "aria-describedby",
273
+ "value",
274
+ "readonly",
275
+ "disabledInteractive"
276
+ ],
277
+ exportAs: ["matInput"]
278
+ },
279
+ {
280
+ kind: "ngmodule",
281
+ type: MatProgressBarModule
282
+ },
283
+ {
284
+ kind: "component",
285
+ type: i5.MatProgressBar,
286
+ selector: "mat-progress-bar",
287
+ inputs: [
288
+ "color",
289
+ "value",
290
+ "bufferValue",
291
+ "mode"
292
+ ],
293
+ outputs: ["animationEnd"],
294
+ exportAs: ["matProgressBar"]
295
+ },
296
+ {
297
+ kind: "component",
298
+ type: ContextBar,
299
+ selector: "jc-context-bar"
300
+ },
301
+ {
302
+ kind: "pipe",
303
+ type: LinkPipe,
304
+ name: "jcLink"
305
+ },
306
+ {
307
+ kind: "pipe",
308
+ type: DatePipe,
309
+ name: "date"
310
+ }
311
+ ],
312
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
313
+ });
314
+ };
315
+ i0.ɵɵngDeclareClassMetadata({
316
+ minVersion: "12.0.0",
317
+ version: "22.2.4",
318
+ ngImport: i0,
319
+ type: IncidentPage,
320
+ decorators: [{
321
+ type: Component,
322
+ args: [{
323
+ changeDetection: ChangeDetectionStrategy.OnPush,
324
+ imports: [
325
+ LinkPipe,
326
+ RouterLink,
327
+ DatePipe,
328
+ FormsModule,
329
+ MatButtonModule,
330
+ MatFormFieldModule,
331
+ MatInputModule,
332
+ MatProgressBarModule,
333
+ ContextBar
334
+ ],
335
+ template: `
336
+ <jc-context-bar>
337
+ <a [routerLink]="['incidents'] | jcLink" [queryParams]="ctx.keep()" class="jc-crumb">Incidents</a><span class="jc-crumb-sep">›</span>
338
+ <h1>{{ detail.value()?.incident?.title ?? '…' }}</h1>
339
+ </jc-context-bar>
340
+ @if (detail.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
341
+ @if (detail.error()) { <p class="jc-error">{{ error() }}</p> }
342
+ @if (detail.value(); as d) {
343
+ <div class="jc-summary">
344
+ <span class="jc-badge" [class.jc-health-unhealthy]="d.incident.severity === 'Critical'" [class.jc-health-degraded]="d.incident.severity === 'Warning'">{{ d.incident.severity }}</span>
345
+ <strong data-testid="incident-state">{{ d.incident.state }}</strong>
346
+ <span class="jc-muted">Opened by {{ d.incident.createdBy }} {{ d.incident.createdAt | date: 'MMM d, HH:mm' }}</span>
347
+ </div>
348
+ @if (d.incident.state !== 'Resolved' && d.openAlerts > 0) {
349
+ <p class="jc-notice" data-testid="incident-open-alerts">{{ d.openAlerts }} of its alerts {{ d.openAlerts === 1 ? 'is' : 'are' }} still open. Resolving the incident won't change them.</p>
350
+ }
351
+ <section class="jc-card">
352
+ <h2>Move or note</h2>
353
+ <form class="jc-form" (ngSubmit)="note()">
354
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>Comment or note</mat-label><input matInput name="text" data-testid="incident-text" [(ngModel)]="text" maxlength="2000"></mat-form-field>
355
+ <button mat-stroked-button type="submit" data-testid="incident-note" [disabled]="sending()">Add note</button>
356
+ @for (m of moves(d.incident.state); track m) {
357
+ <button mat-flat-button type="button" [attr.data-testid]="'incident-move-' + m" [disabled]="sending()" (click)="move(m)">{{ d.incident.state === 'Resolved' ? 'Reopen' : m }}</button>
358
+ }
359
+ @if (actionError()) { <span class="jc-error" data-testid="incident-action-error">{{ actionError() }}</span> }
360
+ </form>
361
+ </section>
362
+ <section class="jc-card" data-testid="incident-alerts">
363
+ <h2>Alerts</h2>
364
+ <table class="jc-table"><tbody>
365
+ @for (a of d.alerts; track a.id) {
366
+ <tr>
367
+ <td><a [routerLink]="['alerts', a.id] | jcLink" [queryParams]="ctx.keep()">{{ a.rule }}</a></td>
368
+ <td>{{ a.subjectKey }}</td><td>{{ a.severity }}</td><td>{{ a.state }}</td>
369
+ <td class="jc-row-actions"><button mat-button type="button" (click)="removeAlert(a.id)">Remove</button></td>
370
+ </tr>
371
+ } @empty { <tr><td class="jc-muted">None. Open an alert and use “Open an incident”, or add one through the API.</td></tr> }
372
+ </tbody></table>
373
+ </section>
374
+ <section class="jc-card" data-testid="incident-events">
375
+ <h2>What happened</h2>
376
+ <table class="jc-table"><tbody>
377
+ @for (e of d.events; track $index) {
378
+ <tr>
379
+ <td class="jc-nowrap">{{ e.at | date: 'MMM d, HH:mm:ss' }}</td>
380
+ <td>{{ e.actor }}</td>
381
+ <td><strong>{{ describe(e) }}</strong>@if (e.text) { <div>{{ e.text }}</div> }</td>
382
+ </tr>
383
+ }
384
+ </tbody></table>
385
+ </section>
386
+ }`
387
+ }]
388
+ }],
389
+ propDecorators: { id: [{
390
+ type: i0.Input,
391
+ args: [{
392
+ isSignal: true,
393
+ alias: "id",
394
+ required: true
395
+ }]
396
+ }] }
397
+ });
398
+ export { IncidentPage };
399
+
400
+ //# sourceMappingURL=juice-js-observability-incident.page-CCHEWXou.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"juice-js-observability-incident.page-CCHEWXou.mjs","names":[],"sources":["../../../../projects/juice-js/observability/src/lib/pages/incident.page.ts"],"sourcesContent":["import { LinkPipe } from '../core/options';\nimport { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core';\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 { Observable } from 'rxjs';\nimport { ApiService } from '../core/api.service';\nimport { ContextService } from '../core/context.service';\nimport { IncidentEvent } from '../core/models';\nimport { apiResource } from '../shared/resource';\nimport { problemText } from '../shared/telemetry';\nimport { ContextBar } from '../shared/ui';\n\nconst MOVES: Record<string, string[]> = {\n Open: ['Investigating', 'Mitigated', 'Resolved'],\n Investigating: ['Mitigated', 'Resolved'],\n Mitigated: ['Investigating', 'Resolved'],\n Resolved: ['Investigating'],\n};\n\n/** One incident: its state and the moves from it, its alerts, and who did what. Resolving it changes no alert, and says so if some are still open. */\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [LinkPipe, RouterLink, DatePipe, FormsModule, MatButtonModule, MatFormFieldModule, MatInputModule, MatProgressBarModule, ContextBar],\n template: `\n <jc-context-bar>\n <a [routerLink]=\"['incidents'] | jcLink\" [queryParams]=\"ctx.keep()\" class=\"jc-crumb\">Incidents</a><span class=\"jc-crumb-sep\">›</span>\n <h1>{{ detail.value()?.incident?.title ?? '…' }}</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.incident.severity === 'Critical'\" [class.jc-health-degraded]=\"d.incident.severity === 'Warning'\">{{ d.incident.severity }}</span>\n <strong data-testid=\"incident-state\">{{ d.incident.state }}</strong>\n <span class=\"jc-muted\">Opened by {{ d.incident.createdBy }} {{ d.incident.createdAt | date: 'MMM d, HH:mm' }}</span>\n </div>\n @if (d.incident.state !== 'Resolved' && d.openAlerts > 0) {\n <p class=\"jc-notice\" data-testid=\"incident-open-alerts\">{{ d.openAlerts }} of its alerts {{ d.openAlerts === 1 ? 'is' : 'are' }} still open. Resolving the incident won't change them.</p>\n }\n <section class=\"jc-card\">\n <h2>Move or note</h2>\n <form class=\"jc-form\" (ngSubmit)=\"note()\">\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-wide\"><mat-label>Comment or note</mat-label><input matInput name=\"text\" data-testid=\"incident-text\" [(ngModel)]=\"text\" maxlength=\"2000\"></mat-form-field>\n <button mat-stroked-button type=\"submit\" data-testid=\"incident-note\" [disabled]=\"sending()\">Add note</button>\n @for (m of moves(d.incident.state); track m) {\n <button mat-flat-button type=\"button\" [attr.data-testid]=\"'incident-move-' + m\" [disabled]=\"sending()\" (click)=\"move(m)\">{{ d.incident.state === 'Resolved' ? 'Reopen' : m }}</button>\n }\n @if (actionError()) { <span class=\"jc-error\" data-testid=\"incident-action-error\">{{ actionError() }}</span> }\n </form>\n </section>\n <section class=\"jc-card\" data-testid=\"incident-alerts\">\n <h2>Alerts</h2>\n <table class=\"jc-table\"><tbody>\n @for (a of d.alerts; track a.id) {\n <tr>\n <td><a [routerLink]=\"['alerts', a.id] | jcLink\" [queryParams]=\"ctx.keep()\">{{ a.rule }}</a></td>\n <td>{{ a.subjectKey }}</td><td>{{ a.severity }}</td><td>{{ a.state }}</td>\n <td class=\"jc-row-actions\"><button mat-button type=\"button\" (click)=\"removeAlert(a.id)\">Remove</button></td>\n </tr>\n } @empty { <tr><td class=\"jc-muted\">None. Open an alert and use “Open an incident”, or add one through the API.</td></tr> }\n </tbody></table>\n </section>\n <section class=\"jc-card\" data-testid=\"incident-events\">\n <h2>What happened</h2>\n <table class=\"jc-table\"><tbody>\n @for (e of d.events; track $index) {\n <tr>\n <td class=\"jc-nowrap\">{{ e.at | date: 'MMM d, HH:mm:ss' }}</td>\n <td>{{ e.actor }}</td>\n <td><strong>{{ describe(e) }}</strong>@if (e.text) { <div>{{ e.text }}</div> }</td>\n </tr>\n }\n </tbody></table>\n </section>\n }`,\n})\nexport class IncidentPage {\n protected readonly ctx = inject(ContextService);\n private readonly api = inject(ApiService);\n readonly id = input.required<string>();\n protected text = '';\n protected readonly sending = signal(false);\n protected readonly actionError = signal('');\n\n protected readonly detail = apiResource({ params: () => this.id(), stream: ({ params }) => this.api.incident(params) });\n protected readonly error = computed(() => problemText(this.detail.error()));\n\n protected moves(state: string): string[] { return MOVES[state] ?? []; }\n\n protected describe(e: IncidentEvent): string {\n switch (e.type) {\n case 'created': return 'Opened';\n case 'state': return `${e.from} → ${e.to}`;\n case 'note': return 'Note';\n case 'alert_added': return 'Alert added';\n case 'alert_removed': return 'Alert removed';\n default: return e.type;\n }\n }\n\n private run<T>(request: Observable<T>) {\n this.sending.set(true);\n this.actionError.set('');\n request.subscribe({\n next: () => { this.sending.set(false); this.text = ''; this.detail.reload(); },\n error: e => { this.sending.set(false); this.actionError.set((e as { status?: number }).status === 403 ? 'This needs the operator or admin role.' : problemText(e)); },\n });\n }\n\n protected move(to: string) { this.run(this.api.moveIncident(this.id(), to, this.text)); }\n protected note() { this.run(this.api.addIncidentNote(this.id(), this.text)); }\n protected removeAlert(alertId: string) { this.run(this.api.removeIncidentAlert(this.id(), alertId)); }\n}\n"],"mappings":"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"}