rl-core-front 0.8.0 → 0.9.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/components/ui/data-table.tsx +7 -4
- package/src/features/audit/audit-screen.tsx +0 -2
- package/src/features/logs/panels/request-logs-panel.tsx +0 -3
- package/src/features/queues/hooks/use-queue-jobs.ts +18 -2
- package/src/features/queues/queues-screen.tsx +12 -8
- package/src/features/queues/services/queues.service.ts +3 -0
- package/src/features/rbac/panels/groups-panel.tsx +4 -1
- package/src/features/users/users-screen.tsx +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rl-core-front",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Telas e componentes Next.js do core: login com 2FA, usu\u00e1rios, RBAC, auditoria, logs e listagens com filtro din\u00e2mico",
|
|
5
5
|
"author": "Rodrigo Liberti",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,9 +22,9 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
ArrowDown,
|
|
24
24
|
ArrowUp,
|
|
25
|
+
ArrowUpDown,
|
|
25
26
|
ChevronLeft,
|
|
26
27
|
ChevronRight,
|
|
27
|
-
ChevronsUpDown,
|
|
28
28
|
Filter,
|
|
29
29
|
SlidersHorizontal,
|
|
30
30
|
} from "lucide-react";
|
|
@@ -307,11 +307,14 @@ export function DataTable<T extends RowData>({
|
|
|
307
307
|
header.getContext(),
|
|
308
308
|
)}
|
|
309
309
|
{sorted === "asc" ? (
|
|
310
|
-
<ArrowUp className="h-3.5 w-3.5" />
|
|
310
|
+
<ArrowUp className="h-3.5 w-3.5 text-primary" />
|
|
311
311
|
) : sorted === "desc" ? (
|
|
312
|
-
<ArrowDown className="h-3.5 w-3.5" />
|
|
312
|
+
<ArrowDown className="h-3.5 w-3.5 text-primary" />
|
|
313
313
|
) : (
|
|
314
|
-
|
|
314
|
+
// Seta dupla apagada na coluna livre e seta única
|
|
315
|
+
// em destaque na que ordena: sem a cor, achar a
|
|
316
|
+
// coluna ativa exige comparar o desenho de todas.
|
|
317
|
+
<ArrowUpDown className="h-3.5 w-3.5 opacity-40" />
|
|
315
318
|
)}
|
|
316
319
|
</button>
|
|
317
320
|
) : (
|
|
@@ -76,7 +76,6 @@ export function AuditScreen(): JSX.Element {
|
|
|
76
76
|
id: "user",
|
|
77
77
|
header: t("auditTrail.user"),
|
|
78
78
|
meta: { label: t("auditTrail.user") },
|
|
79
|
-
enableSorting: false,
|
|
80
79
|
cell: ({ row }) =>
|
|
81
80
|
row.original.user?.name ?? t("auditTrail.systemUser"),
|
|
82
81
|
},
|
|
@@ -84,7 +83,6 @@ export function AuditScreen(): JSX.Element {
|
|
|
84
83
|
accessorKey: "ipAddress",
|
|
85
84
|
header: t("auditTrail.ip"),
|
|
86
85
|
meta: { label: t("auditTrail.ip") },
|
|
87
|
-
enableSorting: false,
|
|
88
86
|
cell: ({ getValue }) => (
|
|
89
87
|
<span className="font-mono text-xs">{String(getValue() ?? "—")}</span>
|
|
90
88
|
),
|
|
@@ -95,7 +95,6 @@ export function RequestLogsPanel(): JSX.Element {
|
|
|
95
95
|
id: "route",
|
|
96
96
|
header: t("logs.route"),
|
|
97
97
|
meta: { label: t("logs.route") },
|
|
98
|
-
enableSorting: false,
|
|
99
98
|
// A ação nomeada (`@Auditable("Login")`) diz mais que o caminho; sem
|
|
100
99
|
// ela, o caminho é o que identifica a requisição.
|
|
101
100
|
cell: ({ row }) => (
|
|
@@ -118,7 +117,6 @@ export function RequestLogsPanel(): JSX.Element {
|
|
|
118
117
|
accessorKey: "errorType",
|
|
119
118
|
header: t("logs.type"),
|
|
120
119
|
meta: { label: t("logs.type") },
|
|
121
|
-
enableSorting: false,
|
|
122
120
|
cell: ({ getValue }) => {
|
|
123
121
|
const type = getValue() as ErrorType | null;
|
|
124
122
|
return type == null ? "—" : t(ErrorTypeLabelKey[type]);
|
|
@@ -137,7 +135,6 @@ export function RequestLogsPanel(): JSX.Element {
|
|
|
137
135
|
id: "user",
|
|
138
136
|
header: t("logs.user"),
|
|
139
137
|
meta: { label: t("logs.user") },
|
|
140
|
-
enableSorting: false,
|
|
141
138
|
cell: ({ row }) => row.original.user?.name ?? t("logs.systemUser"),
|
|
142
139
|
},
|
|
143
140
|
{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import type { SortingState } from "@tanstack/react-table";
|
|
3
4
|
import { useCallback, useEffect, useState } from "react";
|
|
4
5
|
|
|
5
6
|
import {
|
|
@@ -25,9 +26,11 @@ export interface UseQueueJobsResult {
|
|
|
25
26
|
page: number;
|
|
26
27
|
pageSize: number;
|
|
27
28
|
state: QueueJobState;
|
|
29
|
+
sorting: SortingState;
|
|
28
30
|
loading: boolean;
|
|
29
31
|
setPage: (page: number) => void;
|
|
30
32
|
setPageSize: (size: number) => void;
|
|
33
|
+
setSorting: (sorting: SortingState) => void;
|
|
31
34
|
setState: (state: QueueJobState) => void;
|
|
32
35
|
refresh: () => Promise<void>;
|
|
33
36
|
retry: (id: string) => Promise<void>;
|
|
@@ -49,11 +52,22 @@ export function useQueueJobs(): UseQueueJobsResult {
|
|
|
49
52
|
const [page, setPage] = useState(0);
|
|
50
53
|
const [pageSize, setPageSize] = useState(20);
|
|
51
54
|
const [state, setStateValue] = useState<QueueJobState>(QueueJobState.FAILED);
|
|
55
|
+
// Sem ordem escolhida, a lista sai na ordem do Redis — que é a mais recente
|
|
56
|
+
// primeiro e não custa varredura nenhuma no backend.
|
|
57
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
52
58
|
|
|
53
59
|
const fetch = useCallback(async (): Promise<void> => {
|
|
54
60
|
const [list, summary] = await Promise.all([
|
|
55
61
|
// O `page` da tabela é base zero; o da API, base um.
|
|
56
|
-
run(() =>
|
|
62
|
+
run(() =>
|
|
63
|
+
queuesService.list({
|
|
64
|
+
state,
|
|
65
|
+
page: page + 1,
|
|
66
|
+
limit: pageSize,
|
|
67
|
+
sortBy: sorting[0]?.id,
|
|
68
|
+
sortDir: sorting[0] ? (sorting[0].desc ? "DESC" : "ASC") : undefined,
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
57
71
|
run(() => queuesService.counts()),
|
|
58
72
|
]);
|
|
59
73
|
if (list) {
|
|
@@ -63,7 +77,7 @@ export function useQueueJobs(): UseQueueJobsResult {
|
|
|
63
77
|
if (summary) {
|
|
64
78
|
setCounts(summary);
|
|
65
79
|
}
|
|
66
|
-
}, [run, state, page, pageSize]);
|
|
80
|
+
}, [run, state, page, pageSize, sorting]);
|
|
67
81
|
|
|
68
82
|
useEffect(() => {
|
|
69
83
|
void fetch();
|
|
@@ -92,9 +106,11 @@ export function useQueueJobs(): UseQueueJobsResult {
|
|
|
92
106
|
page,
|
|
93
107
|
pageSize,
|
|
94
108
|
state,
|
|
109
|
+
sorting,
|
|
95
110
|
loading,
|
|
96
111
|
setPage,
|
|
97
112
|
setPageSize,
|
|
113
|
+
setSorting,
|
|
98
114
|
setState,
|
|
99
115
|
refresh: fetch,
|
|
100
116
|
retry: (id) => act(queuesService.retry(id)),
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ColumnDef,
|
|
5
5
|
OnChangeFn,
|
|
6
6
|
PaginationState,
|
|
7
|
+
SortingState,
|
|
7
8
|
} from "@tanstack/react-table";
|
|
8
9
|
import { RotateCcw, Trash2 } from "lucide-react";
|
|
9
10
|
import type { JSX } from "react";
|
|
@@ -90,7 +91,6 @@ export function QueuesScreen(): JSX.Element {
|
|
|
90
91
|
accessorKey: "name",
|
|
91
92
|
header: t("queues.job"),
|
|
92
93
|
meta: { label: t("queues.job") },
|
|
93
|
-
enableSorting: false,
|
|
94
94
|
cell: ({ row }) => (
|
|
95
95
|
<div className="flex flex-col">
|
|
96
96
|
<span className="font-medium">{row.original.name}</span>
|
|
@@ -104,7 +104,6 @@ export function QueuesScreen(): JSX.Element {
|
|
|
104
104
|
accessorKey: "state",
|
|
105
105
|
header: t("queues.state"),
|
|
106
106
|
meta: { label: t("queues.state") },
|
|
107
|
-
enableSorting: false,
|
|
108
107
|
cell: ({ row }) => {
|
|
109
108
|
const state = row.original.state as QueueJobState;
|
|
110
109
|
return (
|
|
@@ -118,7 +117,6 @@ export function QueuesScreen(): JSX.Element {
|
|
|
118
117
|
accessorKey: "percent",
|
|
119
118
|
header: t("queues.progress"),
|
|
120
119
|
meta: { label: t("queues.progress") },
|
|
121
|
-
enableSorting: false,
|
|
122
120
|
cell: ({ getValue }) => {
|
|
123
121
|
const percent = getValue() as number | null;
|
|
124
122
|
return percent == null ? "—" : `${percent}%`;
|
|
@@ -128,20 +126,17 @@ export function QueuesScreen(): JSX.Element {
|
|
|
128
126
|
accessorKey: "attemptsMade",
|
|
129
127
|
header: t("queues.attempts"),
|
|
130
128
|
meta: { label: t("queues.attempts") },
|
|
131
|
-
enableSorting: false,
|
|
132
129
|
},
|
|
133
130
|
{
|
|
134
131
|
accessorKey: "createdAt",
|
|
135
132
|
header: t("queues.createdAt"),
|
|
136
133
|
meta: { label: t("queues.createdAt") },
|
|
137
|
-
enableSorting: false,
|
|
138
134
|
cell: ({ getValue }) => new Date(String(getValue())).toLocaleString(),
|
|
139
135
|
},
|
|
140
136
|
{
|
|
141
137
|
accessorKey: "failedReason",
|
|
142
138
|
header: t("queues.reason"),
|
|
143
139
|
meta: { label: t("queues.reason") },
|
|
144
|
-
enableSorting: false,
|
|
145
140
|
// Cru e por inteiro no `title`: quem lê esta tela é quem vai consertar
|
|
146
141
|
// a causa, e a mensagem truncada some justamente na parte útil.
|
|
147
142
|
cell: ({ getValue }) => {
|
|
@@ -161,9 +156,9 @@ export function QueuesScreen(): JSX.Element {
|
|
|
161
156
|
{
|
|
162
157
|
id: "actions",
|
|
163
158
|
header: () => <div className="text-center">{t("common.actions")}</div>,
|
|
159
|
+
enableSorting: false,
|
|
164
160
|
meta: { shrink: true },
|
|
165
161
|
enableHiding: false,
|
|
166
|
-
enableSorting: false,
|
|
167
162
|
cell: ({ row }) =>
|
|
168
163
|
canManage ? (
|
|
169
164
|
<RowActions>
|
|
@@ -196,6 +191,13 @@ export function QueuesScreen(): JSX.Element {
|
|
|
196
191
|
pageIndex: queue.page,
|
|
197
192
|
pageSize: queue.pageSize,
|
|
198
193
|
};
|
|
194
|
+
/** Ordem nova recomeça na primeira página: a página 3 era da ordem antiga. */
|
|
195
|
+
const onSortingChange: OnChangeFn<SortingState> = (updater) => {
|
|
196
|
+
const next = typeof updater === "function" ? updater(queue.sorting) : updater;
|
|
197
|
+
queue.setSorting(next);
|
|
198
|
+
queue.setPage(0);
|
|
199
|
+
};
|
|
200
|
+
|
|
199
201
|
const onPaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
|
200
202
|
const next = typeof updater === "function" ? updater(pagination) : updater;
|
|
201
203
|
if (next.pageSize !== queue.pageSize) {
|
|
@@ -246,7 +248,9 @@ export function QueuesScreen(): JSX.Element {
|
|
|
246
248
|
rowCount={queue.total}
|
|
247
249
|
pagination={pagination}
|
|
248
250
|
onPaginationChange={onPaginationChange}
|
|
249
|
-
|
|
251
|
+
manualSorting
|
|
252
|
+
sorting={queue.sorting}
|
|
253
|
+
onSortingChange={onSortingChange}
|
|
250
254
|
emptyMessage={t("queues.empty")}
|
|
251
255
|
toolbar={
|
|
252
256
|
<SegmentedControl
|
|
@@ -159,9 +159,12 @@ export function GroupsPanel(): JSX.Element {
|
|
|
159
159
|
{ accessorKey: "type", header: "Tipo", meta: { label: "Tipo" } },
|
|
160
160
|
{
|
|
161
161
|
id: "manager",
|
|
162
|
+
accessorFn: (row) => {
|
|
163
|
+
const manager = candidates.find((c) => c.id === row.managerId);
|
|
164
|
+
return manager ? fullName(manager) : "";
|
|
165
|
+
},
|
|
162
166
|
header: t("rbac.manager"),
|
|
163
167
|
meta: { label: t("rbac.manager") },
|
|
164
|
-
enableSorting: false,
|
|
165
168
|
// Sem gerente, o escopo `team` não alcança ninguém aqui dentro — e é
|
|
166
169
|
// exatamente isso que o traço precisa deixar visível na listagem.
|
|
167
170
|
cell: ({ row }) => {
|
|
@@ -90,7 +90,6 @@ export function UsersScreen(): JSX.Element {
|
|
|
90
90
|
id: "roles",
|
|
91
91
|
header: t("users.roles"),
|
|
92
92
|
meta: { label: t("users.roles") },
|
|
93
|
-
enableSorting: false,
|
|
94
93
|
cell: ({ row }) => (
|
|
95
94
|
<div className="flex flex-wrap gap-1">
|
|
96
95
|
{row.original.roles?.map((r) => (
|
|
@@ -125,7 +124,6 @@ export function UsersScreen(): JSX.Element {
|
|
|
125
124
|
accessorKey: "twoFactorEnabled",
|
|
126
125
|
header: "2FA",
|
|
127
126
|
meta: { label: "2FA" },
|
|
128
|
-
enableSorting: false,
|
|
129
127
|
cell: ({ row }) => (
|
|
130
128
|
<Badge
|
|
131
129
|
variant={row.original.twoFactorEnabled ? "success" : "warning"}
|