rl-core-front 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/_services/api/schema.d.ts +238 -117
- package/src/_utils/filter.ts +15 -34
- package/src/components/app-shell.tsx +51 -1
- package/src/components/brand-header.tsx +23 -8
- package/src/components/brand-panel.tsx +41 -0
- package/src/components/index.ts +1 -0
- package/src/components/ui/button.tsx +5 -0
- package/src/components/ui/confirm-dialog.tsx +3 -1
- package/src/components/ui/data-table.tsx +13 -4
- package/src/components/ui/dialog.tsx +120 -21
- package/src/components/ui/filter-sheet.tsx +47 -25
- package/src/components/ui/index.ts +1 -1
- package/src/components/ui/segmented-control.tsx +74 -0
- package/src/contexts/brand-context.tsx +15 -0
- package/src/contexts/i18n-context.tsx +69 -4
- package/src/features/audit/audit-screen.tsx +13 -140
- package/src/features/audit/components/audit-diff.tsx +156 -0
- package/src/features/audit/components/index.ts +3 -0
- package/src/features/audit/components/record-audit-button.tsx +176 -0
- package/src/features/audit/enums/audit-data-action.enum.ts +20 -0
- package/src/features/audit/hooks/use-audit-trail.ts +9 -2
- package/src/features/audit/services/audit.service.ts +43 -1
- package/src/features/login/login-screen.tsx +86 -40
- package/src/features/logs/enums/error-type.enum.ts +21 -0
- package/src/features/logs/enums/request-outcome.enum.ts +14 -0
- package/src/features/logs/hooks/use-request-logs.ts +26 -3
- package/src/features/logs/logs-screen.tsx +2 -14
- package/src/features/logs/panels/index.ts +0 -1
- package/src/features/logs/panels/request-logs-panel.tsx +177 -25
- package/src/features/logs/services/logs.service.ts +13 -51
- package/src/features/notifications/components/notification-item.tsx +10 -8
- package/src/features/notifications/enums/notification-type.enum.ts +8 -0
- package/src/features/profile/profile-screen.tsx +2 -1
- package/src/features/rbac/panels/groups-panel.tsx +20 -7
- package/src/features/users/components/user-form-dialog.tsx +16 -7
- package/src/features/users/hooks/use-users.ts +9 -2
- package/src/hooks/use-filters.ts +25 -2
- package/src/hooks/use-list-query.ts +14 -1
- package/src/i18n/messages/en.ts +17 -5
- package/src/i18n/messages/pt.ts +18 -5
- package/src/index.ts +12 -1
- package/src/components/ui/filter-chips.tsx +0 -88
- package/src/features/logs/hooks/use-error-logs.ts +0 -65
- package/src/features/logs/panels/error-logs-panel.tsx +0 -183
|
@@ -1,68 +1,30 @@
|
|
|
1
1
|
import { api } from "#core/_services/api/axios.factory";
|
|
2
2
|
import type { ListQuery } from "#core/_services/api/list-query";
|
|
3
3
|
import { toListParams } from "#core/_services/api/list-query";
|
|
4
|
+
import type { components } from "#core/_services/api/schema";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
| "NOT_FOUND"
|
|
9
|
-
| "RATE_LIMIT"
|
|
10
|
-
| "DATABASE"
|
|
11
|
-
| "INTEGRATION"
|
|
12
|
-
| "INTERNAL";
|
|
13
|
-
|
|
14
|
-
export interface LogUserRef {
|
|
15
|
-
id: string;
|
|
16
|
-
name: string;
|
|
17
|
-
email: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface RequestLog {
|
|
21
|
-
id: string;
|
|
22
|
-
createdAt: string;
|
|
23
|
-
userId?: string | null;
|
|
24
|
-
action: string;
|
|
25
|
-
method?: string | null;
|
|
26
|
-
path?: string | null;
|
|
27
|
-
ipAddress?: string | null;
|
|
28
|
-
statusCode?: number | null;
|
|
29
|
-
durationMs?: number | null;
|
|
30
|
-
requestId?: string | null;
|
|
31
|
-
user?: LogUserRef | null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface ErrorLog {
|
|
35
|
-
id: string;
|
|
36
|
-
createdAt: string;
|
|
37
|
-
statusCode?: number | null;
|
|
38
|
-
type: ErrorType;
|
|
39
|
-
message: string;
|
|
40
|
-
stack?: string | null;
|
|
41
|
-
method?: string | null;
|
|
42
|
-
path?: string | null;
|
|
43
|
-
userId?: string | null;
|
|
44
|
-
ipAddress?: string | null;
|
|
45
|
-
requestId?: string | null;
|
|
46
|
-
user?: LogUserRef | null;
|
|
47
|
-
}
|
|
6
|
+
/** Tipos derivados do `openapi.json` (§5) — não redigitar a forma da resposta. */
|
|
7
|
+
export type RequestLog = components["schemas"]["RequestLogResponse"];
|
|
8
|
+
export type LogUserRef = components["schemas"]["AuditUserRefResponse"];
|
|
48
9
|
|
|
49
10
|
export interface Paginated<T> {
|
|
50
11
|
items: T[];
|
|
51
|
-
meta:
|
|
12
|
+
meta: components["schemas"]["PaginationMetaResponse"];
|
|
52
13
|
}
|
|
53
14
|
|
|
54
15
|
export const logsService = {
|
|
16
|
+
/**
|
|
17
|
+
* Log de requisição — uma rota só.
|
|
18
|
+
*
|
|
19
|
+
* Antes eram `/audit/changes` e `/audit/errors`, duas tabelas com nove
|
|
20
|
+
* colunas iguais. Agora é uma linha por requisição, e a distinção entre
|
|
21
|
+
* sucesso e erro é o filtro `outcome`.
|
|
22
|
+
*/
|
|
55
23
|
listRequests(query: ListQuery) {
|
|
56
24
|
return api
|
|
57
|
-
.get<Paginated<RequestLog>>("/audit/
|
|
25
|
+
.get<Paginated<RequestLog>>("/audit/requests", {
|
|
58
26
|
params: toListParams(query),
|
|
59
27
|
})
|
|
60
28
|
.then((r) => r.data);
|
|
61
29
|
},
|
|
62
|
-
|
|
63
|
-
listErrors(query: ListQuery) {
|
|
64
|
-
return api
|
|
65
|
-
.get<Paginated<ErrorLog>>("/audit/errors", { params: toListParams(query) })
|
|
66
|
-
.then((r) => r.data);
|
|
67
|
-
},
|
|
68
30
|
};
|
|
@@ -5,17 +5,19 @@ import type { JSX } from "react";
|
|
|
5
5
|
|
|
6
6
|
import { formatDateTime } from "#core/_utils/format";
|
|
7
7
|
import { useI18n } from "#core/contexts";
|
|
8
|
-
import type
|
|
9
|
-
|
|
10
|
-
UserNotification,
|
|
11
|
-
} from "#core/features/notifications/services/notifications.service";
|
|
8
|
+
import { NotificationType } from "#core/features/notifications/enums/notification-type.enum";
|
|
9
|
+
import type { UserNotification } from "#core/features/notifications/services/notifications.service";
|
|
12
10
|
import { cn } from "#core/lib/utils";
|
|
13
11
|
|
|
14
12
|
const ICONS: Record<NotificationType, JSX.Element> = {
|
|
15
|
-
INFO: <Info className="h-4 w-4 text-sky-500" />,
|
|
16
|
-
SUCCESS:
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
[NotificationType.INFO]: <Info className="h-4 w-4 text-sky-500" />,
|
|
14
|
+
[NotificationType.SUCCESS]: (
|
|
15
|
+
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
|
|
16
|
+
),
|
|
17
|
+
[NotificationType.WARNING]: (
|
|
18
|
+
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
|
19
|
+
),
|
|
20
|
+
[NotificationType.ERROR]: <XCircle className="h-4 w-4 text-destructive" />,
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
export interface NotificationItemProps {
|
|
@@ -339,7 +339,8 @@ export function ProfileScreen(): JSX.Element {
|
|
|
339
339
|
|
|
340
340
|
{/* diálogo: pede código TOTP para regenerar backup codes */}
|
|
341
341
|
<Dialog open={regenOpen} onOpenChange={(o) => !o && setRegenOpen(false)}>
|
|
342
|
-
|
|
342
|
+
{/* Confirmação: um campo de seis dígitos não tem o que ampliar. */}
|
|
343
|
+
<DialogContent className="max-w-sm" expandable={false}>
|
|
343
344
|
<DialogHeader>
|
|
344
345
|
<DialogTitle>{t("auth.backupTitle")}</DialogTitle>
|
|
345
346
|
</DialogHeader>
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
SelectValue,
|
|
28
28
|
} from "#core/components/ui";
|
|
29
29
|
import { useAuth, useI18n } from "#core/contexts";
|
|
30
|
+
import { RecordAuditButton } from "#core/features/audit/components";
|
|
30
31
|
import {
|
|
31
32
|
Group,
|
|
32
33
|
rbacService,
|
|
@@ -251,13 +252,25 @@ export function GroupsPanel(): JSX.Element {
|
|
|
251
252
|
))}
|
|
252
253
|
</div>
|
|
253
254
|
</div>
|
|
254
|
-
<DialogFooter className="gap-2">
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
</
|
|
255
|
+
<DialogFooter className="gap-2 sm:justify-between">
|
|
256
|
+
{/* Só na edição: registro que ainda não existe não tem histórico. */}
|
|
257
|
+
<div>
|
|
258
|
+
{editing && (
|
|
259
|
+
<RecordAuditButton entity="Group" entityId={editing.id} />
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
<div className="flex gap-2">
|
|
263
|
+
<Button
|
|
264
|
+
type="button"
|
|
265
|
+
variant="outline"
|
|
266
|
+
onClick={() => setOpen(false)}
|
|
267
|
+
>
|
|
268
|
+
{t("common.cancel")}
|
|
269
|
+
</Button>
|
|
270
|
+
<Button type="submit" disabled={loading}>
|
|
271
|
+
{t("common.save")}
|
|
272
|
+
</Button>
|
|
273
|
+
</div>
|
|
261
274
|
</DialogFooter>
|
|
262
275
|
</DialogForm>
|
|
263
276
|
</DialogContent>
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
Label,
|
|
23
23
|
} from "#core/components/ui";
|
|
24
24
|
import { useI18n } from "#core/contexts";
|
|
25
|
+
import { RecordAuditButton } from "#core/features/audit/components";
|
|
25
26
|
import {
|
|
26
27
|
CreateUserPayload,
|
|
27
28
|
Role,
|
|
@@ -172,13 +173,21 @@ export function UserFormDialog({
|
|
|
172
173
|
/>
|
|
173
174
|
</div>
|
|
174
175
|
|
|
175
|
-
<DialogFooter className="gap-2">
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
</
|
|
176
|
+
<DialogFooter className="gap-2 sm:justify-between">
|
|
177
|
+
{/* Só na edição: registro que ainda não existe não tem histórico. */}
|
|
178
|
+
<div>
|
|
179
|
+
{editing && (
|
|
180
|
+
<RecordAuditButton entity="User" entityId={editing.id} />
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
<div className="flex gap-2">
|
|
184
|
+
<Button type="button" variant="outline" onClick={onClose}>
|
|
185
|
+
{t("common.cancel")}
|
|
186
|
+
</Button>
|
|
187
|
+
<Button type="submit" disabled={loading}>
|
|
188
|
+
{isEdit ? t("common.save") : t("common.create")}
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
182
191
|
</DialogFooter>
|
|
183
192
|
</DialogForm>
|
|
184
193
|
</DialogContent>
|
|
@@ -68,9 +68,14 @@ export function useUsers(): UseUsersResult {
|
|
|
68
68
|
}
|
|
69
69
|
}, [run, list.query]);
|
|
70
70
|
|
|
71
|
+
// Não busca antes da URL ser lida: a query sairia sem o filtro, e essa
|
|
72
|
+
// resposta pode chegar depois da certa e pintar a tela com tudo.
|
|
71
73
|
useEffect(() => {
|
|
74
|
+
if (!list.ready) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
72
77
|
void fetch();
|
|
73
|
-
}, [fetch]);
|
|
78
|
+
}, [fetch, list.ready]);
|
|
74
79
|
|
|
75
80
|
useEffect(() => {
|
|
76
81
|
void run(() => usersService.listRoles()).then((r) => r && setRoles(r));
|
|
@@ -163,7 +168,9 @@ export function useUsers(): UseUsersResult {
|
|
|
163
168
|
page: list.page,
|
|
164
169
|
pageSize: list.pageSize,
|
|
165
170
|
sorting: list.sorting,
|
|
166
|
-
|
|
171
|
+
// Esperar o catálogo também é carregar: sem isso a tabela pisca
|
|
172
|
+
// "Nenhum registro" antes da primeira busca sair.
|
|
173
|
+
loading: loading || !list.ready,
|
|
167
174
|
acting,
|
|
168
175
|
setPage: list.setPage,
|
|
169
176
|
setPageSize: list.setPageSize,
|
package/src/hooks/use-filters.ts
CHANGED
|
@@ -32,6 +32,15 @@ export interface FiltersState {
|
|
|
32
32
|
validation: AdvancedValidation;
|
|
33
33
|
/** JSON pronto para a query — `undefined` quando não há filtro. */
|
|
34
34
|
filter: string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* A URL já foi lida e o `filter` abaixo é o definitivo.
|
|
37
|
+
*
|
|
38
|
+
* Antes disso `filter` é `undefined` porque ninguém leu a querystring ainda —
|
|
39
|
+
* e não porque a listagem esteja sem filtro. Quem busca precisa esperar, ou
|
|
40
|
+
* dispara uma consulta sem filtro cuja resposta pode chegar depois da certa e
|
|
41
|
+
* pintar a tela com tudo.
|
|
42
|
+
*/
|
|
43
|
+
hydrated: boolean;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
const QUERY_PARAM = "filter";
|
|
@@ -57,8 +66,16 @@ const writeToUrl = (encoded: string | undefined): void => {
|
|
|
57
66
|
* Espelhar na URL faz o filtro sobreviver ao F5 e virar link compartilhável.
|
|
58
67
|
* O espelho é `replaceState` e não `router.replace`: o filtro não é navegação,
|
|
59
68
|
* e trocá-lo não deve empilhar histórico nem refazer o render da rota.
|
|
69
|
+
*
|
|
70
|
+
* `defaultValues` é o filtro de quem chega sem `?filter=` na URL — a listagem
|
|
71
|
+
* abre já filtrada. Vale só na hidratação: link com filtro manda, e limpar os
|
|
72
|
+
* filtros continua limpando (o padrão volta no F5 seguinte, que é o que
|
|
73
|
+
* "padrão" quer dizer).
|
|
60
74
|
*/
|
|
61
|
-
export function useFilters(
|
|
75
|
+
export function useFilters(
|
|
76
|
+
schemaUrl: string,
|
|
77
|
+
defaultValues?: FilterValues,
|
|
78
|
+
): FiltersState {
|
|
62
79
|
const { schema, loading } = useFilterSchema(schemaUrl);
|
|
63
80
|
|
|
64
81
|
const [mode, setModeState] = useState<FilterMode>("simple");
|
|
@@ -82,9 +99,14 @@ export function useFilters(schemaUrl: string): FiltersState {
|
|
|
82
99
|
}
|
|
83
100
|
} else if (raw) {
|
|
84
101
|
setValuesState(parseFilterValues(raw, schema));
|
|
102
|
+
} else if (defaultValues) {
|
|
103
|
+
// Espelha na URL como qualquer outro filtro: sem isso a tela mostraria
|
|
104
|
+
// um estado que o link não carrega.
|
|
105
|
+
setValuesState(defaultValues);
|
|
106
|
+
writeToUrl(buildFilter(defaultValues, schema));
|
|
85
107
|
}
|
|
86
108
|
setHydrated(true);
|
|
87
|
-
}, [hydrated, loading, schema]);
|
|
109
|
+
}, [hydrated, loading, schema, defaultValues]);
|
|
88
110
|
|
|
89
111
|
const setValues = useCallback(
|
|
90
112
|
(next: FilterValues): void => {
|
|
@@ -144,5 +166,6 @@ export function useFilters(schemaUrl: string): FiltersState {
|
|
|
144
166
|
setTree,
|
|
145
167
|
validation,
|
|
146
168
|
filter,
|
|
169
|
+
hydrated,
|
|
147
170
|
};
|
|
148
171
|
}
|
|
@@ -5,12 +5,22 @@ import type { Dispatch, SetStateAction } from "react";
|
|
|
5
5
|
import { useEffect, useMemo, useState } from "react";
|
|
6
6
|
|
|
7
7
|
import type { ListQuery } from "#core/_services/api/list-query";
|
|
8
|
+
import type { FilterValues } from "#core/_utils/filter";
|
|
8
9
|
import type { FiltersState } from "#core/hooks/use-filters";
|
|
9
10
|
import { useFilters } from "#core/hooks/use-filters";
|
|
10
11
|
|
|
11
12
|
export interface UseListQueryResult {
|
|
12
13
|
/** Pronta para o `.service.ts` da feature. */
|
|
13
14
|
query: ListQuery;
|
|
15
|
+
/**
|
|
16
|
+
* A `query` já vale — **o hook da feature só pode buscar depois disso**.
|
|
17
|
+
*
|
|
18
|
+
* Enquanto é `false`, o filtro da URL ainda não foi lido e a `query` sairia
|
|
19
|
+
* sem ele. Buscar assim mesmo faz duas requisições por carregamento, e a
|
|
20
|
+
* errada pode chegar por último: a tela abre mostrando tudo com o filtro
|
|
21
|
+
* marcado na barra.
|
|
22
|
+
*/
|
|
23
|
+
ready: boolean;
|
|
14
24
|
page: number;
|
|
15
25
|
pageSize: number;
|
|
16
26
|
sorting: SortingState;
|
|
@@ -30,8 +40,10 @@ export interface UseListQueryResult {
|
|
|
30
40
|
export function useListQuery(
|
|
31
41
|
schemaUrl: string,
|
|
32
42
|
initialPageSize = 10,
|
|
43
|
+
/** Filtro de quem chega sem `?filter=` — a listagem abre já filtrada. */
|
|
44
|
+
defaultFilterValues?: FilterValues,
|
|
33
45
|
): UseListQueryResult {
|
|
34
|
-
const filters = useFilters(schemaUrl);
|
|
46
|
+
const filters = useFilters(schemaUrl, defaultFilterValues);
|
|
35
47
|
const { filter } = filters;
|
|
36
48
|
|
|
37
49
|
const [page, setPage] = useState(0);
|
|
@@ -58,6 +70,7 @@ export function useListQuery(
|
|
|
58
70
|
|
|
59
71
|
return {
|
|
60
72
|
query,
|
|
73
|
+
ready: filters.hydrated,
|
|
61
74
|
page,
|
|
62
75
|
pageSize,
|
|
63
76
|
sorting,
|
package/src/i18n/messages/en.ts
CHANGED
|
@@ -13,11 +13,20 @@ export const en: Messages = {
|
|
|
13
13
|
audit: "Audit",
|
|
14
14
|
},
|
|
15
15
|
logs: {
|
|
16
|
+
outcomeSuccess: "Success",
|
|
17
|
+
outcomeError: "Error",
|
|
18
|
+
outcomeAborted: "Aborted",
|
|
19
|
+
errorValidation: "Validation",
|
|
20
|
+
errorAuth: "Authentication",
|
|
21
|
+
errorNotFound: "Not found",
|
|
22
|
+
errorRateLimit: "Rate limit",
|
|
23
|
+
errorDatabase: "Database",
|
|
24
|
+
errorIntegration: "Integration",
|
|
25
|
+
errorInternal: "Internal",
|
|
26
|
+
outcome: "Outcome",
|
|
16
27
|
title: "Logs",
|
|
17
28
|
subtitle:
|
|
18
|
-
"
|
|
19
|
-
tabRequests: "Requests",
|
|
20
|
-
tabErrors: "Errors",
|
|
29
|
+
"Errors, slow requests and auditable actions — route, status and duration.",
|
|
21
30
|
date: "Date/time",
|
|
22
31
|
route: "Route",
|
|
23
32
|
status: "Status",
|
|
@@ -29,10 +38,14 @@ export const en: Messages = {
|
|
|
29
38
|
type: "Type",
|
|
30
39
|
message: "Message",
|
|
31
40
|
viewDetails: "View details",
|
|
32
|
-
detailsTitle: "
|
|
41
|
+
detailsTitle: "Request details",
|
|
42
|
+
errorCode: "Error code",
|
|
43
|
+
userAgent: "User agent",
|
|
33
44
|
stackTrace: "Stack trace",
|
|
34
45
|
},
|
|
35
46
|
auditTrail: {
|
|
47
|
+
recordHistory: "Change history",
|
|
48
|
+
noRecordHistory: "This record has no changes yet.",
|
|
36
49
|
title: "Audit Trail",
|
|
37
50
|
subtitle: "Data change history — who changed what, and when.",
|
|
38
51
|
entity: "Entity",
|
|
@@ -87,7 +100,6 @@ export const en: Messages = {
|
|
|
87
100
|
hint: "Fill in only the fields you want to use. All of them are combined with AND.",
|
|
88
101
|
apply: "Apply",
|
|
89
102
|
clear: "Clear",
|
|
90
|
-
clearAll: "Clear all",
|
|
91
103
|
empty: "This listing has no filters yet.",
|
|
92
104
|
noOptions: "No options available",
|
|
93
105
|
all: "All",
|
package/src/i18n/messages/pt.ts
CHANGED
|
@@ -11,10 +11,20 @@ export const pt = {
|
|
|
11
11
|
audit: "Auditoria",
|
|
12
12
|
},
|
|
13
13
|
logs: {
|
|
14
|
+
outcomeSuccess: "Sucesso",
|
|
15
|
+
outcomeError: "Erro",
|
|
16
|
+
outcomeAborted: "Abortada",
|
|
17
|
+
errorValidation: "Validação",
|
|
18
|
+
errorAuth: "Autenticação",
|
|
19
|
+
errorNotFound: "Não encontrado",
|
|
20
|
+
errorRateLimit: "Limite de requisições",
|
|
21
|
+
errorDatabase: "Banco de dados",
|
|
22
|
+
errorIntegration: "Integração",
|
|
23
|
+
errorInternal: "Interno",
|
|
24
|
+
outcome: "Desfecho",
|
|
14
25
|
title: "Logs",
|
|
15
|
-
subtitle:
|
|
16
|
-
|
|
17
|
-
tabErrors: "Erros",
|
|
26
|
+
subtitle:
|
|
27
|
+
"Erros, requisições lentas e ações auditáveis — rota, status e duração.",
|
|
18
28
|
date: "Data/hora",
|
|
19
29
|
route: "Rota",
|
|
20
30
|
status: "Status",
|
|
@@ -26,10 +36,14 @@ export const pt = {
|
|
|
26
36
|
type: "Tipo",
|
|
27
37
|
message: "Mensagem",
|
|
28
38
|
viewDetails: "Ver detalhes",
|
|
29
|
-
detailsTitle: "Detalhes
|
|
39
|
+
detailsTitle: "Detalhes da requisição",
|
|
40
|
+
errorCode: "Código do erro",
|
|
41
|
+
userAgent: "User agent",
|
|
30
42
|
stackTrace: "Stack trace",
|
|
31
43
|
},
|
|
32
44
|
auditTrail: {
|
|
45
|
+
recordHistory: "Histórico de alterações",
|
|
46
|
+
noRecordHistory: "Este registro ainda não sofreu alterações.",
|
|
33
47
|
title: "Trilha de Auditoria",
|
|
34
48
|
subtitle: "Histórico de alterações de dados — quem mudou o quê, e quando.",
|
|
35
49
|
entity: "Entidade",
|
|
@@ -88,7 +102,6 @@ export const pt = {
|
|
|
88
102
|
hint: "Preencha só os campos que quiser usar. Tudo é combinado com E.",
|
|
89
103
|
apply: "Aplicar",
|
|
90
104
|
clear: "Limpar",
|
|
91
|
-
clearAll: "Limpar tudo",
|
|
92
105
|
empty: "Esta listagem ainda não tem filtros.",
|
|
93
106
|
noOptions: "Nenhuma opção disponível",
|
|
94
107
|
all: "Todos",
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,15 @@ export { UsersScreen } from "#core/features/users/users-screen";
|
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
27
|
export * from "#core/components";
|
|
28
28
|
export * from "#core/contexts";
|
|
29
|
+
// Histórico de alterações de um registro, para a tela de edição do projeto.
|
|
30
|
+
export type {
|
|
31
|
+
AuditDiffProps,
|
|
32
|
+
RecordAuditButtonProps,
|
|
33
|
+
} from "#core/features/audit/components";
|
|
34
|
+
export {
|
|
35
|
+
AuditDiff,
|
|
36
|
+
RecordAuditButton,
|
|
37
|
+
} from "#core/features/audit/components";
|
|
29
38
|
|
|
30
39
|
// ---------------------------------------------------------------------------
|
|
31
40
|
// Primitivos de UI — agnósticos de domínio
|
|
@@ -47,7 +56,9 @@ export { useFilters } from "#core/hooks/use-filters";
|
|
|
47
56
|
export type { UseListQueryResult } from "#core/hooks/use-list-query";
|
|
48
57
|
export { useListQuery } from "#core/hooks/use-list-query";
|
|
49
58
|
export type { RunOptions, UseRequestResult } from "#core/hooks/use-request";
|
|
50
|
-
|
|
59
|
+
// RequestOperation é enum (valor, não só tipo): sem ele o projeto não consegue
|
|
60
|
+
// pedir o toast padrão de criado/alterado/removido que o core já traduz.
|
|
61
|
+
export { RequestOperation, useRequest } from "#core/hooks/use-request";
|
|
51
62
|
|
|
52
63
|
// ---------------------------------------------------------------------------
|
|
53
64
|
// HTTP e traduções
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { SlidersHorizontal, X } from "lucide-react";
|
|
4
|
-
import type { JSX } from "react";
|
|
5
|
-
|
|
6
|
-
import { countConditions } from "#core/_utils/advanced-filter";
|
|
7
|
-
import { describeValue, hasValue } from "#core/_utils/filter";
|
|
8
|
-
import { Button } from "#core/components/ui/button";
|
|
9
|
-
import { useFieldLabel } from "#core/components/ui/filter-sheet";
|
|
10
|
-
import { useI18n } from "#core/contexts";
|
|
11
|
-
import type { FiltersState } from "#core/hooks/use-filters";
|
|
12
|
-
|
|
13
|
-
export interface FilterChipsProps {
|
|
14
|
-
filters: FiltersState;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Filtros ativos em forma de chip, cada um removível.
|
|
19
|
-
*
|
|
20
|
-
* Existe para o usuário não precisar abrir o painel só para lembrar o que está
|
|
21
|
-
* filtrando — e para não olhar uma tabela vazia sem saber por quê. No modo
|
|
22
|
-
* avançado a expressão não cabe num chip por campo, então vira um resumo com a
|
|
23
|
-
* contagem de condições.
|
|
24
|
-
*/
|
|
25
|
-
export function FilterChips({ filters }: FilterChipsProps): JSX.Element | null {
|
|
26
|
-
const { t, locale } = useI18n();
|
|
27
|
-
const labelFor = useFieldLabel();
|
|
28
|
-
const { schema, values, mode, tree } = filters;
|
|
29
|
-
|
|
30
|
-
if (mode === "advanced") {
|
|
31
|
-
const total = tree ? countConditions(tree) : 0;
|
|
32
|
-
if (total === 0 || !filters.filter) {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
return (
|
|
36
|
-
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
37
|
-
<span className="flex items-center gap-1.5 rounded-full border border-border bg-muted/40 px-3 py-1 text-xs">
|
|
38
|
-
<SlidersHorizontal className="h-3 w-3" />
|
|
39
|
-
{t("filters.advancedSummary", { count: total })}
|
|
40
|
-
</span>
|
|
41
|
-
<Button variant="ghost" size="sm" onClick={() => filters.setMode("simple")}>
|
|
42
|
-
{t("filters.clearAll")}
|
|
43
|
-
</Button>
|
|
44
|
-
</div>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const active = schema.filter((field) => hasValue(values[field.field]));
|
|
49
|
-
if (active.length === 0) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const remove = (field: string): void => {
|
|
54
|
-
const next = { ...values };
|
|
55
|
-
delete next[field];
|
|
56
|
-
filters.setValues(next);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const labels = {
|
|
60
|
-
from: t("filters.fromChip"),
|
|
61
|
-
to: t("filters.toChip"),
|
|
62
|
-
yes: t("common.yes"),
|
|
63
|
-
no: t("common.no"),
|
|
64
|
-
locale,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
69
|
-
{active.map((field) => (
|
|
70
|
-
<button
|
|
71
|
-
key={field.field}
|
|
72
|
-
type="button"
|
|
73
|
-
onClick={() => remove(field.field)}
|
|
74
|
-
className="flex items-center gap-1.5 rounded-full border border-border bg-muted/40 px-3 py-1 text-xs transition-colors hover:bg-muted"
|
|
75
|
-
>
|
|
76
|
-
<span className="text-muted-foreground">{labelFor(field)}:</span>
|
|
77
|
-
<span className="font-medium">
|
|
78
|
-
{describeValue(field, values[field.field], labels)}
|
|
79
|
-
</span>
|
|
80
|
-
<X className="h-3 w-3" />
|
|
81
|
-
</button>
|
|
82
|
-
))}
|
|
83
|
-
<Button variant="ghost" size="sm" onClick={() => filters.setValues({})}>
|
|
84
|
-
{t("filters.clearAll")}
|
|
85
|
-
</Button>
|
|
86
|
-
</div>
|
|
87
|
-
);
|
|
88
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import type { SortingState } from "@tanstack/react-table";
|
|
4
|
-
import {
|
|
5
|
-
Dispatch,
|
|
6
|
-
SetStateAction,
|
|
7
|
-
useCallback,
|
|
8
|
-
useEffect,
|
|
9
|
-
useState,
|
|
10
|
-
} from "react";
|
|
11
|
-
|
|
12
|
-
import { ErrorLog, logsService } from "#core/features/logs/services/logs.service";
|
|
13
|
-
import type { FiltersState } from "#core/hooks/use-filters";
|
|
14
|
-
import { useListQuery } from "#core/hooks/use-list-query";
|
|
15
|
-
import { useRequest } from "#core/hooks/use-request";
|
|
16
|
-
|
|
17
|
-
export interface UseErrorLogsResult {
|
|
18
|
-
rows: ErrorLog[];
|
|
19
|
-
total: number;
|
|
20
|
-
page: number;
|
|
21
|
-
pageSize: number;
|
|
22
|
-
sorting: SortingState;
|
|
23
|
-
loading: boolean;
|
|
24
|
-
setPage: Dispatch<SetStateAction<number>>;
|
|
25
|
-
setPageSize: Dispatch<SetStateAction<number>>;
|
|
26
|
-
setSorting: Dispatch<SetStateAction<SortingState>>;
|
|
27
|
-
/** Filtros dinâmicos — repassar para o `DataTable`. */
|
|
28
|
-
filters: FiltersState;
|
|
29
|
-
refresh: () => Promise<void>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function useErrorLogs(): UseErrorLogsResult {
|
|
33
|
-
const { run, loading } = useRequest();
|
|
34
|
-
|
|
35
|
-
const list = useListQuery("/audit/errors/filter-schema", 20);
|
|
36
|
-
|
|
37
|
-
const [rows, setRows] = useState<ErrorLog[]>([]);
|
|
38
|
-
const [total, setTotal] = useState(0);
|
|
39
|
-
|
|
40
|
-
const fetch = useCallback(async () => {
|
|
41
|
-
const res = await run(() => logsService.listErrors(list.query));
|
|
42
|
-
if (res) {
|
|
43
|
-
setRows(res.items);
|
|
44
|
-
setTotal(res.meta.total);
|
|
45
|
-
}
|
|
46
|
-
}, [run, list.query]);
|
|
47
|
-
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
void fetch();
|
|
50
|
-
}, [fetch]);
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
rows,
|
|
54
|
-
total,
|
|
55
|
-
page: list.page,
|
|
56
|
-
pageSize: list.pageSize,
|
|
57
|
-
sorting: list.sorting,
|
|
58
|
-
loading,
|
|
59
|
-
setPage: list.setPage,
|
|
60
|
-
setPageSize: list.setPageSize,
|
|
61
|
-
setSorting: list.setSorting,
|
|
62
|
-
filters: list.filters,
|
|
63
|
-
refresh: fetch,
|
|
64
|
-
};
|
|
65
|
-
}
|