Skip to content

Commit 90c344c

Browse files
alexcastrodevAlexandro castro
andauthored
Feat/datatables (#5)
* chore: start integration * refactoring safe browsing service * finish safety backend part * fix: authorize * fix: shortlink * finish frontend panel * finish implementation * feat: update tables * cops --------- Co-authored-by: Alexandro castro <[email protected]>
1 parent b29e882 commit 90c344c

File tree

9 files changed

+212
-85
lines changed

9 files changed

+212
-85
lines changed

backend/.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,6 @@ Style/ClassAndModuleChildren:
2929

3030
Style/FrozenStringLiteralComment:
3131
Enabled: false
32+
33+
Style/SymbolProc:
34+
Enabled: false

backend/app/serializers/shortlink_serializer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
class ShortlinkSerializer < BaseSerializer
2525
with_id
2626
root_key_for_collection :shortlink
27+
maybe_one :user, resource: UserSerializer, if: proc { |_s| params[:admin] }
2728

2829
#------------
2930
# Attributes

backend/spec/requests/api/admin/shortlinks_spec.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@
2525
expect(json_response["shortlink"].size).to(eq(2))
2626
expect(json_response["shortlink"].first).to(include("original_url", "short_code"))
2727
end
28+
29+
it "includes associated user when present", dev: true do
30+
user = FactoryBot.create(:user)
31+
shortlink = FactoryBot.create(:shortlink, user: user)
32+
33+
get "/api/admin/shortlinks", headers: admin_auth_headers
34+
35+
expect(response).to(have_http_status(:ok))
36+
json_response = JSON.parse(response.body)
37+
first_shortlink = json_response["shortlink"].find { |s| s["id"] == shortlink.id }
38+
expect(first_shortlink).to(include("user"))
39+
expect(first_shortlink["user"]).to(include("id" => user.id, "email" => user.email))
40+
end
2841
end
2942

3043
describe "POST /api/admin/shortlinks/:id/toggle_safe" do

frontend/app/layout/providers.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import '@mantine/core/styles.css';
22
import '@mantine/charts/styles.css';
33
import '@mantine/notifications/styles.css';
4+
import 'mantine-react-table/styles.css';
45
import '../i18n';
56

67
import { MantineProvider } from '@mantine/core';

frontend/app/routes/admin/shortlinks.tsx

Lines changed: 73 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import {
44
Text,
55
Paper,
66
Alert,
7-
Table,
87
Group,
98
Loader,
109
Center,
1110
Switch,
11+
ScrollArea,
1212
} from '@mantine/core';
1313
import { IconLock } from '@tabler/icons-react';
1414
import { useGetLoggedUser } from '@internal/core/actions/get-logged-user/get-logged-user.hook';
@@ -19,6 +19,12 @@ import {
1919
import { useToggleShortlinkSafe } from '@internal/core/actions/admin-shortlink-toggle-safe/admin-shortlink-toggle-safe.hook';
2020
import { useQueryClient } from '@tanstack/react-query';
2121
import type { Shortlink } from '@internal/core/types/Shortlink';
22+
import { useMemo } from 'react';
23+
import {
24+
type MRT_ColumnDef,
25+
MRT_Table,
26+
useMantineReactTable,
27+
} from 'mantine-react-table';
2228

2329
export const ssr = false;
2430

@@ -41,6 +47,69 @@ export default function AdminShortlinksPage() {
4147
},
4248
});
4349

50+
const columns = useMemo<MRT_ColumnDef<Shortlink>[]>(
51+
() => [
52+
{
53+
accessorKey: 'id',
54+
header: 'ID',
55+
},
56+
{
57+
accessorKey: 'title',
58+
header: 'Title',
59+
Cell: ({ row }) => (
60+
<Text maw={300} className="truncate whitespace-pre-wrap break-words">
61+
{row.original.title || row.original.original_url}
62+
</Text>
63+
),
64+
},
65+
{
66+
accessorKey: 'short_code',
67+
header: 'Short Code',
68+
},
69+
{
70+
accessorKey: 'events_count',
71+
header: 'Events',
72+
},
73+
{
74+
accessorKey: 'user.email',
75+
header: 'User Email',
76+
Cell: ({ row }) => row.original.user?.email ?? '',
77+
},
78+
{
79+
accessorKey: 'safe',
80+
header: 'Safe',
81+
enableSorting: false,
82+
Cell: ({ row }) => (
83+
<Group>
84+
<Switch
85+
checked={Boolean(row.original.safe)}
86+
onChange={() => toggleMutation.mutate(row.original.id)}
87+
size="sm"
88+
aria-label={`toggle-safe-${row.original.id}`}
89+
/>
90+
</Group>
91+
),
92+
},
93+
],
94+
[toggleMutation, shortlinksData]
95+
);
96+
97+
const table = useMantineReactTable({
98+
columns,
99+
data: shortlinksData?.shortlink || [],
100+
enableColumnActions: false,
101+
enableColumnFilters: true,
102+
enablePagination: true,
103+
enableSorting: true,
104+
mantineTableProps: {
105+
highlightOnHover: false,
106+
striped: 'odd',
107+
withColumnBorders: true,
108+
withRowBorders: true,
109+
withTableBorder: true,
110+
},
111+
});
112+
44113
if (data && !data.user?.admin) {
45114
return (
46115
<Container size="xl" py="xl">
@@ -72,25 +141,6 @@ export default function AdminShortlinksPage() {
72141
);
73142
}
74143

75-
const rows = (shortlinksData?.shortlink || []).map((s: Shortlink) => (
76-
<Table.Tr key={s.id}>
77-
<Table.Td>{s.id}</Table.Td>
78-
<Table.Td>{s.title || s.original_url}</Table.Td>
79-
<Table.Td>{s.short_code}</Table.Td>
80-
<Table.Td>{s.events_count}</Table.Td>
81-
<Table.Td>
82-
<Group>
83-
<Switch
84-
checked={Boolean(s.safe)}
85-
onChange={() => toggleMutation.mutate(s.id)}
86-
size="sm"
87-
aria-label={`toggle-safe-${s.id}`}
88-
/>
89-
</Group>
90-
</Table.Td>
91-
</Table.Tr>
92-
));
93-
94144
return (
95145
<Container size="xl" py="xl">
96146
<Paper shadow="sm" p="xl" radius="md" withBorder>
@@ -106,30 +156,9 @@ export default function AdminShortlinksPage() {
106156
</div>
107157
</Group>
108158

109-
<Table striped highlightOnHover>
110-
<Table.Thead>
111-
<Table.Tr>
112-
<Table.Th>Id</Table.Th>
113-
<Table.Th>Title / URL</Table.Th>
114-
<Table.Th>Code</Table.Th>
115-
<Table.Th>Events</Table.Th>
116-
<Table.Th>Safe</Table.Th>
117-
</Table.Tr>
118-
</Table.Thead>
119-
<Table.Tbody>
120-
{rows && rows.length > 0 ? (
121-
rows
122-
) : (
123-
<Table.Tr>
124-
<Table.Td colSpan={5}>
125-
<Text ta="center" c="dimmed">
126-
No shortlinks found
127-
</Text>
128-
</Table.Td>
129-
</Table.Tr>
130-
)}
131-
</Table.Tbody>
132-
</Table>
159+
<ScrollArea>
160+
<MRT_Table table={table} />
161+
</ScrollArea>
133162
</Paper>
134163
</Container>
135164
);

frontend/app/routes/admin/users.tsx

Lines changed: 54 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@ import {
44
Text,
55
Paper,
66
Alert,
7-
Table,
87
Badge,
98
Group,
109
Loader,
1110
Center,
11+
ScrollArea,
1212
} from '@mantine/core';
1313
import { IconUsers, IconLock } from '@tabler/icons-react';
1414
import { useGetLoggedUser } from '@internal/core/actions/get-logged-user/get-logged-user.hook';
1515
import { useGetManageUsers } from '@internal/core/actions/get-manage-users/get-manage-users.hook';
16+
import { useMemo } from 'react';
17+
import {
18+
type MRT_ColumnDef,
19+
MRT_Table,
20+
useMantineReactTable,
21+
} from 'mantine-react-table';
1622

1723
export const ssr = false;
1824

@@ -27,6 +33,50 @@ export default function UsersPage() {
2733
const { data } = useGetLoggedUser();
2834
const { data: usersData, isLoading, error } = useGetManageUsers();
2935

36+
const columns = useMemo<MRT_ColumnDef<any>[]>(
37+
() => [
38+
{
39+
accessorKey: 'id',
40+
header: 'ID',
41+
},
42+
{
43+
accessorKey: 'email',
44+
header: 'Email',
45+
},
46+
{
47+
accessorKey: 'admin',
48+
header: 'Role',
49+
enableSorting: false,
50+
Cell: ({ row }) => (
51+
<Badge color={row.original.admin ? 'blue' : 'gray'} variant="light">
52+
{row.original.admin ? 'Admin' : 'User'}
53+
</Badge>
54+
),
55+
},
56+
{
57+
accessorKey: 'shortlinks_count',
58+
header: 'Shortlinks',
59+
},
60+
],
61+
[]
62+
);
63+
64+
const table = useMantineReactTable({
65+
columns,
66+
data: usersData?.user || [],
67+
enableColumnActions: false,
68+
enableColumnFilters: true,
69+
enablePagination: true,
70+
enableSorting: true,
71+
mantineTableProps: {
72+
highlightOnHover: false,
73+
striped: 'odd',
74+
withColumnBorders: true,
75+
withRowBorders: true,
76+
withTableBorder: true,
77+
},
78+
});
79+
3080
if (data && !data.user?.admin) {
3181
return (
3282
<Container size="xl" py="xl">
@@ -58,19 +108,6 @@ export default function UsersPage() {
58108
);
59109
}
60110

61-
const rows = (usersData?.user || []).map(user => (
62-
<Table.Tr key={user.id}>
63-
<Table.Td>{user.id}</Table.Td>
64-
<Table.Td>{user.email}</Table.Td>
65-
<Table.Td>
66-
<Badge color={user.admin ? 'blue' : 'gray'} variant="light">
67-
{user.admin ? 'Admin' : 'User'}
68-
</Badge>
69-
</Table.Td>
70-
<Table.Td>{user.shortlinks_count}</Table.Td>
71-
</Table.Tr>
72-
));
73-
74111
return (
75112
<Container size="xl" py="xl">
76113
<Paper shadow="sm" p="xl" radius="md" withBorder>
@@ -86,29 +123,9 @@ export default function UsersPage() {
86123
</div>
87124
</Group>
88125

89-
<Table striped highlightOnHover>
90-
<Table.Thead>
91-
<Table.Tr>
92-
<Table.Th>Id</Table.Th>
93-
<Table.Th>Email</Table.Th>
94-
<Table.Th>Role</Table.Th>
95-
<Table.Th>Shortlinks</Table.Th>
96-
</Table.Tr>
97-
</Table.Thead>
98-
<Table.Tbody>
99-
{rows && rows.length > 0 ? (
100-
rows
101-
) : (
102-
<Table.Tr>
103-
<Table.Td colSpan={4}>
104-
<Text ta="center" c="dimmed">
105-
No users found
106-
</Text>
107-
</Table.Td>
108-
</Table.Tr>
109-
)}
110-
</Table.Tbody>
111-
</Table>
126+
<ScrollArea>
127+
<MRT_Table table={table} />
128+
</ScrollArea>
112129
</Paper>
113130
</Container>
114131
);

frontend/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
"dependencies": {
1313
"@internal/core": "0.0.0",
1414
"@mantine/charts": "^8.3.3",
15-
"@mantine/core": "^8.3.3",
15+
"@mantine/core": "^8.3.5",
16+
"@mantine/dates": "^8.3.5",
1617
"@mantine/form": "^8.3.3",
17-
"@mantine/hooks": "^8.3.3",
18+
"@mantine/hooks": "^8.3.5",
1819
"@mantine/modals": "^8.3.3",
1920
"@mantine/notifications": "^8.3.3",
2021
"@react-router/node": "^7.9.2",
@@ -23,9 +24,11 @@
2324
"@tanstack/react-query": "^5.90.2",
2425
"axios": "^1.12.2",
2526
"clsx": "^2.1.1",
27+
"dayjs": "^1.11.18",
2628
"i18next": "^25.5.3",
2729
"isbot": "^5.1.31",
2830
"mantine-form-zod-resolver": "^1.3.0",
31+
"mantine-react-table": "^2.0.0-beta.9",
2932
"react": "^19.1.1",
3033
"react-dom": "^19.1.1",
3134
"react-i18next": "^16.0.0",

frontend/packages/core/types/Shortlink.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { User } from './User';
2+
13
export type Shortlink = {
24
id: number;
35
original_url: string;
@@ -9,4 +11,5 @@ export type Shortlink = {
911
is_active: boolean;
1012
safe?: boolean;
1113
safe_checked_at?: string | null;
14+
user?: User | null;
1215
};

0 commit comments

Comments
 (0)