@trismegisto/spine 3.3.0 → 3.3.1
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 +19 -11
- package/fesm2022/trismegisto-spine-audit.mjs +16 -12
- package/fesm2022/trismegisto-spine-audit.mjs.map +1 -1
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-tfGcVR3i.mjs → trismegisto-spine-auto-service-auto-service-audit.component-D8HUpZtA.mjs} +4 -4
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-tfGcVR3i.mjs.map → trismegisto-spine-auto-service-auto-service-audit.component-D8HUpZtA.mjs.map} +1 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs +65 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-IATeedbr.mjs → trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs} +27 -19
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-BAfeLyTB.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs +635 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-BYjIQ9ID.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-login-attempts.component-CRvxfSWD.mjs → trismegisto-spine-auto-service-auto-service-login-attempts.component-DoyhkSYc.mjs} +4 -4
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-login-attempts.component-CRvxfSWD.mjs.map → trismegisto-spine-auto-service-auto-service-login-attempts.component-DoyhkSYc.mjs.map} +1 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-news-page.component-CBemH4hr.mjs +432 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-news-page.component-CBemH4hr.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs +769 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-ratings-page.component-C_THGTkw.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs +2123 -0
- package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-DIeunpQ9.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service.component-DGrs_Jvw.mjs → trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs} +31 -23
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-QsIQ61w2.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-client.usage-C_2zBCRN.mjs → trismegisto-spine-auto-service-client.usage-CJxn9Ysz.mjs} +5 -3
- package/fesm2022/trismegisto-spine-auto-service-client.usage-CJxn9Ysz.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-groups.technical-9aPPulhy.mjs → trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs} +7 -5
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-_jQrg1Qh.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-groups.usage-CYh61LFL.mjs → trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs} +7 -6
- package/fesm2022/{trismegisto-spine-auto-service-groups.usage-CYh61LFL.mjs.map → trismegisto-spine-auto-service-groups.usage-OvWjNeeT.mjs.map} +1 -1
- package/fesm2022/trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs +315 -0
- package/fesm2022/trismegisto-spine-auto-service-home.component-rQ3dIGIE.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-home.technical-Ce7oYRdg.mjs +46 -0
- package/fesm2022/trismegisto-spine-auto-service-home.technical-Ce7oYRdg.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-home.usage-D9R_ZMuU.mjs +48 -0
- package/fesm2022/trismegisto-spine-auto-service-home.usage-D9R_ZMuU.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-news.technical-DV9UpobG.mjs +38 -0
- package/fesm2022/trismegisto-spine-auto-service-news.technical-DV9UpobG.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-news.usage-CeuZnG0V.mjs +48 -0
- package/fesm2022/trismegisto-spine-auto-service-news.usage-CeuZnG0V.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-ratings.technical-QIVj6HBU.mjs +52 -0
- package/fesm2022/trismegisto-spine-auto-service-ratings.technical-QIVj6HBU.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-ratings.usage-BkteY8lz.mjs +45 -0
- package/fesm2022/trismegisto-spine-auto-service-ratings.usage-BkteY8lz.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs +69 -0
- package/fesm2022/trismegisto-spine-auto-service-suggestions.technical-BtUK6sKD.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs +72 -0
- package/fesm2022/trismegisto-spine-auto-service-suggestions.usage-BwO9Hjsx.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service.mjs +49 -19
- package/fesm2022/trismegisto-spine-auto-service.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-graphql.mjs +11 -2
- package/fesm2022/trismegisto-spine-graphql.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-layout.mjs +3160 -1279
- package/fesm2022/trismegisto-spine-layout.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-modal.mjs +7 -5
- package/fesm2022/trismegisto-spine-modal.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-routing.mjs +12 -20
- package/fesm2022/trismegisto-spine-routing.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-shell.mjs +15 -39
- package/fesm2022/trismegisto-spine-shell.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-side-panel.mjs +7 -5
- package/fesm2022/trismegisto-spine-side-panel.mjs.map +1 -1
- package/fesm2022/trismegisto-spine.mjs +1969 -279
- package/fesm2022/trismegisto-spine.mjs.map +1 -1
- package/package.json +6 -6
- package/types/trismegisto-spine-audit.d.ts +2 -12
- package/types/trismegisto-spine-audit.d.ts.map +1 -1
- package/types/trismegisto-spine-auto-service.d.ts +22 -5
- package/types/trismegisto-spine-auto-service.d.ts.map +1 -1
- package/types/trismegisto-spine-layout.d.ts +1086 -283
- package/types/trismegisto-spine-layout.d.ts.map +1 -1
- package/types/trismegisto-spine-modal.d.ts +1 -0
- package/types/trismegisto-spine-modal.d.ts.map +1 -1
- package/types/trismegisto-spine-routing.d.ts +3 -12
- package/types/trismegisto-spine-routing.d.ts.map +1 -1
- package/types/trismegisto-spine-shell.d.ts +2 -15
- package/types/trismegisto-spine-shell.d.ts.map +1 -1
- package/types/trismegisto-spine-side-panel.d.ts +1 -0
- package/types/trismegisto-spine-side-panel.d.ts.map +1 -1
- package/types/trismegisto-spine.d.ts +739 -165
- package/types/trismegisto-spine.d.ts.map +1 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-Cl7ahisu.mjs +0 -65
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-Cl7ahisu.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-IATeedbr.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-D6L0gTmd.mjs +0 -605
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-D6L0gTmd.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-DGrs_Jvw.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-client.usage-C_2zBCRN.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-groups.technical-9aPPulhy.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-home.component-5_aji1hf.mjs +0 -239
- package/fesm2022/trismegisto-spine-auto-service-home.component-5_aji1hf.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-home.technical-BrVhcgDO.mjs +0 -35
- package/fesm2022/trismegisto-spine-auto-service-home.technical-BrVhcgDO.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-home.usage-DWYGtngX.mjs +0 -43
- package/fesm2022/trismegisto-spine-auto-service-home.usage-DWYGtngX.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
Shell de aplicação pronto sobre o `@trismegisto/angular`: cabeçalho, menu lateral, área de trabalho em abas
|
|
4
4
|
(`hm-workspace`), roteamento hierárquico a partir da árvore de rotas do servidor de autorização, telas de
|
|
5
5
|
login/estado (sem permissão, bloqueado, carregando), modal, painel lateral e notificações por serviço,
|
|
6
|
-
configurações (tema, família, densidade, fonte, atalhos),
|
|
7
|
-
tela.
|
|
6
|
+
configurações (tema, família, densidade, fonte, atalhos), configuração inicial em tela cheia, novidades e
|
|
7
|
+
documentação por tela.
|
|
8
8
|
|
|
9
9
|
Na cadeia de camadas (`tokens ← icons ← primitives ← core ← angular ← maps · charts · spine`) é a ponta: monta
|
|
10
10
|
peças grandes sobre o `@trismegisto/angular` e é o que os apps usam como esqueleto. Ao contrário
|
|
@@ -30,7 +30,7 @@ Peers: os cinco pacotes base do Hermes (mesma versão), `@angular/core`, `common
|
|
|
30
30
|
import { ApplicationConfig } from '@angular/core';
|
|
31
31
|
import { provideRouter } from '@angular/router';
|
|
32
32
|
import { provideAuth } from '@orfeu/angular/core';
|
|
33
|
-
import { provideUi
|
|
33
|
+
import { provideUi } from '@trismegisto/spine';
|
|
34
34
|
import { APP_ROUTES, provideShell } from '@trismegisto/spine/shell';
|
|
35
35
|
import { components } from './features/features.routes';
|
|
36
36
|
import { environment } from './environments/environment';
|
|
@@ -42,12 +42,14 @@ export const appConfig: ApplicationConfig = {
|
|
|
42
42
|
provideRouter(APP_ROUTES),
|
|
43
43
|
provideAuth(environment.authorizationServer),
|
|
44
44
|
provideShell({ appComponents: components, packageInfo }),
|
|
45
|
-
//
|
|
45
|
+
// Novidades: cadastradas no Argos (auto-serviço › Novidades) e lidas de /me/news com SPINE_ADMIN_API
|
|
46
|
+
// (o provideAutoService já fornece): aviso discreto uma vez por novidade, painel lateral no "Ver".
|
|
46
47
|
],
|
|
47
48
|
};
|
|
48
49
|
```
|
|
49
50
|
|
|
50
|
-
- **`provideUi(config)`** registra o
|
|
51
|
+
- **`provideUi(config)`** registra os ícones que o próprio spine desenha (`SPINE_ICONS`) e pede o resto do catálogo
|
|
52
|
+
sob demanda (`provideHermesIconLoader`, chunk à parte logo depois do bootstrap; ver "Ícones do app"), aplica
|
|
51
53
|
o tema no `<body>` e usa `clientId` como prefixo de **todas** as chaves salvas (tema, menu lateral, abas):
|
|
52
54
|
apps diferentes na mesma origem não misturam preferências. Opções: `clientId`, `appName` (nome ao lado do
|
|
53
55
|
logo no cabeçalho), `logo`, `showAppName` e `favicon` (ver "Logo e favicon" abaixo), `technicalDocs` (mostra a
|
|
@@ -197,7 +199,7 @@ antes de "Sobre".
|
|
|
197
199
|
| Notificações | `notificacoes` | Tipos de aviso do app, aviso do navegador | Com `SpineConfig.features.notices` |
|
|
198
200
|
| Aparência | `aparencia` | Tema, cor, densidade, fonte, menu lateral | Sempre |
|
|
199
201
|
| Região | `regiao` | Idioma, fuso horário | Sempre |
|
|
200
|
-
| Atalhos | `atalhos` | Lista de atalhos
|
|
202
|
+
| Atalhos | `atalhos` | Lista de atalhos | Sempre |
|
|
201
203
|
| Sobre | `sobre` | Aplicação, versão, build, ambiente, links, dependências, informações de suporte | Sempre |
|
|
202
204
|
|
|
203
205
|
Linhas da seção **Sessões**: "Sessão" sempre; "Token de acesso" só com `SpineConfig.technicalDocs` (sem configuração,
|
|
@@ -221,7 +223,7 @@ O grupo de administração do projeto (crachá Administrador, o do auto-serviço
|
|
|
221
223
|
grupo recebe o crachá dele nesta aplicação no próximo token.
|
|
222
224
|
|
|
223
225
|
**Chave antiga**: até a versão 2.1.2 a seção "Conta e segurança" (`seguranca`) reunia MFA e sessões, e "Sessão" e os
|
|
224
|
-
tokens ficavam em "Conta". `?settings=seguranca` (
|
|
226
|
+
tokens ficavam em "Conta". `?settings=seguranca` (a constante `SPINE_SETTINGS_KEYS.security` saiu na 3.3.1) abre a seção
|
|
225
227
|
**MFA** (`SPINE_SETTINGS_ALIASES`, `resolveSettingsSection`); sem a API da conta, como qualquer chave sem seção, abre
|
|
226
228
|
a primeira. `?settings=conta` continua abrindo Conta.
|
|
227
229
|
|
|
@@ -280,14 +282,14 @@ ou código de recuperação); o Argos guarda essa prova por 5 minutos.
|
|
|
280
282
|
|
|
281
283
|
| Import | Conteúdo |
|
|
282
284
|
| --- | --- |
|
|
283
|
-
| `@trismegisto/spine` | `provideUi`, `SpineConfig`, `ThemeService`, `DensityService`, `SpinePreferencesService`,
|
|
285
|
+
| `@trismegisto/spine` | `provideUi`, `SpineConfig`, `ThemeService`, `DensityService`, `SpinePreferencesService`, textos (`SPINE_MESSAGES`, `provideSpineMessages`) |
|
|
284
286
|
| `@trismegisto/spine/shell` | `APP_ROUTES`, `PUBLIC_ROUTES`, `PRIVATE_ROUTES`, `provideShell`, telas de login e de estado |
|
|
285
287
|
| `@trismegisto/spine/routing` | `ComponentRoute`, `COMPONENT_ROUTES`, `ViewGuard`, `RouteIdInterceptor`, resolução de rota |
|
|
286
|
-
| `@trismegisto/spine/layout` | `LayoutComponent` (`spine-layout`), cabeçalho, menu lateral,
|
|
288
|
+
| `@trismegisto/spine/layout` | `LayoutComponent` (`spine-layout`), cabeçalho, menu lateral, configuração inicial, atalhos, configurações, novidades, documentação, `SpineLogoutService` |
|
|
287
289
|
| `@trismegisto/spine/modal` | `ModalService` (`open(config)` devolve um `Observable` com o resultado), `ModalContent` |
|
|
288
290
|
| `@trismegisto/spine/side-panel` | `SidePanelService`, `SidePanelContent` |
|
|
289
291
|
| `@trismegisto/spine/notification` | `NotificationService` (`addNotification`) |
|
|
290
|
-
| `@trismegisto/spine/auto-service` | `provideAutoService({ adminApi })`: telas prontas de autosserviço (início,
|
|
292
|
+
| `@trismegisto/spine/auto-service` | `provideAutoService({ adminApi })`: telas prontas de autosserviço (início, novidades, sugestões e avaliações na raiz do menu; grupos, aplicações e cliente em Administração; ações e tentativas de login em Auditoria), sempre carregadas sob demanda (`AUTO_SERVICE_SCREENS`); as classes das telas não são exportadas. Cada tela já vem com a documentação (manual de uso e especificação técnica, em `AUTO_SERVICE_DOCUMENTS`, também sob demanda) no botão de documentação da página. `AutoServiceService` (com os tipos de grupo `AutoServiceGroup`, `AutoServiceGroupType`, `AutoServiceGroupCreate`, `AutoServiceGroupUpdate`) e o tipo `AutoServiceClient` são exportados |
|
|
291
293
|
| `@trismegisto/spine/audit` | As telas de auditoria — Ações (`SpineAuditComponent`, `spine-audit`) e Tentativas de login (`SpineLoginAttemptsComponent`, `spine-login-attempts`) —, as mesmas para o painel central (`scope="admin"`) e o auto-serviço (`scope="client"`), + `SpineAuditService` e os tipos/funções puras (`AuditEvent`, `AuditQuery`, `buildAuditQuery`, `auditApiBase`…). **Exporta o componente: importe só de código lazy** |
|
|
292
294
|
| `@trismegisto/spine/graphql` | `provideGraphql({ endpoints })`: um cliente Apollo nomeado por backend |
|
|
293
295
|
|
|
@@ -345,7 +347,13 @@ component: async () => (await import('./admin-audit-page.component')).AdminAudit
|
|
|
345
347
|
- **Idioma:** os textos do spine seguem `HermesI18nService.setLocale` (o idioma em si se configura com
|
|
346
348
|
`provideHermesI18n` do `@trismegisto/angular`). `provideSpineMessages({ override, catalogs })` só sobrescreve
|
|
347
349
|
textos ou acrescenta idiomas.
|
|
348
|
-
- **Ícones do app**
|
|
350
|
+
- **Ícones do app** (desde a 3.3.1): o `provideUi` registra no bootstrap só `SPINE_ICONS` (os ícones do próprio spine,
|
|
351
|
+
gerados por `scripts/generate-spine-icons.mjs` no build) e carrega o catálogo inteiro sob demanda. Ícone do app que
|
|
352
|
+
ninguém registrou (o da rota vinda do servidor, um `[icon]` de botão) aparece quando o chunk chega: até lá, o
|
|
353
|
+
genérico. Para desenhar já no primeiro quadro, registre os do app com `provideHermesIcons(...)`.
|
|
354
|
+
- **Detecção de mudanças:** todos os componentes do spine são `OnPush`. O componente que o app abre no
|
|
355
|
+
`ModalService`/`SidePanelService` (como as telas no layout) precisa atualizar a view por signal, `async` ou
|
|
356
|
+
`markForCheck()` quando o estado muda depois de `await`/`subscribe`.
|
|
349
357
|
- `ModalService`/`SidePanelService` abrem um componente que implementa `ModalContent` (`getResult()`,
|
|
350
358
|
`isValid?()`). Para diálogos fora do shell, o `@trismegisto/angular` tem `HermesDialogService`.
|
|
351
359
|
|
|
@@ -3,7 +3,7 @@ import * as i0 from '@angular/core';
|
|
|
3
3
|
import { inject, Injectable, input, output, computed, ChangeDetectionStrategy, Component, signal, effect, untracked, DestroyRef } from '@angular/core';
|
|
4
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, CardComponent, ComboboxComponent, DateRangePickerComponent, InputComponent } from '@trismegisto/angular';
|
|
6
|
+
import { IconButtonComponent, SkeletonComponent, SpinnerComponent, AlertComponent, ButtonComponent, DataGridCellDirective, DataGridComponent, DrawerComponent, FilterBarComponent, ProgressComponent, SegmentedControlComponent, StatCardComponent, StateScreenComponent, SwitchComponent, TagComponent, AccountAvatarComponent, 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';
|
|
@@ -462,8 +462,10 @@ class SpineAuditTopCardComponent {
|
|
|
462
462
|
@if (failed()) {
|
|
463
463
|
<p class="muted top__error" role="status">{{ text.topsError }}</p>
|
|
464
464
|
}
|
|
465
|
+
} @else if (loading()) {
|
|
466
|
+
<hm-skeleton shape="list" [count]="pageSize()" [label]="text.loading" />
|
|
465
467
|
} @else {
|
|
466
|
-
<p class="muted">{{
|
|
468
|
+
<p class="muted">{{ failed() ? text.topsError : text.noTop }}</p>
|
|
467
469
|
}
|
|
468
470
|
@if (total() > 0) {
|
|
469
471
|
<div class="top__pager">
|
|
@@ -479,11 +481,11 @@ class SpineAuditTopCardComponent {
|
|
|
479
481
|
</div>
|
|
480
482
|
}
|
|
481
483
|
</section>
|
|
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 }); }
|
|
484
|
+
`, 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-raised)}.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: SkeletonComponent, selector: "hm-skeleton", inputs: ["shape", "count", "label"] }, { kind: "component", type: SpinnerComponent, selector: "hm-spinner", inputs: ["size", "inverse", "label", "labelVisible", "decorative", "done", "doneLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
483
485
|
}
|
|
484
486
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAuditTopCardComponent, decorators: [{
|
|
485
487
|
type: Component,
|
|
486
|
-
args: [{ selector: 'spine-audit-top-card', imports: [IconButtonComponent, SpinnerComponent], template: `
|
|
488
|
+
args: [{ selector: 'spine-audit-top-card', imports: [IconButtonComponent, SkeletonComponent, SpinnerComponent], template: `
|
|
487
489
|
<section class="card card-block top" [attr.aria-labelledby]="headingId" [attr.aria-busy]="loading()">
|
|
488
490
|
<div class="top__head">
|
|
489
491
|
<h3 class="top__title" [id]="headingId">{{ heading() }}</h3>
|
|
@@ -520,8 +522,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
520
522
|
@if (failed()) {
|
|
521
523
|
<p class="muted top__error" role="status">{{ text.topsError }}</p>
|
|
522
524
|
}
|
|
525
|
+
} @else if (loading()) {
|
|
526
|
+
<hm-skeleton shape="list" [count]="pageSize()" [label]="text.loading" />
|
|
523
527
|
} @else {
|
|
524
|
-
<p class="muted">{{
|
|
528
|
+
<p class="muted">{{ failed() ? text.topsError : text.noTop }}</p>
|
|
525
529
|
}
|
|
526
530
|
@if (total() > 0) {
|
|
527
531
|
<div class="top__pager">
|
|
@@ -537,7 +541,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
537
541
|
</div>
|
|
538
542
|
}
|
|
539
543
|
</section>
|
|
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"] }]
|
|
544
|
+
`, 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-raised)}.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"] }]
|
|
541
545
|
}], 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"] }] } });
|
|
542
546
|
|
|
543
547
|
/** Eventos por página da tabela, de saída (o servidor aceita até 200 — `AUDIT_MAX_PAGE_SIZE`). */
|
|
@@ -1329,15 +1333,15 @@ class SpineAuditComponent {
|
|
|
1329
1333
|
return outcome === 'success' ? 'success' : outcome === 'failure' ? 'danger' : outcome === 'denied' ? 'warning' : 'neutral';
|
|
1330
1334
|
}
|
|
1331
1335
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAuditComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
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 }); }
|
|
1336
|
+
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 if (loading()) {\n <hm-skeleton shape=\"card\" [label]=\"text.loading\" />\n } @else {\n <p class=\"muted\">{{ 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-raised)}.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", "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: SkeletonComponent, selector: "hm-skeleton", inputs: ["shape", "count", "label"] }, { 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", "labelPosition", "loading"], outputs: ["valueChange"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1333
1337
|
}
|
|
1334
1338
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAuditComponent, decorators: [{
|
|
1335
1339
|
type: Component,
|
|
1336
1340
|
args: [{ selector: 'spine-audit', imports: [
|
|
1337
1341
|
AlertComponent, ButtonComponent, DataGridCellDirective, DataGridComponent, DrawerComponent, FilterBarComponent,
|
|
1338
|
-
ProgressComponent, SegmentedControlComponent, SpineAuditTopCardComponent, StatCardComponent, StateScreenComponent,
|
|
1342
|
+
ProgressComponent, SegmentedControlComponent, SkeletonComponent, SpineAuditTopCardComponent, StatCardComponent, StateScreenComponent,
|
|
1339
1343
|
SwitchComponent, TagComponent,
|
|
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"] }]
|
|
1344
|
+
], 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 if (loading()) {\n <hm-skeleton shape=\"card\" [label]=\"text.loading\" />\n } @else {\n <p class=\"muted\">{{ 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-raised)}.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"] }]
|
|
1341
1345
|
}], 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 }] }] } });
|
|
1342
1346
|
|
|
1343
1347
|
/** Tentativas por página, de saída (o servidor aceita até 200). */
|
|
@@ -1524,14 +1528,14 @@ class SpineLoginAttemptsComponent {
|
|
|
1524
1528
|
return why ? formatMessage(this.text.loadErrorDetail, { detail: why }) : this.text.loadError;
|
|
1525
1529
|
}
|
|
1526
1530
|
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 }); }
|
|
1531
|
+
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__person\">\n <hm-account-avatar size=\"small\" [name]=\"a.user_name || a.user_username || ''\" [imageUrl]=\"a.user_picture\" />\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 </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__person{display:inline-flex;align-items:center;gap:var(--hm-space-sm);min-width:0}.attempts__person hm-account-avatar{flex:none}.attempts__user{display:inline-flex;flex-direction:column;min-width:0;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: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading"] }, { 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", "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", "size", "labelHidden", "clearable", "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", "label", "labelHidden", "hint", "min", "max", "valueFormat", "presets"], outputs: ["valueChange"] }, { 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
1532
|
}
|
|
1529
1533
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineLoginAttemptsComponent, decorators: [{
|
|
1530
1534
|
type: Component,
|
|
1531
1535
|
args: [{ selector: 'spine-login-attempts', imports: [
|
|
1532
|
-
ReactiveFormsModule, AlertComponent, ButtonComponent, CardComponent, ComboboxComponent, DataGridCellDirective, DataGridComponent,
|
|
1536
|
+
ReactiveFormsModule, AccountAvatarComponent, AlertComponent, ButtonComponent, CardComponent, ComboboxComponent, DataGridCellDirective, DataGridComponent,
|
|
1533
1537
|
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=\"
|
|
1538
|
+
], 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__person\">\n <hm-account-avatar size=\"small\" [name]=\"a.user_name || a.user_username || ''\" [imageUrl]=\"a.user_picture\" />\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 </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__person{display:inline-flex;align-items:center;gap:var(--hm-space-sm);min-width:0}.attempts__person hm-account-avatar{flex:none}.attempts__user{display:inline-flex;flex-direction:column;min-width:0;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
1539
|
}], 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
1540
|
|
|
1537
1541
|
/*
|