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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rl-core-front",
3
- "version": "0.8.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
- <ChevronsUpDown className="h-3.5 w-3.5 opacity-50" />
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(() => queuesService.list({ state, page: page + 1, limit: pageSize })),
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
- enableSorting={false}
251
+ manualSorting
252
+ sorting={queue.sorting}
253
+ onSortingChange={onSortingChange}
250
254
  emptyMessage={t("queues.empty")}
251
255
  toolbar={
252
256
  <SegmentedControl
@@ -28,6 +28,9 @@ export interface QueueJobsQuery {
28
28
  state: QueueJobState;
29
29
  page: number;
30
30
  limit: number;
31
+ /** Campo da whitelist do backend (`SORTABLE_JOB_FIELDS`). */
32
+ sortBy?: string;
33
+ sortDir?: "ASC" | "DESC";
31
34
  }
32
35
 
33
36
  export const queuesService = {
@@ -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"}