@juice-js/observability 1.1.0-alpha.21 → 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 (25) hide show
  1. package/fesm2022/{juice-js-observability-alert.page-nEy5Gc6j.mjs → juice-js-observability-alert.page-DOAX9WWa.mjs} +154 -2
  2. package/fesm2022/juice-js-observability-alert.page-DOAX9WWa.mjs.map +1 -0
  3. package/fesm2022/juice-js-observability-incident.page-CCHEWXou.mjs +400 -0
  4. package/fesm2022/juice-js-observability-incident.page-CCHEWXou.mjs.map +1 -0
  5. package/fesm2022/juice-js-observability-incidents.page-CpNqYp2W.mjs +438 -0
  6. package/fesm2022/juice-js-observability-incidents.page-CpNqYp2W.mjs.map +1 -0
  7. package/fesm2022/juice-js-observability-notifications.page-BDqb10C6.mjs +725 -0
  8. package/fesm2022/juice-js-observability-notifications.page-BDqb10C6.mjs.map +1 -0
  9. package/fesm2022/juice-js-observability-rules.page-Df2uDVwz.mjs +580 -0
  10. package/fesm2022/juice-js-observability-rules.page-Df2uDVwz.mjs.map +1 -0
  11. package/fesm2022/{juice-js-observability-service.page-BrcID0Bs.mjs → juice-js-observability-service.page-BFXUe4mX.mjs} +3 -1
  12. package/fesm2022/juice-js-observability-service.page-BFXUe4mX.mjs.map +1 -0
  13. package/fesm2022/juice-js-observability-silences.page-Cg53dJAy.mjs +523 -0
  14. package/fesm2022/juice-js-observability-silences.page-Cg53dJAy.mjs.map +1 -0
  15. package/fesm2022/juice-js-observability-timeline.page-CtU2EoDN.mjs +213 -0
  16. package/fesm2022/juice-js-observability-timeline.page-CtU2EoDN.mjs.map +1 -0
  17. package/fesm2022/juice-js-observability.mjs +128 -3
  18. package/fesm2022/juice-js-observability.mjs.map +1 -1
  19. package/package.json +1 -1
  20. package/styles/_observability.scss +4 -0
  21. package/types/juice-js-observability.d.ts +197 -0
  22. package/fesm2022/juice-js-observability-alert.page-nEy5Gc6j.mjs.map +0 -1
  23. package/fesm2022/juice-js-observability-rules.page-Crb07uWm.mjs +0 -175
  24. package/fesm2022/juice-js-observability-rules.page-Crb07uWm.mjs.map +0 -1
  25. package/fesm2022/juice-js-observability-service.page-BrcID0Bs.mjs.map +0 -1
@@ -0,0 +1,725 @@
1
+ import { ApiService } 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, signal } from "@angular/core";
5
+ import { forkJoin, map } from "rxjs";
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 i5 from "@angular/material/input";
13
+ import { MatInputModule } from "@angular/material/input";
14
+ import * as i6 from "@angular/material/progress-bar";
15
+ import { MatProgressBarModule } from "@angular/material/progress-bar";
16
+ import * as i4 from "@angular/material/select";
17
+ import { MatSelectModule } from "@angular/material/select";
18
+ import * as i3 from "@angular/material/checkbox";
19
+ import { MatCheckboxModule } from "@angular/material/checkbox";
20
+ const list = (text) => text.split(",").map((s) => s.trim()).filter(Boolean);
21
+ const NEW_CHANNEL = {
22
+ id: null,
23
+ key: "",
24
+ kind: "Slack",
25
+ url: "",
26
+ secretRef: "",
27
+ recipients: "",
28
+ ruleKeys: "",
29
+ environments: "",
30
+ minimumSeverity: "Info",
31
+ groupWindowSeconds: 0,
32
+ firing: true,
33
+ resolved: true,
34
+ enabled: true
35
+ };
36
+ const NEW_POLICY = {
37
+ editing: false,
38
+ key: "",
39
+ name: "",
40
+ ruleKeys: "",
41
+ environments: "",
42
+ minimumSeverity: "Critical",
43
+ stopOnAcknowledge: true,
44
+ enabled: true,
45
+ steps: ""
46
+ };
47
+ var NotificationsPage = class NotificationsPage {
48
+ api = inject(ApiService);
49
+ channel = signal(null, ...ngDevMode ? [{ debugName: "channel" }] : /* istanbul ignore next */ []);
50
+ policy = signal(null, ...ngDevMode ? [{ debugName: "policy" }] : /* istanbul ignore next */ []);
51
+ history = signal(null, ...ngDevMode ? [{ debugName: "history" }] : /* istanbul ignore next */ []);
52
+ saving = signal(false, ...ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []);
53
+ channelError = signal("", ...ngDevMode ? [{ debugName: "channelError" }] : /* istanbul ignore next */ []);
54
+ policyError = signal("", ...ngDevMode ? [{ debugName: "policyError" }] : /* istanbul ignore next */ []);
55
+ allowError = signal("", ...ngDevMode ? [{ debugName: "allowError" }] : /* istanbul ignore next */ []);
56
+ destination = "";
57
+ ports = "";
58
+ data = apiResource({ stream: () => forkJoin({
59
+ channels: this.api.channels().pipe(map((p) => p.items)),
60
+ policies: this.api.escalationPolicies().pipe(map((p) => p.items)),
61
+ allowlist: this.api.allowlist().pipe(map((p) => p.items))
62
+ }) });
63
+ error = computed(() => problemText(this.data.error()), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
64
+ channels = computed(() => this.data.value()?.channels ?? [], ...ngDevMode ? [{ debugName: "channels" }] : /* istanbul ignore next */ []);
65
+ policies = computed(() => this.data.value()?.policies ?? [], ...ngDevMode ? [{ debugName: "policies" }] : /* istanbul ignore next */ []);
66
+ allowed = computed(() => this.data.value()?.allowlist ?? [], ...ngDevMode ? [{ debugName: "allowed" }] : /* istanbul ignore next */ []);
67
+ deliveries = apiResource({
68
+ params: () => this.history() ?? void 0,
69
+ stream: ({ params }) => this.api.deliveries(params)
70
+ });
71
+ deliveriesError = computed(() => problemText(this.deliveries.error()), ...ngDevMode ? [{ debugName: "deliveriesError" }] : /* istanbul ignore next */ []);
72
+ newChannel() {
73
+ this.channelError.set("");
74
+ return { ...NEW_CHANNEL };
75
+ }
76
+ newPolicy() {
77
+ this.policyError.set("");
78
+ return { ...NEW_POLICY };
79
+ }
80
+ editChannel(c) {
81
+ this.channelError.set("");
82
+ this.channel.set({
83
+ id: c.id,
84
+ key: c.key,
85
+ kind: c.kind,
86
+ url: "",
87
+ secretRef: c.secretRef ?? "",
88
+ recipients: (c.recipients ?? []).join(", "),
89
+ ruleKeys: c.ruleKeys.join(", "),
90
+ environments: c.environments.join(", "),
91
+ minimumSeverity: c.minimumSeverity,
92
+ groupWindowSeconds: c.groupWindowSeconds,
93
+ firing: c.events.includes("alert.firing"),
94
+ resolved: c.events.includes("alert.resolved"),
95
+ enabled: c.enabled
96
+ });
97
+ }
98
+ run(request, error, done) {
99
+ this.saving.set(true);
100
+ error("");
101
+ request.subscribe({
102
+ next: () => {
103
+ this.saving.set(false);
104
+ done();
105
+ this.data.reload();
106
+ },
107
+ error: (e) => {
108
+ this.saving.set(false);
109
+ error(e.status === 403 ? "This needs the admin role." : problemText(e));
110
+ }
111
+ });
112
+ }
113
+ saveChannel(f) {
114
+ const events = [f.firing ? "alert.firing" : "", f.resolved ? "alert.resolved" : ""].filter(Boolean);
115
+ const body = {
116
+ key: f.key,
117
+ kind: f.kind,
118
+ url: f.kind === "Webhook" && f.url.trim() ? f.url.trim() : void 0,
119
+ secretRef: f.kind === "Email" ? void 0 : f.secretRef.trim(),
120
+ recipients: f.kind === "Email" ? list(f.recipients) : void 0,
121
+ events,
122
+ ruleKeys: list(f.ruleKeys),
123
+ environments: list(f.environments).map((e) => e.toLowerCase()),
124
+ minimumSeverity: f.minimumSeverity,
125
+ groupWindowSeconds: f.groupWindowSeconds || 0,
126
+ enabled: f.enabled
127
+ };
128
+ this.run(f.id ? this.api.updateChannel(f.id, body) : this.api.createChannel(body), (m) => this.channelError.set(m), () => this.channel.set(null));
129
+ }
130
+ editPolicy(p) {
131
+ this.policyError.set("");
132
+ this.policy.set({
133
+ editing: true,
134
+ key: p.key,
135
+ name: p.name,
136
+ ruleKeys: p.ruleKeys.join(", "),
137
+ environments: p.environments.join(", "),
138
+ minimumSeverity: p.minimumSeverity,
139
+ stopOnAcknowledge: p.stopOnAcknowledge,
140
+ enabled: p.enabled,
141
+ steps: p.steps.map((s) => `${s.afterMinutes}: ${s.endpointKeys.join(", ")}`).join("\n")
142
+ });
143
+ }
144
+ parseSteps(text) {
145
+ return text.split("\n").map((l) => l.trim()).filter(Boolean).map((line) => {
146
+ const [minutes, ...rest] = line.split(":");
147
+ return {
148
+ afterMinutes: Number(minutes.trim()),
149
+ endpointKeys: list(rest.join(":"))
150
+ };
151
+ });
152
+ }
153
+ savePolicy(f) {
154
+ const body = {
155
+ key: f.key,
156
+ name: f.name,
157
+ enabled: f.enabled,
158
+ ruleKeys: list(f.ruleKeys),
159
+ environments: list(f.environments).map((e) => e.toLowerCase()),
160
+ minimumSeverity: f.minimumSeverity,
161
+ stopOnAcknowledge: f.stopOnAcknowledge,
162
+ steps: this.parseSteps(f.steps)
163
+ };
164
+ this.run(f.editing ? this.api.updateEscalationPolicy(f.key, body) : this.api.createEscalationPolicy(body), (m) => this.policyError.set(m), () => this.policy.set(null));
165
+ }
166
+ addAllowed() {
167
+ this.run(this.api.putAllowlist(this.destination.trim(), list(this.ports).map(Number)), (m) => this.allowError.set(m), () => {
168
+ this.destination = "";
169
+ this.ports = "";
170
+ });
171
+ }
172
+ removeAllowed(destination) {
173
+ this.run(this.api.removeAllowlist(destination), (m) => this.allowError.set(m), () => void 0);
174
+ }
175
+ static ɵfac = i0.ɵɵngDeclareFactory({
176
+ minVersion: "12.0.0",
177
+ version: "22.2.4",
178
+ ngImport: i0,
179
+ type: NotificationsPage,
180
+ deps: [],
181
+ target: i0.ɵɵFactoryTarget.Component
182
+ });
183
+ static ɵcmp = i0.ɵɵngDeclareComponent({
184
+ minVersion: "17.0.0",
185
+ version: "22.2.4",
186
+ type: NotificationsPage,
187
+ isStandalone: true,
188
+ selector: "ng-component",
189
+ ngImport: i0,
190
+ template: `
191
+ <jc-context-bar><h1>Notifications</h1></jc-context-bar>
192
+ @if (data.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
193
+ @if (data.error()) { <p class="jc-error">{{ error() }}</p> }
194
+
195
+ <section class="jc-card">
196
+ <h2>Channels <button mat-button type="button" data-testid="channel-new" (click)="channel.set(newChannel())">New channel</button></h2>
197
+ @if (channel(); as f) {
198
+ <form class="jc-form" data-testid="channel-form" (ngSubmit)="saveChannel(f)">
199
+ @if (!f.id) {
200
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Key</mat-label><input matInput name="key" [(ngModel)]="f.key" required></mat-form-field>
201
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Kind</mat-label>
202
+ <mat-select name="kind" [(ngModel)]="f.kind"><mat-option value="Slack">Slack</mat-option><mat-option value="Teams">Teams</mat-option><mat-option value="Email">Email</mat-option><mat-option value="Webhook">Signed webhook</mat-option></mat-select>
203
+ </mat-form-field>
204
+ }
205
+ @if (f.kind === 'Webhook') { <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>{{ f.id ? 'New URL (keeps the current one if empty)' : 'URL' }}</mat-label><input matInput name="url" [(ngModel)]="f.url"></mat-form-field> }
206
+ @if (f.kind === 'Email') { <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>Recipients, comma separated</mat-label><input matInput name="recipients" [(ngModel)]="f.recipients"></mat-form-field> }
207
+ @else {
208
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>{{ f.kind === 'Webhook' ? 'Signing secret reference' : 'Reference to the channel URL' }}</mat-label><input matInput name="secretRef" [(ngModel)]="f.secretRef" placeholder="env:JUICE_WEBHOOK_OPS"></mat-form-field>
209
+ <p class="jc-muted jc-small jc-form-actions">{{ f.kind === 'Webhook' ? 'The name of where the signing secret is, set on the host.' : 'A Slack or Teams URL is itself a credential, so it stays on the host: this is the name of where it is.' }} Not the secret itself.</p>
210
+ }
211
+ <mat-form-field subscriptSizing="dynamic"><mat-label>Only these rules</mat-label><input matInput name="ruleKeys" [(ngModel)]="f.ruleKeys"></mat-form-field>
212
+ <mat-form-field subscriptSizing="dynamic"><mat-label>Only these environments</mat-label><input matInput name="environments" [(ngModel)]="f.environments"></mat-form-field>
213
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Severity at least</mat-label>
214
+ <mat-select name="minimumSeverity" [(ngModel)]="f.minimumSeverity"><mat-option value="Info">Info</mat-option><mat-option value="Warning">Warning</mat-option><mat-option value="Critical">Critical</mat-option></mat-select>
215
+ </mat-form-field>
216
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Group for (s)</mat-label><input matInput type="number" min="0" max="3600" name="group" [(ngModel)]="f.groupWindowSeconds"></mat-form-field>
217
+ <mat-checkbox name="firing" [(ngModel)]="f.firing">Firing</mat-checkbox>
218
+ <mat-checkbox name="resolved" [(ngModel)]="f.resolved">Resolved</mat-checkbox>
219
+ <mat-checkbox name="enabled" [(ngModel)]="f.enabled">Enabled</mat-checkbox>
220
+ <div class="jc-form-actions">
221
+ <button mat-flat-button type="submit" data-testid="channel-save" [disabled]="saving()">Save</button>
222
+ <button mat-button type="button" (click)="channel.set(null)">Cancel</button>
223
+ @if (channelError()) { <span class="jc-error" data-testid="channel-error">{{ channelError() }}</span> }
224
+ </div>
225
+ </form>
226
+ }
227
+ <table class="jc-table" data-testid="channels">
228
+ <thead><tr><th>Channel</th><th>Kind</th><th>Destination</th><th>Takes</th><th>Failed (24 h)</th><th>Waiting</th><th></th></tr></thead>
229
+ <tbody>
230
+ @for (c of channels(); track c.id) {
231
+ <tr [class.jc-muted]="!c.enabled">
232
+ <td><strong>{{ c.key }}</strong><div class="jc-muted jc-small">@if (!c.enabled) { disabled · }@if (c.managedBy === 'config') { from the webhooks file }</div></td>
233
+ <td>{{ c.kind }}</td>
234
+ <td>{{ c.destination }}</td>
235
+ <td class="jc-small">{{ c.minimumSeverity }} and above@if (c.environments.length) { · {{ c.environments.join(', ') }} }@if (c.ruleKeys.length) { · {{ c.ruleKeys.join(', ') }} }@if (c.groupWindowSeconds) { · grouped {{ c.groupWindowSeconds }} s }</td>
236
+ <td [class.jc-bad]="c.recentFailures > 0">{{ c.recentFailures }}</td>
237
+ <td>{{ c.waiting }}</td>
238
+ <td class="jc-row-actions">
239
+ <button mat-button type="button" [attr.data-testid]="'channel-history-' + c.key" (click)="history.set(history() === c.id ? null : c.id)">History</button>
240
+ @if (c.managedBy !== 'config') { <button mat-button type="button" [attr.data-testid]="'channel-edit-' + c.key" (click)="editChannel(c)">Change</button> }
241
+ </td>
242
+ </tr>
243
+ } @empty { <tr><td colspan="7" class="jc-muted">No channels: alerts fire, but nobody is told.</td></tr> }
244
+ </tbody>
245
+ </table>
246
+ @if (history()) {
247
+ <h3>Delivery history</h3>
248
+ @if (deliveries.error()) { <p class="jc-error">{{ deliveriesError() }}</p> }
249
+ <table class="jc-table" data-testid="channel-history">
250
+ <thead><tr><th>Created</th><th>Event</th><th>Status</th><th>Attempts</th><th>Last result</th></tr></thead>
251
+ <tbody>
252
+ @for (d of deliveries.value()?.items ?? []; track d.id) {
253
+ <tr>
254
+ <td class="jc-nowrap">{{ d.createdAt | date: 'MMM d, HH:mm:ss' }}</td>
255
+ <td>{{ d.event }}@if (d.escalationStep) { step {{ d.escalationStep }} }</td>
256
+ <td [class.jc-bad]="d.status === 'Failed'">{{ d.status }}</td>
257
+ <td>{{ d.attempts }}</td>
258
+ <td class="jc-small">{{ d.lastError ?? (d.lastStatusCode ? 'HTTP ' + d.lastStatusCode : '') }}</td>
259
+ </tr>
260
+ } @empty { <tr><td colspan="5" class="jc-muted">Nothing sent yet.</td></tr> }
261
+ </tbody>
262
+ </table>
263
+ }
264
+ </section>
265
+
266
+ <section class="jc-card">
267
+ <h2>Escalation <button mat-button type="button" data-testid="policy-new" (click)="policy.set(newPolicy())">New policy</button></h2>
268
+ @if (policy(); as f) {
269
+ <form class="jc-form" data-testid="policy-form" (ngSubmit)="savePolicy(f)">
270
+ @if (!f.editing) { <mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Key</mat-label><input matInput name="key" [(ngModel)]="f.key" required></mat-form-field> }
271
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>Name</mat-label><input matInput name="name" [(ngModel)]="f.name" required></mat-form-field>
272
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>Steps, one a line</mat-label><textarea matInput name="steps" rows="3" [(ngModel)]="f.steps" placeholder="5: oncall-slack&#10;15: pager, lead-email"></textarea></mat-form-field>
273
+ <p class="jc-muted jc-small jc-form-actions">Minutes after firing, then the channels to tell if the alert is still open.</p>
274
+ <mat-form-field subscriptSizing="dynamic"><mat-label>Only these rules</mat-label><input matInput name="ruleKeys" [(ngModel)]="f.ruleKeys"></mat-form-field>
275
+ <mat-form-field subscriptSizing="dynamic"><mat-label>Only these environments</mat-label><input matInput name="environments" [(ngModel)]="f.environments"></mat-form-field>
276
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Severity at least</mat-label>
277
+ <mat-select name="minimumSeverity" [(ngModel)]="f.minimumSeverity"><mat-option value="Info">Info</mat-option><mat-option value="Warning">Warning</mat-option><mat-option value="Critical">Critical</mat-option></mat-select>
278
+ </mat-form-field>
279
+ <mat-checkbox name="stop" [(ngModel)]="f.stopOnAcknowledge">Stop when acknowledged</mat-checkbox>
280
+ <mat-checkbox name="policyEnabled" [(ngModel)]="f.enabled">Enabled</mat-checkbox>
281
+ <div class="jc-form-actions">
282
+ <button mat-flat-button type="submit" data-testid="policy-save" [disabled]="saving()">Save</button>
283
+ <button mat-button type="button" (click)="policy.set(null)">Cancel</button>
284
+ @if (policyError()) { <span class="jc-error" data-testid="policy-error">{{ policyError() }}</span> }
285
+ </div>
286
+ </form>
287
+ }
288
+ <table class="jc-table" data-testid="policies">
289
+ <thead><tr><th>Policy</th><th>Steps</th><th>Applies to</th><th>Stops when</th><th></th></tr></thead>
290
+ <tbody>
291
+ @for (p of policies(); track p.id) {
292
+ <tr [class.jc-muted]="!p.enabled">
293
+ <td><strong>{{ p.name }}</strong><div class="jc-muted jc-small">{{ p.key }}@if (!p.enabled) { · disabled }</div></td>
294
+ <td class="jc-small">@for (s of p.steps; track s.afterMinutes) { <div>{{ s.afterMinutes }} min: {{ s.endpointKeys.join(', ') }}</div> }</td>
295
+ <td class="jc-small">{{ p.minimumSeverity }} and above@if (p.environments.length) { · {{ p.environments.join(', ') }} }@if (p.ruleKeys.length) { · {{ p.ruleKeys.join(', ') }} }</td>
296
+ <td class="jc-small">{{ p.stopOnAcknowledge ? 'Acknowledged or resolved' : 'Resolved' }}</td>
297
+ <td class="jc-row-actions"><button mat-button type="button" (click)="editPolicy(p)">Change</button></td>
298
+ </tr>
299
+ } @empty { <tr><td colspan="5" class="jc-muted">No policies: a firing alert notifies once.</td></tr> }
300
+ </tbody>
301
+ </table>
302
+ </section>
303
+
304
+ <section class="jc-card">
305
+ <h2>Private destinations</h2>
306
+ <p class="jc-muted jc-small">A channel may reach a private address only if it's listed here. Loopback, link-local and cloud metadata addresses are never reachable.</p>
307
+ <form class="jc-form" (ngSubmit)="addAllowed()">
308
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>CIDR or host name</mat-label><input matInput name="destination" data-testid="allow-destination" [(ngModel)]="destination"></mat-form-field>
309
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-md"><mat-label>Ports (443 if empty)</mat-label><input matInput name="ports" data-testid="allow-ports" [(ngModel)]="ports"></mat-form-field>
310
+ <button mat-stroked-button type="submit" data-testid="allow-add" [disabled]="saving()">Allow</button>
311
+ @if (allowError()) { <span class="jc-error" data-testid="allow-error">{{ allowError() }}</span> }
312
+ </form>
313
+ <table class="jc-table" data-testid="allowlist">
314
+ <tbody>
315
+ @for (a of allowed(); track a.destination) {
316
+ <tr>
317
+ <td><strong>{{ a.destination }}</strong> @if (a.managedBy === 'config') { <span class="jc-muted jc-small">from the webhooks file</span> }</td>
318
+ <td>{{ a.ports.length ? a.ports.join(', ') : '443' }}</td>
319
+ <td class="jc-row-actions">@if (a.managedBy !== 'config') { <button mat-button type="button" (click)="removeAllowed(a.destination)">Remove</button> }</td>
320
+ </tr>
321
+ } @empty { <tr><td class="jc-muted">None: channels reach public addresses only.</td></tr> }
322
+ </tbody>
323
+ </table>
324
+ </section>`,
325
+ isInline: true,
326
+ dependencies: [
327
+ {
328
+ kind: "ngmodule",
329
+ type: FormsModule
330
+ },
331
+ {
332
+ kind: "directive",
333
+ type: i1.ɵNgNoValidate,
334
+ selector: "form:not([ngNoForm]):not([ngNativeValidate])"
335
+ },
336
+ {
337
+ kind: "directive",
338
+ type: i1.DefaultValueAccessor,
339
+ 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]"
340
+ },
341
+ {
342
+ kind: "directive",
343
+ type: i1.NumberValueAccessor,
344
+ selector: "input[type=number]:not([ngNoCva])[formControlName],input[type=number]:not([ngNoCva])[formControl],input[type=number]:not([ngNoCva])[ngModel]"
345
+ },
346
+ {
347
+ kind: "directive",
348
+ type: i1.NgControlStatus,
349
+ selector: "[formControlName],[ngModel],[formControl]"
350
+ },
351
+ {
352
+ kind: "directive",
353
+ type: i1.NgControlStatusGroup,
354
+ selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]"
355
+ },
356
+ {
357
+ kind: "directive",
358
+ type: i1.RequiredValidator,
359
+ selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",
360
+ inputs: ["required"]
361
+ },
362
+ {
363
+ kind: "directive",
364
+ type: i1.MinValidator,
365
+ selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",
366
+ inputs: ["min"]
367
+ },
368
+ {
369
+ kind: "directive",
370
+ type: i1.MaxValidator,
371
+ selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",
372
+ inputs: ["max"]
373
+ },
374
+ {
375
+ kind: "directive",
376
+ type: i1.NgModel,
377
+ selector: "[ngModel]:not([formControlName]):not([formControl])",
378
+ inputs: [
379
+ "name",
380
+ "disabled",
381
+ "ngModel",
382
+ "ngModelOptions"
383
+ ],
384
+ outputs: ["ngModelChange"],
385
+ exportAs: ["ngModel"]
386
+ },
387
+ {
388
+ kind: "directive",
389
+ type: i1.NgForm,
390
+ selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]",
391
+ inputs: ["ngFormOptions"],
392
+ outputs: ["ngSubmit"],
393
+ exportAs: ["ngForm"]
394
+ },
395
+ {
396
+ kind: "ngmodule",
397
+ type: MatButtonModule
398
+ },
399
+ {
400
+ kind: "component",
401
+ type: i2.MatButton,
402
+ 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] ",
403
+ inputs: ["matButton"],
404
+ exportAs: ["matButton", "matAnchor"]
405
+ },
406
+ {
407
+ kind: "ngmodule",
408
+ type: MatCheckboxModule
409
+ },
410
+ {
411
+ kind: "component",
412
+ type: i3.MatCheckbox,
413
+ selector: "mat-checkbox",
414
+ inputs: [
415
+ "aria-label",
416
+ "aria-labelledby",
417
+ "aria-describedby",
418
+ "aria-expanded",
419
+ "aria-controls",
420
+ "aria-owns",
421
+ "id",
422
+ "required",
423
+ "labelPosition",
424
+ "name",
425
+ "value",
426
+ "disableRipple",
427
+ "tabIndex",
428
+ "color",
429
+ "disabledInteractive",
430
+ "checked",
431
+ "disabled",
432
+ "indeterminate"
433
+ ],
434
+ outputs: ["change", "indeterminateChange"],
435
+ exportAs: ["matCheckbox"]
436
+ },
437
+ {
438
+ kind: "ngmodule",
439
+ type: MatFormFieldModule
440
+ },
441
+ {
442
+ kind: "component",
443
+ type: i4.MatFormField,
444
+ selector: "mat-form-field",
445
+ inputs: [
446
+ "hideRequiredMarker",
447
+ "color",
448
+ "floatLabel",
449
+ "appearance",
450
+ "subscriptSizing",
451
+ "hintLabel"
452
+ ],
453
+ exportAs: ["matFormField"]
454
+ },
455
+ {
456
+ kind: "directive",
457
+ type: i4.MatLabel,
458
+ selector: "mat-label"
459
+ },
460
+ {
461
+ kind: "ngmodule",
462
+ type: MatInputModule
463
+ },
464
+ {
465
+ kind: "directive",
466
+ type: i5.MatInput,
467
+ selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",
468
+ inputs: [
469
+ "disabled",
470
+ "id",
471
+ "placeholder",
472
+ "name",
473
+ "required",
474
+ "type",
475
+ "errorStateMatcher",
476
+ "aria-describedby",
477
+ "value",
478
+ "readonly",
479
+ "disabledInteractive"
480
+ ],
481
+ exportAs: ["matInput"]
482
+ },
483
+ {
484
+ kind: "ngmodule",
485
+ type: MatProgressBarModule
486
+ },
487
+ {
488
+ kind: "component",
489
+ type: i6.MatProgressBar,
490
+ selector: "mat-progress-bar",
491
+ inputs: [
492
+ "color",
493
+ "value",
494
+ "bufferValue",
495
+ "mode"
496
+ ],
497
+ outputs: ["animationEnd"],
498
+ exportAs: ["matProgressBar"]
499
+ },
500
+ {
501
+ kind: "ngmodule",
502
+ type: MatSelectModule
503
+ },
504
+ {
505
+ kind: "component",
506
+ type: i4.MatSelect,
507
+ selector: "mat-select",
508
+ inputs: [
509
+ "aria-describedby",
510
+ "panelClass",
511
+ "disabled",
512
+ "disableRipple",
513
+ "tabIndex",
514
+ "hideSingleSelectionIndicator",
515
+ "placeholder",
516
+ "required",
517
+ "multiple",
518
+ "disableOptionCentering",
519
+ "compareWith",
520
+ "value",
521
+ "aria-label",
522
+ "aria-labelledby",
523
+ "errorStateMatcher",
524
+ "typeaheadDebounceInterval",
525
+ "sortComparator",
526
+ "id",
527
+ "panelWidth",
528
+ "canSelectNullableOptions"
529
+ ],
530
+ outputs: [
531
+ "openedChange",
532
+ "opened",
533
+ "closed",
534
+ "selectionChange",
535
+ "valueChange"
536
+ ],
537
+ exportAs: ["matSelect"]
538
+ },
539
+ {
540
+ kind: "component",
541
+ type: i4.MatOption,
542
+ selector: "mat-option",
543
+ inputs: [
544
+ "value",
545
+ "id",
546
+ "disabled"
547
+ ],
548
+ outputs: ["onSelectionChange"],
549
+ exportAs: ["matOption"]
550
+ },
551
+ {
552
+ kind: "component",
553
+ type: ContextBar,
554
+ selector: "jc-context-bar"
555
+ },
556
+ {
557
+ kind: "pipe",
558
+ type: DatePipe,
559
+ name: "date"
560
+ }
561
+ ],
562
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
563
+ });
564
+ };
565
+ i0.ɵɵngDeclareClassMetadata({
566
+ minVersion: "12.0.0",
567
+ version: "22.2.4",
568
+ ngImport: i0,
569
+ type: NotificationsPage,
570
+ decorators: [{
571
+ type: Component,
572
+ args: [{
573
+ changeDetection: ChangeDetectionStrategy.OnPush,
574
+ imports: [
575
+ DatePipe,
576
+ FormsModule,
577
+ MatButtonModule,
578
+ MatCheckboxModule,
579
+ MatFormFieldModule,
580
+ MatInputModule,
581
+ MatProgressBarModule,
582
+ MatSelectModule,
583
+ ContextBar
584
+ ],
585
+ template: `
586
+ <jc-context-bar><h1>Notifications</h1></jc-context-bar>
587
+ @if (data.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
588
+ @if (data.error()) { <p class="jc-error">{{ error() }}</p> }
589
+
590
+ <section class="jc-card">
591
+ <h2>Channels <button mat-button type="button" data-testid="channel-new" (click)="channel.set(newChannel())">New channel</button></h2>
592
+ @if (channel(); as f) {
593
+ <form class="jc-form" data-testid="channel-form" (ngSubmit)="saveChannel(f)">
594
+ @if (!f.id) {
595
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Key</mat-label><input matInput name="key" [(ngModel)]="f.key" required></mat-form-field>
596
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Kind</mat-label>
597
+ <mat-select name="kind" [(ngModel)]="f.kind"><mat-option value="Slack">Slack</mat-option><mat-option value="Teams">Teams</mat-option><mat-option value="Email">Email</mat-option><mat-option value="Webhook">Signed webhook</mat-option></mat-select>
598
+ </mat-form-field>
599
+ }
600
+ @if (f.kind === 'Webhook') { <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>{{ f.id ? 'New URL (keeps the current one if empty)' : 'URL' }}</mat-label><input matInput name="url" [(ngModel)]="f.url"></mat-form-field> }
601
+ @if (f.kind === 'Email') { <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>Recipients, comma separated</mat-label><input matInput name="recipients" [(ngModel)]="f.recipients"></mat-form-field> }
602
+ @else {
603
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>{{ f.kind === 'Webhook' ? 'Signing secret reference' : 'Reference to the channel URL' }}</mat-label><input matInput name="secretRef" [(ngModel)]="f.secretRef" placeholder="env:JUICE_WEBHOOK_OPS"></mat-form-field>
604
+ <p class="jc-muted jc-small jc-form-actions">{{ f.kind === 'Webhook' ? 'The name of where the signing secret is, set on the host.' : 'A Slack or Teams URL is itself a credential, so it stays on the host: this is the name of where it is.' }} Not the secret itself.</p>
605
+ }
606
+ <mat-form-field subscriptSizing="dynamic"><mat-label>Only these rules</mat-label><input matInput name="ruleKeys" [(ngModel)]="f.ruleKeys"></mat-form-field>
607
+ <mat-form-field subscriptSizing="dynamic"><mat-label>Only these environments</mat-label><input matInput name="environments" [(ngModel)]="f.environments"></mat-form-field>
608
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Severity at least</mat-label>
609
+ <mat-select name="minimumSeverity" [(ngModel)]="f.minimumSeverity"><mat-option value="Info">Info</mat-option><mat-option value="Warning">Warning</mat-option><mat-option value="Critical">Critical</mat-option></mat-select>
610
+ </mat-form-field>
611
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Group for (s)</mat-label><input matInput type="number" min="0" max="3600" name="group" [(ngModel)]="f.groupWindowSeconds"></mat-form-field>
612
+ <mat-checkbox name="firing" [(ngModel)]="f.firing">Firing</mat-checkbox>
613
+ <mat-checkbox name="resolved" [(ngModel)]="f.resolved">Resolved</mat-checkbox>
614
+ <mat-checkbox name="enabled" [(ngModel)]="f.enabled">Enabled</mat-checkbox>
615
+ <div class="jc-form-actions">
616
+ <button mat-flat-button type="submit" data-testid="channel-save" [disabled]="saving()">Save</button>
617
+ <button mat-button type="button" (click)="channel.set(null)">Cancel</button>
618
+ @if (channelError()) { <span class="jc-error" data-testid="channel-error">{{ channelError() }}</span> }
619
+ </div>
620
+ </form>
621
+ }
622
+ <table class="jc-table" data-testid="channels">
623
+ <thead><tr><th>Channel</th><th>Kind</th><th>Destination</th><th>Takes</th><th>Failed (24 h)</th><th>Waiting</th><th></th></tr></thead>
624
+ <tbody>
625
+ @for (c of channels(); track c.id) {
626
+ <tr [class.jc-muted]="!c.enabled">
627
+ <td><strong>{{ c.key }}</strong><div class="jc-muted jc-small">@if (!c.enabled) { disabled · }@if (c.managedBy === 'config') { from the webhooks file }</div></td>
628
+ <td>{{ c.kind }}</td>
629
+ <td>{{ c.destination }}</td>
630
+ <td class="jc-small">{{ c.minimumSeverity }} and above@if (c.environments.length) { · {{ c.environments.join(', ') }} }@if (c.ruleKeys.length) { · {{ c.ruleKeys.join(', ') }} }@if (c.groupWindowSeconds) { · grouped {{ c.groupWindowSeconds }} s }</td>
631
+ <td [class.jc-bad]="c.recentFailures > 0">{{ c.recentFailures }}</td>
632
+ <td>{{ c.waiting }}</td>
633
+ <td class="jc-row-actions">
634
+ <button mat-button type="button" [attr.data-testid]="'channel-history-' + c.key" (click)="history.set(history() === c.id ? null : c.id)">History</button>
635
+ @if (c.managedBy !== 'config') { <button mat-button type="button" [attr.data-testid]="'channel-edit-' + c.key" (click)="editChannel(c)">Change</button> }
636
+ </td>
637
+ </tr>
638
+ } @empty { <tr><td colspan="7" class="jc-muted">No channels: alerts fire, but nobody is told.</td></tr> }
639
+ </tbody>
640
+ </table>
641
+ @if (history()) {
642
+ <h3>Delivery history</h3>
643
+ @if (deliveries.error()) { <p class="jc-error">{{ deliveriesError() }}</p> }
644
+ <table class="jc-table" data-testid="channel-history">
645
+ <thead><tr><th>Created</th><th>Event</th><th>Status</th><th>Attempts</th><th>Last result</th></tr></thead>
646
+ <tbody>
647
+ @for (d of deliveries.value()?.items ?? []; track d.id) {
648
+ <tr>
649
+ <td class="jc-nowrap">{{ d.createdAt | date: 'MMM d, HH:mm:ss' }}</td>
650
+ <td>{{ d.event }}@if (d.escalationStep) { step {{ d.escalationStep }} }</td>
651
+ <td [class.jc-bad]="d.status === 'Failed'">{{ d.status }}</td>
652
+ <td>{{ d.attempts }}</td>
653
+ <td class="jc-small">{{ d.lastError ?? (d.lastStatusCode ? 'HTTP ' + d.lastStatusCode : '') }}</td>
654
+ </tr>
655
+ } @empty { <tr><td colspan="5" class="jc-muted">Nothing sent yet.</td></tr> }
656
+ </tbody>
657
+ </table>
658
+ }
659
+ </section>
660
+
661
+ <section class="jc-card">
662
+ <h2>Escalation <button mat-button type="button" data-testid="policy-new" (click)="policy.set(newPolicy())">New policy</button></h2>
663
+ @if (policy(); as f) {
664
+ <form class="jc-form" data-testid="policy-form" (ngSubmit)="savePolicy(f)">
665
+ @if (!f.editing) { <mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Key</mat-label><input matInput name="key" [(ngModel)]="f.key" required></mat-form-field> }
666
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>Name</mat-label><input matInput name="name" [(ngModel)]="f.name" required></mat-form-field>
667
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>Steps, one a line</mat-label><textarea matInput name="steps" rows="3" [(ngModel)]="f.steps" placeholder="5: oncall-slack&#10;15: pager, lead-email"></textarea></mat-form-field>
668
+ <p class="jc-muted jc-small jc-form-actions">Minutes after firing, then the channels to tell if the alert is still open.</p>
669
+ <mat-form-field subscriptSizing="dynamic"><mat-label>Only these rules</mat-label><input matInput name="ruleKeys" [(ngModel)]="f.ruleKeys"></mat-form-field>
670
+ <mat-form-field subscriptSizing="dynamic"><mat-label>Only these environments</mat-label><input matInput name="environments" [(ngModel)]="f.environments"></mat-form-field>
671
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Severity at least</mat-label>
672
+ <mat-select name="minimumSeverity" [(ngModel)]="f.minimumSeverity"><mat-option value="Info">Info</mat-option><mat-option value="Warning">Warning</mat-option><mat-option value="Critical">Critical</mat-option></mat-select>
673
+ </mat-form-field>
674
+ <mat-checkbox name="stop" [(ngModel)]="f.stopOnAcknowledge">Stop when acknowledged</mat-checkbox>
675
+ <mat-checkbox name="policyEnabled" [(ngModel)]="f.enabled">Enabled</mat-checkbox>
676
+ <div class="jc-form-actions">
677
+ <button mat-flat-button type="submit" data-testid="policy-save" [disabled]="saving()">Save</button>
678
+ <button mat-button type="button" (click)="policy.set(null)">Cancel</button>
679
+ @if (policyError()) { <span class="jc-error" data-testid="policy-error">{{ policyError() }}</span> }
680
+ </div>
681
+ </form>
682
+ }
683
+ <table class="jc-table" data-testid="policies">
684
+ <thead><tr><th>Policy</th><th>Steps</th><th>Applies to</th><th>Stops when</th><th></th></tr></thead>
685
+ <tbody>
686
+ @for (p of policies(); track p.id) {
687
+ <tr [class.jc-muted]="!p.enabled">
688
+ <td><strong>{{ p.name }}</strong><div class="jc-muted jc-small">{{ p.key }}@if (!p.enabled) { · disabled }</div></td>
689
+ <td class="jc-small">@for (s of p.steps; track s.afterMinutes) { <div>{{ s.afterMinutes }} min: {{ s.endpointKeys.join(', ') }}</div> }</td>
690
+ <td class="jc-small">{{ p.minimumSeverity }} and above@if (p.environments.length) { · {{ p.environments.join(', ') }} }@if (p.ruleKeys.length) { · {{ p.ruleKeys.join(', ') }} }</td>
691
+ <td class="jc-small">{{ p.stopOnAcknowledge ? 'Acknowledged or resolved' : 'Resolved' }}</td>
692
+ <td class="jc-row-actions"><button mat-button type="button" (click)="editPolicy(p)">Change</button></td>
693
+ </tr>
694
+ } @empty { <tr><td colspan="5" class="jc-muted">No policies: a firing alert notifies once.</td></tr> }
695
+ </tbody>
696
+ </table>
697
+ </section>
698
+
699
+ <section class="jc-card">
700
+ <h2>Private destinations</h2>
701
+ <p class="jc-muted jc-small">A channel may reach a private address only if it's listed here. Loopback, link-local and cloud metadata addresses are never reachable.</p>
702
+ <form class="jc-form" (ngSubmit)="addAllowed()">
703
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>CIDR or host name</mat-label><input matInput name="destination" data-testid="allow-destination" [(ngModel)]="destination"></mat-form-field>
704
+ <mat-form-field subscriptSizing="dynamic" class="jc-field-md"><mat-label>Ports (443 if empty)</mat-label><input matInput name="ports" data-testid="allow-ports" [(ngModel)]="ports"></mat-form-field>
705
+ <button mat-stroked-button type="submit" data-testid="allow-add" [disabled]="saving()">Allow</button>
706
+ @if (allowError()) { <span class="jc-error" data-testid="allow-error">{{ allowError() }}</span> }
707
+ </form>
708
+ <table class="jc-table" data-testid="allowlist">
709
+ <tbody>
710
+ @for (a of allowed(); track a.destination) {
711
+ <tr>
712
+ <td><strong>{{ a.destination }}</strong> @if (a.managedBy === 'config') { <span class="jc-muted jc-small">from the webhooks file</span> }</td>
713
+ <td>{{ a.ports.length ? a.ports.join(', ') : '443' }}</td>
714
+ <td class="jc-row-actions">@if (a.managedBy !== 'config') { <button mat-button type="button" (click)="removeAllowed(a.destination)">Remove</button> }</td>
715
+ </tr>
716
+ } @empty { <tr><td class="jc-muted">None: channels reach public addresses only.</td></tr> }
717
+ </tbody>
718
+ </table>
719
+ </section>`
720
+ }]
721
+ }]
722
+ });
723
+ export { NotificationsPage };
724
+
725
+ //# sourceMappingURL=juice-js-observability-notifications.page-BDqb10C6.mjs.map