@trismegisto/spine 2.1.1 → 3.0.0
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/README.md +105 -19
- package/fesm2022/trismegisto-spine-audit.mjs +264 -10
- package/fesm2022/trismegisto-spine-audit.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-auto-service-apps.technical-BAKdkVdz.mjs +84 -0
- package/fesm2022/trismegisto-spine-auto-service-apps.technical-BAKdkVdz.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-apps.usage-CAXMP5r8.mjs +87 -0
- package/fesm2022/trismegisto-spine-auto-service-apps.usage-CAXMP5r8.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-audit.technical-C901aIIQ.mjs → trismegisto-spine-auto-service-audit.technical-B75d4mh2.mjs} +6 -5
- package/fesm2022/trismegisto-spine-auto-service-audit.technical-B75d4mh2.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-audit.usage-GIJ2RwuH.mjs → trismegisto-spine-auto-service-audit.usage-IdXMruv5.mjs} +9 -8
- package/fesm2022/trismegisto-spine-auto-service-audit.usage-IdXMruv5.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-CCiz-q8o.mjs → trismegisto-spine-auto-service-auto-service-audit.component-BIL8tH1I.mjs} +4 -4
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-CCiz-q8o.mjs.map → trismegisto-spine-auto-service-auto-service-audit.component-BIL8tH1I.mjs.map} +1 -1
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-client-picker.component-Z0wCANF6.mjs → trismegisto-spine-auto-service-auto-service-client-picker.component-CBt_0Uli.mjs} +3 -3
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-client-picker.component-Z0wCANF6.mjs.map → trismegisto-spine-auto-service-auto-service-client-picker.component-CBt_0Uli.mjs.map} +1 -1
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-C7SPavrX.mjs → trismegisto-spine-auto-service-auto-service-client.component-DAxhZC-a.mjs} +123 -27
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-DAxhZC-a.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-CoJYTtBU.mjs +87 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-CoJYTtBU.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-UVMGF7-B.mjs +544 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-UVMGF7-B.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-login-attempts.component-Boi-kG0G.mjs +37 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-login-attempts.component-Boi-kG0G.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-DmLafUkX.mjs +943 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-DmLafUkX.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-client.technical-BHWhpyWv.mjs → trismegisto-spine-auto-service-client.technical-DP5G8i3o.mjs} +24 -3
- package/fesm2022/trismegisto-spine-auto-service-client.technical-DP5G8i3o.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-client.usage-CM2WB2e2.mjs → trismegisto-spine-auto-service-client.usage-CcJANb20.mjs} +15 -6
- package/fesm2022/trismegisto-spine-auto-service-client.usage-CcJANb20.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-BlktCM70.mjs +65 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-BlktCM70.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.usage-Cz5sCKgn.mjs +81 -0
- package/fesm2022/trismegisto-spine-auto-service-groups.usage-Cz5sCKgn.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-home.component-vl3BPFnk.mjs → trismegisto-spine-auto-service-home.component-5_aji1hf.mjs} +5 -4
- package/fesm2022/trismegisto-spine-auto-service-home.component-5_aji1hf.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-login-attempts.technical-CasKIIws.mjs +37 -0
- package/fesm2022/trismegisto-spine-auto-service-login-attempts.technical-CasKIIws.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-login-attempts.usage-BMgLNu7W.mjs +60 -0
- package/fesm2022/trismegisto-spine-auto-service-login-attempts.usage-BMgLNu7W.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service.mjs +49 -29
- package/fesm2022/trismegisto-spine-auto-service.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-layout.mjs +1850 -217
- package/fesm2022/trismegisto-spine-layout.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-shell.mjs +28 -8
- package/fesm2022/trismegisto-spine-shell.mjs.map +1 -1
- package/fesm2022/trismegisto-spine.mjs +1163 -87
- package/fesm2022/trismegisto-spine.mjs.map +1 -1
- package/package.json +6 -6
- package/types/trismegisto-spine-audit.d.ts +180 -5
- package/types/trismegisto-spine-audit.d.ts.map +1 -1
- package/types/trismegisto-spine-auto-service.d.ts +100 -7
- package/types/trismegisto-spine-auto-service.d.ts.map +1 -1
- package/types/trismegisto-spine-layout.d.ts +885 -22
- package/types/trismegisto-spine-layout.d.ts.map +1 -1
- package/types/trismegisto-spine-shell.d.ts +11 -2
- package/types/trismegisto-spine-shell.d.ts.map +1 -1
- package/types/trismegisto-spine.d.ts +496 -34
- package/types/trismegisto-spine.d.ts.map +1 -1
- package/fesm2022/trismegisto-spine-auto-service-apps.technical-ClMKB9cK.mjs +0 -48
- package/fesm2022/trismegisto-spine-auto-service-apps.technical-ClMKB9cK.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-apps.usage-Cf5SG-oQ.mjs +0 -55
- package/fesm2022/trismegisto-spine-auto-service-apps.usage-Cf5SG-oQ.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-audit.technical-C901aIIQ.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-audit.usage-GIJ2RwuH.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-C7SPavrX.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-6Bk46MOf.mjs +0 -39
- package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-6Bk46MOf.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-CqIVe2LU.mjs +0 -357
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-CqIVe2LU.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-BLN49F4a.mjs +0 -502
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-BLN49F4a.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-client.technical-BHWhpyWv.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-client.usage-CM2WB2e2.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-jQlwaQPn.mjs +0 -39
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-jQlwaQPn.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-groups.usage-Bh7ig2zZ.mjs +0 -54
- package/fesm2022/trismegisto-spine-auto-service-groups.usage-Bh7ig2zZ.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-home.component-vl3BPFnk.mjs.map +0 -1
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { HttpParams, HttpClient, HttpHeaders } from '@angular/common/http';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { inject, Injectable, input, output, computed, ChangeDetectionStrategy, Component, signal, effect, untracked } from '@angular/core';
|
|
4
|
-
import { firstValueFrom } from 'rxjs';
|
|
3
|
+
import { inject, Injectable, input, output, computed, ChangeDetectionStrategy, Component, signal, effect, untracked, DestroyRef } from '@angular/core';
|
|
4
|
+
import { firstValueFrom, merge, debounceTime, distinctUntilChanged } from 'rxjs';
|
|
5
5
|
import { AuthorizationService } from '@orfeu/angular/core';
|
|
6
|
-
import { IconButtonComponent, SpinnerComponent, AlertComponent, ButtonComponent, DataGridCellDirective, DataGridComponent, DrawerComponent, FilterBarComponent, ProgressComponent, SegmentedControlComponent, StatCardComponent, StateScreenComponent, SwitchComponent, TagComponent } from '@trismegisto/angular';
|
|
6
|
+
import { IconButtonComponent, SpinnerComponent, AlertComponent, ButtonComponent, DataGridCellDirective, DataGridComponent, DrawerComponent, FilterBarComponent, ProgressComponent, SegmentedControlComponent, StatCardComponent, StateScreenComponent, SwitchComponent, TagComponent, CardComponent, ComboboxComponent, DateRangePickerComponent, InputComponent } from '@trismegisto/angular';
|
|
7
7
|
import { formatMessage, pluralMessage } from '@trismegisto/primitives';
|
|
8
8
|
import { Router } from '@angular/router';
|
|
9
9
|
import { SPINE_MESSAGES, SPINE_ADMIN_API } from '@trismegisto/spine';
|
|
10
|
+
import * as i1 from '@angular/forms';
|
|
11
|
+
import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms';
|
|
12
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* Leitura da auditoria (`audit.events`) do argos admin-server, nos dois escopos:
|
|
@@ -261,6 +264,58 @@ function auditErrorCode(err) {
|
|
|
261
264
|
};
|
|
262
265
|
}
|
|
263
266
|
|
|
267
|
+
/** Métodos gravados pelo argos auth-server em `auth.auth_attempts.method`. */
|
|
268
|
+
const LOGIN_ATTEMPT_METHODS = ['local', 'mfa', 'github', 'google', 'keycloak', 'entraid'];
|
|
269
|
+
const LOGIN_ATTEMPT_RESULTS = ['success', 'failure', 'cancelled'];
|
|
270
|
+
/** Códigos de `auth.auth_attempts.reason` que a tela traduz; código desconhecido aparece como veio. */
|
|
271
|
+
const LOGIN_ATTEMPT_REASONS = [
|
|
272
|
+
'bad_password', 'unknown_user', 'locked', 'mfa_failed', 'mfa_expired', 'mfa_not_allowed', 'mfa_invalid_method',
|
|
273
|
+
'recovery_failed', 'recovery_refused', 'not_linked', 'wrong_realm',
|
|
274
|
+
];
|
|
275
|
+
/**
|
|
276
|
+
* Endpoint das tentativas no escopo: `{adminApi}/admin/attempts` (todos os realms, operação `admin:attempts:list`) ou
|
|
277
|
+
* `{adminApi}/auto-service/:clientId/attempts` (só as do client, recorte feito no servidor).
|
|
278
|
+
*/
|
|
279
|
+
function loginAttemptsUrl(adminApi, scope, clientId) {
|
|
280
|
+
const api = (adminApi ?? '').replace(/\/+$/, '');
|
|
281
|
+
if (!api)
|
|
282
|
+
return null;
|
|
283
|
+
if (scope === 'admin')
|
|
284
|
+
return `${api}/admin/attempts`;
|
|
285
|
+
return clientId ? `${api}/auto-service/${encodeURIComponent(clientId)}/attempts` : null;
|
|
286
|
+
}
|
|
287
|
+
/** Query string da listagem: só os campos com valor. */
|
|
288
|
+
function loginAttemptParams(query) {
|
|
289
|
+
let params = new HttpParams();
|
|
290
|
+
for (const [key, value] of Object.entries(query)) {
|
|
291
|
+
if (value !== undefined && value !== null && value !== '')
|
|
292
|
+
params = params.set(key, String(value));
|
|
293
|
+
}
|
|
294
|
+
return params;
|
|
295
|
+
}
|
|
296
|
+
/** Dia local AAAA-MM-DD (o valor do `hm-date-range-picker`). */
|
|
297
|
+
function isoDay(d) {
|
|
298
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
299
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
300
|
+
}
|
|
301
|
+
/** Período padrão: os últimos 7 dias, hoje incluído. */
|
|
302
|
+
function defaultAttemptsPeriod(today = new Date()) {
|
|
303
|
+
const start = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6);
|
|
304
|
+
return { start: isoDay(start), end: isoDay(today) };
|
|
305
|
+
}
|
|
306
|
+
/** Dias locais do período -> janela ISO [início do primeiro dia, início do dia seguinte ao último). */
|
|
307
|
+
function attemptsPeriodToWindow(range) {
|
|
308
|
+
const out = {};
|
|
309
|
+
if (range?.start)
|
|
310
|
+
out.from = new Date(`${range.start}T00:00:00`).toISOString();
|
|
311
|
+
if (range?.end) {
|
|
312
|
+
const end = new Date(`${range.end}T00:00:00`);
|
|
313
|
+
end.setDate(end.getDate() + 1);
|
|
314
|
+
out.to = end.toISOString();
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
264
319
|
/**
|
|
265
320
|
* HTTP da auditoria (só leitura), igual nos dois escopos: cada método recebe a `base` já resolvida
|
|
266
321
|
* por `auditApiBase` (`{adminApi}/admin/audit` ou `{adminApi}/auto-service/:clientId/audit`). O
|
|
@@ -305,6 +360,10 @@ class SpineAuditService {
|
|
|
305
360
|
apps(url) {
|
|
306
361
|
return firstValueFrom(this.http.get(url, { headers: this.headers }));
|
|
307
362
|
}
|
|
363
|
+
/** Uma página das tentativas de login (`url` = `loginAttemptsUrl`), mais recentes primeiro, com o total do filtro. */
|
|
364
|
+
loginAttempts(url, query) {
|
|
365
|
+
return firstValueFrom(this.http.get(url, { headers: this.headers, params: loginAttemptParams(query) }));
|
|
366
|
+
}
|
|
308
367
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAuditService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
309
368
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAuditService, providedIn: 'root' }); }
|
|
310
369
|
}
|
|
@@ -420,7 +479,7 @@ class SpineAuditTopCardComponent {
|
|
|
420
479
|
</div>
|
|
421
480
|
}
|
|
422
481
|
</section>
|
|
423
|
-
`, isInline: true, styles: [":host{display:block;min-inline-size:0}.card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}.top{display:flex;flex-direction:column;block-size:100%;box-sizing:border-box}.top__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm, 8px);margin:0 0 var(--hm-space-sm, 8px);min-block-size:24px}.top__title{margin:0;font-size:14px;font-weight:600;color:var(--hm-text)}.top__error{margin:var(--hm-space-xs, 4px) 0 0;font-size:12px}.top__pager{display:flex;align-items:center;justify-content:flex-end;gap:var(--hm-space-xs, 4px);margin-block-start:auto;padding-block-start:var(--hm-space-sm, 8px)}.top__range{font-size:12px;color:var(--hm-muted);font-variant-numeric:tabular-nums}.top__buttons{display:inline-flex;gap:2px}.rank{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:2px;transition:opacity .12s ease}.rank--busy{opacity:.55}.rank__item{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.rank__item:hover:not(:disabled){background:color-mix(in srgb,var(--hm-action) 8%,transparent)}.rank__item:focus-visible{outline:2px solid var(--hm-action);outline-offset:1px}.rank__item:disabled{cursor:default}.rank__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rank__value{font-variant-numeric:tabular-nums;white-space:nowrap}.rank__fail{color:var(--hm-danger)}.rank__denied{color:var(--hm-warning, #c98a00)}.rank__track{grid-column:1/-1;display:block;block-size:4px;border-radius:2px;background:color-mix(in srgb,var(--hm-text) 8%,transparent);overflow:hidden}.rank__bar{display:flex;justify-content:flex-end;inline-size:100%;block-size:100%;background:var(--hm-success)}.rank__seg{display:block;block-size:100%}.rank__seg--fail{background:var(--hm-danger)}.rank__seg--denied{background:var(--hm-warning)}@media(prefers-reduced-motion:reduce){.rank{transition:none}}\n"], dependencies: [{ kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "component", type: SpinnerComponent, selector: "hm-spinner", inputs: ["size", "inverse", "label", "labelVisible", "decorative", "done", "doneLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
482
|
+
`, isInline: true, styles: [":host{display:block;min-inline-size:0}.card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}.top{display:flex;flex-direction:column;block-size:100%;box-sizing:border-box}.top__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm, 8px);margin:0 0 var(--hm-space-sm, 8px);min-block-size:24px}.top__title{margin:0;font-size:14px;font-weight:600;color:var(--hm-text)}.top__error{margin:var(--hm-space-xs, 4px) 0 0;font-size:12px}.top__pager{display:flex;align-items:center;justify-content:flex-end;gap:var(--hm-space-xs, 4px);margin-block-start:auto;padding-block-start:var(--hm-space-sm, 8px)}.top__range{font-size:12px;color:var(--hm-muted);font-variant-numeric:tabular-nums}.top__buttons{display:inline-flex;gap:2px}.rank{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:2px;transition:opacity .12s ease}.rank--busy{opacity:.55}.rank__item{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.rank__item:hover:not(:disabled){background:color-mix(in srgb,var(--hm-action) 8%,transparent)}.rank__item:focus-visible{outline:2px solid var(--hm-action);outline-offset:1px}.rank__item:disabled{cursor:default}.rank__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rank__value{font-variant-numeric:tabular-nums;white-space:nowrap}.rank__fail{color:var(--hm-danger)}.rank__denied{color:var(--hm-warning, #c98a00)}.rank__track{grid-column:1/-1;display:block;block-size:4px;border-radius:2px;background:color-mix(in srgb,var(--hm-text) 8%,transparent);overflow:hidden}.rank__bar{display:flex;justify-content:flex-end;inline-size:100%;block-size:100%;background:var(--hm-success)}.rank__seg{display:block;block-size:100%}.rank__seg--fail{background:var(--hm-danger)}.rank__seg--denied{background:var(--hm-warning)}@media(prefers-reduced-motion:reduce){.rank{transition:none}}:host-context([data-motion=reduce]) .rank{transition:none}\n"], dependencies: [{ kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "component", type: SpinnerComponent, selector: "hm-spinner", inputs: ["size", "inverse", "label", "labelVisible", "decorative", "done", "doneLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
424
483
|
}
|
|
425
484
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAuditTopCardComponent, decorators: [{
|
|
426
485
|
type: Component,
|
|
@@ -478,7 +537,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
478
537
|
</div>
|
|
479
538
|
}
|
|
480
539
|
</section>
|
|
481
|
-
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;min-inline-size:0}.card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}.top{display:flex;flex-direction:column;block-size:100%;box-sizing:border-box}.top__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm, 8px);margin:0 0 var(--hm-space-sm, 8px);min-block-size:24px}.top__title{margin:0;font-size:14px;font-weight:600;color:var(--hm-text)}.top__error{margin:var(--hm-space-xs, 4px) 0 0;font-size:12px}.top__pager{display:flex;align-items:center;justify-content:flex-end;gap:var(--hm-space-xs, 4px);margin-block-start:auto;padding-block-start:var(--hm-space-sm, 8px)}.top__range{font-size:12px;color:var(--hm-muted);font-variant-numeric:tabular-nums}.top__buttons{display:inline-flex;gap:2px}.rank{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:2px;transition:opacity .12s ease}.rank--busy{opacity:.55}.rank__item{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.rank__item:hover:not(:disabled){background:color-mix(in srgb,var(--hm-action) 8%,transparent)}.rank__item:focus-visible{outline:2px solid var(--hm-action);outline-offset:1px}.rank__item:disabled{cursor:default}.rank__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rank__value{font-variant-numeric:tabular-nums;white-space:nowrap}.rank__fail{color:var(--hm-danger)}.rank__denied{color:var(--hm-warning, #c98a00)}.rank__track{grid-column:1/-1;display:block;block-size:4px;border-radius:2px;background:color-mix(in srgb,var(--hm-text) 8%,transparent);overflow:hidden}.rank__bar{display:flex;justify-content:flex-end;inline-size:100%;block-size:100%;background:var(--hm-success)}.rank__seg{display:block;block-size:100%}.rank__seg--fail{background:var(--hm-danger)}.rank__seg--denied{background:var(--hm-warning)}@media(prefers-reduced-motion:reduce){.rank{transition:none}}\n"] }]
|
|
540
|
+
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;min-inline-size:0}.card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}.top{display:flex;flex-direction:column;block-size:100%;box-sizing:border-box}.top__head{display:flex;align-items:center;justify-content:space-between;gap:var(--hm-space-sm, 8px);margin:0 0 var(--hm-space-sm, 8px);min-block-size:24px}.top__title{margin:0;font-size:14px;font-weight:600;color:var(--hm-text)}.top__error{margin:var(--hm-space-xs, 4px) 0 0;font-size:12px}.top__pager{display:flex;align-items:center;justify-content:flex-end;gap:var(--hm-space-xs, 4px);margin-block-start:auto;padding-block-start:var(--hm-space-sm, 8px)}.top__range{font-size:12px;color:var(--hm-muted);font-variant-numeric:tabular-nums}.top__buttons{display:inline-flex;gap:2px}.rank{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:2px;transition:opacity .12s ease}.rank--busy{opacity:.55}.rank__item{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;border-radius:var(--hm-radius-control);background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.rank__item:hover:not(:disabled){background:color-mix(in srgb,var(--hm-action) 8%,transparent)}.rank__item:focus-visible{outline:2px solid var(--hm-action);outline-offset:1px}.rank__item:disabled{cursor:default}.rank__name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rank__value{font-variant-numeric:tabular-nums;white-space:nowrap}.rank__fail{color:var(--hm-danger)}.rank__denied{color:var(--hm-warning, #c98a00)}.rank__track{grid-column:1/-1;display:block;block-size:4px;border-radius:2px;background:color-mix(in srgb,var(--hm-text) 8%,transparent);overflow:hidden}.rank__bar{display:flex;justify-content:flex-end;inline-size:100%;block-size:100%;background:var(--hm-success)}.rank__seg{display:block;block-size:100%}.rank__seg--fail{background:var(--hm-danger)}.rank__seg--denied{background:var(--hm-warning)}@media(prefers-reduced-motion:reduce){.rank{transition:none}}:host-context([data-motion=reduce]) .rank{transition:none}\n"] }]
|
|
482
541
|
}], propDecorators: { heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], failed: [{ type: i0.Input, args: [{ isSignal: true, alias: "failed", required: false }] }], picked: [{ type: i0.Output, args: ["picked"] }], offsetChange: [{ type: i0.Output, args: ["offsetChange"] }] } });
|
|
483
542
|
|
|
484
543
|
/** Eventos por página da tabela, de saída (o servidor aceita até 200 — `AUDIT_MAX_PAGE_SIZE`). */
|
|
@@ -1270,7 +1329,7 @@ class SpineAuditComponent {
|
|
|
1270
1329
|
return outcome === 'success' ? 'success' : outcome === 'failure' ? 'danger' : outcome === 'denied' ? 'warning' : 'neutral';
|
|
1271
1330
|
}
|
|
1272
1331
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAuditComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1273
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SpineAuditComponent, isStandalone: true, selector: "spine-audit", inputs: { scope: { classPropertyName: "scope", publicName: "scope", isSignal: true, isRequired: true, transformFunction: null }, clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: false, transformFunction: null }, adminApi: { classPropertyName: "adminApi", publicName: "adminApi", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"audit\" [attr.aria-busy]=\"loading() || pageLoading()\">\n <!-- Per\u00EDodo (escolha r\u00E1pida; as datas da faixa de filtros vencem) + atualizar. -->\n <div class=\"audit__toolbar\">\n <!-- Unidade: por a\u00E7\u00E3o (uma linha por trace, padr\u00E3o) ou por evento (cada registro). -->\n <hm-segmented-control\n [label]=\"text.unitLabel\"\n [options]=\"unitOptions\"\n [value]=\"unit()\"\n (valueChange)=\"setUnit($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n <hm-segmented-control\n [label]=\"text.period\"\n [options]=\"periodOptions\"\n [value]=\"period()\"\n (valueChange)=\"setPeriod($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n @if (customDates()) {\n <span class=\"muted audit__period-note\">{{ text.periodFromFilters }}</span>\n }\n <span class=\"audit__toolbar-end\">\n @if (lastUpdated(); as at) {\n <span class=\"muted audit__updated\" aria-live=\"polite\">{{ fmt(text.updatedAt, { time: clock(at) }) }}</span>\n }\n <hm-switch [label]=\"text.autoRefresh\" [value]=\"autoRefresh()\" (valueChange)=\"autoRefresh.set($event)\" />\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"refresh\" [loading]=\"loading()\" (clicked)=\"reload()\">{{ text.refresh }}</hm-button>\n </span>\n </div>\n\n <hm-filter-bar\n class=\"audit__filters\"\n [label]=\"text.filters\"\n [filters]=\"filterFields()\"\n [value]=\"filterValue()\"\n (valueChange)=\"setFilters($event)\"\n [savedFilters]=\"true\"\n [storageKey]=\"storageKey()\"\n />\n\n @if (errorKind()) {\n <hm-state-screen\n class=\"audit__state\"\n [situation]=\"errorSituation()\"\n [heading]=\"text.errorTitle\"\n [description]=\"errorText()\"\n [action]=\"errorKind() === 'generic' ? text.retry : (hasFilters() ? text.clearFilters : '')\"\n actionIcon=\"refresh\"\n (actionTriggered)=\"errorKind() === 'generic' ? reload() : clearFilters()\"\n />\n } @else if (empty()) {\n <hm-state-screen\n class=\"audit__state\"\n situation=\"empty\"\n [heading]=\"unitText().emptyTitle\"\n [description]=\"text.emptyText\"\n [action]=\"hasFilters() ? text.clearFilters : ''\"\n (actionTriggered)=\"clearFilters()\"\n />\n } @else {\n <!-- KPIs: falhas/negados s\u00E3o filtros (altern\u00E2ncia). -->\n <section class=\"audit__kpis\" [attr.aria-label]=\"text.kpis\">\n <hm-stat-card\n icon=\"list\"\n [label]=\"unitText().kpiTotal\"\n [value]=\"number(stats()?.total ?? 0)\"\n [description]=\"unitText().kpiTotalDescription\"\n [loading]=\"loading() && !stats()\"\n />\n <hm-stat-card\n icon=\"error-standard\"\n [label]=\"text.kpiFailures\"\n [value]=\"number(stats()?.by_outcome?.failure ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.failure ?? 0) > 0 ? 'danger' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.failure ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'failure'\"\n (selectedChange)=\"toggleOutcome('failure', $event)\"\n />\n <hm-stat-card\n icon=\"ban\"\n [label]=\"text.kpiDenied\"\n [value]=\"number(stats()?.by_outcome?.denied ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.denied ?? 0) > 0 ? 'warning' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.denied ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'denied'\"\n (selectedChange)=\"toggleOutcome('denied', $event)\"\n />\n </section>\n\n <!-- Linha do tempo: barras empilhadas em CSS (sucesso embaixo, falha, negado). Cada barra tem\n o resumo em texto (title + aria-label); o gr\u00E1fico n\u00E3o \u00E9 a \u00FAnica forma de ler o n\u00FAmero. -->\n <section class=\"card card-block audit__timeline\" aria-labelledby=\"audit-timeline-title\">\n <h3 class=\"audit__section-title\" id=\"audit-timeline-title\">{{ text.timeline }}</h3>\n @if (hasTimeline()) {\n <ol class=\"timeline\" [attr.aria-label]=\"fmt(unitText().timelineSummary, { from: timelineEdges()?.from ?? '', to: timelineEdges()?.to ?? '' })\">\n @for (bar of timelineBars(); track bar.key) {\n <li class=\"timeline__bar\" [attr.title]=\"bar.summary\" [attr.aria-label]=\"bar.summary\">\n <span class=\"timeline__seg timeline__seg--denied\" [style.height.%]=\"bar.denied\"></span>\n <span class=\"timeline__seg timeline__seg--fail\" [style.height.%]=\"bar.fail\"></span>\n <span class=\"timeline__seg timeline__seg--ok\" [style.height.%]=\"bar.ok\"></span>\n </li>\n }\n </ol>\n <div class=\"timeline__axis\" aria-hidden=\"true\">\n <span>{{ timelineEdges()?.from }}</span>\n <span>{{ timelineEdges()?.to }}</span>\n </div>\n <ul class=\"timeline__legend\">\n <li><span class=\"swatch swatch--ok\"></span>{{ text.outcomeSuccess }}</li>\n <li><span class=\"swatch swatch--fail\"></span>{{ text.outcomeFailure }}</li>\n <li><span class=\"swatch swatch--denied\"></span>{{ text.outcomeDenied }}</li>\n </ul>\n } @else {\n <p class=\"muted\">{{ loading() ? text.loading : unitText().timelineEmpty }}</p>\n }\n </section>\n\n <!-- Rankings paginados no servidor (tops/:kind). Clicar num item vira filtro. -->\n <div class=\"audit__tops\" [class.audit__tops--three]=\"multiApp()\">\n @if (multiApp()) {\n <spine-audit-top-card\n class=\"audit__top audit__top--apps\"\n [heading]=\"text.topApps\"\n [rows]=\"topAppRows()\"\n [total]=\"tops.apps().total\"\n [offset]=\"tops.apps().offset\"\n [max]=\"tops.apps().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.apps().loading\"\n [failed]=\"tops.apps().failed\"\n (offsetChange)=\"loadTop('apps', $event)\"\n (picked)=\"pickTop('apps', $event)\"\n />\n }\n <spine-audit-top-card\n class=\"audit__top audit__top--operations\"\n [heading]=\"text.topOperations\"\n [rows]=\"topOperationRows()\"\n [total]=\"tops.operations().total\"\n [offset]=\"tops.operations().offset\"\n [max]=\"tops.operations().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.operations().loading\"\n [failed]=\"tops.operations().failed\"\n (offsetChange)=\"loadTop('operations', $event)\"\n (picked)=\"pickTop('operations', $event)\"\n />\n <spine-audit-top-card\n class=\"audit__top audit__top--users\"\n [heading]=\"text.topUsers\"\n [rows]=\"topUserRows()\"\n [total]=\"tops.users().total\"\n [offset]=\"tops.users().offset\"\n [max]=\"tops.users().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.users().loading\"\n [failed]=\"tops.users().failed\"\n (offsetChange)=\"loadTop('users', $event)\"\n (picked)=\"pickTop('users', $event)\"\n />\n </div>\n\n <!-- Tabela: pagina\u00E7\u00E3o no servidor. A grade n\u00E3o corta nada ([paginated]=false): mostra a p\u00E1gina que veio\n (limit = itens por p\u00E1gina) e o rodap\u00E9 usa o total do resumo (mesma consulta). Esqueleto s\u00F3 na 1\u00AA\n carga; nas seguintes as linhas ficam e o indicador aparece por cima (sem piscar). -->\n <div class=\"audit__grid\" [class.audit__grid--busy]=\"gridBusy()\" [attr.aria-busy]=\"gridBusy()\">\n @if (gridBusy()) {\n <hm-progress class=\"audit__grid-progress\" indeterminate [label]=\"text.updating\" labelHidden />\n }\n <hm-data-grid\n [caption]=\"unitText().caption\"\n [columns]=\"columns()\"\n [rows]=\"events()\"\n [rowId]=\"rowId\"\n [loading]=\"gridSkeleton()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [totalItems]=\"total()\"\n [pageState]=\"pageState()\"\n (pageStateChange)=\"changePage($event)\"\n [pageSizes]=\"pageSizes\"\n [itemsLabel]=\"unitText().pageItems\"\n [pageSizeLabel]=\"text.pageSize\"\n remoteSort\n rowClickable\n (rowClicked)=\"openRow($event)\"\n >\n <ng-template hmCell=\"operation\" let-item>\n <span class=\"audit__op\" [attr.title]=\"item.operation_label || null\">{{ operationName(item) }}</span>\n </ng-template>\n <ng-template hmCell=\"spans\" let-item>\n <span class=\"audit__spans\" [attr.title]=\"spansHint(item) || null\">\n {{ number(item.span_count ?? 1) }}\n @if (item.orphan) {\n <hm-tag class=\"audit__orphan\" [name]=\"text.orphanTag\" color=\"neutral\" />\n }\n </span>\n </ng-template>\n <ng-template hmCell=\"outcome\" let-item>\n <hm-tag [name]=\"outcomeLabel(item.outcome)\" [color]=\"outcomeColor(item.outcome)\" />\n </ng-template>\n </hm-data-grid>\n </div>\n\n @if (pageFailed()) {\n <hm-alert class=\"audit__page-error\" variant=\"error\" size=\"small\">{{ unitText().pageError }}</hm-alert>\n }\n }\n</div>\n\n<!-- Detalhe do evento + a cadeia (trace). -->\n<hm-drawer [open]=\"detailOpen()\" (openChange)=\"onDetailOpenChange($event)\" [heading]=\"showingAction() ? text.actionDetailHeading : text.detailHeading\">\n @if (selected(); as ev) {\n <div class=\"detail\">\n <div class=\"detail__head\">\n <strong class=\"detail__title\">{{ operationName(ev) }}</strong>\n <hm-tag [name]=\"outcomeLabel(headOutcome())\" [color]=\"outcomeColor(headOutcome())\" />\n </div>\n @if (showingAction() && selectedAction()?.orphan) {\n <hm-alert class=\"detail__orphan\" variant=\"info\" size=\"small\">{{ text.orphanNote }}</hm-alert>\n }\n @if (ev.operation_label && ev.operation_identifier) {\n <p class=\"muted detail__description\">{{ ev.operation_label }}</p>\n }\n\n @if (detailLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.detailLoading }}</p>\n }\n @if (detailFailed()) {\n <hm-alert variant=\"warning\" size=\"small\">{{ text.detailError }}</hm-alert>\n }\n\n <dl class=\"detail__list\">\n @for (field of detailFields(); track field.label) {\n <div class=\"detail__row\">\n <dt>{{ field.label }}</dt>\n <dd>\n @if (field.code) { <code>{{ field.value }}</code> } @else { {{ field.value }} }\n </dd>\n </div>\n }\n </dl>\n\n @if (detailsJson()) {\n <h3 class=\"audit__section-title\">{{ text.fieldDetails }}</h3>\n <pre class=\"detail__json\">{{ detailsJson() }}</pre>\n }\n\n <!-- Por a\u00E7\u00E3o, os spans sobem para logo abaixo do cabe\u00E7alho (order no CSS); por evento, ficam no fim. -->\n @if (ev.trace_id) {\n <div class=\"detail__chain\" [class.detail__chain--first]=\"showingAction()\">\n <div class=\"detail__actions\">\n @if (!trace() && !traceLoading()) {\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"tree-view\" (clicked)=\"showTrace()\">{{ text.viewTrace }}</hm-button>\n }\n <hm-button emphasis=\"minimal\" size=\"small\" icon=\"filter\" (clicked)=\"filterByTrace(ev.trace_id)\">{{ text.filterByTrace }}</hm-button>\n </div>\n @if (traceLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.traceLoading }}</p>\n }\n @if (traceFailed()) {\n <hm-alert variant=\"error\" size=\"small\">{{ text.traceError }}</hm-alert>\n }\n @if (trace(); as tr) {\n <section class=\"trace\" aria-labelledby=\"audit-trace-title\">\n <h3 class=\"audit__section-title\" id=\"audit-trace-title\">{{ showingAction() ? text.spansHeading : text.traceHeading }}</h3>\n @if (tr.hidden_count) {\n <hm-alert class=\"trace__hidden\" variant=\"info\" size=\"small\">{{ plural(text.traceHidden, tr.hidden_count) }}</hm-alert>\n }\n @if (!tr.items.length) {\n <p class=\"muted\">{{ text.traceEmpty }}</p>\n } @else if (spanTimeline(); as tl) {\n <p class=\"muted trace__summary\">{{ fmt(text.spansSummary, { count: number(tl.rows.length), duration: duration(tl.totalMs) }) }}</p>\n <!-- Linha do tempo: cada span \u00E9 uma linha (recuo = profundidade pela cadeia parent_span_id) com a\n barra posicionada no tempo, proporcional \u00E0 dura\u00E7\u00E3o. O texto da linha (e o aria-label) traz\n tudo o que a barra mostra. -->\n <ol class=\"spans\">\n @for (row of tl.rows; track row.event.event_id) {\n <li class=\"spans__item\" [class.spans__item--current]=\"row.event.event_id === ev.event_id\">\n <button type=\"button\" class=\"spans__link\"\n [attr.aria-current]=\"row.event.event_id === ev.event_id ? 'true' : null\"\n [attr.aria-label]=\"spanAria(row)\"\n (click)=\"openEvent(row.event, true)\">\n <span class=\"spans__head\" [style.padding-inline-start.rem]=\"row.depth * 1\">\n <span class=\"spans__app\">{{ row.event.app_name ?? '' }}</span>\n <code class=\"spans__op\">{{ operationName(row.event) }}</code>\n </span>\n <hm-tag class=\"spans__tag\" [name]=\"outcomeLabel(row.event.outcome)\" [color]=\"outcomeColor(row.event.outcome)\" />\n <span class=\"spans__dur\">{{ duration(row.event.duration_ms) }}</span>\n <span class=\"spans__track\" aria-hidden=\"true\">\n <span class=\"spans__bar spans__bar--{{ row.event.outcome }}\" [style.left.%]=\"row.left\" [style.width.%]=\"row.width\"></span>\n </span>\n </button>\n </li>\n }\n </ol>\n }\n </section>\n }\n </div>\n }\n </div>\n }\n</hm-drawer>\n", styles: [".card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}:host{display:block}.audit{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.audit__toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.audit__period-note{font-size:13px}.audit__toolbar-end{display:inline-flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin-inline-start:auto}.audit__updated{font-size:12px;font-variant-numeric:tabular-nums}.audit__state{min-block-size:320px}.audit__section-title{margin:0 0 var(--hm-space-sm, 8px);font-size:14px;font-weight:600;color:var(--hm-text)}.audit__kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.timeline{display:flex;align-items:flex-end;gap:2px;block-size:120px;margin:0;padding:0;list-style:none;border-block-end:1px solid var(--hm-border)}.timeline__bar{flex:1 1 0;min-inline-size:2px;block-size:100%;display:flex;flex-direction:column;justify-content:flex-end;border-radius:2px 2px 0 0;overflow:hidden}.timeline__bar:hover{background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.timeline__seg{display:block;inline-size:100%}.timeline__seg--ok,.swatch--ok{background:color-mix(in srgb,var(--hm-action) 55%,transparent)}.timeline__seg--fail,.swatch--fail{background:var(--hm-danger)}.timeline__seg--denied,.swatch--denied{background:var(--hm-warning, #c98a00)}.timeline__axis{display:flex;justify-content:space-between;margin-block-start:4px;font-size:12px;color:var(--hm-muted)}.timeline__legend{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin:var(--hm-space-sm, 8px) 0 0;padding:0;list-style:none;font-size:12px;color:var(--hm-muted)}.timeline__legend li{display:inline-flex;align-items:center;gap:6px}.swatch{display:inline-block;inline-size:10px;block-size:10px;border-radius:2px}.audit__tops{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.audit__grid{position:relative;display:block}.audit__grid-progress{position:absolute;inset-inline:0;inset-block-start:0;z-index:2;pointer-events:none}.audit__grid--busy hm-data-grid{opacity:.6;transition:opacity .12s ease}.audit__op{font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.audit__page-error{display:block}@media(prefers-reduced-motion:reduce){.audit__grid--busy hm-data-grid{transition:none}}.detail{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__head{display:flex;align-items:center;gap:var(--hm-space-sm, 8px);flex-wrap:wrap}.detail__title{font-size:16px}.detail__list{display:grid;grid-template-columns:minmax(8rem,max-content) minmax(0,1fr);gap:6px var(--hm-space-md, 16px);margin:0}.detail__row{display:contents}.detail__row dt{color:var(--hm-muted);font-size:13px}.detail__row dd{margin:0;font-size:13px;overflow-wrap:anywhere}.detail__json{margin:0;padding:var(--hm-space-sm, 8px);max-block-size:280px;overflow:auto;border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:color-mix(in srgb,var(--hm-text) 4%,transparent);font-size:12px}.detail__description{margin:0;font-size:13px}.detail__actions{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.trace{margin-block-start:var(--hm-space-sm, 8px)}.trace__hidden{margin-block-end:var(--hm-space-sm, 8px)}.detail__head,.detail__description,.detail__orphan{order:-3}.detail__chain{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__chain--first{order:-2}.trace__summary{margin:0 0 var(--hm-space-xs, 4px);font-size:13px}.spans{margin:0;padding:0;list-style:none;border-block-start:1px solid var(--hm-border)}.spans__item{border-block-end:1px solid var(--hm-border)}.spans__item--current .spans__link{background:color-mix(in srgb,var(--hm-action) 10%,transparent);box-shadow:inset 2px 0 0 var(--hm-action)}.spans__link{display:grid;grid-template-columns:minmax(0,1fr) auto auto;grid-template-areas:\"head tag dur\" \"track track track\";align-items:center;gap:4px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.spans__link:hover{background:color-mix(in srgb,var(--hm-action) 6%,transparent)}.spans__link:focus-visible{outline:2px solid var(--hm-action);outline-offset:-2px}.spans__head{grid-area:head;display:flex;flex-direction:column;min-inline-size:0}.spans__app{color:var(--hm-muted);font-size:12px}.spans__op{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.spans__tag{grid-area:tag}.spans__dur{grid-area:dur;min-inline-size:4.5rem;text-align:end;font-variant-numeric:tabular-nums;color:var(--hm-muted)}.spans__track{grid-area:track;position:relative;block-size:6px;border-radius:3px;background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.spans__bar{position:absolute;inset-block:0;border-radius:3px;background:color-mix(in srgb,var(--hm-action) 70%,transparent)}.spans__bar--failure{background:var(--hm-danger)}.spans__bar--denied{background:var(--hm-warning, #c98a00)}.audit__spans{display:inline-flex;align-items:center;gap:6px;font-variant-numeric:tabular-nums}\n"], dependencies: [{ kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "busy", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "directive", type: DataGridCellDirective, selector: "ng-template[hmCell]", inputs: ["hmCell"] }, { kind: "component", type: DataGridComponent, selector: "hm-data-grid", inputs: ["columns", "rows", "rowId", "caption", "rowActions", "loading", "error", "emptyMessage", "noResultsMessage", "filterable", "paginated", "pageState", "pagination", "totalItems", "sort", "selectable", "selectionMode", "selection", "rowSelectable", "rowSelectableReason", "rowDetailDisabled", "rowDetailHidden", "virtual", "rowHeight", "pinnedColumns", "columnState", "pageSizes", "fillHeight", "maxHeight", "bordered", "rowState", "rowClickable", "remoteSearch", "remoteSort", "itemsLabel", "pageSizeLabel", "exportable", "exportFileName", "groupBy", "aggregates", "aggregateFormatter", "persistKey", "persistStorage", "advancedFilter", "filterQuery", "columnManager", "columnOrder", "searchTerm"], outputs: ["pageStateChange", "sortChange", "selectionChange", "pinnedColumnsChange", "columnStateChange", "actionTriggered", "detailToggled", "rowClicked", "clearFiltersRequested", "filteredChange", "searchTermChanged", "groupByChange", "filterQueryChange", "columnOrderChange", "searchTermChange", "detailPaneToggled"] }, { kind: "component", type: DrawerComponent, selector: "hm-drawer", inputs: ["open", "heading", "size", "position", "pinned", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: FilterBarComponent, selector: "hm-filter-bar", inputs: ["filters", "value", "label", "savedFilters", "storageKey", "storage", "applyDefault"], outputs: ["valueChange", "savedChange", "savedApplied"] }, { kind: "component", type: ProgressComponent, selector: "hm-progress", inputs: ["value", "max", "label", "labelHidden", "showValue", "indeterminate", "intent", "size", "hint"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SpineAuditTopCardComponent, selector: "spine-audit-top-card", inputs: ["heading", "rows", "total", "offset", "pageSize", "max", "loading", "failed"], outputs: ["picked", "offsetChange"] }, { kind: "component", type: StatCardComponent, selector: "hm-stat-card", inputs: ["icon", "label", "value", "intent", "fill", "description", "details", "trend", "hint", "size", "selectable", "selected", "loading"], outputs: ["selectedChange"] }, { kind: "component", type: StateScreenComponent, selector: "hm-state-screen", inputs: ["heading", "description", "situation", "action", "actionIcon"], outputs: ["actionTriggered"] }, { kind: "component", type: SwitchComponent, selector: "hm-switch", inputs: ["value", "label", "hint", "help", "labelPosition", "loading"], outputs: ["valueChange"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1332
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SpineAuditComponent, isStandalone: true, selector: "spine-audit", inputs: { scope: { classPropertyName: "scope", publicName: "scope", isSignal: true, isRequired: true, transformFunction: null }, clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: false, transformFunction: null }, adminApi: { classPropertyName: "adminApi", publicName: "adminApi", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"audit\" [attr.aria-busy]=\"loading() || pageLoading()\">\n <!-- Per\u00EDodo (escolha r\u00E1pida; as datas da faixa de filtros vencem) + atualizar. -->\n <div class=\"audit__toolbar\">\n <!-- Unidade: por a\u00E7\u00E3o (uma linha por trace, padr\u00E3o) ou por evento (cada registro). -->\n <hm-segmented-control\n [label]=\"text.unitLabel\"\n [options]=\"unitOptions\"\n [value]=\"unit()\"\n (valueChange)=\"setUnit($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n <hm-segmented-control\n [label]=\"text.period\"\n [options]=\"periodOptions\"\n [value]=\"period()\"\n (valueChange)=\"setPeriod($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n @if (customDates()) {\n <span class=\"muted audit__period-note\">{{ text.periodFromFilters }}</span>\n }\n <span class=\"audit__toolbar-end\">\n @if (lastUpdated(); as at) {\n <span class=\"muted audit__updated\" aria-live=\"polite\">{{ fmt(text.updatedAt, { time: clock(at) }) }}</span>\n }\n <hm-switch [label]=\"text.autoRefresh\" [value]=\"autoRefresh()\" (valueChange)=\"autoRefresh.set($event)\" />\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"refresh\" [loading]=\"loading()\" (clicked)=\"reload()\">{{ text.refresh }}</hm-button>\n </span>\n </div>\n\n <hm-filter-bar\n class=\"audit__filters\"\n [label]=\"text.filters\"\n [filters]=\"filterFields()\"\n [value]=\"filterValue()\"\n (valueChange)=\"setFilters($event)\"\n [savedFilters]=\"true\"\n [storageKey]=\"storageKey()\"\n />\n\n @if (errorKind()) {\n <hm-state-screen\n class=\"audit__state\"\n [situation]=\"errorSituation()\"\n [heading]=\"text.errorTitle\"\n [description]=\"errorText()\"\n [action]=\"errorKind() === 'generic' ? text.retry : (hasFilters() ? text.clearFilters : '')\"\n actionIcon=\"refresh\"\n (actionTriggered)=\"errorKind() === 'generic' ? reload() : clearFilters()\"\n />\n } @else if (empty()) {\n <hm-state-screen\n class=\"audit__state\"\n situation=\"empty\"\n [heading]=\"unitText().emptyTitle\"\n [description]=\"text.emptyText\"\n [action]=\"hasFilters() ? text.clearFilters : ''\"\n (actionTriggered)=\"clearFilters()\"\n />\n } @else {\n <!-- KPIs: falhas/negados s\u00E3o filtros (altern\u00E2ncia). -->\n <section class=\"audit__kpis\" [attr.aria-label]=\"text.kpis\">\n <hm-stat-card\n icon=\"list\"\n [label]=\"unitText().kpiTotal\"\n [value]=\"number(stats()?.total ?? 0)\"\n [description]=\"unitText().kpiTotalDescription\"\n [loading]=\"loading() && !stats()\"\n />\n <hm-stat-card\n icon=\"error-standard\"\n [label]=\"text.kpiFailures\"\n [value]=\"number(stats()?.by_outcome?.failure ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.failure ?? 0) > 0 ? 'danger' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.failure ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'failure'\"\n (selectedChange)=\"toggleOutcome('failure', $event)\"\n />\n <hm-stat-card\n icon=\"ban\"\n [label]=\"text.kpiDenied\"\n [value]=\"number(stats()?.by_outcome?.denied ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.denied ?? 0) > 0 ? 'warning' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.denied ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'denied'\"\n (selectedChange)=\"toggleOutcome('denied', $event)\"\n />\n </section>\n\n <!-- Linha do tempo: barras empilhadas em CSS (sucesso embaixo, falha, negado). Cada barra tem\n o resumo em texto (title + aria-label); o gr\u00E1fico n\u00E3o \u00E9 a \u00FAnica forma de ler o n\u00FAmero. -->\n <section class=\"card card-block audit__timeline\" aria-labelledby=\"audit-timeline-title\">\n <h3 class=\"audit__section-title\" id=\"audit-timeline-title\">{{ text.timeline }}</h3>\n @if (hasTimeline()) {\n <ol class=\"timeline\" [attr.aria-label]=\"fmt(unitText().timelineSummary, { from: timelineEdges()?.from ?? '', to: timelineEdges()?.to ?? '' })\">\n @for (bar of timelineBars(); track bar.key) {\n <li class=\"timeline__bar\" [attr.title]=\"bar.summary\" [attr.aria-label]=\"bar.summary\">\n <span class=\"timeline__seg timeline__seg--denied\" [style.height.%]=\"bar.denied\"></span>\n <span class=\"timeline__seg timeline__seg--fail\" [style.height.%]=\"bar.fail\"></span>\n <span class=\"timeline__seg timeline__seg--ok\" [style.height.%]=\"bar.ok\"></span>\n </li>\n }\n </ol>\n <div class=\"timeline__axis\" aria-hidden=\"true\">\n <span>{{ timelineEdges()?.from }}</span>\n <span>{{ timelineEdges()?.to }}</span>\n </div>\n <ul class=\"timeline__legend\">\n <li><span class=\"swatch swatch--ok\"></span>{{ text.outcomeSuccess }}</li>\n <li><span class=\"swatch swatch--fail\"></span>{{ text.outcomeFailure }}</li>\n <li><span class=\"swatch swatch--denied\"></span>{{ text.outcomeDenied }}</li>\n </ul>\n } @else {\n <p class=\"muted\">{{ loading() ? text.loading : unitText().timelineEmpty }}</p>\n }\n </section>\n\n <!-- Rankings paginados no servidor (tops/:kind). Clicar num item vira filtro. -->\n <div class=\"audit__tops\" [class.audit__tops--three]=\"multiApp()\">\n @if (multiApp()) {\n <spine-audit-top-card\n class=\"audit__top audit__top--apps\"\n [heading]=\"text.topApps\"\n [rows]=\"topAppRows()\"\n [total]=\"tops.apps().total\"\n [offset]=\"tops.apps().offset\"\n [max]=\"tops.apps().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.apps().loading\"\n [failed]=\"tops.apps().failed\"\n (offsetChange)=\"loadTop('apps', $event)\"\n (picked)=\"pickTop('apps', $event)\"\n />\n }\n <spine-audit-top-card\n class=\"audit__top audit__top--operations\"\n [heading]=\"text.topOperations\"\n [rows]=\"topOperationRows()\"\n [total]=\"tops.operations().total\"\n [offset]=\"tops.operations().offset\"\n [max]=\"tops.operations().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.operations().loading\"\n [failed]=\"tops.operations().failed\"\n (offsetChange)=\"loadTop('operations', $event)\"\n (picked)=\"pickTop('operations', $event)\"\n />\n <spine-audit-top-card\n class=\"audit__top audit__top--users\"\n [heading]=\"text.topUsers\"\n [rows]=\"topUserRows()\"\n [total]=\"tops.users().total\"\n [offset]=\"tops.users().offset\"\n [max]=\"tops.users().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.users().loading\"\n [failed]=\"tops.users().failed\"\n (offsetChange)=\"loadTop('users', $event)\"\n (picked)=\"pickTop('users', $event)\"\n />\n </div>\n\n <!-- Tabela: pagina\u00E7\u00E3o no servidor. A grade n\u00E3o corta nada ([paginated]=false): mostra a p\u00E1gina que veio\n (limit = itens por p\u00E1gina) e o rodap\u00E9 usa o total do resumo (mesma consulta). Esqueleto s\u00F3 na 1\u00AA\n carga; nas seguintes as linhas ficam e o indicador aparece por cima (sem piscar). -->\n <div class=\"audit__grid\" [class.audit__grid--busy]=\"gridBusy()\" [attr.aria-busy]=\"gridBusy()\">\n @if (gridBusy()) {\n <hm-progress class=\"audit__grid-progress\" indeterminate [label]=\"text.updating\" labelHidden />\n }\n <hm-data-grid\n [caption]=\"unitText().caption\"\n [columns]=\"columns()\"\n [rows]=\"events()\"\n [rowId]=\"rowId\"\n [loading]=\"gridSkeleton()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [totalItems]=\"total()\"\n [pageState]=\"pageState()\"\n (pageStateChange)=\"changePage($event)\"\n [pageSizes]=\"pageSizes\"\n [itemsLabel]=\"unitText().pageItems\"\n [pageSizeLabel]=\"text.pageSize\"\n remoteSort\n rowClickable\n (rowClicked)=\"openRow($event)\"\n >\n <ng-template hmCell=\"operation\" let-item>\n <span class=\"audit__op\" [attr.title]=\"item.operation_label || null\">{{ operationName(item) }}</span>\n </ng-template>\n <ng-template hmCell=\"spans\" let-item>\n <span class=\"audit__spans\" [attr.title]=\"spansHint(item) || null\">\n {{ number(item.span_count ?? 1) }}\n @if (item.orphan) {\n <hm-tag class=\"audit__orphan\" [name]=\"text.orphanTag\" color=\"neutral\" />\n }\n </span>\n </ng-template>\n <ng-template hmCell=\"outcome\" let-item>\n <hm-tag [name]=\"outcomeLabel(item.outcome)\" [color]=\"outcomeColor(item.outcome)\" />\n </ng-template>\n </hm-data-grid>\n </div>\n\n @if (pageFailed()) {\n <hm-alert class=\"audit__page-error\" variant=\"error\" size=\"small\">{{ unitText().pageError }}</hm-alert>\n }\n }\n</div>\n\n<!-- Detalhe do evento + a cadeia (trace). -->\n<hm-drawer [open]=\"detailOpen()\" (openChange)=\"onDetailOpenChange($event)\" [heading]=\"showingAction() ? text.actionDetailHeading : text.detailHeading\">\n @if (selected(); as ev) {\n <div class=\"detail\">\n <div class=\"detail__head\">\n <strong class=\"detail__title\">{{ operationName(ev) }}</strong>\n <hm-tag [name]=\"outcomeLabel(headOutcome())\" [color]=\"outcomeColor(headOutcome())\" />\n </div>\n @if (showingAction() && selectedAction()?.orphan) {\n <hm-alert class=\"detail__orphan\" variant=\"info\" size=\"small\">{{ text.orphanNote }}</hm-alert>\n }\n @if (ev.operation_label && ev.operation_identifier) {\n <p class=\"muted detail__description\">{{ ev.operation_label }}</p>\n }\n\n @if (detailLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.detailLoading }}</p>\n }\n @if (detailFailed()) {\n <hm-alert variant=\"warning\" size=\"small\">{{ text.detailError }}</hm-alert>\n }\n\n <dl class=\"detail__list\">\n @for (field of detailFields(); track field.label) {\n <div class=\"detail__row\">\n <dt>{{ field.label }}</dt>\n <dd>\n @if (field.code) { <code>{{ field.value }}</code> } @else { {{ field.value }} }\n </dd>\n </div>\n }\n </dl>\n\n @if (detailsJson()) {\n <h3 class=\"audit__section-title\">{{ text.fieldDetails }}</h3>\n <pre class=\"detail__json\">{{ detailsJson() }}</pre>\n }\n\n <!-- Por a\u00E7\u00E3o, os spans sobem para logo abaixo do cabe\u00E7alho (order no CSS); por evento, ficam no fim. -->\n @if (ev.trace_id) {\n <div class=\"detail__chain\" [class.detail__chain--first]=\"showingAction()\">\n <div class=\"detail__actions\">\n @if (!trace() && !traceLoading()) {\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"tree-view\" (clicked)=\"showTrace()\">{{ text.viewTrace }}</hm-button>\n }\n <hm-button emphasis=\"minimal\" size=\"small\" icon=\"filter\" (clicked)=\"filterByTrace(ev.trace_id)\">{{ text.filterByTrace }}</hm-button>\n </div>\n @if (traceLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.traceLoading }}</p>\n }\n @if (traceFailed()) {\n <hm-alert variant=\"error\" size=\"small\">{{ text.traceError }}</hm-alert>\n }\n @if (trace(); as tr) {\n <section class=\"trace\" aria-labelledby=\"audit-trace-title\">\n <h3 class=\"audit__section-title\" id=\"audit-trace-title\">{{ showingAction() ? text.spansHeading : text.traceHeading }}</h3>\n @if (tr.hidden_count) {\n <hm-alert class=\"trace__hidden\" variant=\"info\" size=\"small\">{{ plural(text.traceHidden, tr.hidden_count) }}</hm-alert>\n }\n @if (!tr.items.length) {\n <p class=\"muted\">{{ text.traceEmpty }}</p>\n } @else if (spanTimeline(); as tl) {\n <p class=\"muted trace__summary\">{{ fmt(text.spansSummary, { count: number(tl.rows.length), duration: duration(tl.totalMs) }) }}</p>\n <!-- Linha do tempo: cada span \u00E9 uma linha (recuo = profundidade pela cadeia parent_span_id) com a\n barra posicionada no tempo, proporcional \u00E0 dura\u00E7\u00E3o. O texto da linha (e o aria-label) traz\n tudo o que a barra mostra. -->\n <ol class=\"spans\">\n @for (row of tl.rows; track row.event.event_id) {\n <li class=\"spans__item\" [class.spans__item--current]=\"row.event.event_id === ev.event_id\">\n <button type=\"button\" class=\"spans__link\"\n [attr.aria-current]=\"row.event.event_id === ev.event_id ? 'true' : null\"\n [attr.aria-label]=\"spanAria(row)\"\n (click)=\"openEvent(row.event, true)\">\n <span class=\"spans__head\" [style.padding-inline-start.rem]=\"row.depth * 1\">\n <span class=\"spans__app\">{{ row.event.app_name ?? '' }}</span>\n <code class=\"spans__op\">{{ operationName(row.event) }}</code>\n </span>\n <hm-tag class=\"spans__tag\" [name]=\"outcomeLabel(row.event.outcome)\" [color]=\"outcomeColor(row.event.outcome)\" />\n <span class=\"spans__dur\">{{ duration(row.event.duration_ms) }}</span>\n <span class=\"spans__track\" aria-hidden=\"true\">\n <span class=\"spans__bar spans__bar--{{ row.event.outcome }}\" [style.left.%]=\"row.left\" [style.width.%]=\"row.width\"></span>\n </span>\n </button>\n </li>\n }\n </ol>\n }\n </section>\n }\n </div>\n }\n </div>\n }\n</hm-drawer>\n", styles: [".card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}:host{display:block}.audit{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.audit__toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.audit__period-note{font-size:13px}.audit__toolbar-end{display:inline-flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin-inline-start:auto}.audit__updated{font-size:12px;font-variant-numeric:tabular-nums}.audit__state{min-block-size:320px}.audit__section-title{margin:0 0 var(--hm-space-sm, 8px);font-size:14px;font-weight:600;color:var(--hm-text)}.audit__kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.timeline{display:flex;align-items:flex-end;gap:2px;block-size:120px;margin:0;padding:0;list-style:none;border-block-end:1px solid var(--hm-border)}.timeline__bar{flex:1 1 0;min-inline-size:2px;block-size:100%;display:flex;flex-direction:column;justify-content:flex-end;border-radius:2px 2px 0 0;overflow:hidden}.timeline__bar:hover{background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.timeline__seg{display:block;inline-size:100%}.timeline__seg--ok,.swatch--ok{background:color-mix(in srgb,var(--hm-action) 55%,transparent)}.timeline__seg--fail,.swatch--fail{background:var(--hm-danger)}.timeline__seg--denied,.swatch--denied{background:var(--hm-warning, #c98a00)}.timeline__axis{display:flex;justify-content:space-between;margin-block-start:4px;font-size:12px;color:var(--hm-muted)}.timeline__legend{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin:var(--hm-space-sm, 8px) 0 0;padding:0;list-style:none;font-size:12px;color:var(--hm-muted)}.timeline__legend li{display:inline-flex;align-items:center;gap:6px}.swatch{display:inline-block;inline-size:10px;block-size:10px;border-radius:2px}.audit__tops{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.audit__grid{position:relative;display:block}.audit__grid-progress{position:absolute;inset-inline:0;inset-block-start:0;z-index:2;pointer-events:none}.audit__grid--busy hm-data-grid{opacity:.6;transition:opacity .12s ease}.audit__op{font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.audit__page-error{display:block}@media(prefers-reduced-motion:reduce){.audit__grid--busy hm-data-grid{transition:none}}:host-context([data-motion=reduce]) .audit__grid--busy hm-data-grid{transition:none}.detail{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__head{display:flex;align-items:center;gap:var(--hm-space-sm, 8px);flex-wrap:wrap}.detail__title{font-size:16px}.detail__list{display:grid;grid-template-columns:minmax(8rem,max-content) minmax(0,1fr);gap:6px var(--hm-space-md, 16px);margin:0}.detail__row{display:contents}.detail__row dt{color:var(--hm-muted);font-size:13px}.detail__row dd{margin:0;font-size:13px;overflow-wrap:anywhere}.detail__json{margin:0;padding:var(--hm-space-sm, 8px);max-block-size:280px;overflow:auto;border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:color-mix(in srgb,var(--hm-text) 4%,transparent);font-size:12px}.detail__description{margin:0;font-size:13px}.detail__actions{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.trace{margin-block-start:var(--hm-space-sm, 8px)}.trace__hidden{margin-block-end:var(--hm-space-sm, 8px)}.detail__head,.detail__description,.detail__orphan{order:-3}.detail__chain{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__chain--first{order:-2}.trace__summary{margin:0 0 var(--hm-space-xs, 4px);font-size:13px}.spans{margin:0;padding:0;list-style:none;border-block-start:1px solid var(--hm-border)}.spans__item{border-block-end:1px solid var(--hm-border)}.spans__item--current .spans__link{background:color-mix(in srgb,var(--hm-action) 10%,transparent);box-shadow:inset 2px 0 0 var(--hm-action)}.spans__link{display:grid;grid-template-columns:minmax(0,1fr) auto auto;grid-template-areas:\"head tag dur\" \"track track track\";align-items:center;gap:4px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.spans__link:hover{background:color-mix(in srgb,var(--hm-action) 6%,transparent)}.spans__link:focus-visible{outline:2px solid var(--hm-action);outline-offset:-2px}.spans__head{grid-area:head;display:flex;flex-direction:column;min-inline-size:0}.spans__app{color:var(--hm-muted);font-size:12px}.spans__op{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.spans__tag{grid-area:tag}.spans__dur{grid-area:dur;min-inline-size:4.5rem;text-align:end;font-variant-numeric:tabular-nums;color:var(--hm-muted)}.spans__track{grid-area:track;position:relative;block-size:6px;border-radius:3px;background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.spans__bar{position:absolute;inset-block:0;border-radius:3px;background:color-mix(in srgb,var(--hm-action) 70%,transparent)}.spans__bar--failure{background:var(--hm-danger)}.spans__bar--denied{background:var(--hm-warning, #c98a00)}.audit__spans{display:inline-flex;align-items:center;gap:6px;font-variant-numeric:tabular-nums}\n"], dependencies: [{ kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "busy", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "directive", type: DataGridCellDirective, selector: "ng-template[hmCell]", inputs: ["hmCell"] }, { kind: "component", type: DataGridComponent, selector: "hm-data-grid", inputs: ["columns", "rows", "rowId", "caption", "rowActions", "loading", "error", "emptyMessage", "noResultsMessage", "filterable", "paginated", "pageState", "pagination", "totalItems", "sort", "selectable", "selectionMode", "selection", "rowSelectable", "rowSelectableReason", "rowDetailDisabled", "rowDetailHidden", "virtual", "rowHeight", "pinnedColumns", "columnState", "pageSizes", "fillHeight", "maxHeight", "bordered", "rowState", "rowClickable", "remoteSearch", "remoteSort", "itemsLabel", "pageSizeLabel", "exportable", "exportFileName", "groupBy", "aggregates", "aggregateFormatter", "persistKey", "persistStorage", "advancedFilter", "filterQuery", "columnManager", "columnOrder", "searchTerm"], outputs: ["pageStateChange", "sortChange", "selectionChange", "pinnedColumnsChange", "columnStateChange", "actionTriggered", "detailToggled", "rowClicked", "clearFiltersRequested", "filteredChange", "searchTermChanged", "groupByChange", "filterQueryChange", "columnOrderChange", "searchTermChange", "detailPaneToggled"] }, { kind: "component", type: DrawerComponent, selector: "hm-drawer", inputs: ["open", "heading", "size", "position", "pinned", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: FilterBarComponent, selector: "hm-filter-bar", inputs: ["filters", "value", "label", "savedFilters", "storageKey", "storage", "applyDefault"], outputs: ["valueChange", "savedChange", "savedApplied"] }, { kind: "component", type: ProgressComponent, selector: "hm-progress", inputs: ["value", "max", "label", "labelHidden", "showValue", "indeterminate", "intent", "size", "hint"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SpineAuditTopCardComponent, selector: "spine-audit-top-card", inputs: ["heading", "rows", "total", "offset", "pageSize", "max", "loading", "failed"], outputs: ["picked", "offsetChange"] }, { kind: "component", type: StatCardComponent, selector: "hm-stat-card", inputs: ["icon", "label", "value", "intent", "fill", "description", "details", "trend", "hint", "size", "selectable", "selected", "loading"], outputs: ["selectedChange"] }, { kind: "component", type: StateScreenComponent, selector: "hm-state-screen", inputs: ["heading", "description", "situation", "action", "actionIcon"], outputs: ["actionTriggered"] }, { kind: "component", type: SwitchComponent, selector: "hm-switch", inputs: ["value", "label", "hint", "help", "labelPosition", "loading"], outputs: ["valueChange"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1274
1333
|
}
|
|
1275
1334
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAuditComponent, decorators: [{
|
|
1276
1335
|
type: Component,
|
|
@@ -1278,12 +1337,207 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
1278
1337
|
AlertComponent, ButtonComponent, DataGridCellDirective, DataGridComponent, DrawerComponent, FilterBarComponent,
|
|
1279
1338
|
ProgressComponent, SegmentedControlComponent, SpineAuditTopCardComponent, StatCardComponent, StateScreenComponent,
|
|
1280
1339
|
SwitchComponent, TagComponent,
|
|
1281
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"audit\" [attr.aria-busy]=\"loading() || pageLoading()\">\n <!-- Per\u00EDodo (escolha r\u00E1pida; as datas da faixa de filtros vencem) + atualizar. -->\n <div class=\"audit__toolbar\">\n <!-- Unidade: por a\u00E7\u00E3o (uma linha por trace, padr\u00E3o) ou por evento (cada registro). -->\n <hm-segmented-control\n [label]=\"text.unitLabel\"\n [options]=\"unitOptions\"\n [value]=\"unit()\"\n (valueChange)=\"setUnit($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n <hm-segmented-control\n [label]=\"text.period\"\n [options]=\"periodOptions\"\n [value]=\"period()\"\n (valueChange)=\"setPeriod($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n @if (customDates()) {\n <span class=\"muted audit__period-note\">{{ text.periodFromFilters }}</span>\n }\n <span class=\"audit__toolbar-end\">\n @if (lastUpdated(); as at) {\n <span class=\"muted audit__updated\" aria-live=\"polite\">{{ fmt(text.updatedAt, { time: clock(at) }) }}</span>\n }\n <hm-switch [label]=\"text.autoRefresh\" [value]=\"autoRefresh()\" (valueChange)=\"autoRefresh.set($event)\" />\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"refresh\" [loading]=\"loading()\" (clicked)=\"reload()\">{{ text.refresh }}</hm-button>\n </span>\n </div>\n\n <hm-filter-bar\n class=\"audit__filters\"\n [label]=\"text.filters\"\n [filters]=\"filterFields()\"\n [value]=\"filterValue()\"\n (valueChange)=\"setFilters($event)\"\n [savedFilters]=\"true\"\n [storageKey]=\"storageKey()\"\n />\n\n @if (errorKind()) {\n <hm-state-screen\n class=\"audit__state\"\n [situation]=\"errorSituation()\"\n [heading]=\"text.errorTitle\"\n [description]=\"errorText()\"\n [action]=\"errorKind() === 'generic' ? text.retry : (hasFilters() ? text.clearFilters : '')\"\n actionIcon=\"refresh\"\n (actionTriggered)=\"errorKind() === 'generic' ? reload() : clearFilters()\"\n />\n } @else if (empty()) {\n <hm-state-screen\n class=\"audit__state\"\n situation=\"empty\"\n [heading]=\"unitText().emptyTitle\"\n [description]=\"text.emptyText\"\n [action]=\"hasFilters() ? text.clearFilters : ''\"\n (actionTriggered)=\"clearFilters()\"\n />\n } @else {\n <!-- KPIs: falhas/negados s\u00E3o filtros (altern\u00E2ncia). -->\n <section class=\"audit__kpis\" [attr.aria-label]=\"text.kpis\">\n <hm-stat-card\n icon=\"list\"\n [label]=\"unitText().kpiTotal\"\n [value]=\"number(stats()?.total ?? 0)\"\n [description]=\"unitText().kpiTotalDescription\"\n [loading]=\"loading() && !stats()\"\n />\n <hm-stat-card\n icon=\"error-standard\"\n [label]=\"text.kpiFailures\"\n [value]=\"number(stats()?.by_outcome?.failure ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.failure ?? 0) > 0 ? 'danger' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.failure ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'failure'\"\n (selectedChange)=\"toggleOutcome('failure', $event)\"\n />\n <hm-stat-card\n icon=\"ban\"\n [label]=\"text.kpiDenied\"\n [value]=\"number(stats()?.by_outcome?.denied ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.denied ?? 0) > 0 ? 'warning' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.denied ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'denied'\"\n (selectedChange)=\"toggleOutcome('denied', $event)\"\n />\n </section>\n\n <!-- Linha do tempo: barras empilhadas em CSS (sucesso embaixo, falha, negado). Cada barra tem\n o resumo em texto (title + aria-label); o gr\u00E1fico n\u00E3o \u00E9 a \u00FAnica forma de ler o n\u00FAmero. -->\n <section class=\"card card-block audit__timeline\" aria-labelledby=\"audit-timeline-title\">\n <h3 class=\"audit__section-title\" id=\"audit-timeline-title\">{{ text.timeline }}</h3>\n @if (hasTimeline()) {\n <ol class=\"timeline\" [attr.aria-label]=\"fmt(unitText().timelineSummary, { from: timelineEdges()?.from ?? '', to: timelineEdges()?.to ?? '' })\">\n @for (bar of timelineBars(); track bar.key) {\n <li class=\"timeline__bar\" [attr.title]=\"bar.summary\" [attr.aria-label]=\"bar.summary\">\n <span class=\"timeline__seg timeline__seg--denied\" [style.height.%]=\"bar.denied\"></span>\n <span class=\"timeline__seg timeline__seg--fail\" [style.height.%]=\"bar.fail\"></span>\n <span class=\"timeline__seg timeline__seg--ok\" [style.height.%]=\"bar.ok\"></span>\n </li>\n }\n </ol>\n <div class=\"timeline__axis\" aria-hidden=\"true\">\n <span>{{ timelineEdges()?.from }}</span>\n <span>{{ timelineEdges()?.to }}</span>\n </div>\n <ul class=\"timeline__legend\">\n <li><span class=\"swatch swatch--ok\"></span>{{ text.outcomeSuccess }}</li>\n <li><span class=\"swatch swatch--fail\"></span>{{ text.outcomeFailure }}</li>\n <li><span class=\"swatch swatch--denied\"></span>{{ text.outcomeDenied }}</li>\n </ul>\n } @else {\n <p class=\"muted\">{{ loading() ? text.loading : unitText().timelineEmpty }}</p>\n }\n </section>\n\n <!-- Rankings paginados no servidor (tops/:kind). Clicar num item vira filtro. -->\n <div class=\"audit__tops\" [class.audit__tops--three]=\"multiApp()\">\n @if (multiApp()) {\n <spine-audit-top-card\n class=\"audit__top audit__top--apps\"\n [heading]=\"text.topApps\"\n [rows]=\"topAppRows()\"\n [total]=\"tops.apps().total\"\n [offset]=\"tops.apps().offset\"\n [max]=\"tops.apps().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.apps().loading\"\n [failed]=\"tops.apps().failed\"\n (offsetChange)=\"loadTop('apps', $event)\"\n (picked)=\"pickTop('apps', $event)\"\n />\n }\n <spine-audit-top-card\n class=\"audit__top audit__top--operations\"\n [heading]=\"text.topOperations\"\n [rows]=\"topOperationRows()\"\n [total]=\"tops.operations().total\"\n [offset]=\"tops.operations().offset\"\n [max]=\"tops.operations().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.operations().loading\"\n [failed]=\"tops.operations().failed\"\n (offsetChange)=\"loadTop('operations', $event)\"\n (picked)=\"pickTop('operations', $event)\"\n />\n <spine-audit-top-card\n class=\"audit__top audit__top--users\"\n [heading]=\"text.topUsers\"\n [rows]=\"topUserRows()\"\n [total]=\"tops.users().total\"\n [offset]=\"tops.users().offset\"\n [max]=\"tops.users().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.users().loading\"\n [failed]=\"tops.users().failed\"\n (offsetChange)=\"loadTop('users', $event)\"\n (picked)=\"pickTop('users', $event)\"\n />\n </div>\n\n <!-- Tabela: pagina\u00E7\u00E3o no servidor. A grade n\u00E3o corta nada ([paginated]=false): mostra a p\u00E1gina que veio\n (limit = itens por p\u00E1gina) e o rodap\u00E9 usa o total do resumo (mesma consulta). Esqueleto s\u00F3 na 1\u00AA\n carga; nas seguintes as linhas ficam e o indicador aparece por cima (sem piscar). -->\n <div class=\"audit__grid\" [class.audit__grid--busy]=\"gridBusy()\" [attr.aria-busy]=\"gridBusy()\">\n @if (gridBusy()) {\n <hm-progress class=\"audit__grid-progress\" indeterminate [label]=\"text.updating\" labelHidden />\n }\n <hm-data-grid\n [caption]=\"unitText().caption\"\n [columns]=\"columns()\"\n [rows]=\"events()\"\n [rowId]=\"rowId\"\n [loading]=\"gridSkeleton()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [totalItems]=\"total()\"\n [pageState]=\"pageState()\"\n (pageStateChange)=\"changePage($event)\"\n [pageSizes]=\"pageSizes\"\n [itemsLabel]=\"unitText().pageItems\"\n [pageSizeLabel]=\"text.pageSize\"\n remoteSort\n rowClickable\n (rowClicked)=\"openRow($event)\"\n >\n <ng-template hmCell=\"operation\" let-item>\n <span class=\"audit__op\" [attr.title]=\"item.operation_label || null\">{{ operationName(item) }}</span>\n </ng-template>\n <ng-template hmCell=\"spans\" let-item>\n <span class=\"audit__spans\" [attr.title]=\"spansHint(item) || null\">\n {{ number(item.span_count ?? 1) }}\n @if (item.orphan) {\n <hm-tag class=\"audit__orphan\" [name]=\"text.orphanTag\" color=\"neutral\" />\n }\n </span>\n </ng-template>\n <ng-template hmCell=\"outcome\" let-item>\n <hm-tag [name]=\"outcomeLabel(item.outcome)\" [color]=\"outcomeColor(item.outcome)\" />\n </ng-template>\n </hm-data-grid>\n </div>\n\n @if (pageFailed()) {\n <hm-alert class=\"audit__page-error\" variant=\"error\" size=\"small\">{{ unitText().pageError }}</hm-alert>\n }\n }\n</div>\n\n<!-- Detalhe do evento + a cadeia (trace). -->\n<hm-drawer [open]=\"detailOpen()\" (openChange)=\"onDetailOpenChange($event)\" [heading]=\"showingAction() ? text.actionDetailHeading : text.detailHeading\">\n @if (selected(); as ev) {\n <div class=\"detail\">\n <div class=\"detail__head\">\n <strong class=\"detail__title\">{{ operationName(ev) }}</strong>\n <hm-tag [name]=\"outcomeLabel(headOutcome())\" [color]=\"outcomeColor(headOutcome())\" />\n </div>\n @if (showingAction() && selectedAction()?.orphan) {\n <hm-alert class=\"detail__orphan\" variant=\"info\" size=\"small\">{{ text.orphanNote }}</hm-alert>\n }\n @if (ev.operation_label && ev.operation_identifier) {\n <p class=\"muted detail__description\">{{ ev.operation_label }}</p>\n }\n\n @if (detailLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.detailLoading }}</p>\n }\n @if (detailFailed()) {\n <hm-alert variant=\"warning\" size=\"small\">{{ text.detailError }}</hm-alert>\n }\n\n <dl class=\"detail__list\">\n @for (field of detailFields(); track field.label) {\n <div class=\"detail__row\">\n <dt>{{ field.label }}</dt>\n <dd>\n @if (field.code) { <code>{{ field.value }}</code> } @else { {{ field.value }} }\n </dd>\n </div>\n }\n </dl>\n\n @if (detailsJson()) {\n <h3 class=\"audit__section-title\">{{ text.fieldDetails }}</h3>\n <pre class=\"detail__json\">{{ detailsJson() }}</pre>\n }\n\n <!-- Por a\u00E7\u00E3o, os spans sobem para logo abaixo do cabe\u00E7alho (order no CSS); por evento, ficam no fim. -->\n @if (ev.trace_id) {\n <div class=\"detail__chain\" [class.detail__chain--first]=\"showingAction()\">\n <div class=\"detail__actions\">\n @if (!trace() && !traceLoading()) {\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"tree-view\" (clicked)=\"showTrace()\">{{ text.viewTrace }}</hm-button>\n }\n <hm-button emphasis=\"minimal\" size=\"small\" icon=\"filter\" (clicked)=\"filterByTrace(ev.trace_id)\">{{ text.filterByTrace }}</hm-button>\n </div>\n @if (traceLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.traceLoading }}</p>\n }\n @if (traceFailed()) {\n <hm-alert variant=\"error\" size=\"small\">{{ text.traceError }}</hm-alert>\n }\n @if (trace(); as tr) {\n <section class=\"trace\" aria-labelledby=\"audit-trace-title\">\n <h3 class=\"audit__section-title\" id=\"audit-trace-title\">{{ showingAction() ? text.spansHeading : text.traceHeading }}</h3>\n @if (tr.hidden_count) {\n <hm-alert class=\"trace__hidden\" variant=\"info\" size=\"small\">{{ plural(text.traceHidden, tr.hidden_count) }}</hm-alert>\n }\n @if (!tr.items.length) {\n <p class=\"muted\">{{ text.traceEmpty }}</p>\n } @else if (spanTimeline(); as tl) {\n <p class=\"muted trace__summary\">{{ fmt(text.spansSummary, { count: number(tl.rows.length), duration: duration(tl.totalMs) }) }}</p>\n <!-- Linha do tempo: cada span \u00E9 uma linha (recuo = profundidade pela cadeia parent_span_id) com a\n barra posicionada no tempo, proporcional \u00E0 dura\u00E7\u00E3o. O texto da linha (e o aria-label) traz\n tudo o que a barra mostra. -->\n <ol class=\"spans\">\n @for (row of tl.rows; track row.event.event_id) {\n <li class=\"spans__item\" [class.spans__item--current]=\"row.event.event_id === ev.event_id\">\n <button type=\"button\" class=\"spans__link\"\n [attr.aria-current]=\"row.event.event_id === ev.event_id ? 'true' : null\"\n [attr.aria-label]=\"spanAria(row)\"\n (click)=\"openEvent(row.event, true)\">\n <span class=\"spans__head\" [style.padding-inline-start.rem]=\"row.depth * 1\">\n <span class=\"spans__app\">{{ row.event.app_name ?? '' }}</span>\n <code class=\"spans__op\">{{ operationName(row.event) }}</code>\n </span>\n <hm-tag class=\"spans__tag\" [name]=\"outcomeLabel(row.event.outcome)\" [color]=\"outcomeColor(row.event.outcome)\" />\n <span class=\"spans__dur\">{{ duration(row.event.duration_ms) }}</span>\n <span class=\"spans__track\" aria-hidden=\"true\">\n <span class=\"spans__bar spans__bar--{{ row.event.outcome }}\" [style.left.%]=\"row.left\" [style.width.%]=\"row.width\"></span>\n </span>\n </button>\n </li>\n }\n </ol>\n }\n </section>\n }\n </div>\n }\n </div>\n }\n</hm-drawer>\n", styles: [".card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}:host{display:block}.audit{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.audit__toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.audit__period-note{font-size:13px}.audit__toolbar-end{display:inline-flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin-inline-start:auto}.audit__updated{font-size:12px;font-variant-numeric:tabular-nums}.audit__state{min-block-size:320px}.audit__section-title{margin:0 0 var(--hm-space-sm, 8px);font-size:14px;font-weight:600;color:var(--hm-text)}.audit__kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.timeline{display:flex;align-items:flex-end;gap:2px;block-size:120px;margin:0;padding:0;list-style:none;border-block-end:1px solid var(--hm-border)}.timeline__bar{flex:1 1 0;min-inline-size:2px;block-size:100%;display:flex;flex-direction:column;justify-content:flex-end;border-radius:2px 2px 0 0;overflow:hidden}.timeline__bar:hover{background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.timeline__seg{display:block;inline-size:100%}.timeline__seg--ok,.swatch--ok{background:color-mix(in srgb,var(--hm-action) 55%,transparent)}.timeline__seg--fail,.swatch--fail{background:var(--hm-danger)}.timeline__seg--denied,.swatch--denied{background:var(--hm-warning, #c98a00)}.timeline__axis{display:flex;justify-content:space-between;margin-block-start:4px;font-size:12px;color:var(--hm-muted)}.timeline__legend{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin:var(--hm-space-sm, 8px) 0 0;padding:0;list-style:none;font-size:12px;color:var(--hm-muted)}.timeline__legend li{display:inline-flex;align-items:center;gap:6px}.swatch{display:inline-block;inline-size:10px;block-size:10px;border-radius:2px}.audit__tops{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.audit__grid{position:relative;display:block}.audit__grid-progress{position:absolute;inset-inline:0;inset-block-start:0;z-index:2;pointer-events:none}.audit__grid--busy hm-data-grid{opacity:.6;transition:opacity .12s ease}.audit__op{font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.audit__page-error{display:block}@media(prefers-reduced-motion:reduce){.audit__grid--busy hm-data-grid{transition:none}}.detail{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__head{display:flex;align-items:center;gap:var(--hm-space-sm, 8px);flex-wrap:wrap}.detail__title{font-size:16px}.detail__list{display:grid;grid-template-columns:minmax(8rem,max-content) minmax(0,1fr);gap:6px var(--hm-space-md, 16px);margin:0}.detail__row{display:contents}.detail__row dt{color:var(--hm-muted);font-size:13px}.detail__row dd{margin:0;font-size:13px;overflow-wrap:anywhere}.detail__json{margin:0;padding:var(--hm-space-sm, 8px);max-block-size:280px;overflow:auto;border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:color-mix(in srgb,var(--hm-text) 4%,transparent);font-size:12px}.detail__description{margin:0;font-size:13px}.detail__actions{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.trace{margin-block-start:var(--hm-space-sm, 8px)}.trace__hidden{margin-block-end:var(--hm-space-sm, 8px)}.detail__head,.detail__description,.detail__orphan{order:-3}.detail__chain{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__chain--first{order:-2}.trace__summary{margin:0 0 var(--hm-space-xs, 4px);font-size:13px}.spans{margin:0;padding:0;list-style:none;border-block-start:1px solid var(--hm-border)}.spans__item{border-block-end:1px solid var(--hm-border)}.spans__item--current .spans__link{background:color-mix(in srgb,var(--hm-action) 10%,transparent);box-shadow:inset 2px 0 0 var(--hm-action)}.spans__link{display:grid;grid-template-columns:minmax(0,1fr) auto auto;grid-template-areas:\"head tag dur\" \"track track track\";align-items:center;gap:4px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.spans__link:hover{background:color-mix(in srgb,var(--hm-action) 6%,transparent)}.spans__link:focus-visible{outline:2px solid var(--hm-action);outline-offset:-2px}.spans__head{grid-area:head;display:flex;flex-direction:column;min-inline-size:0}.spans__app{color:var(--hm-muted);font-size:12px}.spans__op{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.spans__tag{grid-area:tag}.spans__dur{grid-area:dur;min-inline-size:4.5rem;text-align:end;font-variant-numeric:tabular-nums;color:var(--hm-muted)}.spans__track{grid-area:track;position:relative;block-size:6px;border-radius:3px;background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.spans__bar{position:absolute;inset-block:0;border-radius:3px;background:color-mix(in srgb,var(--hm-action) 70%,transparent)}.spans__bar--failure{background:var(--hm-danger)}.spans__bar--denied{background:var(--hm-warning, #c98a00)}.audit__spans{display:inline-flex;align-items:center;gap:6px;font-variant-numeric:tabular-nums}\n"] }]
|
|
1340
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"audit\" [attr.aria-busy]=\"loading() || pageLoading()\">\n <!-- Per\u00EDodo (escolha r\u00E1pida; as datas da faixa de filtros vencem) + atualizar. -->\n <div class=\"audit__toolbar\">\n <!-- Unidade: por a\u00E7\u00E3o (uma linha por trace, padr\u00E3o) ou por evento (cada registro). -->\n <hm-segmented-control\n [label]=\"text.unitLabel\"\n [options]=\"unitOptions\"\n [value]=\"unit()\"\n (valueChange)=\"setUnit($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n <hm-segmented-control\n [label]=\"text.period\"\n [options]=\"periodOptions\"\n [value]=\"period()\"\n (valueChange)=\"setPeriod($event)\"\n size=\"small\"\n emphasis=\"action\"\n />\n @if (customDates()) {\n <span class=\"muted audit__period-note\">{{ text.periodFromFilters }}</span>\n }\n <span class=\"audit__toolbar-end\">\n @if (lastUpdated(); as at) {\n <span class=\"muted audit__updated\" aria-live=\"polite\">{{ fmt(text.updatedAt, { time: clock(at) }) }}</span>\n }\n <hm-switch [label]=\"text.autoRefresh\" [value]=\"autoRefresh()\" (valueChange)=\"autoRefresh.set($event)\" />\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"refresh\" [loading]=\"loading()\" (clicked)=\"reload()\">{{ text.refresh }}</hm-button>\n </span>\n </div>\n\n <hm-filter-bar\n class=\"audit__filters\"\n [label]=\"text.filters\"\n [filters]=\"filterFields()\"\n [value]=\"filterValue()\"\n (valueChange)=\"setFilters($event)\"\n [savedFilters]=\"true\"\n [storageKey]=\"storageKey()\"\n />\n\n @if (errorKind()) {\n <hm-state-screen\n class=\"audit__state\"\n [situation]=\"errorSituation()\"\n [heading]=\"text.errorTitle\"\n [description]=\"errorText()\"\n [action]=\"errorKind() === 'generic' ? text.retry : (hasFilters() ? text.clearFilters : '')\"\n actionIcon=\"refresh\"\n (actionTriggered)=\"errorKind() === 'generic' ? reload() : clearFilters()\"\n />\n } @else if (empty()) {\n <hm-state-screen\n class=\"audit__state\"\n situation=\"empty\"\n [heading]=\"unitText().emptyTitle\"\n [description]=\"text.emptyText\"\n [action]=\"hasFilters() ? text.clearFilters : ''\"\n (actionTriggered)=\"clearFilters()\"\n />\n } @else {\n <!-- KPIs: falhas/negados s\u00E3o filtros (altern\u00E2ncia). -->\n <section class=\"audit__kpis\" [attr.aria-label]=\"text.kpis\">\n <hm-stat-card\n icon=\"list\"\n [label]=\"unitText().kpiTotal\"\n [value]=\"number(stats()?.total ?? 0)\"\n [description]=\"unitText().kpiTotalDescription\"\n [loading]=\"loading() && !stats()\"\n />\n <hm-stat-card\n icon=\"error-standard\"\n [label]=\"text.kpiFailures\"\n [value]=\"number(stats()?.by_outcome?.failure ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.failure ?? 0) > 0 ? 'danger' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.failure ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'failure'\"\n (selectedChange)=\"toggleOutcome('failure', $event)\"\n />\n <hm-stat-card\n icon=\"ban\"\n [label]=\"text.kpiDenied\"\n [value]=\"number(stats()?.by_outcome?.denied ?? 0)\"\n [intent]=\"(stats()?.by_outcome?.denied ?? 0) > 0 ? 'warning' : 'neutral'\"\n [description]=\"fmt(text.kpiFilterHint, { share: share(stats()?.by_outcome?.denied ?? 0) })\"\n [loading]=\"loading() && !stats()\"\n selectable\n [selected]=\"selectedOutcome() === 'denied'\"\n (selectedChange)=\"toggleOutcome('denied', $event)\"\n />\n </section>\n\n <!-- Linha do tempo: barras empilhadas em CSS (sucesso embaixo, falha, negado). Cada barra tem\n o resumo em texto (title + aria-label); o gr\u00E1fico n\u00E3o \u00E9 a \u00FAnica forma de ler o n\u00FAmero. -->\n <section class=\"card card-block audit__timeline\" aria-labelledby=\"audit-timeline-title\">\n <h3 class=\"audit__section-title\" id=\"audit-timeline-title\">{{ text.timeline }}</h3>\n @if (hasTimeline()) {\n <ol class=\"timeline\" [attr.aria-label]=\"fmt(unitText().timelineSummary, { from: timelineEdges()?.from ?? '', to: timelineEdges()?.to ?? '' })\">\n @for (bar of timelineBars(); track bar.key) {\n <li class=\"timeline__bar\" [attr.title]=\"bar.summary\" [attr.aria-label]=\"bar.summary\">\n <span class=\"timeline__seg timeline__seg--denied\" [style.height.%]=\"bar.denied\"></span>\n <span class=\"timeline__seg timeline__seg--fail\" [style.height.%]=\"bar.fail\"></span>\n <span class=\"timeline__seg timeline__seg--ok\" [style.height.%]=\"bar.ok\"></span>\n </li>\n }\n </ol>\n <div class=\"timeline__axis\" aria-hidden=\"true\">\n <span>{{ timelineEdges()?.from }}</span>\n <span>{{ timelineEdges()?.to }}</span>\n </div>\n <ul class=\"timeline__legend\">\n <li><span class=\"swatch swatch--ok\"></span>{{ text.outcomeSuccess }}</li>\n <li><span class=\"swatch swatch--fail\"></span>{{ text.outcomeFailure }}</li>\n <li><span class=\"swatch swatch--denied\"></span>{{ text.outcomeDenied }}</li>\n </ul>\n } @else {\n <p class=\"muted\">{{ loading() ? text.loading : unitText().timelineEmpty }}</p>\n }\n </section>\n\n <!-- Rankings paginados no servidor (tops/:kind). Clicar num item vira filtro. -->\n <div class=\"audit__tops\" [class.audit__tops--three]=\"multiApp()\">\n @if (multiApp()) {\n <spine-audit-top-card\n class=\"audit__top audit__top--apps\"\n [heading]=\"text.topApps\"\n [rows]=\"topAppRows()\"\n [total]=\"tops.apps().total\"\n [offset]=\"tops.apps().offset\"\n [max]=\"tops.apps().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.apps().loading\"\n [failed]=\"tops.apps().failed\"\n (offsetChange)=\"loadTop('apps', $event)\"\n (picked)=\"pickTop('apps', $event)\"\n />\n }\n <spine-audit-top-card\n class=\"audit__top audit__top--operations\"\n [heading]=\"text.topOperations\"\n [rows]=\"topOperationRows()\"\n [total]=\"tops.operations().total\"\n [offset]=\"tops.operations().offset\"\n [max]=\"tops.operations().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.operations().loading\"\n [failed]=\"tops.operations().failed\"\n (offsetChange)=\"loadTop('operations', $event)\"\n (picked)=\"pickTop('operations', $event)\"\n />\n <spine-audit-top-card\n class=\"audit__top audit__top--users\"\n [heading]=\"text.topUsers\"\n [rows]=\"topUserRows()\"\n [total]=\"tops.users().total\"\n [offset]=\"tops.users().offset\"\n [max]=\"tops.users().max\"\n [pageSize]=\"topPageSize\"\n [loading]=\"tops.users().loading\"\n [failed]=\"tops.users().failed\"\n (offsetChange)=\"loadTop('users', $event)\"\n (picked)=\"pickTop('users', $event)\"\n />\n </div>\n\n <!-- Tabela: pagina\u00E7\u00E3o no servidor. A grade n\u00E3o corta nada ([paginated]=false): mostra a p\u00E1gina que veio\n (limit = itens por p\u00E1gina) e o rodap\u00E9 usa o total do resumo (mesma consulta). Esqueleto s\u00F3 na 1\u00AA\n carga; nas seguintes as linhas ficam e o indicador aparece por cima (sem piscar). -->\n <div class=\"audit__grid\" [class.audit__grid--busy]=\"gridBusy()\" [attr.aria-busy]=\"gridBusy()\">\n @if (gridBusy()) {\n <hm-progress class=\"audit__grid-progress\" indeterminate [label]=\"text.updating\" labelHidden />\n }\n <hm-data-grid\n [caption]=\"unitText().caption\"\n [columns]=\"columns()\"\n [rows]=\"events()\"\n [rowId]=\"rowId\"\n [loading]=\"gridSkeleton()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [totalItems]=\"total()\"\n [pageState]=\"pageState()\"\n (pageStateChange)=\"changePage($event)\"\n [pageSizes]=\"pageSizes\"\n [itemsLabel]=\"unitText().pageItems\"\n [pageSizeLabel]=\"text.pageSize\"\n remoteSort\n rowClickable\n (rowClicked)=\"openRow($event)\"\n >\n <ng-template hmCell=\"operation\" let-item>\n <span class=\"audit__op\" [attr.title]=\"item.operation_label || null\">{{ operationName(item) }}</span>\n </ng-template>\n <ng-template hmCell=\"spans\" let-item>\n <span class=\"audit__spans\" [attr.title]=\"spansHint(item) || null\">\n {{ number(item.span_count ?? 1) }}\n @if (item.orphan) {\n <hm-tag class=\"audit__orphan\" [name]=\"text.orphanTag\" color=\"neutral\" />\n }\n </span>\n </ng-template>\n <ng-template hmCell=\"outcome\" let-item>\n <hm-tag [name]=\"outcomeLabel(item.outcome)\" [color]=\"outcomeColor(item.outcome)\" />\n </ng-template>\n </hm-data-grid>\n </div>\n\n @if (pageFailed()) {\n <hm-alert class=\"audit__page-error\" variant=\"error\" size=\"small\">{{ unitText().pageError }}</hm-alert>\n }\n }\n</div>\n\n<!-- Detalhe do evento + a cadeia (trace). -->\n<hm-drawer [open]=\"detailOpen()\" (openChange)=\"onDetailOpenChange($event)\" [heading]=\"showingAction() ? text.actionDetailHeading : text.detailHeading\">\n @if (selected(); as ev) {\n <div class=\"detail\">\n <div class=\"detail__head\">\n <strong class=\"detail__title\">{{ operationName(ev) }}</strong>\n <hm-tag [name]=\"outcomeLabel(headOutcome())\" [color]=\"outcomeColor(headOutcome())\" />\n </div>\n @if (showingAction() && selectedAction()?.orphan) {\n <hm-alert class=\"detail__orphan\" variant=\"info\" size=\"small\">{{ text.orphanNote }}</hm-alert>\n }\n @if (ev.operation_label && ev.operation_identifier) {\n <p class=\"muted detail__description\">{{ ev.operation_label }}</p>\n }\n\n @if (detailLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.detailLoading }}</p>\n }\n @if (detailFailed()) {\n <hm-alert variant=\"warning\" size=\"small\">{{ text.detailError }}</hm-alert>\n }\n\n <dl class=\"detail__list\">\n @for (field of detailFields(); track field.label) {\n <div class=\"detail__row\">\n <dt>{{ field.label }}</dt>\n <dd>\n @if (field.code) { <code>{{ field.value }}</code> } @else { {{ field.value }} }\n </dd>\n </div>\n }\n </dl>\n\n @if (detailsJson()) {\n <h3 class=\"audit__section-title\">{{ text.fieldDetails }}</h3>\n <pre class=\"detail__json\">{{ detailsJson() }}</pre>\n }\n\n <!-- Por a\u00E7\u00E3o, os spans sobem para logo abaixo do cabe\u00E7alho (order no CSS); por evento, ficam no fim. -->\n @if (ev.trace_id) {\n <div class=\"detail__chain\" [class.detail__chain--first]=\"showingAction()\">\n <div class=\"detail__actions\">\n @if (!trace() && !traceLoading()) {\n <hm-button emphasis=\"medium\" size=\"small\" icon=\"tree-view\" (clicked)=\"showTrace()\">{{ text.viewTrace }}</hm-button>\n }\n <hm-button emphasis=\"minimal\" size=\"small\" icon=\"filter\" (clicked)=\"filterByTrace(ev.trace_id)\">{{ text.filterByTrace }}</hm-button>\n </div>\n @if (traceLoading()) {\n <p class=\"muted\" role=\"status\">{{ text.traceLoading }}</p>\n }\n @if (traceFailed()) {\n <hm-alert variant=\"error\" size=\"small\">{{ text.traceError }}</hm-alert>\n }\n @if (trace(); as tr) {\n <section class=\"trace\" aria-labelledby=\"audit-trace-title\">\n <h3 class=\"audit__section-title\" id=\"audit-trace-title\">{{ showingAction() ? text.spansHeading : text.traceHeading }}</h3>\n @if (tr.hidden_count) {\n <hm-alert class=\"trace__hidden\" variant=\"info\" size=\"small\">{{ plural(text.traceHidden, tr.hidden_count) }}</hm-alert>\n }\n @if (!tr.items.length) {\n <p class=\"muted\">{{ text.traceEmpty }}</p>\n } @else if (spanTimeline(); as tl) {\n <p class=\"muted trace__summary\">{{ fmt(text.spansSummary, { count: number(tl.rows.length), duration: duration(tl.totalMs) }) }}</p>\n <!-- Linha do tempo: cada span \u00E9 uma linha (recuo = profundidade pela cadeia parent_span_id) com a\n barra posicionada no tempo, proporcional \u00E0 dura\u00E7\u00E3o. O texto da linha (e o aria-label) traz\n tudo o que a barra mostra. -->\n <ol class=\"spans\">\n @for (row of tl.rows; track row.event.event_id) {\n <li class=\"spans__item\" [class.spans__item--current]=\"row.event.event_id === ev.event_id\">\n <button type=\"button\" class=\"spans__link\"\n [attr.aria-current]=\"row.event.event_id === ev.event_id ? 'true' : null\"\n [attr.aria-label]=\"spanAria(row)\"\n (click)=\"openEvent(row.event, true)\">\n <span class=\"spans__head\" [style.padding-inline-start.rem]=\"row.depth * 1\">\n <span class=\"spans__app\">{{ row.event.app_name ?? '' }}</span>\n <code class=\"spans__op\">{{ operationName(row.event) }}</code>\n </span>\n <hm-tag class=\"spans__tag\" [name]=\"outcomeLabel(row.event.outcome)\" [color]=\"outcomeColor(row.event.outcome)\" />\n <span class=\"spans__dur\">{{ duration(row.event.duration_ms) }}</span>\n <span class=\"spans__track\" aria-hidden=\"true\">\n <span class=\"spans__bar spans__bar--{{ row.event.outcome }}\" [style.left.%]=\"row.left\" [style.width.%]=\"row.width\"></span>\n </span>\n </button>\n </li>\n }\n </ol>\n }\n </section>\n }\n </div>\n }\n </div>\n }\n</hm-drawer>\n", styles: [".card{border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.card-block{padding:var(--hm-space-sm, 8px)}.muted{color:var(--hm-muted)}:host{display:block}.audit{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.audit__toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.audit__period-note{font-size:13px}.audit__toolbar-end{display:inline-flex;align-items:center;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin-inline-start:auto}.audit__updated{font-size:12px;font-variant-numeric:tabular-nums}.audit__state{min-block-size:320px}.audit__section-title{margin:0 0 var(--hm-space-sm, 8px);font-size:14px;font-weight:600;color:var(--hm-text)}.audit__kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.timeline{display:flex;align-items:flex-end;gap:2px;block-size:120px;margin:0;padding:0;list-style:none;border-block-end:1px solid var(--hm-border)}.timeline__bar{flex:1 1 0;min-inline-size:2px;block-size:100%;display:flex;flex-direction:column;justify-content:flex-end;border-radius:2px 2px 0 0;overflow:hidden}.timeline__bar:hover{background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.timeline__seg{display:block;inline-size:100%}.timeline__seg--ok,.swatch--ok{background:color-mix(in srgb,var(--hm-action) 55%,transparent)}.timeline__seg--fail,.swatch--fail{background:var(--hm-danger)}.timeline__seg--denied,.swatch--denied{background:var(--hm-warning, #c98a00)}.timeline__axis{display:flex;justify-content:space-between;margin-block-start:4px;font-size:12px;color:var(--hm-muted)}.timeline__legend{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px) var(--hm-space-md, 16px);margin:var(--hm-space-sm, 8px) 0 0;padding:0;list-style:none;font-size:12px;color:var(--hm-muted)}.timeline__legend li{display:inline-flex;align-items:center;gap:6px}.swatch{display:inline-block;inline-size:10px;block-size:10px;border-radius:2px}.audit__tops{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:var(--hm-space-sm, 8px);align-items:stretch}.audit__grid{position:relative;display:block}.audit__grid-progress{position:absolute;inset-inline:0;inset-block-start:0;z-index:2;pointer-events:none}.audit__grid--busy hm-data-grid{opacity:.6;transition:opacity .12s ease}.audit__op{font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.audit__page-error{display:block}@media(prefers-reduced-motion:reduce){.audit__grid--busy hm-data-grid{transition:none}}:host-context([data-motion=reduce]) .audit__grid--busy hm-data-grid{transition:none}.detail{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__head{display:flex;align-items:center;gap:var(--hm-space-sm, 8px);flex-wrap:wrap}.detail__title{font-size:16px}.detail__list{display:grid;grid-template-columns:minmax(8rem,max-content) minmax(0,1fr);gap:6px var(--hm-space-md, 16px);margin:0}.detail__row{display:contents}.detail__row dt{color:var(--hm-muted);font-size:13px}.detail__row dd{margin:0;font-size:13px;overflow-wrap:anywhere}.detail__json{margin:0;padding:var(--hm-space-sm, 8px);max-block-size:280px;overflow:auto;border:1px solid var(--hm-border);border-radius:var(--hm-radius-control);background:color-mix(in srgb,var(--hm-text) 4%,transparent);font-size:12px}.detail__description{margin:0;font-size:13px}.detail__actions{display:flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.trace{margin-block-start:var(--hm-space-sm, 8px)}.trace__hidden{margin-block-end:var(--hm-space-sm, 8px)}.detail__head,.detail__description,.detail__orphan{order:-3}.detail__chain{display:flex;flex-direction:column;gap:var(--hm-space-sm, 8px)}.detail__chain--first{order:-2}.trace__summary{margin:0 0 var(--hm-space-xs, 4px);font-size:13px}.spans{margin:0;padding:0;list-style:none;border-block-start:1px solid var(--hm-border)}.spans__item{border-block-end:1px solid var(--hm-border)}.spans__item--current .spans__link{background:color-mix(in srgb,var(--hm-action) 10%,transparent);box-shadow:inset 2px 0 0 var(--hm-action)}.spans__link{display:grid;grid-template-columns:minmax(0,1fr) auto auto;grid-template-areas:\"head tag dur\" \"track track track\";align-items:center;gap:4px var(--hm-space-sm, 8px);inline-size:100%;padding:6px 8px;border:0;background:transparent;color:var(--hm-text);font:inherit;font-size:13px;text-align:start;cursor:pointer}.spans__link:hover{background:color-mix(in srgb,var(--hm-action) 6%,transparent)}.spans__link:focus-visible{outline:2px solid var(--hm-action);outline-offset:-2px}.spans__head{grid-area:head;display:flex;flex-direction:column;min-inline-size:0}.spans__app{color:var(--hm-muted);font-size:12px}.spans__op{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--hm-font-mono, ui-monospace, monospace);font-size:12px}.spans__tag{grid-area:tag}.spans__dur{grid-area:dur;min-inline-size:4.5rem;text-align:end;font-variant-numeric:tabular-nums;color:var(--hm-muted)}.spans__track{grid-area:track;position:relative;block-size:6px;border-radius:3px;background:color-mix(in srgb,var(--hm-text) 6%,transparent)}.spans__bar{position:absolute;inset-block:0;border-radius:3px;background:color-mix(in srgb,var(--hm-action) 70%,transparent)}.spans__bar--failure{background:var(--hm-danger)}.spans__bar--denied{background:var(--hm-warning, #c98a00)}.audit__spans{display:inline-flex;align-items:center;gap:6px;font-variant-numeric:tabular-nums}\n"] }]
|
|
1282
1341
|
}], ctorParameters: () => [], propDecorators: { scope: [{ type: i0.Input, args: [{ isSignal: true, alias: "scope", required: true }] }], clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: false }] }], adminApi: [{ type: i0.Input, args: [{ isSignal: true, alias: "adminApi", required: false }] }] } });
|
|
1283
1342
|
|
|
1343
|
+
/** Tentativas por página, de saída (o servidor aceita até 200). */
|
|
1344
|
+
const LOGIN_ATTEMPTS_PAGE_SIZE = 50;
|
|
1345
|
+
const LOGIN_ATTEMPTS_PAGE_SIZES = [20, 50, 100];
|
|
1346
|
+
const RESULT_COLORS = {
|
|
1347
|
+
success: 'success', failure: 'danger', cancelled: 'neutral',
|
|
1348
|
+
};
|
|
1349
|
+
/**
|
|
1350
|
+
* A tela de Tentativas de login (`auth.auth_attempts` do argos): quando, quem (usuário ou o que foi digitado), de qual
|
|
1351
|
+
* IP e navegador, por qual método, com que resultado e, nas falhas, o motivo. Só leitura, com filtros e paginação no
|
|
1352
|
+
* servidor. A mesma no painel central e no auto-serviço; só muda o escopo:
|
|
1353
|
+
* - `scope="client"` + `clientId`: `{adminApi}/auto-service/:clientId/attempts`, só as tentativas na aplicação do
|
|
1354
|
+
* client (recorte feito no servidor) -- sem as colunas Realm e Client;
|
|
1355
|
+
* - `scope="admin"`: `{adminApi}/admin/attempts`, todos os realms (operação `admin:attempts:list`); `realmId` filtra.
|
|
1356
|
+
* `adminApi` vem do input ou do `SPINE_ADMIN_API` (que o `provideAutoService` já fornece).
|
|
1357
|
+
*
|
|
1358
|
+
* Importe só de código carregado sob demanda (rota lazy, `import()`), como a `SpineAuditComponent`.
|
|
1359
|
+
*/
|
|
1360
|
+
class SpineLoginAttemptsComponent {
|
|
1361
|
+
constructor() {
|
|
1362
|
+
/** `admin` = painel central (todos os realms); `client` = só as tentativas na aplicação do `clientId`. */
|
|
1363
|
+
this.scope = input.required(...(ngDevMode ? [{ debugName: "scope" }] : /* istanbul ignore next */ []));
|
|
1364
|
+
/** Obrigatório com `scope="client"`. */
|
|
1365
|
+
this.clientId = input(null, ...(ngDevMode ? [{ debugName: "clientId" }] : /* istanbul ignore next */ []));
|
|
1366
|
+
/** Só no escopo `admin`: filtra por realm (vazio = todos). */
|
|
1367
|
+
this.realmId = input(null, ...(ngDevMode ? [{ debugName: "realmId" }] : /* istanbul ignore next */ []));
|
|
1368
|
+
/** Base do argos admin-server; sem ele, vale o `SPINE_ADMIN_API`. */
|
|
1369
|
+
this.adminApi = input(null, ...(ngDevMode ? [{ debugName: "adminApi" }] : /* istanbul ignore next */ []));
|
|
1370
|
+
this.service = inject(SpineAuditService);
|
|
1371
|
+
this.tokenApi = inject(SPINE_ADMIN_API, { optional: true });
|
|
1372
|
+
this.messages = inject(SPINE_MESSAGES);
|
|
1373
|
+
this.text = this.messages.loginAttempts;
|
|
1374
|
+
this.dateFormat = new Intl.DateTimeFormat(this.messages.meta.locale, { dateStyle: 'short', timeStyle: 'medium' });
|
|
1375
|
+
this.url = computed(() => loginAttemptsUrl(this.adminApi() || this.tokenApi, this.scope(), this.clientId()), ...(ngDevMode ? [{ debugName: "url" }] : /* istanbul ignore next */ []));
|
|
1376
|
+
this.attempts = signal([], ...(ngDevMode ? [{ debugName: "attempts" }] : /* istanbul ignore next */ []));
|
|
1377
|
+
this.total = signal(0, ...(ngDevMode ? [{ debugName: "total" }] : /* istanbul ignore next */ []));
|
|
1378
|
+
this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1379
|
+
this.loadError = signal(null, ...(ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []));
|
|
1380
|
+
this.pageState = signal({ page: 1, size: LOGIN_ATTEMPTS_PAGE_SIZE }, ...(ngDevMode ? [{ debugName: "pageState" }] : /* istanbul ignore next */ []));
|
|
1381
|
+
this.showAdvancedFilters = signal(false, ...(ngDevMode ? [{ debugName: "showAdvancedFilters" }] : /* istanbul ignore next */ []));
|
|
1382
|
+
this.pageSizes = LOGIN_ATTEMPTS_PAGE_SIZES;
|
|
1383
|
+
this.all = { value: '', label: this.text.all };
|
|
1384
|
+
this.resultOptions = [
|
|
1385
|
+
this.all, ...LOGIN_ATTEMPT_RESULTS.map((value) => ({ value, label: this.text.results[value] })),
|
|
1386
|
+
];
|
|
1387
|
+
this.methodOptions = [
|
|
1388
|
+
this.all, ...LOGIN_ATTEMPT_METHODS.map((value) => ({ value, label: this.text.methods[value] })),
|
|
1389
|
+
];
|
|
1390
|
+
this.reasonOptions = [
|
|
1391
|
+
this.all, ...LOGIN_ATTEMPT_REASONS.map((value) => ({ value, label: this.text.reasons[value] })),
|
|
1392
|
+
];
|
|
1393
|
+
this.periodPresets = ['today', 'last7Days', 'last30Days', 'thisMonth', 'lastMonth'];
|
|
1394
|
+
this.filtersForm = new FormGroup({
|
|
1395
|
+
period: new FormControl(defaultAttemptsPeriod(), { nonNullable: true }),
|
|
1396
|
+
user: new FormControl('', { nonNullable: true }),
|
|
1397
|
+
ip: new FormControl('', { nonNullable: true }),
|
|
1398
|
+
result: new FormControl('', { nonNullable: true }),
|
|
1399
|
+
method: new FormControl('', { nonNullable: true }),
|
|
1400
|
+
reason: new FormControl('', { nonNullable: true }),
|
|
1401
|
+
});
|
|
1402
|
+
this.columns = computed(() => {
|
|
1403
|
+
const t = this.text;
|
|
1404
|
+
const cols = [
|
|
1405
|
+
{ key: 'created_at', title: t.colDate, value: (a) => a.created_at, sortable: false },
|
|
1406
|
+
{ key: 'result', title: t.colResult, value: (a) => this.resultLabel(a.result), sortable: false },
|
|
1407
|
+
{ key: 'method', title: t.colMethod, value: (a) => this.methodLabel(a.method), sortable: false },
|
|
1408
|
+
{ key: 'user', title: t.colUser, value: (a) => a.user_username ?? a.username ?? '', sortable: false },
|
|
1409
|
+
{ key: 'reason', title: t.colReason, value: (a) => this.reasonLabel(a.reason), sortable: false },
|
|
1410
|
+
{ key: 'ip', title: t.colIp, value: (a) => a.ip ?? '', sortable: false },
|
|
1411
|
+
];
|
|
1412
|
+
if (this.scope() === 'admin') {
|
|
1413
|
+
cols.push({ key: 'realm', title: t.colRealm, value: (a) => a.realm_name ?? a.realm_id, sortable: false }, { key: 'client', title: t.colClient, value: (a) => a.client_name ?? '', sortable: false });
|
|
1414
|
+
}
|
|
1415
|
+
cols.push({ key: 'user_agent', title: t.colUserAgent, value: (a) => a.user_agent ?? '', sortable: false, hideable: true });
|
|
1416
|
+
return cols;
|
|
1417
|
+
}, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
1418
|
+
this.rowActions = [
|
|
1419
|
+
{ id: 'filter-ip', icon: 'search', label: this.text.filterByIp },
|
|
1420
|
+
{ id: 'filter-user', icon: 'users', label: this.text.filterByUser },
|
|
1421
|
+
];
|
|
1422
|
+
this.rowId = (a) => a.id;
|
|
1423
|
+
/** Só a resposta da consulta mais recente vale (filtros mudam enquanto outra ainda está em andamento). */
|
|
1424
|
+
this.requestSeq = 0;
|
|
1425
|
+
const { user, ip, period, result, method, reason } = this.filtersForm.controls;
|
|
1426
|
+
const destroyRef = inject(DestroyRef);
|
|
1427
|
+
merge(merge(user.valueChanges, ip.valueChanges).pipe(debounceTime(350), distinctUntilChanged()), period.valueChanges, result.valueChanges, method.valueChanges, reason.valueChanges).pipe(takeUntilDestroyed(destroyRef)).subscribe(() => this.applyFilters());
|
|
1428
|
+
// Escopo (url) ou realm mudou: recomeça da página 1.
|
|
1429
|
+
effect(() => {
|
|
1430
|
+
this.url();
|
|
1431
|
+
this.realmId();
|
|
1432
|
+
untracked(() => void this.load(1));
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
methodLabel(method) {
|
|
1436
|
+
return this.text.methods[method] ?? method;
|
|
1437
|
+
}
|
|
1438
|
+
resultLabel(result) {
|
|
1439
|
+
return this.text.results[result] ?? result;
|
|
1440
|
+
}
|
|
1441
|
+
resultColor(result) {
|
|
1442
|
+
return RESULT_COLORS[result] ?? 'neutral';
|
|
1443
|
+
}
|
|
1444
|
+
reasonLabel(code) {
|
|
1445
|
+
if (!code)
|
|
1446
|
+
return '';
|
|
1447
|
+
return this.text.reasons[code] ?? code;
|
|
1448
|
+
}
|
|
1449
|
+
formatDate(iso) {
|
|
1450
|
+
const d = new Date(iso);
|
|
1451
|
+
return Number.isNaN(d.getTime()) ? iso : this.dateFormat.format(d);
|
|
1452
|
+
}
|
|
1453
|
+
onPageStateChange(state) {
|
|
1454
|
+
void this.load(state.page, state.size);
|
|
1455
|
+
}
|
|
1456
|
+
applyFilters() {
|
|
1457
|
+
void this.load(1);
|
|
1458
|
+
}
|
|
1459
|
+
clearFilters() {
|
|
1460
|
+
this.filtersForm.reset({ period: defaultAttemptsPeriod(), user: '', ip: '', result: '', method: '', reason: '' }, { emitEvent: false });
|
|
1461
|
+
this.applyFilters();
|
|
1462
|
+
}
|
|
1463
|
+
onRowAction(event) {
|
|
1464
|
+
const a = event.row;
|
|
1465
|
+
if (event.action === 'filter-ip') {
|
|
1466
|
+
if (!a.ip)
|
|
1467
|
+
return;
|
|
1468
|
+
this.filtersForm.controls.ip.setValue(a.ip, { emitEvent: false });
|
|
1469
|
+
}
|
|
1470
|
+
if (event.action === 'filter-user') {
|
|
1471
|
+
const who = a.user_username ?? a.username;
|
|
1472
|
+
if (!who)
|
|
1473
|
+
return;
|
|
1474
|
+
this.filtersForm.controls.user.setValue(who, { emitEvent: false });
|
|
1475
|
+
}
|
|
1476
|
+
this.applyFilters();
|
|
1477
|
+
}
|
|
1478
|
+
async load(page = 1, size = this.pageState().size) {
|
|
1479
|
+
const url = this.url();
|
|
1480
|
+
const seq = ++this.requestSeq;
|
|
1481
|
+
if (!url) {
|
|
1482
|
+
this.attempts.set([]);
|
|
1483
|
+
this.total.set(0);
|
|
1484
|
+
this.loading.set(false);
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
const f = this.filtersForm.getRawValue();
|
|
1488
|
+
this.loading.set(true);
|
|
1489
|
+
this.loadError.set(null);
|
|
1490
|
+
try {
|
|
1491
|
+
const res = await this.service.loginAttempts(url, {
|
|
1492
|
+
page, limit: size,
|
|
1493
|
+
...attemptsPeriodToWindow(f.period),
|
|
1494
|
+
realm: this.scope() === 'admin' ? this.realmId() || undefined : undefined,
|
|
1495
|
+
user: f.user.trim() || undefined,
|
|
1496
|
+
ip: f.ip.trim() || undefined,
|
|
1497
|
+
result: f.result || undefined,
|
|
1498
|
+
method: f.method || undefined,
|
|
1499
|
+
reason: f.reason || undefined,
|
|
1500
|
+
});
|
|
1501
|
+
if (seq !== this.requestSeq)
|
|
1502
|
+
return;
|
|
1503
|
+
this.attempts.set(res.items);
|
|
1504
|
+
this.total.set(res.total);
|
|
1505
|
+
this.pageState.set({ page, size });
|
|
1506
|
+
}
|
|
1507
|
+
catch (err) {
|
|
1508
|
+
if (seq !== this.requestSeq)
|
|
1509
|
+
return;
|
|
1510
|
+
this.attempts.set([]);
|
|
1511
|
+
this.total.set(0);
|
|
1512
|
+
this.loadError.set(this.errorMessage(err));
|
|
1513
|
+
}
|
|
1514
|
+
finally {
|
|
1515
|
+
if (seq === this.requestSeq)
|
|
1516
|
+
this.loading.set(false);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
errorMessage(err) {
|
|
1520
|
+
const { status, code, detail } = auditErrorCode(err);
|
|
1521
|
+
if (status === 403)
|
|
1522
|
+
return this.scope() === 'admin' ? this.text.forbiddenAdmin : this.text.forbiddenClient;
|
|
1523
|
+
const why = detail || code;
|
|
1524
|
+
return why ? formatMessage(this.text.loadErrorDetail, { detail: why }) : this.text.loadError;
|
|
1525
|
+
}
|
|
1526
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineLoginAttemptsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1527
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SpineLoginAttemptsComponent, isStandalone: true, selector: "spine-login-attempts", inputs: { scope: { classPropertyName: "scope", publicName: "scope", isSignal: true, isRequired: true, transformFunction: null }, clientId: { classPropertyName: "clientId", publicName: "clientId", isSignal: true, isRequired: false, transformFunction: null }, realmId: { classPropertyName: "realmId", publicName: "realmId", isSignal: true, isRequired: false, transformFunction: null }, adminApi: { classPropertyName: "adminApi", publicName: "adminApi", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"attempts\">\n <div class=\"attempts__actions\">\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"loading()\" (clicked)=\"load(pageState().page)\">{{ text.refresh }}</hm-button>\n </div>\n\n @if (loadError(); as message) {\n <hm-alert variant=\"error\" [closable]=\"true\" (closed)=\"loadError.set(null)\">{{ message }}</hm-alert>\n }\n\n <hm-card>\n <div class=\"attempts__filters\" [formGroup]=\"filtersForm\">\n <div class=\"attempts__row\">\n <hm-input class=\"attempts__search\" [label]=\"text.user\" [placeholder]=\"text.userPlaceholder\" formControlName=\"user\" (keyup.enter)=\"applyFilters()\" />\n <hm-date-range-picker class=\"attempts__period\" [label]=\"text.period\" formControlName=\"period\" [presets]=\"periodPresets\" />\n <hm-combobox class=\"attempts__field\" [label]=\"text.result\" formControlName=\"result\" [options]=\"resultOptions\" />\n <hm-combobox class=\"attempts__field\" [label]=\"text.method\" formControlName=\"method\" [options]=\"methodOptions\" />\n <div class=\"attempts__buttons\">\n <hm-button emphasis=\"medium\" (clicked)=\"clearFilters()\">{{ text.clear }}</hm-button>\n <hm-button emphasis=\"minimal\" [iconEnd]=\"showAdvancedFilters() ? 'chevron-up' : 'chevron-down'\" (clicked)=\"showAdvancedFilters.update(v => !v)\">\n {{ text.advancedFilters }}\n </hm-button>\n </div>\n </div>\n @if (showAdvancedFilters()) {\n <div class=\"attempts__row\">\n <hm-input class=\"attempts__field\" [label]=\"text.ip\" formControlName=\"ip\" [placeholder]=\"text.ipPlaceholder\" [hint]=\"text.ipHint\" />\n <hm-combobox class=\"attempts__field attempts__field--wide\" [label]=\"text.reason\" formControlName=\"reason\" [options]=\"reasonOptions\" />\n </div>\n }\n </div>\n </hm-card>\n\n <hm-card>\n <hm-data-grid\n [caption]=\"text.caption\"\n [columns]=\"columns()\"\n [rows]=\"attempts()\"\n [rowId]=\"rowId\"\n [rowActions]=\"rowActions\"\n [loading]=\"loading()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [totalItems]=\"total()\"\n [pageState]=\"pageState()\"\n (pageStateChange)=\"onPageStateChange($event)\"\n [pageSizes]=\"pageSizes\"\n [itemsLabel]=\"text.pageItems\"\n [pageSizeLabel]=\"text.pageSize\"\n (actionTriggered)=\"onRowAction($event)\"\n >\n <!-- Data/hora no fuso do navegador (a API manda ISO em UTC). -->\n <ng-template hmCell=\"created_at\" let-a>{{ formatDate(a.created_at) }}</ng-template>\n <ng-template hmCell=\"result\" let-a>\n <hm-tag [name]=\"resultLabel(a.result)\" [color]=\"resultColor(a.result)\" />\n </ng-template>\n <ng-template hmCell=\"user\" let-a>\n @if (a.user_id) {\n <span class=\"attempts__user\">\n <span class=\"attempts__user-name\">{{ a.user_name || a.user_username }}</span>\n <span class=\"attempts__user-detail\">{{ a.user_username }}@if (a.user_email) { \u00B7 {{ a.user_email }} }</span>\n </span>\n } @else if (a.username) {\n <span class=\"attempts__user\">\n <span class=\"attempts__user-name\">{{ a.username }}</span>\n <span class=\"attempts__user-detail\">{{ text.noMatchingUser }}</span>\n </span>\n } @else {\n <span class=\"attempts__muted\">\u2014</span>\n }\n </ng-template>\n <ng-template hmCell=\"reason\" let-a>\n @if (a.reason) { {{ reasonLabel(a.reason) }} } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"ip\" let-a>\n @if (a.ip) { <code class=\"attempts__ip\">{{ a.ip }}</code> } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"client\" let-a>\n @if (a.client_name) { {{ a.client_name }} } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"user_agent\" let-a>\n @if (a.user_agent) { <span class=\"attempts__ua\" [attr.title]=\"a.user_agent\">{{ a.user_agent }}</span> } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n </hm-data-grid>\n </hm-card>\n</div>\n", styles: [":host{display:block}.attempts{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.attempts__actions{display:flex;justify-content:flex-end}.attempts__filters{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.attempts__row{display:flex;align-items:flex-end;flex-wrap:wrap;gap:var(--hm-space-sm, 12px)}.attempts__row+.attempts__row{padding-top:var(--hm-space-md, 16px);border-top:1px solid var(--hm-divider)}.attempts__search{display:block;flex:1 1 16rem;min-width:0}.attempts__period{display:block;flex:0 0 auto}.attempts__field{display:block;flex:0 1 12rem;min-width:0}.attempts__field--wide{flex-basis:18rem}.attempts__buttons{display:inline-flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.attempts__user{display:inline-flex;flex-direction:column;line-height:1.25}.attempts__user-name{font-weight:600}.attempts__user-detail{color:var(--hm-muted);font-size:.8rem}.attempts__ip{font-family:var(--hm-font-mono, monospace);font-size:.85rem}.attempts__ua{display:inline-block;max-width:22rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}.attempts__muted{color:var(--hm-muted)}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: AlertComponent, selector: "hm-alert", inputs: ["variant", "appearance", "heading", "closable", "size", "showIcon", "link", "linkLabel", "linkTarget", "dismissKey", "duration"], outputs: ["closed"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "busy", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: CardComponent, selector: "hm-card", inputs: ["heading", "icon", "selected", "fill", "media", "mediaAlt", "mediaDecorative", "mediaPosition"] }, { kind: "component", type: ComboboxComponent, selector: "hm-combobox", inputs: ["label", "options", "value", "placeholder", "icon", "hint", "help", "size", "labelHidden", "clearable", "requiredManual", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "directive", type: DataGridCellDirective, selector: "ng-template[hmCell]", inputs: ["hmCell"] }, { kind: "component", type: DataGridComponent, selector: "hm-data-grid", inputs: ["columns", "rows", "rowId", "caption", "rowActions", "loading", "error", "emptyMessage", "noResultsMessage", "filterable", "paginated", "pageState", "pagination", "totalItems", "sort", "selectable", "selectionMode", "selection", "rowSelectable", "rowSelectableReason", "rowDetailDisabled", "rowDetailHidden", "virtual", "rowHeight", "pinnedColumns", "columnState", "pageSizes", "fillHeight", "maxHeight", "bordered", "rowState", "rowClickable", "remoteSearch", "remoteSort", "itemsLabel", "pageSizeLabel", "exportable", "exportFileName", "groupBy", "aggregates", "aggregateFormatter", "persistKey", "persistStorage", "advancedFilter", "filterQuery", "columnManager", "columnOrder", "searchTerm"], outputs: ["pageStateChange", "sortChange", "selectionChange", "pinnedColumnsChange", "columnStateChange", "actionTriggered", "detailToggled", "rowClicked", "clearFiltersRequested", "filteredChange", "searchTermChanged", "groupByChange", "filterQueryChange", "columnOrderChange", "searchTermChange", "detailPaneToggled"] }, { kind: "component", type: DateRangePickerComponent, selector: "hm-date-range-picker", inputs: ["value", "startControl", "endControl", "start", "end", "label", "labelHidden", "hint", "min", "max", "valueFormat", "presets"], outputs: ["valueChange", "startChange", "endChange"] }, { kind: "component", type: InputComponent, selector: "hm-input", inputs: ["value", "label", "type", "placeholder", "icon", "labelHidden", "hint", "prefix", "suffix", "prefixIcon", "mask", "maskValue", "min", "max", "step", "stepButtons", "maxLength", "autocomplete", "inputMode"], outputs: ["valueChange"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1528
|
+
}
|
|
1529
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineLoginAttemptsComponent, decorators: [{
|
|
1530
|
+
type: Component,
|
|
1531
|
+
args: [{ selector: 'spine-login-attempts', imports: [
|
|
1532
|
+
ReactiveFormsModule, AlertComponent, ButtonComponent, CardComponent, ComboboxComponent, DataGridCellDirective, DataGridComponent,
|
|
1533
|
+
DateRangePickerComponent, InputComponent, TagComponent,
|
|
1534
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"attempts\">\n <div class=\"attempts__actions\">\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"loading()\" (clicked)=\"load(pageState().page)\">{{ text.refresh }}</hm-button>\n </div>\n\n @if (loadError(); as message) {\n <hm-alert variant=\"error\" [closable]=\"true\" (closed)=\"loadError.set(null)\">{{ message }}</hm-alert>\n }\n\n <hm-card>\n <div class=\"attempts__filters\" [formGroup]=\"filtersForm\">\n <div class=\"attempts__row\">\n <hm-input class=\"attempts__search\" [label]=\"text.user\" [placeholder]=\"text.userPlaceholder\" formControlName=\"user\" (keyup.enter)=\"applyFilters()\" />\n <hm-date-range-picker class=\"attempts__period\" [label]=\"text.period\" formControlName=\"period\" [presets]=\"periodPresets\" />\n <hm-combobox class=\"attempts__field\" [label]=\"text.result\" formControlName=\"result\" [options]=\"resultOptions\" />\n <hm-combobox class=\"attempts__field\" [label]=\"text.method\" formControlName=\"method\" [options]=\"methodOptions\" />\n <div class=\"attempts__buttons\">\n <hm-button emphasis=\"medium\" (clicked)=\"clearFilters()\">{{ text.clear }}</hm-button>\n <hm-button emphasis=\"minimal\" [iconEnd]=\"showAdvancedFilters() ? 'chevron-up' : 'chevron-down'\" (clicked)=\"showAdvancedFilters.update(v => !v)\">\n {{ text.advancedFilters }}\n </hm-button>\n </div>\n </div>\n @if (showAdvancedFilters()) {\n <div class=\"attempts__row\">\n <hm-input class=\"attempts__field\" [label]=\"text.ip\" formControlName=\"ip\" [placeholder]=\"text.ipPlaceholder\" [hint]=\"text.ipHint\" />\n <hm-combobox class=\"attempts__field attempts__field--wide\" [label]=\"text.reason\" formControlName=\"reason\" [options]=\"reasonOptions\" />\n </div>\n }\n </div>\n </hm-card>\n\n <hm-card>\n <hm-data-grid\n [caption]=\"text.caption\"\n [columns]=\"columns()\"\n [rows]=\"attempts()\"\n [rowId]=\"rowId\"\n [rowActions]=\"rowActions\"\n [loading]=\"loading()\"\n [filterable]=\"false\"\n [paginated]=\"false\"\n [totalItems]=\"total()\"\n [pageState]=\"pageState()\"\n (pageStateChange)=\"onPageStateChange($event)\"\n [pageSizes]=\"pageSizes\"\n [itemsLabel]=\"text.pageItems\"\n [pageSizeLabel]=\"text.pageSize\"\n (actionTriggered)=\"onRowAction($event)\"\n >\n <!-- Data/hora no fuso do navegador (a API manda ISO em UTC). -->\n <ng-template hmCell=\"created_at\" let-a>{{ formatDate(a.created_at) }}</ng-template>\n <ng-template hmCell=\"result\" let-a>\n <hm-tag [name]=\"resultLabel(a.result)\" [color]=\"resultColor(a.result)\" />\n </ng-template>\n <ng-template hmCell=\"user\" let-a>\n @if (a.user_id) {\n <span class=\"attempts__user\">\n <span class=\"attempts__user-name\">{{ a.user_name || a.user_username }}</span>\n <span class=\"attempts__user-detail\">{{ a.user_username }}@if (a.user_email) { \u00B7 {{ a.user_email }} }</span>\n </span>\n } @else if (a.username) {\n <span class=\"attempts__user\">\n <span class=\"attempts__user-name\">{{ a.username }}</span>\n <span class=\"attempts__user-detail\">{{ text.noMatchingUser }}</span>\n </span>\n } @else {\n <span class=\"attempts__muted\">\u2014</span>\n }\n </ng-template>\n <ng-template hmCell=\"reason\" let-a>\n @if (a.reason) { {{ reasonLabel(a.reason) }} } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"ip\" let-a>\n @if (a.ip) { <code class=\"attempts__ip\">{{ a.ip }}</code> } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"client\" let-a>\n @if (a.client_name) { {{ a.client_name }} } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n <ng-template hmCell=\"user_agent\" let-a>\n @if (a.user_agent) { <span class=\"attempts__ua\" [attr.title]=\"a.user_agent\">{{ a.user_agent }}</span> } @else { <span class=\"attempts__muted\">\u2014</span> }\n </ng-template>\n </hm-data-grid>\n </hm-card>\n</div>\n", styles: [":host{display:block}.attempts{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.attempts__actions{display:flex;justify-content:flex-end}.attempts__filters{display:flex;flex-direction:column;gap:var(--hm-space-md, 16px)}.attempts__row{display:flex;align-items:flex-end;flex-wrap:wrap;gap:var(--hm-space-sm, 12px)}.attempts__row+.attempts__row{padding-top:var(--hm-space-md, 16px);border-top:1px solid var(--hm-divider)}.attempts__search{display:block;flex:1 1 16rem;min-width:0}.attempts__period{display:block;flex:0 0 auto}.attempts__field{display:block;flex:0 1 12rem;min-width:0}.attempts__field--wide{flex-basis:18rem}.attempts__buttons{display:inline-flex;flex-wrap:wrap;gap:var(--hm-space-sm, 8px)}.attempts__user{display:inline-flex;flex-direction:column;line-height:1.25}.attempts__user-name{font-weight:600}.attempts__user-detail{color:var(--hm-muted);font-size:.8rem}.attempts__ip{font-family:var(--hm-font-mono, monospace);font-size:.85rem}.attempts__ua{display:inline-block;max-width:22rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}.attempts__muted{color:var(--hm-muted)}\n"] }]
|
|
1535
|
+
}], ctorParameters: () => [], propDecorators: { scope: [{ type: i0.Input, args: [{ isSignal: true, alias: "scope", required: true }] }], clientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "clientId", required: false }] }], realmId: [{ type: i0.Input, args: [{ isSignal: true, alias: "realmId", required: false }] }], adminApi: [{ type: i0.Input, args: [{ isSignal: true, alias: "adminApi", required: false }] }] } });
|
|
1536
|
+
|
|
1284
1537
|
/*
|
|
1285
|
-
* @trismegisto/spine/audit —
|
|
1286
|
-
*
|
|
1538
|
+
* @trismegisto/spine/audit — as telas de auditoria do argos admin-server, as mesmas para o painel central
|
|
1539
|
+
* (`scope="admin"`) e para o auto-serviço (`scope="client"` + `clientId`): Ações (`SpineAuditComponent`, leitura de
|
|
1540
|
+
* `audit.events`) e Tentativas de login (`SpineLoginAttemptsComponent`, leitura de `auth.auth_attempts`).
|
|
1287
1541
|
*
|
|
1288
1542
|
* Este ponto de entrada EXPORTA o componente: importe-o só de código carregado sob demanda (rota lazy,
|
|
1289
1543
|
* `import('@trismegisto/spine/audit')`). O `@trismegisto/spine/auto-service` só o alcança por `import()`
|
|
@@ -1295,5 +1549,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
1295
1549
|
* Generated bundle index. Do not edit.
|
|
1296
1550
|
*/
|
|
1297
1551
|
|
|
1298
|
-
export { AUDIT_AUTO_REFRESH_MS, AUDIT_MAX_PAGE_SIZE, AUDIT_MAX_WINDOW_MS, AUDIT_METHODS, AUDIT_OUTCOMES, AUDIT_PAGE_SIZE, AUDIT_PAGE_SIZES, AUDIT_PERIOD_MS, AUDIT_TOP_KINDS, AUDIT_TOP_PAGE_SIZE, AUDIT_UNITS, AuditCursorPager, SpineAuditComponent, SpineAuditService, SpineAuditTopCardComponent, auditApiBase, auditAppsUrl, auditErrorCode, auditPagerTotal, auditParams, buildAuditQuery, buildSpanTimeline, parseAuditDate, worstOutcome };
|
|
1552
|
+
export { AUDIT_AUTO_REFRESH_MS, AUDIT_MAX_PAGE_SIZE, AUDIT_MAX_WINDOW_MS, AUDIT_METHODS, AUDIT_OUTCOMES, AUDIT_PAGE_SIZE, AUDIT_PAGE_SIZES, AUDIT_PERIOD_MS, AUDIT_TOP_KINDS, AUDIT_TOP_PAGE_SIZE, AUDIT_UNITS, AuditCursorPager, LOGIN_ATTEMPTS_PAGE_SIZE, LOGIN_ATTEMPTS_PAGE_SIZES, LOGIN_ATTEMPT_METHODS, LOGIN_ATTEMPT_REASONS, LOGIN_ATTEMPT_RESULTS, SpineAuditComponent, SpineAuditService, SpineAuditTopCardComponent, SpineLoginAttemptsComponent, attemptsPeriodToWindow, auditApiBase, auditAppsUrl, auditErrorCode, auditPagerTotal, auditParams, buildAuditQuery, buildSpanTimeline, defaultAttemptsPeriod, isoDay, loginAttemptParams, loginAttemptsUrl, parseAuditDate, worstOutcome };
|
|
1299
1553
|
//# sourceMappingURL=trismegisto-spine-audit.mjs.map
|