@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.
- package/fesm2022/{juice-js-observability-alert.page-nEy5Gc6j.mjs → juice-js-observability-alert.page-DOAX9WWa.mjs} +154 -2
- package/fesm2022/juice-js-observability-alert.page-DOAX9WWa.mjs.map +1 -0
- package/fesm2022/juice-js-observability-incident.page-CCHEWXou.mjs +400 -0
- package/fesm2022/juice-js-observability-incident.page-CCHEWXou.mjs.map +1 -0
- package/fesm2022/juice-js-observability-incidents.page-CpNqYp2W.mjs +438 -0
- package/fesm2022/juice-js-observability-incidents.page-CpNqYp2W.mjs.map +1 -0
- package/fesm2022/juice-js-observability-notifications.page-BDqb10C6.mjs +725 -0
- package/fesm2022/juice-js-observability-notifications.page-BDqb10C6.mjs.map +1 -0
- package/fesm2022/juice-js-observability-rules.page-Df2uDVwz.mjs +580 -0
- package/fesm2022/juice-js-observability-rules.page-Df2uDVwz.mjs.map +1 -0
- package/fesm2022/{juice-js-observability-service.page-BrcID0Bs.mjs → juice-js-observability-service.page-BFXUe4mX.mjs} +3 -1
- package/fesm2022/juice-js-observability-service.page-BFXUe4mX.mjs.map +1 -0
- package/fesm2022/juice-js-observability-silences.page-Cg53dJAy.mjs +523 -0
- package/fesm2022/juice-js-observability-silences.page-Cg53dJAy.mjs.map +1 -0
- package/fesm2022/juice-js-observability-timeline.page-CtU2EoDN.mjs +213 -0
- package/fesm2022/juice-js-observability-timeline.page-CtU2EoDN.mjs.map +1 -0
- package/fesm2022/juice-js-observability.mjs +128 -3
- package/fesm2022/juice-js-observability.mjs.map +1 -1
- package/package.json +1 -1
- package/styles/_observability.scss +4 -0
- package/types/juice-js-observability.d.ts +197 -0
- package/fesm2022/juice-js-observability-alert.page-nEy5Gc6j.mjs.map +0 -1
- package/fesm2022/juice-js-observability-rules.page-Crb07uWm.mjs +0 -175
- package/fesm2022/juice-js-observability-rules.page-Crb07uWm.mjs.map +0 -1
- package/fesm2022/juice-js-observability-service.page-BrcID0Bs.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"juice-js-observability-notifications.page-BDqb10C6.mjs","names":[],"sources":["../../../../projects/juice-js/observability/src/lib/pages/notifications.page.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';\nimport { DatePipe } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatCheckboxModule } from '@angular/material/checkbox';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { MatSelectModule } from '@angular/material/select';\nimport { Observable, forkJoin, map } from 'rxjs';\nimport { ApiService } from '../core/api.service';\nimport { Channel, EscalationPolicy, EscalationStep } from '../core/models';\nimport { apiResource } from '../shared/resource';\nimport { problemText } from '../shared/telemetry';\nimport { ContextBar } from '../shared/ui';\n\nconst list = (text: string) => text.split(',').map(s => s.trim()).filter(Boolean);\n\ninterface ChannelForm {\n id: string | null; key: string; kind: string; url: string; secretRef: string; recipients: string; ruleKeys: string; environments: string; minimumSeverity: string; groupWindowSeconds: number; firing: boolean; resolved: boolean; enabled: boolean;\n}\n\ninterface PolicyForm { editing: boolean; key: string; name: string; ruleKeys: string; environments: string; minimumSeverity: string; stopOnAcknowledge: boolean; enabled: boolean; steps: string; }\n\nconst NEW_CHANNEL: ChannelForm = { id: null, key: '', kind: 'Slack', url: '', secretRef: '', recipients: '', ruleKeys: '', environments: '', minimumSeverity: 'Info', groupWindowSeconds: 0, firing: true, resolved: true, enabled: true };\nconst NEW_POLICY: PolicyForm = { editing: false, key: '', name: '', ruleKeys: '', environments: '', minimumSeverity: 'Critical', stopOnAcknowledge: true, enabled: true, steps: '' };\n\n/**\n * Where notifications go: channels with their failures and history, the private destinations they may reach, and who is told as an alert stays open. Changing any of these\n * needs the admin role. A channel's address and secret are never shown: only the name of where the secret is, to admins.\n */\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [DatePipe, FormsModule, MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatInputModule, MatProgressBarModule, MatSelectModule, ContextBar],\n template: `\n <jc-context-bar><h1>Notifications</h1></jc-context-bar>\n @if (data.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (data.error()) { <p class=\"jc-error\">{{ error() }}</p> }\n\n <section class=\"jc-card\">\n <h2>Channels <button mat-button type=\"button\" data-testid=\"channel-new\" (click)=\"channel.set(newChannel())\">New channel</button></h2>\n @if (channel(); as f) {\n <form class=\"jc-form\" data-testid=\"channel-form\" (ngSubmit)=\"saveChannel(f)\">\n @if (!f.id) {\n <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>\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-sm\"><mat-label>Kind</mat-label>\n <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>\n </mat-form-field>\n }\n @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> }\n @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> }\n @else {\n <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>\n <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>\n }\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Only these rules</mat-label><input matInput name=\"ruleKeys\" [(ngModel)]=\"f.ruleKeys\"></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Only these environments</mat-label><input matInput name=\"environments\" [(ngModel)]=\"f.environments\"></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-sm\"><mat-label>Severity at least</mat-label>\n <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>\n </mat-form-field>\n <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>\n <mat-checkbox name=\"firing\" [(ngModel)]=\"f.firing\">Firing</mat-checkbox>\n <mat-checkbox name=\"resolved\" [(ngModel)]=\"f.resolved\">Resolved</mat-checkbox>\n <mat-checkbox name=\"enabled\" [(ngModel)]=\"f.enabled\">Enabled</mat-checkbox>\n <div class=\"jc-form-actions\">\n <button mat-flat-button type=\"submit\" data-testid=\"channel-save\" [disabled]=\"saving()\">Save</button>\n <button mat-button type=\"button\" (click)=\"channel.set(null)\">Cancel</button>\n @if (channelError()) { <span class=\"jc-error\" data-testid=\"channel-error\">{{ channelError() }}</span> }\n </div>\n </form>\n }\n <table class=\"jc-table\" data-testid=\"channels\">\n <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>\n <tbody>\n @for (c of channels(); track c.id) {\n <tr [class.jc-muted]=\"!c.enabled\">\n <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>\n <td>{{ c.kind }}</td>\n <td>{{ c.destination }}</td>\n <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>\n <td [class.jc-bad]=\"c.recentFailures > 0\">{{ c.recentFailures }}</td>\n <td>{{ c.waiting }}</td>\n <td class=\"jc-row-actions\">\n <button mat-button type=\"button\" [attr.data-testid]=\"'channel-history-' + c.key\" (click)=\"history.set(history() === c.id ? null : c.id)\">History</button>\n @if (c.managedBy !== 'config') { <button mat-button type=\"button\" [attr.data-testid]=\"'channel-edit-' + c.key\" (click)=\"editChannel(c)\">Change</button> }\n </td>\n </tr>\n } @empty { <tr><td colspan=\"7\" class=\"jc-muted\">No channels: alerts fire, but nobody is told.</td></tr> }\n </tbody>\n </table>\n @if (history()) {\n <h3>Delivery history</h3>\n @if (deliveries.error()) { <p class=\"jc-error\">{{ deliveriesError() }}</p> }\n <table class=\"jc-table\" data-testid=\"channel-history\">\n <thead><tr><th>Created</th><th>Event</th><th>Status</th><th>Attempts</th><th>Last result</th></tr></thead>\n <tbody>\n @for (d of deliveries.value()?.items ?? []; track d.id) {\n <tr>\n <td class=\"jc-nowrap\">{{ d.createdAt | date: 'MMM d, HH:mm:ss' }}</td>\n <td>{{ d.event }}@if (d.escalationStep) { step {{ d.escalationStep }} }</td>\n <td [class.jc-bad]=\"d.status === 'Failed'\">{{ d.status }}</td>\n <td>{{ d.attempts }}</td>\n <td class=\"jc-small\">{{ d.lastError ?? (d.lastStatusCode ? 'HTTP ' + d.lastStatusCode : '') }}</td>\n </tr>\n } @empty { <tr><td colspan=\"5\" class=\"jc-muted\">Nothing sent yet.</td></tr> }\n </tbody>\n </table>\n }\n </section>\n\n <section class=\"jc-card\">\n <h2>Escalation <button mat-button type=\"button\" data-testid=\"policy-new\" (click)=\"policy.set(newPolicy())\">New policy</button></h2>\n @if (policy(); as f) {\n <form class=\"jc-form\" data-testid=\"policy-form\" (ngSubmit)=\"savePolicy(f)\">\n @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> }\n <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>\n <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 15: pager, lead-email\"></textarea></mat-form-field>\n <p class=\"jc-muted jc-small jc-form-actions\">Minutes after firing, then the channels to tell if the alert is still open.</p>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Only these rules</mat-label><input matInput name=\"ruleKeys\" [(ngModel)]=\"f.ruleKeys\"></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Only these environments</mat-label><input matInput name=\"environments\" [(ngModel)]=\"f.environments\"></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-sm\"><mat-label>Severity at least</mat-label>\n <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>\n </mat-form-field>\n <mat-checkbox name=\"stop\" [(ngModel)]=\"f.stopOnAcknowledge\">Stop when acknowledged</mat-checkbox>\n <mat-checkbox name=\"policyEnabled\" [(ngModel)]=\"f.enabled\">Enabled</mat-checkbox>\n <div class=\"jc-form-actions\">\n <button mat-flat-button type=\"submit\" data-testid=\"policy-save\" [disabled]=\"saving()\">Save</button>\n <button mat-button type=\"button\" (click)=\"policy.set(null)\">Cancel</button>\n @if (policyError()) { <span class=\"jc-error\" data-testid=\"policy-error\">{{ policyError() }}</span> }\n </div>\n </form>\n }\n <table class=\"jc-table\" data-testid=\"policies\">\n <thead><tr><th>Policy</th><th>Steps</th><th>Applies to</th><th>Stops when</th><th></th></tr></thead>\n <tbody>\n @for (p of policies(); track p.id) {\n <tr [class.jc-muted]=\"!p.enabled\">\n <td><strong>{{ p.name }}</strong><div class=\"jc-muted jc-small\">{{ p.key }}@if (!p.enabled) { · disabled }</div></td>\n <td class=\"jc-small\">@for (s of p.steps; track s.afterMinutes) { <div>{{ s.afterMinutes }} min: {{ s.endpointKeys.join(', ') }}</div> }</td>\n <td class=\"jc-small\">{{ p.minimumSeverity }} and above@if (p.environments.length) { · {{ p.environments.join(', ') }} }@if (p.ruleKeys.length) { · {{ p.ruleKeys.join(', ') }} }</td>\n <td class=\"jc-small\">{{ p.stopOnAcknowledge ? 'Acknowledged or resolved' : 'Resolved' }}</td>\n <td class=\"jc-row-actions\"><button mat-button type=\"button\" (click)=\"editPolicy(p)\">Change</button></td>\n </tr>\n } @empty { <tr><td colspan=\"5\" class=\"jc-muted\">No policies: a firing alert notifies once.</td></tr> }\n </tbody>\n </table>\n </section>\n\n <section class=\"jc-card\">\n <h2>Private destinations</h2>\n <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>\n <form class=\"jc-form\" (ngSubmit)=\"addAllowed()\">\n <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>\n <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>\n <button mat-stroked-button type=\"submit\" data-testid=\"allow-add\" [disabled]=\"saving()\">Allow</button>\n @if (allowError()) { <span class=\"jc-error\" data-testid=\"allow-error\">{{ allowError() }}</span> }\n </form>\n <table class=\"jc-table\" data-testid=\"allowlist\">\n <tbody>\n @for (a of allowed(); track a.destination) {\n <tr>\n <td><strong>{{ a.destination }}</strong> @if (a.managedBy === 'config') { <span class=\"jc-muted jc-small\">from the webhooks file</span> }</td>\n <td>{{ a.ports.length ? a.ports.join(', ') : '443' }}</td>\n <td class=\"jc-row-actions\">@if (a.managedBy !== 'config') { <button mat-button type=\"button\" (click)=\"removeAllowed(a.destination)\">Remove</button> }</td>\n </tr>\n } @empty { <tr><td class=\"jc-muted\">None: channels reach public addresses only.</td></tr> }\n </tbody>\n </table>\n </section>`,\n})\nexport class NotificationsPage {\n private readonly api = inject(ApiService);\n protected readonly channel = signal<ChannelForm | null>(null);\n protected readonly policy = signal<PolicyForm | null>(null);\n protected readonly history = signal<string | null>(null);\n protected readonly saving = signal(false);\n protected readonly channelError = signal('');\n protected readonly policyError = signal('');\n protected readonly allowError = signal('');\n protected destination = '';\n protected ports = '';\n\n protected readonly data = apiResource({\n stream: () => forkJoin({\n channels: this.api.channels().pipe(map(p => p.items)),\n policies: this.api.escalationPolicies().pipe(map(p => p.items)),\n allowlist: this.api.allowlist().pipe(map(p => p.items)),\n }),\n });\n protected readonly error = computed(() => problemText(this.data.error()));\n protected readonly channels = computed(() => this.data.value()?.channels ?? []);\n protected readonly policies = computed(() => this.data.value()?.policies ?? []);\n protected readonly allowed = computed(() => this.data.value()?.allowlist ?? []);\n protected readonly deliveries = apiResource({ params: () => this.history() ?? undefined, stream: ({ params }) => this.api.deliveries(params) });\n protected readonly deliveriesError = computed(() => problemText(this.deliveries.error()));\n\n protected newChannel(): ChannelForm { this.channelError.set(''); return { ...NEW_CHANNEL }; }\n protected newPolicy(): PolicyForm { this.policyError.set(''); return { ...NEW_POLICY }; }\n\n protected editChannel(c: Channel) {\n this.channelError.set('');\n this.channel.set({\n id: c.id, key: c.key, kind: c.kind, url: '', secretRef: c.secretRef ?? '', recipients: (c.recipients ?? []).join(', '), ruleKeys: c.ruleKeys.join(', '), environments: c.environments.join(', '),\n minimumSeverity: c.minimumSeverity, groupWindowSeconds: c.groupWindowSeconds, firing: c.events.includes('alert.firing'), resolved: c.events.includes('alert.resolved'), enabled: c.enabled,\n });\n }\n\n private run<T>(request: Observable<T>, error: (message: string) => void, done: () => void) {\n this.saving.set(true);\n error('');\n request.subscribe({\n next: () => { this.saving.set(false); done(); this.data.reload(); },\n error: e => { this.saving.set(false); error((e as { status?: number }).status === 403 ? 'This needs the admin role.' : problemText(e)); },\n });\n }\n\n protected saveChannel(f: ChannelForm) {\n const events = [f.firing ? 'alert.firing' : '', f.resolved ? 'alert.resolved' : ''].filter(Boolean);\n const body = {\n key: f.key, kind: f.kind, url: f.kind === 'Webhook' && f.url.trim() ? f.url.trim() : undefined, secretRef: f.kind === 'Email' ? undefined : f.secretRef.trim(), recipients: f.kind === 'Email' ? list(f.recipients) : undefined,\n events, ruleKeys: list(f.ruleKeys), environments: list(f.environments).map(e => e.toLowerCase()), minimumSeverity: f.minimumSeverity, groupWindowSeconds: f.groupWindowSeconds || 0, enabled: f.enabled,\n };\n this.run(f.id ? this.api.updateChannel(f.id, body) : this.api.createChannel(body), m => this.channelError.set(m), () => this.channel.set(null));\n }\n\n protected editPolicy(p: EscalationPolicy) {\n this.policyError.set('');\n this.policy.set({\n editing: true, key: p.key, name: p.name, ruleKeys: p.ruleKeys.join(', '), environments: p.environments.join(', '), minimumSeverity: p.minimumSeverity, stopOnAcknowledge: p.stopOnAcknowledge, enabled: p.enabled,\n steps: p.steps.map(s => `${s.afterMinutes}: ${s.endpointKeys.join(', ')}`).join('\\n'),\n });\n }\n\n /** `5: oncall, pager` per line. A line that doesn't read goes to the server as no channels, which says what's wrong. */\n private parseSteps(text: string): EscalationStep[] {\n return text.split('\\n').map(l => l.trim()).filter(Boolean).map(line => {\n const [minutes, ...rest] = line.split(':');\n return { afterMinutes: Number(minutes.trim()), endpointKeys: list(rest.join(':')) };\n });\n }\n\n protected savePolicy(f: PolicyForm) {\n const body = {\n key: f.key, name: f.name, enabled: f.enabled, ruleKeys: list(f.ruleKeys), environments: list(f.environments).map(e => e.toLowerCase()), minimumSeverity: f.minimumSeverity,\n stopOnAcknowledge: f.stopOnAcknowledge, steps: this.parseSteps(f.steps),\n };\n this.run(f.editing ? this.api.updateEscalationPolicy(f.key, body) : this.api.createEscalationPolicy(body), m => this.policyError.set(m), () => this.policy.set(null));\n }\n\n protected addAllowed() {\n this.run(this.api.putAllowlist(this.destination.trim(), list(this.ports).map(Number)), m => this.allowError.set(m), () => { this.destination = ''; this.ports = ''; });\n }\n\n protected removeAllowed(destination: string) {\n this.run(this.api.removeAllowlist(destination), m => this.allowError.set(m), () => undefined);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAgBA,MAAM,QAAQ,SAAiB,KAAK,MAAM,GAAG,CAAC,CAAC,KAAI,MAAK,EAAE,KAAI,CAAE,CAAC,CAAC,OAAO,OAAO;AAQhF,MAAM,cAA2B;CAAE,IAAI;CAAM,KAAK;CAAI,MAAM;CAAS,KAAK;CAAI,WAAW;CAAI,YAAY;CAAI,UAAU;CAAI,cAAc;CAAI,iBAAiB;CAAQ,oBAAoB;CAAG,QAAQ;CAAM,UAAU;CAAM,SAAS;AAAI;AACxO,MAAM,aAAyB;CAAE,SAAS;CAAO,KAAK;CAAI,MAAM;CAAI,UAAU;CAAI,cAAc;CAAI,iBAAiB;CAAY,mBAAmB;CAAM,SAAS;CAAM,OAAO;AAAE;AAiJlL,IAAa,oBAAb,MAAa,kBAAiB;CACX,MAAM,OAAO,UAAU;CACrB,UAAU,OAA2B,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAC1C,SAAS,OAA0B,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CACxC,UAAU,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CACrC,SAAS,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CACtB,eAAe,OAAO,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CACzB,cAAc,OAAO,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CACxB,aAAa,OAAO,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAChC,cAAc;CACd,QAAQ;CAEC,OAAO,YAAY,EACpC,cAAc,SAAS;EACrB,UAAU,KAAK,IAAI,SAAQ,CAAE,CAAC,KAAK,KAAI,MAAK,EAAE,KAAK,CAAC;EACpD,UAAU,KAAK,IAAI,mBAAkB,CAAE,CAAC,KAAK,KAAI,MAAK,EAAE,KAAK,CAAC;EAC9D,WAAW,KAAK,IAAI,UAAS,CAAE,CAAC,KAAK,KAAI,MAAK,EAAE,KAAK,CAAC;CACvD,CAAA,EACF,CAAA;CACkB,QAAQ,eAAe,YAAY,KAAK,KAAK,MAAK,CAAE,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CACtD,WAAW,eAAe,KAAK,KAAK,MAAK,CAAE,EAAE,YAAY,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAC5D,WAAW,eAAe,KAAK,KAAK,MAAK,CAAE,EAAE,YAAY,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAC5D,UAAU,eAAe,KAAK,KAAK,MAAK,CAAE,EAAE,aAAa,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAC5D,aAAa,YAAY;EAAE,cAAc,KAAK,QAAO,KAAM;EAAW,SAAS,EAAE,aAAa,KAAK,IAAI,WAAW,MAAM;CAAC,CAAE;CAC3H,kBAAkB,eAAe,YAAY,KAAK,WAAW,MAAK,CAAE,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAE/E,aAAU;EAAkB,KAAK,aAAa,IAAI,EAAE;EAAG,OAAO,EAAE,GAAG,YAAW;CAAI;CAClF,YAAS;EAAiB,KAAK,YAAY,IAAI,EAAE;EAAG,OAAO,EAAE,GAAG,WAAU;CAAI;CAE9E,YAAY,GAAU;EAC9B,KAAK,aAAa,IAAI,EAAE;EACxB,KAAK,QAAQ,IAAI;GACf,IAAI,EAAE;GAAI,KAAK,EAAE;GAAK,MAAM,EAAE;GAAM,KAAK;GAAI,WAAW,EAAE,aAAa;GAAI,aAAa,EAAE,cAAc,CAAA,EAAA,CAAI,KAAK,IAAI;GAAG,UAAU,EAAE,SAAS,KAAK,IAAI;GAAG,cAAc,EAAE,aAAa,KAAK,IAAI;GAC/L,iBAAiB,EAAE;GAAiB,oBAAoB,EAAE;GAAoB,QAAQ,EAAE,OAAO,SAAS,cAAc;GAAG,UAAU,EAAE,OAAO,SAAS,gBAAgB;GAAG,SAAS,EAAE;EACpL,CAAA;CACH;CAEQ,IAAO,SAAwB,OAAkC,MAAgB;EACvF,KAAK,OAAO,IAAI,IAAI;EACpB,MAAM,EAAE;EACR,QAAQ,UAAU;GAChB,YAAW;IAAG,KAAK,OAAO,IAAI,KAAK;IAAG,KAAI;IAAI,KAAK,KAAK,OAAM;GAAI;GAClE,QAAO,MAAI;IAAG,KAAK,OAAO,IAAI,KAAK;IAAG,MAAO,EAA0B,WAAW,MAAM,+BAA+B,YAAY,CAAC,CAAC;GAAG;EACzI,CAAA;CACH;CAEU,YAAY,GAAc;EAClC,MAAM,SAAS,CAAC,EAAE,SAAS,iBAAiB,IAAI,EAAE,WAAW,mBAAmB,EAAE,CAAC,CAAC,OAAO,OAAO;EAClG,MAAM,OAAO;GACX,KAAK,EAAE;GAAK,MAAM,EAAE;GAAM,KAAK,EAAE,SAAS,aAAa,EAAE,IAAI,KAAI,IAAK,EAAE,IAAI,KAAI,IAAK;GAAW,WAAW,EAAE,SAAS,UAAU,SAAY,EAAE,UAAU,KAAI;GAAI,YAAY,EAAE,SAAS,UAAU,KAAK,EAAE,UAAU,IAAI;GACtN;GAAQ,UAAU,KAAK,EAAE,QAAQ;GAAG,cAAc,KAAK,EAAE,YAAY,CAAC,CAAC,KAAI,MAAK,EAAE,YAAW,CAAE;GAAG,iBAAiB,EAAE;GAAiB,oBAAoB,EAAE,sBAAsB;GAAG,SAAS,EAAE;EACjM;EACD,KAAK,IAAI,EAAE,KAAK,KAAK,IAAI,cAAc,EAAE,IAAI,IAAI,IAAI,KAAK,IAAI,cAAc,IAAI,IAAG,MAAK,KAAK,aAAa,IAAI,CAAC,SAAS,KAAK,QAAQ,IAAI,IAAI,CAAC;CAChJ;CAEU,WAAW,GAAmB;EACtC,KAAK,YAAY,IAAI,EAAE;EACvB,KAAK,OAAO,IAAI;GACd,SAAS;GAAM,KAAK,EAAE;GAAK,MAAM,EAAE;GAAM,UAAU,EAAE,SAAS,KAAK,IAAI;GAAG,cAAc,EAAE,aAAa,KAAK,IAAI;GAAG,iBAAiB,EAAE;GAAiB,mBAAmB,EAAE;GAAmB,SAAS,EAAE;GAC1M,OAAO,EAAE,MAAM,KAAI,MAAK,GAAG,EAAE,aAAY,IAAK,EAAE,aAAa,KAAK,IAAI,GAAG,CAAC,CAAC,KAAK,IAAI;EACrF,CAAA;CACH;CAGQ,WAAW,MAAY;EAC7B,OAAO,KAAK,MAAM,IAAI,CAAC,CAAC,KAAI,MAAK,EAAE,KAAI,CAAE,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAI,SAAO;GACpE,MAAM,CAAC,SAAS,GAAG,QAAQ,KAAK,MAAM,GAAG;GACzC,OAAO;IAAE,cAAc,OAAO,QAAQ,KAAI,CAAE;IAAG,cAAc,KAAK,KAAK,KAAK,GAAG,CAAC;GAAC;EACnF,CAAC;CACH;CAEU,WAAW,GAAa;EAChC,MAAM,OAAO;GACX,KAAK,EAAE;GAAK,MAAM,EAAE;GAAM,SAAS,EAAE;GAAS,UAAU,KAAK,EAAE,QAAQ;GAAG,cAAc,KAAK,EAAE,YAAY,CAAC,CAAC,KAAI,MAAK,EAAE,YAAW,CAAE;GAAG,iBAAiB,EAAE;GAC3J,mBAAmB,EAAE;GAAmB,OAAO,KAAK,WAAW,EAAE,KAAK;EACvE;EACD,KAAK,IAAI,EAAE,UAAU,KAAK,IAAI,uBAAuB,EAAE,KAAK,IAAI,IAAI,KAAK,IAAI,uBAAuB,IAAI,IAAG,MAAK,KAAK,YAAY,IAAI,CAAC,SAAS,KAAK,OAAO,IAAI,IAAI,CAAC;CACtK;CAEU,aAAU;EAClB,KAAK,IAAI,KAAK,IAAI,aAAa,KAAK,YAAY,KAAI,GAAI,KAAK,KAAK,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC,IAAG,MAAK,KAAK,WAAW,IAAI,CAAC,SAAQ;GAAG,KAAK,cAAc;GAAI,KAAK,QAAQ;EAAI,CAAC;CACvK;CAEU,cAAc,aAAmB;EACzC,KAAK,IAAI,KAAK,IAAI,gBAAgB,WAAW,IAAG,MAAK,KAAK,WAAW,IAAI,CAAC,SAAS,MAAS;CAC9F;CArFW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAiB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAjB,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAiB,cAAA;EAAA,UAAA;EAAA,UAAA;EAAA,UAxIlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsIG,UAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MAvIO;GAAW;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA,CAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA,CAAA,KAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA,CAAA,KAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,eAAA;IAAA,UAAA,CAAA,SAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA,CAAA,eAAA;IAAA,SAAA,CAAA,UAAA;IAAA,UAAA,CAAA,QAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAe;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA,CAAA,WAAA;IAAA,UAAA,CAAA,aAAA,WAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAiB;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,UAAA,qBAAA;IAAA,UAAA,CAAA,aAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAkB;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,UAAA,CAAA,cAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAc;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,UAAA,CAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAoB;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,cAAA;IAAA,UAAA,CAAA,gBAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAe;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,UAAA,CAAA,WAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,mBAAA;IAAA,UAAA,CAAA,WAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAU,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAhJ;IAAQ,MAAA;GAAA;EAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAyIP,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAiB,YAAA,CAAA;EA3I7B,MAAA;EAAU,MAAA,CAAA;GACT,iBAAiB,wBAAwB;GACzC,SAAS;IAAC;IAAU;IAAa;IAAiB;IAAmB;IAAoB;IAAgB;IAAsB;IAAiB;GAAU;GAC1J,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuIX,CAAA"}
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import { ApiService, ContextService, LinkPipe } from "./juice-js-observability.mjs";
|
|
2
|
+
import { S as apiResource, h as AgoPipe, 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 { RouterLink } from "@angular/router";
|
|
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 TEMPLATES = {
|
|
21
|
+
ErrorRate: "Ratio of failed requests, 0 to 1. Fires above the threshold.",
|
|
22
|
+
LatencyP95: "Milliseconds at the 95th percentile. Fires above the threshold.",
|
|
23
|
+
ThroughputAbsent: "Requests in the window, for a service that had traffic. Fires at or below the threshold.",
|
|
24
|
+
DeliveryFailureRate: "Ratio of failed deliveries, 0 to 1. Fires above the threshold.",
|
|
25
|
+
HostMemory: "Ratio of host memory used, 0 to 1. Fires above the threshold.",
|
|
26
|
+
MediatorErrors: "Mediator errors in the window. Fires above the threshold.",
|
|
27
|
+
ConsumerFailures: "Failed consumer messages in the window. Fires above the threshold."
|
|
28
|
+
};
|
|
29
|
+
function seconds(value) {
|
|
30
|
+
const match = /^(?:(\d+)\.)?(\d+):(\d+):(\d+)/.exec(String(value ?? ""));
|
|
31
|
+
return match ? +(match[1] ?? 0) * 86400 + +match[2] * 3600 + +match[3] * 60 + +match[4] : 0;
|
|
32
|
+
}
|
|
33
|
+
function span(total) {
|
|
34
|
+
const s = Math.max(0, Math.round(total || 0));
|
|
35
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
36
|
+
const days = Math.floor(s / 86400);
|
|
37
|
+
return `${days ? `${days}.` : ""}${pad(Math.floor(s / 3600) % 24)}:${pad(Math.floor(s / 60) % 60)}:${pad(s % 60)}`;
|
|
38
|
+
}
|
|
39
|
+
const NEW_RULE = {
|
|
40
|
+
key: "",
|
|
41
|
+
name: "",
|
|
42
|
+
template: "ErrorRate",
|
|
43
|
+
severity: "Warning",
|
|
44
|
+
service: "",
|
|
45
|
+
environment: "",
|
|
46
|
+
host: "",
|
|
47
|
+
threshold: null,
|
|
48
|
+
recoveryThreshold: null,
|
|
49
|
+
minimumSamples: 10,
|
|
50
|
+
scheduleSeconds: 60,
|
|
51
|
+
windowSeconds: 300,
|
|
52
|
+
forSeconds: 120,
|
|
53
|
+
recoverySeconds: 120,
|
|
54
|
+
cooldownSeconds: 600,
|
|
55
|
+
enabled: true
|
|
56
|
+
};
|
|
57
|
+
var RulesPage = class RulesPage {
|
|
58
|
+
ctx = inject(ContextService);
|
|
59
|
+
api = inject(ApiService);
|
|
60
|
+
templates = Object.keys(TEMPLATES);
|
|
61
|
+
form = signal(null, ...ngDevMode ? [{ debugName: "form" }] : /* istanbul ignore next */ []);
|
|
62
|
+
editing = signal(null, ...ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []);
|
|
63
|
+
saving = signal(false, ...ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []);
|
|
64
|
+
saveError = signal("", ...ngDevMode ? [{ debugName: "saveError" }] : /* istanbul ignore next */ []);
|
|
65
|
+
data = apiResource({ stream: () => forkJoin({
|
|
66
|
+
rules: this.api.rules().pipe(map((p) => p.items)),
|
|
67
|
+
firing: this.api.allAlerts({ state: "firing" }).pipe(map((p) => p.items)),
|
|
68
|
+
pending: this.api.allAlerts({ state: "pending" }).pipe(map((p) => p.items))
|
|
69
|
+
}) });
|
|
70
|
+
error = computed(() => problemText(this.data.error()), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
|
|
71
|
+
rows = computed(() => {
|
|
72
|
+
const d = this.data.value();
|
|
73
|
+
if (!d) return [];
|
|
74
|
+
const open = [...d.firing, ...d.pending];
|
|
75
|
+
return d.rules.map((r) => {
|
|
76
|
+
const mine = open.filter((a) => a.rule === r.key);
|
|
77
|
+
const last = mine.map((a) => a.lastEvaluatedAt).sort().at(-1) ?? null;
|
|
78
|
+
return {
|
|
79
|
+
...r,
|
|
80
|
+
open: mine,
|
|
81
|
+
lastEvaluatedAt: last
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}, ...ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []);
|
|
85
|
+
hint(template) {
|
|
86
|
+
return TEMPLATES[template] ?? "";
|
|
87
|
+
}
|
|
88
|
+
startNew() {
|
|
89
|
+
this.editing.set(null);
|
|
90
|
+
this.saveError.set("");
|
|
91
|
+
this.form.set({ ...NEW_RULE });
|
|
92
|
+
}
|
|
93
|
+
edit(rule) {
|
|
94
|
+
const d = rule.definition;
|
|
95
|
+
this.editing.set(rule);
|
|
96
|
+
this.saveError.set("");
|
|
97
|
+
this.form.set({
|
|
98
|
+
key: rule.key,
|
|
99
|
+
name: rule.name,
|
|
100
|
+
template: rule.template,
|
|
101
|
+
severity: rule.severity,
|
|
102
|
+
service: String(d["service"] ?? ""),
|
|
103
|
+
environment: String(d["environment"] ?? ""),
|
|
104
|
+
host: String(d["host"] ?? ""),
|
|
105
|
+
threshold: d["threshold"],
|
|
106
|
+
recoveryThreshold: d["recoveryThreshold"] ?? null,
|
|
107
|
+
minimumSamples: d["minimumSamples"],
|
|
108
|
+
scheduleSeconds: seconds(d["schedule"]),
|
|
109
|
+
windowSeconds: seconds(d["window"]),
|
|
110
|
+
forSeconds: seconds(d["forDuration"]),
|
|
111
|
+
recoverySeconds: seconds(d["recoveryDuration"]),
|
|
112
|
+
cooldownSeconds: seconds(d["cooldown"]),
|
|
113
|
+
enabled: rule.enabled
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
save(f) {
|
|
117
|
+
const host = f.template === "HostMemory";
|
|
118
|
+
const body = {
|
|
119
|
+
key: f.key,
|
|
120
|
+
name: f.name,
|
|
121
|
+
template: f.template,
|
|
122
|
+
severity: f.severity,
|
|
123
|
+
service: host ? "" : f.service.trim(),
|
|
124
|
+
environment: host ? "" : f.environment.trim().toLowerCase(),
|
|
125
|
+
host: host ? f.host.trim() : "",
|
|
126
|
+
threshold: f.threshold,
|
|
127
|
+
recoveryThreshold: f.recoveryThreshold,
|
|
128
|
+
minimumSamples: f.minimumSamples,
|
|
129
|
+
schedule: span(f.scheduleSeconds),
|
|
130
|
+
window: span(f.windowSeconds),
|
|
131
|
+
forDuration: span(f.forSeconds),
|
|
132
|
+
recoveryDuration: span(f.recoverySeconds),
|
|
133
|
+
cooldown: span(f.cooldownSeconds),
|
|
134
|
+
enabled: f.enabled
|
|
135
|
+
};
|
|
136
|
+
const editing = this.editing();
|
|
137
|
+
const request = editing ? this.api.updateRule(editing.key, body, editing.etag) : this.api.createRule(body);
|
|
138
|
+
this.saving.set(true);
|
|
139
|
+
this.saveError.set("");
|
|
140
|
+
request.subscribe({
|
|
141
|
+
next: () => {
|
|
142
|
+
this.saving.set(false);
|
|
143
|
+
this.form.set(null);
|
|
144
|
+
this.data.reload();
|
|
145
|
+
},
|
|
146
|
+
error: (e) => {
|
|
147
|
+
this.saving.set(false);
|
|
148
|
+
this.saveError.set(problemText(e));
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
153
|
+
minVersion: "12.0.0",
|
|
154
|
+
version: "22.2.4",
|
|
155
|
+
ngImport: i0,
|
|
156
|
+
type: RulesPage,
|
|
157
|
+
deps: [],
|
|
158
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
159
|
+
});
|
|
160
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
161
|
+
minVersion: "17.0.0",
|
|
162
|
+
version: "22.2.4",
|
|
163
|
+
type: RulesPage,
|
|
164
|
+
isStandalone: true,
|
|
165
|
+
selector: "ng-component",
|
|
166
|
+
ngImport: i0,
|
|
167
|
+
template: `
|
|
168
|
+
<jc-context-bar>
|
|
169
|
+
<h1>Rules</h1>
|
|
170
|
+
<span class="jc-spacer"></span>
|
|
171
|
+
<button mat-flat-button type="button" data-testid="rule-new" (click)="startNew()">New rule</button>
|
|
172
|
+
</jc-context-bar>
|
|
173
|
+
@if (data.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
|
|
174
|
+
@if (data.error()) { <p class="jc-error">{{ error() }}</p> }
|
|
175
|
+
@if (form(); as f) {
|
|
176
|
+
<section class="jc-card" data-testid="rule-form">
|
|
177
|
+
<h2>{{ editing() ? 'Change ' + editing()!.key : 'New rule' }}</h2>
|
|
178
|
+
<form class="jc-form" (ngSubmit)="save(f)">
|
|
179
|
+
@if (!editing()) { <mat-form-field subscriptSizing="dynamic"><mat-label>Key</mat-label><input matInput name="key" [(ngModel)]="f.key" required></mat-form-field> }
|
|
180
|
+
<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>
|
|
181
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Template</mat-label>
|
|
182
|
+
<mat-select name="template" [(ngModel)]="f.template">@for (t of templates; track t) { <mat-option [value]="t">{{ t }}</mat-option> }</mat-select>
|
|
183
|
+
</mat-form-field>
|
|
184
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Severity</mat-label>
|
|
185
|
+
<mat-select name="severity" [(ngModel)]="f.severity"><mat-option value="Info">Info</mat-option><mat-option value="Warning">Warning</mat-option><mat-option value="Critical">Critical</mat-option></mat-select>
|
|
186
|
+
</mat-form-field>
|
|
187
|
+
@if (f.template === 'HostMemory') {
|
|
188
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Host (all if empty)</mat-label><input matInput name="host" [(ngModel)]="f.host"></mat-form-field>
|
|
189
|
+
} @else {
|
|
190
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Service (all if empty)</mat-label><input matInput name="service" [(ngModel)]="f.service"></mat-form-field>
|
|
191
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Environment (all if empty)</mat-label><input matInput name="environment" [(ngModel)]="f.environment"></mat-form-field>
|
|
192
|
+
}
|
|
193
|
+
<p class="jc-muted jc-small jc-form-actions">{{ hint(f.template) }}</p>
|
|
194
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Threshold</mat-label><input matInput type="number" step="any" name="threshold" [(ngModel)]="f.threshold" required></mat-form-field>
|
|
195
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Recovery threshold</mat-label><input matInput type="number" step="any" name="recoveryThreshold" [(ngModel)]="f.recoveryThreshold"></mat-form-field>
|
|
196
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Minimum samples</mat-label><input matInput type="number" name="minimumSamples" [(ngModel)]="f.minimumSamples"></mat-form-field>
|
|
197
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Every (s)</mat-label><input matInput type="number" name="schedule" [(ngModel)]="f.scheduleSeconds"></mat-form-field>
|
|
198
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Window (s)</mat-label><input matInput type="number" name="window" [(ngModel)]="f.windowSeconds"></mat-form-field>
|
|
199
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Fire after (s)</mat-label><input matInput type="number" name="for" [(ngModel)]="f.forSeconds"></mat-form-field>
|
|
200
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Recover after (s)</mat-label><input matInput type="number" name="recovery" [(ngModel)]="f.recoverySeconds"></mat-form-field>
|
|
201
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Cooldown (s)</mat-label><input matInput type="number" name="cooldown" [(ngModel)]="f.cooldownSeconds"></mat-form-field>
|
|
202
|
+
<mat-checkbox name="enabled" [(ngModel)]="f.enabled">Enabled</mat-checkbox>
|
|
203
|
+
@if (editing()) { <p class="jc-muted jc-small jc-form-actions">Disabling this rule, or changing its template or scope, resolves its open alerts: nothing would be watching them any more.</p> }
|
|
204
|
+
<div class="jc-form-actions">
|
|
205
|
+
<button mat-flat-button type="submit" data-testid="rule-save" [disabled]="saving()">Save</button>
|
|
206
|
+
<button mat-button type="button" (click)="form.set(null)">Cancel</button>
|
|
207
|
+
@if (saveError()) { <span class="jc-error" data-testid="rule-error">{{ saveError() }}</span> }
|
|
208
|
+
</div>
|
|
209
|
+
</form>
|
|
210
|
+
</section>
|
|
211
|
+
}
|
|
212
|
+
<table class="jc-table" data-testid="rules">
|
|
213
|
+
<thead><tr><th>Rule</th><th>Template</th><th>Severity</th><th>Threshold</th><th>Recovery</th><th>Wait</th><th>Open alerts</th><th>Last evaluation</th><th></th></tr></thead>
|
|
214
|
+
<tbody>
|
|
215
|
+
@for (r of rows(); track r.key) {
|
|
216
|
+
<tr [class.jc-muted]="!r.enabled">
|
|
217
|
+
<td><strong>{{ r.name }}</strong><div class="jc-muted jc-small">{{ r.key }}@if (!r.enabled) { · disabled }@if (r.managedBy === 'config') { · from the rules file }</div></td>
|
|
218
|
+
<td>{{ r.template }}</td>
|
|
219
|
+
<td>{{ r.severity }}</td>
|
|
220
|
+
<td>{{ r.definition['threshold'] ?? '—' }}</td>
|
|
221
|
+
<td>{{ r.definition['recoveryThreshold'] ?? '—' }}</td>
|
|
222
|
+
<td>{{ r.definition['forDuration'] ?? '—' }}</td>
|
|
223
|
+
<td>
|
|
224
|
+
@for (a of r.open; track a.id) {
|
|
225
|
+
<div><a [routerLink]="['alerts', a.id] | jcLink" [queryParams]="ctx.keep()">{{ a.state }}</a> {{ a.subjectKey }} = {{ a.lastValue ?? 'no data' }}</div>
|
|
226
|
+
} @empty { <span class="jc-muted">none</span> }
|
|
227
|
+
</td>
|
|
228
|
+
<td>{{ r.lastEvaluatedAt | ago }}</td>
|
|
229
|
+
<td class="jc-row-actions">@if (r.managedBy !== 'config') { <button mat-button type="button" [attr.data-testid]="'rule-edit-' + r.key" (click)="edit(r)">Change</button> }</td>
|
|
230
|
+
</tr>
|
|
231
|
+
}
|
|
232
|
+
</tbody>
|
|
233
|
+
</table>`,
|
|
234
|
+
isInline: true,
|
|
235
|
+
dependencies: [
|
|
236
|
+
{
|
|
237
|
+
kind: "directive",
|
|
238
|
+
type: RouterLink,
|
|
239
|
+
selector: "[routerLink]",
|
|
240
|
+
inputs: [
|
|
241
|
+
"target",
|
|
242
|
+
"queryParams",
|
|
243
|
+
"fragment",
|
|
244
|
+
"queryParamsHandling",
|
|
245
|
+
"state",
|
|
246
|
+
"info",
|
|
247
|
+
"relativeTo",
|
|
248
|
+
"preserveFragment",
|
|
249
|
+
"skipLocationChange",
|
|
250
|
+
"replaceUrl",
|
|
251
|
+
"browserUrl",
|
|
252
|
+
"routerLink"
|
|
253
|
+
]
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
kind: "ngmodule",
|
|
257
|
+
type: FormsModule
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
kind: "directive",
|
|
261
|
+
type: i1.ɵNgNoValidate,
|
|
262
|
+
selector: "form:not([ngNoForm]):not([ngNativeValidate])"
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
kind: "directive",
|
|
266
|
+
type: i1.DefaultValueAccessor,
|
|
267
|
+
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]"
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
kind: "directive",
|
|
271
|
+
type: i1.NumberValueAccessor,
|
|
272
|
+
selector: "input[type=number]:not([ngNoCva])[formControlName],input[type=number]:not([ngNoCva])[formControl],input[type=number]:not([ngNoCva])[ngModel]"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
kind: "directive",
|
|
276
|
+
type: i1.NgControlStatus,
|
|
277
|
+
selector: "[formControlName],[ngModel],[formControl]"
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
kind: "directive",
|
|
281
|
+
type: i1.NgControlStatusGroup,
|
|
282
|
+
selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]"
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
kind: "directive",
|
|
286
|
+
type: i1.RequiredValidator,
|
|
287
|
+
selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]",
|
|
288
|
+
inputs: ["required"]
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
kind: "directive",
|
|
292
|
+
type: i1.NgModel,
|
|
293
|
+
selector: "[ngModel]:not([formControlName]):not([formControl])",
|
|
294
|
+
inputs: [
|
|
295
|
+
"name",
|
|
296
|
+
"disabled",
|
|
297
|
+
"ngModel",
|
|
298
|
+
"ngModelOptions"
|
|
299
|
+
],
|
|
300
|
+
outputs: ["ngModelChange"],
|
|
301
|
+
exportAs: ["ngModel"]
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
kind: "directive",
|
|
305
|
+
type: i1.NgForm,
|
|
306
|
+
selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]",
|
|
307
|
+
inputs: ["ngFormOptions"],
|
|
308
|
+
outputs: ["ngSubmit"],
|
|
309
|
+
exportAs: ["ngForm"]
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
kind: "ngmodule",
|
|
313
|
+
type: MatButtonModule
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
kind: "component",
|
|
317
|
+
type: i2.MatButton,
|
|
318
|
+
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] ",
|
|
319
|
+
inputs: ["matButton"],
|
|
320
|
+
exportAs: ["matButton", "matAnchor"]
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
kind: "ngmodule",
|
|
324
|
+
type: MatCheckboxModule
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
kind: "component",
|
|
328
|
+
type: i3.MatCheckbox,
|
|
329
|
+
selector: "mat-checkbox",
|
|
330
|
+
inputs: [
|
|
331
|
+
"aria-label",
|
|
332
|
+
"aria-labelledby",
|
|
333
|
+
"aria-describedby",
|
|
334
|
+
"aria-expanded",
|
|
335
|
+
"aria-controls",
|
|
336
|
+
"aria-owns",
|
|
337
|
+
"id",
|
|
338
|
+
"required",
|
|
339
|
+
"labelPosition",
|
|
340
|
+
"name",
|
|
341
|
+
"value",
|
|
342
|
+
"disableRipple",
|
|
343
|
+
"tabIndex",
|
|
344
|
+
"color",
|
|
345
|
+
"disabledInteractive",
|
|
346
|
+
"checked",
|
|
347
|
+
"disabled",
|
|
348
|
+
"indeterminate"
|
|
349
|
+
],
|
|
350
|
+
outputs: ["change", "indeterminateChange"],
|
|
351
|
+
exportAs: ["matCheckbox"]
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
kind: "ngmodule",
|
|
355
|
+
type: MatFormFieldModule
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
kind: "component",
|
|
359
|
+
type: i4.MatFormField,
|
|
360
|
+
selector: "mat-form-field",
|
|
361
|
+
inputs: [
|
|
362
|
+
"hideRequiredMarker",
|
|
363
|
+
"color",
|
|
364
|
+
"floatLabel",
|
|
365
|
+
"appearance",
|
|
366
|
+
"subscriptSizing",
|
|
367
|
+
"hintLabel"
|
|
368
|
+
],
|
|
369
|
+
exportAs: ["matFormField"]
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
kind: "directive",
|
|
373
|
+
type: i4.MatLabel,
|
|
374
|
+
selector: "mat-label"
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
kind: "ngmodule",
|
|
378
|
+
type: MatInputModule
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
kind: "directive",
|
|
382
|
+
type: i5.MatInput,
|
|
383
|
+
selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",
|
|
384
|
+
inputs: [
|
|
385
|
+
"disabled",
|
|
386
|
+
"id",
|
|
387
|
+
"placeholder",
|
|
388
|
+
"name",
|
|
389
|
+
"required",
|
|
390
|
+
"type",
|
|
391
|
+
"errorStateMatcher",
|
|
392
|
+
"aria-describedby",
|
|
393
|
+
"value",
|
|
394
|
+
"readonly",
|
|
395
|
+
"disabledInteractive"
|
|
396
|
+
],
|
|
397
|
+
exportAs: ["matInput"]
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
kind: "ngmodule",
|
|
401
|
+
type: MatProgressBarModule
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
kind: "component",
|
|
405
|
+
type: i6.MatProgressBar,
|
|
406
|
+
selector: "mat-progress-bar",
|
|
407
|
+
inputs: [
|
|
408
|
+
"color",
|
|
409
|
+
"value",
|
|
410
|
+
"bufferValue",
|
|
411
|
+
"mode"
|
|
412
|
+
],
|
|
413
|
+
outputs: ["animationEnd"],
|
|
414
|
+
exportAs: ["matProgressBar"]
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
kind: "ngmodule",
|
|
418
|
+
type: MatSelectModule
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
kind: "component",
|
|
422
|
+
type: i4.MatSelect,
|
|
423
|
+
selector: "mat-select",
|
|
424
|
+
inputs: [
|
|
425
|
+
"aria-describedby",
|
|
426
|
+
"panelClass",
|
|
427
|
+
"disabled",
|
|
428
|
+
"disableRipple",
|
|
429
|
+
"tabIndex",
|
|
430
|
+
"hideSingleSelectionIndicator",
|
|
431
|
+
"placeholder",
|
|
432
|
+
"required",
|
|
433
|
+
"multiple",
|
|
434
|
+
"disableOptionCentering",
|
|
435
|
+
"compareWith",
|
|
436
|
+
"value",
|
|
437
|
+
"aria-label",
|
|
438
|
+
"aria-labelledby",
|
|
439
|
+
"errorStateMatcher",
|
|
440
|
+
"typeaheadDebounceInterval",
|
|
441
|
+
"sortComparator",
|
|
442
|
+
"id",
|
|
443
|
+
"panelWidth",
|
|
444
|
+
"canSelectNullableOptions"
|
|
445
|
+
],
|
|
446
|
+
outputs: [
|
|
447
|
+
"openedChange",
|
|
448
|
+
"opened",
|
|
449
|
+
"closed",
|
|
450
|
+
"selectionChange",
|
|
451
|
+
"valueChange"
|
|
452
|
+
],
|
|
453
|
+
exportAs: ["matSelect"]
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
kind: "component",
|
|
457
|
+
type: i4.MatOption,
|
|
458
|
+
selector: "mat-option",
|
|
459
|
+
inputs: [
|
|
460
|
+
"value",
|
|
461
|
+
"id",
|
|
462
|
+
"disabled"
|
|
463
|
+
],
|
|
464
|
+
outputs: ["onSelectionChange"],
|
|
465
|
+
exportAs: ["matOption"]
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
kind: "component",
|
|
469
|
+
type: ContextBar,
|
|
470
|
+
selector: "jc-context-bar"
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
kind: "pipe",
|
|
474
|
+
type: LinkPipe,
|
|
475
|
+
name: "jcLink"
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
kind: "pipe",
|
|
479
|
+
type: AgoPipe,
|
|
480
|
+
name: "ago"
|
|
481
|
+
}
|
|
482
|
+
],
|
|
483
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
487
|
+
minVersion: "12.0.0",
|
|
488
|
+
version: "22.2.4",
|
|
489
|
+
ngImport: i0,
|
|
490
|
+
type: RulesPage,
|
|
491
|
+
decorators: [{
|
|
492
|
+
type: Component,
|
|
493
|
+
args: [{
|
|
494
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
495
|
+
imports: [
|
|
496
|
+
LinkPipe,
|
|
497
|
+
RouterLink,
|
|
498
|
+
FormsModule,
|
|
499
|
+
MatButtonModule,
|
|
500
|
+
MatCheckboxModule,
|
|
501
|
+
MatFormFieldModule,
|
|
502
|
+
MatInputModule,
|
|
503
|
+
MatProgressBarModule,
|
|
504
|
+
MatSelectModule,
|
|
505
|
+
ContextBar,
|
|
506
|
+
AgoPipe
|
|
507
|
+
],
|
|
508
|
+
template: `
|
|
509
|
+
<jc-context-bar>
|
|
510
|
+
<h1>Rules</h1>
|
|
511
|
+
<span class="jc-spacer"></span>
|
|
512
|
+
<button mat-flat-button type="button" data-testid="rule-new" (click)="startNew()">New rule</button>
|
|
513
|
+
</jc-context-bar>
|
|
514
|
+
@if (data.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
|
|
515
|
+
@if (data.error()) { <p class="jc-error">{{ error() }}</p> }
|
|
516
|
+
@if (form(); as f) {
|
|
517
|
+
<section class="jc-card" data-testid="rule-form">
|
|
518
|
+
<h2>{{ editing() ? 'Change ' + editing()!.key : 'New rule' }}</h2>
|
|
519
|
+
<form class="jc-form" (ngSubmit)="save(f)">
|
|
520
|
+
@if (!editing()) { <mat-form-field subscriptSizing="dynamic"><mat-label>Key</mat-label><input matInput name="key" [(ngModel)]="f.key" required></mat-form-field> }
|
|
521
|
+
<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>
|
|
522
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Template</mat-label>
|
|
523
|
+
<mat-select name="template" [(ngModel)]="f.template">@for (t of templates; track t) { <mat-option [value]="t">{{ t }}</mat-option> }</mat-select>
|
|
524
|
+
</mat-form-field>
|
|
525
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Severity</mat-label>
|
|
526
|
+
<mat-select name="severity" [(ngModel)]="f.severity"><mat-option value="Info">Info</mat-option><mat-option value="Warning">Warning</mat-option><mat-option value="Critical">Critical</mat-option></mat-select>
|
|
527
|
+
</mat-form-field>
|
|
528
|
+
@if (f.template === 'HostMemory') {
|
|
529
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Host (all if empty)</mat-label><input matInput name="host" [(ngModel)]="f.host"></mat-form-field>
|
|
530
|
+
} @else {
|
|
531
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Service (all if empty)</mat-label><input matInput name="service" [(ngModel)]="f.service"></mat-form-field>
|
|
532
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Environment (all if empty)</mat-label><input matInput name="environment" [(ngModel)]="f.environment"></mat-form-field>
|
|
533
|
+
}
|
|
534
|
+
<p class="jc-muted jc-small jc-form-actions">{{ hint(f.template) }}</p>
|
|
535
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Threshold</mat-label><input matInput type="number" step="any" name="threshold" [(ngModel)]="f.threshold" required></mat-form-field>
|
|
536
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Recovery threshold</mat-label><input matInput type="number" step="any" name="recoveryThreshold" [(ngModel)]="f.recoveryThreshold"></mat-form-field>
|
|
537
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Minimum samples</mat-label><input matInput type="number" name="minimumSamples" [(ngModel)]="f.minimumSamples"></mat-form-field>
|
|
538
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Every (s)</mat-label><input matInput type="number" name="schedule" [(ngModel)]="f.scheduleSeconds"></mat-form-field>
|
|
539
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Window (s)</mat-label><input matInput type="number" name="window" [(ngModel)]="f.windowSeconds"></mat-form-field>
|
|
540
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Fire after (s)</mat-label><input matInput type="number" name="for" [(ngModel)]="f.forSeconds"></mat-form-field>
|
|
541
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Recover after (s)</mat-label><input matInput type="number" name="recovery" [(ngModel)]="f.recoverySeconds"></mat-form-field>
|
|
542
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-sm"><mat-label>Cooldown (s)</mat-label><input matInput type="number" name="cooldown" [(ngModel)]="f.cooldownSeconds"></mat-form-field>
|
|
543
|
+
<mat-checkbox name="enabled" [(ngModel)]="f.enabled">Enabled</mat-checkbox>
|
|
544
|
+
@if (editing()) { <p class="jc-muted jc-small jc-form-actions">Disabling this rule, or changing its template or scope, resolves its open alerts: nothing would be watching them any more.</p> }
|
|
545
|
+
<div class="jc-form-actions">
|
|
546
|
+
<button mat-flat-button type="submit" data-testid="rule-save" [disabled]="saving()">Save</button>
|
|
547
|
+
<button mat-button type="button" (click)="form.set(null)">Cancel</button>
|
|
548
|
+
@if (saveError()) { <span class="jc-error" data-testid="rule-error">{{ saveError() }}</span> }
|
|
549
|
+
</div>
|
|
550
|
+
</form>
|
|
551
|
+
</section>
|
|
552
|
+
}
|
|
553
|
+
<table class="jc-table" data-testid="rules">
|
|
554
|
+
<thead><tr><th>Rule</th><th>Template</th><th>Severity</th><th>Threshold</th><th>Recovery</th><th>Wait</th><th>Open alerts</th><th>Last evaluation</th><th></th></tr></thead>
|
|
555
|
+
<tbody>
|
|
556
|
+
@for (r of rows(); track r.key) {
|
|
557
|
+
<tr [class.jc-muted]="!r.enabled">
|
|
558
|
+
<td><strong>{{ r.name }}</strong><div class="jc-muted jc-small">{{ r.key }}@if (!r.enabled) { · disabled }@if (r.managedBy === 'config') { · from the rules file }</div></td>
|
|
559
|
+
<td>{{ r.template }}</td>
|
|
560
|
+
<td>{{ r.severity }}</td>
|
|
561
|
+
<td>{{ r.definition['threshold'] ?? '—' }}</td>
|
|
562
|
+
<td>{{ r.definition['recoveryThreshold'] ?? '—' }}</td>
|
|
563
|
+
<td>{{ r.definition['forDuration'] ?? '—' }}</td>
|
|
564
|
+
<td>
|
|
565
|
+
@for (a of r.open; track a.id) {
|
|
566
|
+
<div><a [routerLink]="['alerts', a.id] | jcLink" [queryParams]="ctx.keep()">{{ a.state }}</a> {{ a.subjectKey }} = {{ a.lastValue ?? 'no data' }}</div>
|
|
567
|
+
} @empty { <span class="jc-muted">none</span> }
|
|
568
|
+
</td>
|
|
569
|
+
<td>{{ r.lastEvaluatedAt | ago }}</td>
|
|
570
|
+
<td class="jc-row-actions">@if (r.managedBy !== 'config') { <button mat-button type="button" [attr.data-testid]="'rule-edit-' + r.key" (click)="edit(r)">Change</button> }</td>
|
|
571
|
+
</tr>
|
|
572
|
+
}
|
|
573
|
+
</tbody>
|
|
574
|
+
</table>`
|
|
575
|
+
}]
|
|
576
|
+
}]
|
|
577
|
+
});
|
|
578
|
+
export { RulesPage };
|
|
579
|
+
|
|
580
|
+
//# sourceMappingURL=juice-js-observability-rules.page-Df2uDVwz.mjs.map
|