@hed-hog/core 0.0.278 → 0.0.285
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 +60 -0
- package/dist/auth/auth.controller.d.ts +3 -3
- package/dist/auth/auth.service.d.ts +8 -8
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +12 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js +9 -0
- package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +12 -0
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.js +25 -0
- package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
- package/dist/file/file.controller.d.ts +2 -2
- package/dist/file/file.service.d.ts +4 -4
- package/dist/role/guards/role.guard.d.ts.map +1 -1
- package/dist/role/guards/role.guard.js +1 -1
- package/dist/role/guards/role.guard.js.map +1 -1
- package/dist/session/session.controller.d.ts +1 -1
- package/dist/session/session.service.d.ts +3 -3
- package/dist/user/user.controller.d.ts +2 -2
- package/dist/user/user.service.d.ts +6 -6
- package/hedhog/data/dashboard_component.yaml +95 -77
- package/hedhog/data/dashboard_component_role.yaml +91 -79
- package/hedhog/data/dashboard_item.yaml +121 -101
- package/hedhog/data/route.yaml +8 -0
- package/hedhog/frontend/app/ai_agent/page.tsx.ejs +69 -62
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +23 -12
- package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +80 -5
- package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +33 -29
- package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +18 -14
- package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +39 -32
- package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +22 -19
- package/hedhog/frontend/app/dashboard/components/widgets/menus-card.tsx.ejs +58 -0
- package/hedhog/frontend/app/dashboard/components/widgets/routes-card.tsx.ejs +58 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +18 -18
- package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +18 -18
- package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +18 -18
- package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +18 -18
- package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +15 -11
- package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +39 -37
- package/hedhog/frontend/app/dashboard/dashboard.css.ejs +20 -4
- package/hedhog/frontend/app/mail/log/page.tsx.ejs +36 -47
- package/hedhog/frontend/app/mail/template/page.tsx.ejs +176 -126
- package/hedhog/frontend/app/menu/page.tsx.ejs +45 -39
- package/hedhog/frontend/app/roles/page.tsx.ejs +45 -46
- package/hedhog/frontend/app/users/page.tsx.ejs +70 -73
- package/hedhog/frontend/messages/en.json +15 -2
- package/hedhog/frontend/messages/pt.json +15 -2
- package/package.json +4 -4
- package/src/dashboard/dashboard-core/dashboard-core.controller.ts +5 -0
- package/src/dashboard/dashboard-core/dashboard-core.service.ts +34 -0
- package/src/role/guards/role.guard.ts +9 -8
|
@@ -68,6 +68,26 @@
|
|
|
68
68
|
height: 2
|
|
69
69
|
x_axis: 2
|
|
70
70
|
y_axis: 0
|
|
71
|
+
- component_id:
|
|
72
|
+
where:
|
|
73
|
+
slug: menus-card
|
|
74
|
+
dashboard_id:
|
|
75
|
+
where:
|
|
76
|
+
slug: default
|
|
77
|
+
width: 2
|
|
78
|
+
height: 2
|
|
79
|
+
x_axis: 8
|
|
80
|
+
y_axis: 0
|
|
81
|
+
- component_id:
|
|
82
|
+
where:
|
|
83
|
+
slug: routes-card
|
|
84
|
+
dashboard_id:
|
|
85
|
+
where:
|
|
86
|
+
slug: default
|
|
87
|
+
width: 2
|
|
88
|
+
height: 2
|
|
89
|
+
x_axis: 10
|
|
90
|
+
y_axis: 0
|
|
71
91
|
- component_id:
|
|
72
92
|
where:
|
|
73
93
|
slug: user-growth-chart
|
|
@@ -86,109 +106,109 @@
|
|
|
86
106
|
where:
|
|
87
107
|
slug: user
|
|
88
108
|
width: 12
|
|
89
|
-
height:
|
|
109
|
+
height: 3
|
|
90
110
|
x_axis: 0
|
|
91
111
|
y_axis: 0
|
|
92
|
-
- component_id:
|
|
93
|
-
where:
|
|
94
|
-
slug: stat-online-time
|
|
95
|
-
dashboard_id:
|
|
96
|
-
where:
|
|
97
|
-
slug: user
|
|
98
|
-
width: 3
|
|
99
|
-
height: 1
|
|
100
|
-
x_axis: 0
|
|
101
|
-
y_axis:
|
|
102
|
-
- component_id:
|
|
103
|
-
where:
|
|
104
|
-
slug: stat-actions-today
|
|
105
|
-
dashboard_id:
|
|
106
|
-
where:
|
|
107
|
-
slug: user
|
|
108
|
-
width: 3
|
|
109
|
-
height: 1
|
|
110
|
-
x_axis: 3
|
|
111
|
-
y_axis:
|
|
112
|
-
- component_id:
|
|
113
|
-
where:
|
|
114
|
-
slug: stat-consecutive-days
|
|
115
|
-
dashboard_id:
|
|
116
|
-
where:
|
|
117
|
-
slug: user
|
|
118
|
-
width: 3
|
|
119
|
-
height: 1
|
|
120
|
-
x_axis: 6
|
|
121
|
-
y_axis:
|
|
122
|
-
- component_id:
|
|
123
|
-
where:
|
|
124
|
-
slug: stat-access-level
|
|
125
|
-
dashboard_id:
|
|
126
|
-
where:
|
|
127
|
-
slug: user
|
|
128
|
-
width: 3
|
|
129
|
-
height: 1
|
|
130
|
-
x_axis: 9
|
|
131
|
-
y_axis:
|
|
132
|
-
- component_id:
|
|
133
|
-
where:
|
|
134
|
-
slug: account-security
|
|
135
|
-
dashboard_id:
|
|
136
|
-
where:
|
|
137
|
-
slug: user
|
|
138
|
-
width: 5
|
|
139
|
-
height: 5
|
|
140
|
-
x_axis: 0
|
|
141
|
-
y_axis: 7
|
|
142
|
-
- component_id:
|
|
143
|
-
where:
|
|
144
|
-
slug: login-history-chart
|
|
145
|
-
dashboard_id:
|
|
146
|
-
where:
|
|
147
|
-
slug: user
|
|
148
|
-
width: 7
|
|
149
|
-
height: 4
|
|
150
|
-
x_axis: 0
|
|
151
|
-
y_axis: 3
|
|
152
|
-
- component_id:
|
|
153
|
-
where:
|
|
154
|
-
slug: email-notifications
|
|
155
|
-
dashboard_id:
|
|
156
|
-
where:
|
|
157
|
-
slug: user
|
|
158
|
-
width: 7
|
|
159
|
-
height: 5
|
|
160
|
-
x_axis: 5
|
|
161
|
-
y_axis: 7
|
|
162
|
-
- component_id:
|
|
163
|
-
where:
|
|
164
|
-
slug: activity-timeline
|
|
165
|
-
dashboard_id:
|
|
166
|
-
where:
|
|
167
|
-
slug: user
|
|
168
|
-
width: 5
|
|
169
|
-
height: 3
|
|
170
|
-
x_axis: 7
|
|
171
|
-
y_axis:
|
|
172
|
-
- component_id:
|
|
173
|
-
where:
|
|
174
|
-
slug: user-sessions
|
|
175
|
-
dashboard_id:
|
|
176
|
-
where:
|
|
177
|
-
slug: user
|
|
178
|
-
width: 5
|
|
179
|
-
height: 4
|
|
180
|
-
x_axis: 7
|
|
181
|
-
y_axis:
|
|
182
|
-
- component_id:
|
|
183
|
-
where:
|
|
184
|
-
slug: user-roles
|
|
185
|
-
dashboard_id:
|
|
186
|
-
where:
|
|
187
|
-
slug: user
|
|
188
|
-
width: 7
|
|
189
|
-
height: 3
|
|
190
|
-
x_axis: 0
|
|
191
|
-
y_axis:
|
|
112
|
+
- component_id:
|
|
113
|
+
where:
|
|
114
|
+
slug: stat-online-time
|
|
115
|
+
dashboard_id:
|
|
116
|
+
where:
|
|
117
|
+
slug: user
|
|
118
|
+
width: 3
|
|
119
|
+
height: 1
|
|
120
|
+
x_axis: 0
|
|
121
|
+
y_axis: 3
|
|
122
|
+
- component_id:
|
|
123
|
+
where:
|
|
124
|
+
slug: stat-actions-today
|
|
125
|
+
dashboard_id:
|
|
126
|
+
where:
|
|
127
|
+
slug: user
|
|
128
|
+
width: 3
|
|
129
|
+
height: 1
|
|
130
|
+
x_axis: 3
|
|
131
|
+
y_axis: 3
|
|
132
|
+
- component_id:
|
|
133
|
+
where:
|
|
134
|
+
slug: stat-consecutive-days
|
|
135
|
+
dashboard_id:
|
|
136
|
+
where:
|
|
137
|
+
slug: user
|
|
138
|
+
width: 3
|
|
139
|
+
height: 1
|
|
140
|
+
x_axis: 6
|
|
141
|
+
y_axis: 3
|
|
142
|
+
- component_id:
|
|
143
|
+
where:
|
|
144
|
+
slug: stat-access-level
|
|
145
|
+
dashboard_id:
|
|
146
|
+
where:
|
|
147
|
+
slug: user
|
|
148
|
+
width: 3
|
|
149
|
+
height: 1
|
|
150
|
+
x_axis: 9
|
|
151
|
+
y_axis: 3
|
|
152
|
+
- component_id:
|
|
153
|
+
where:
|
|
154
|
+
slug: account-security
|
|
155
|
+
dashboard_id:
|
|
156
|
+
where:
|
|
157
|
+
slug: user
|
|
158
|
+
width: 5
|
|
159
|
+
height: 5
|
|
160
|
+
x_axis: 0
|
|
161
|
+
y_axis: 7
|
|
162
|
+
- component_id:
|
|
163
|
+
where:
|
|
164
|
+
slug: login-history-chart
|
|
165
|
+
dashboard_id:
|
|
166
|
+
where:
|
|
167
|
+
slug: user
|
|
168
|
+
width: 7
|
|
169
|
+
height: 4
|
|
170
|
+
x_axis: 0
|
|
171
|
+
y_axis: 3
|
|
172
|
+
- component_id:
|
|
173
|
+
where:
|
|
174
|
+
slug: email-notifications
|
|
175
|
+
dashboard_id:
|
|
176
|
+
where:
|
|
177
|
+
slug: user
|
|
178
|
+
width: 7
|
|
179
|
+
height: 5
|
|
180
|
+
x_axis: 5
|
|
181
|
+
y_axis: 7
|
|
182
|
+
- component_id:
|
|
183
|
+
where:
|
|
184
|
+
slug: activity-timeline
|
|
185
|
+
dashboard_id:
|
|
186
|
+
where:
|
|
187
|
+
slug: user
|
|
188
|
+
width: 5
|
|
189
|
+
height: 3
|
|
190
|
+
x_axis: 7
|
|
191
|
+
y_axis: 13
|
|
192
|
+
- component_id:
|
|
193
|
+
where:
|
|
194
|
+
slug: user-sessions
|
|
195
|
+
dashboard_id:
|
|
196
|
+
where:
|
|
197
|
+
slug: user
|
|
198
|
+
width: 5
|
|
199
|
+
height: 4
|
|
200
|
+
x_axis: 7
|
|
201
|
+
y_axis: 4
|
|
202
|
+
- component_id:
|
|
203
|
+
where:
|
|
204
|
+
slug: user-roles
|
|
205
|
+
dashboard_id:
|
|
206
|
+
where:
|
|
207
|
+
slug: user
|
|
208
|
+
width: 7
|
|
209
|
+
height: 3
|
|
210
|
+
x_axis: 0
|
|
211
|
+
y_axis: 13
|
|
192
212
|
|
|
193
213
|
- component_id:
|
|
194
214
|
where:
|
package/hedhog/data/route.yaml
CHANGED
|
@@ -716,6 +716,14 @@
|
|
|
716
716
|
role:
|
|
717
717
|
- where:
|
|
718
718
|
slug: admin-mail
|
|
719
|
+
- url: /dashboard-core/stats/overview/system
|
|
720
|
+
method: GET
|
|
721
|
+
relations:
|
|
722
|
+
role:
|
|
723
|
+
- where:
|
|
724
|
+
slug: admin
|
|
725
|
+
- where:
|
|
726
|
+
slug: admin-access
|
|
719
727
|
- url: /dashboard-core/layout/:slug
|
|
720
728
|
method: GET
|
|
721
729
|
relations:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
EmptyState,
|
|
4
5
|
Page,
|
|
5
6
|
PageHeader,
|
|
6
7
|
PaginationFooter,
|
|
@@ -130,14 +131,14 @@ export default function AiAgentPage() {
|
|
|
130
131
|
|
|
131
132
|
return response.data;
|
|
132
133
|
},
|
|
133
|
-
initialData: {
|
|
134
|
-
data: [],
|
|
135
|
-
total: 0,
|
|
136
|
-
page: 1,
|
|
137
|
-
pageSize: 10,
|
|
138
|
-
totalPages: 1,
|
|
139
|
-
},
|
|
140
134
|
});
|
|
135
|
+
const agentList = data ?? {
|
|
136
|
+
data: [],
|
|
137
|
+
total: 0,
|
|
138
|
+
page: 1,
|
|
139
|
+
pageSize: 10,
|
|
140
|
+
totalPages: 1,
|
|
141
|
+
};
|
|
141
142
|
|
|
142
143
|
useEffect(() => {
|
|
143
144
|
if (editingAgent) {
|
|
@@ -261,68 +262,74 @@ export default function AiAgentPage() {
|
|
|
261
262
|
placeholder={t('searchPlaceholder')}
|
|
262
263
|
/>
|
|
263
264
|
|
|
264
|
-
|
|
265
|
-
<
|
|
266
|
-
<
|
|
267
|
-
<
|
|
268
|
-
<
|
|
269
|
-
<TableHead>Slug</TableHead>
|
|
270
|
-
<TableHead>{t('columnProvider')}</TableHead>
|
|
271
|
-
<TableHead>{t('columnModel')}</TableHead>
|
|
272
|
-
<TableHead>{t('columnExternalId')}</TableHead>
|
|
273
|
-
<TableHead className="text-right">
|
|
274
|
-
{t('columnActions')}
|
|
275
|
-
</TableHead>
|
|
276
|
-
</TableRow>
|
|
277
|
-
</TableHeader>
|
|
278
|
-
<TableBody>
|
|
279
|
-
{isLoading ? (
|
|
265
|
+
{isLoading || agentList.data.length > 0 ? (
|
|
266
|
+
<Card>
|
|
267
|
+
<CardContent className="p-0">
|
|
268
|
+
<Table>
|
|
269
|
+
<TableHeader>
|
|
280
270
|
<TableRow>
|
|
281
|
-
<
|
|
271
|
+
<TableHead>Slug</TableHead>
|
|
272
|
+
<TableHead>{t('columnProvider')}</TableHead>
|
|
273
|
+
<TableHead>{t('columnModel')}</TableHead>
|
|
274
|
+
<TableHead>{t('columnExternalId')}</TableHead>
|
|
275
|
+
<TableHead className="text-right">
|
|
276
|
+
{t('columnActions')}
|
|
277
|
+
</TableHead>
|
|
282
278
|
</TableRow>
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
data.data.map((agent) => (
|
|
289
|
-
<TableRow key={agent.id}>
|
|
290
|
-
<TableCell>{agent.slug}</TableCell>
|
|
291
|
-
<TableCell className="uppercase">
|
|
292
|
-
{agent.provider}
|
|
293
|
-
</TableCell>
|
|
294
|
-
<TableCell>{agent.model || '-'}</TableCell>
|
|
295
|
-
<TableCell>{agent.external_agent_id || '-'}</TableCell>
|
|
296
|
-
<TableCell className="text-right">
|
|
297
|
-
<div className="flex items-center justify-end gap-2">
|
|
298
|
-
<Button
|
|
299
|
-
variant="outline"
|
|
300
|
-
size="icon"
|
|
301
|
-
onClick={() => handleOpenEdit(agent)}
|
|
302
|
-
>
|
|
303
|
-
<Pencil className="h-4 w-4" />
|
|
304
|
-
</Button>
|
|
305
|
-
<Button
|
|
306
|
-
variant="destructive"
|
|
307
|
-
size="icon"
|
|
308
|
-
onClick={() => setAgentToDelete(agent)}
|
|
309
|
-
>
|
|
310
|
-
<Trash2 className="h-4 w-4" />
|
|
311
|
-
</Button>
|
|
312
|
-
</div>
|
|
313
|
-
</TableCell>
|
|
279
|
+
</TableHeader>
|
|
280
|
+
<TableBody>
|
|
281
|
+
{isLoading ? (
|
|
282
|
+
<TableRow>
|
|
283
|
+
<TableCell colSpan={5}>{t('loading')}</TableCell>
|
|
314
284
|
</TableRow>
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
285
|
+
) : (
|
|
286
|
+
agentList.data.map((agent) => (
|
|
287
|
+
<TableRow key={agent.id}>
|
|
288
|
+
<TableCell>{agent.slug}</TableCell>
|
|
289
|
+
<TableCell className="uppercase">
|
|
290
|
+
{agent.provider}
|
|
291
|
+
</TableCell>
|
|
292
|
+
<TableCell>{agent.model || '-'}</TableCell>
|
|
293
|
+
<TableCell>{agent.external_agent_id || '-'}</TableCell>
|
|
294
|
+
<TableCell className="text-right">
|
|
295
|
+
<div className="flex items-center justify-end gap-2">
|
|
296
|
+
<Button
|
|
297
|
+
variant="outline"
|
|
298
|
+
size="icon"
|
|
299
|
+
onClick={() => handleOpenEdit(agent)}
|
|
300
|
+
>
|
|
301
|
+
<Pencil className="h-4 w-4" />
|
|
302
|
+
</Button>
|
|
303
|
+
<Button
|
|
304
|
+
variant="destructive"
|
|
305
|
+
size="icon"
|
|
306
|
+
onClick={() => setAgentToDelete(agent)}
|
|
307
|
+
>
|
|
308
|
+
<Trash2 className="h-4 w-4" />
|
|
309
|
+
</Button>
|
|
310
|
+
</div>
|
|
311
|
+
</TableCell>
|
|
312
|
+
</TableRow>
|
|
313
|
+
))
|
|
314
|
+
)}
|
|
315
|
+
</TableBody>
|
|
316
|
+
</Table>
|
|
317
|
+
</CardContent>
|
|
318
|
+
</Card>
|
|
319
|
+
) : (
|
|
320
|
+
<EmptyState
|
|
321
|
+
icon={<Bot className="h-12 w-12" />}
|
|
322
|
+
title={t('empty')}
|
|
323
|
+
description={t('description')}
|
|
324
|
+
actionLabel={t('newAgent')}
|
|
325
|
+
onAction={handleOpenCreate}
|
|
326
|
+
/>
|
|
327
|
+
)}
|
|
321
328
|
|
|
322
329
|
<PaginationFooter
|
|
323
330
|
currentPage={page}
|
|
324
331
|
pageSize={pageSize}
|
|
325
|
-
totalItems={
|
|
332
|
+
totalItems={agentList.total}
|
|
326
333
|
onPageChange={setPage}
|
|
327
334
|
onPageSizeChange={(value) => {
|
|
328
335
|
setPageSize(value);
|
|
@@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button';
|
|
|
12
12
|
import { Separator } from '@/components/ui/separator';
|
|
13
13
|
import { SidebarTrigger } from '@/components/ui/sidebar';
|
|
14
14
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
15
|
+
import { useIsMobile } from '@/components/ui/use-mobile';
|
|
15
16
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
16
17
|
import { IconDeviceFloppy } from '@tabler/icons-react';
|
|
17
18
|
import { useTranslations } from 'next-intl';
|
|
@@ -38,6 +39,7 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
|
|
|
38
39
|
const t = useTranslations('core.DashboardPage');
|
|
39
40
|
const { request } = useApp();
|
|
40
41
|
const router = useRouter();
|
|
42
|
+
const isMobile = useIsMobile();
|
|
41
43
|
|
|
42
44
|
const [layout, setLayout] = useState<LayoutItem[]>([]);
|
|
43
45
|
const [widgets, setWidgets] = useState<WidgetLayout[]>([]);
|
|
@@ -106,7 +108,10 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
|
|
|
106
108
|
x: item.x,
|
|
107
109
|
y: item.y,
|
|
108
110
|
w: item.w,
|
|
109
|
-
h:
|
|
111
|
+
h:
|
|
112
|
+
dashboardSlug === 'user' && item.slug === 'profile-card'
|
|
113
|
+
? Math.max(item.h, 3)
|
|
114
|
+
: item.h,
|
|
110
115
|
minW: item.minW || 1,
|
|
111
116
|
maxW: item.maxW || 12,
|
|
112
117
|
minH: item.minH || 1,
|
|
@@ -277,37 +282,42 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
|
|
|
277
282
|
|
|
278
283
|
return (
|
|
279
284
|
<>
|
|
280
|
-
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
|
|
281
|
-
<div className="flex w-full
|
|
282
|
-
<div className="flex items-center gap-2">
|
|
285
|
+
<header className="flex min-h-16 shrink-0 items-center gap-2 py-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 sm:h-16 sm:py-0">
|
|
286
|
+
<div className="flex w-full flex-col gap-2 px-4 sm:flex-row sm:items-center sm:justify-between">
|
|
287
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
283
288
|
<SidebarTrigger className="-ml-1" />
|
|
284
289
|
<Separator
|
|
285
290
|
orientation="vertical"
|
|
286
291
|
className="mr-2 data-[orientation=vertical]:h-4"
|
|
287
292
|
/>
|
|
288
|
-
<Breadcrumb>
|
|
293
|
+
<Breadcrumb className="min-w-0">
|
|
289
294
|
<BreadcrumbList>
|
|
290
295
|
<BreadcrumbItem className="hidden md:block">
|
|
291
296
|
<BreadcrumbLink href="#">{t('dashboard')}</BreadcrumbLink>
|
|
292
297
|
</BreadcrumbItem>
|
|
293
298
|
<BreadcrumbSeparator className="hidden md:block" />
|
|
294
299
|
<BreadcrumbItem>
|
|
295
|
-
<BreadcrumbPage>
|
|
300
|
+
<BreadcrumbPage className="truncate">
|
|
301
|
+
{dashboardName}
|
|
302
|
+
</BreadcrumbPage>
|
|
296
303
|
</BreadcrumbItem>
|
|
297
304
|
</BreadcrumbList>
|
|
298
305
|
</Breadcrumb>
|
|
299
306
|
</div>
|
|
300
|
-
<div className="flex items-center gap-2">
|
|
307
|
+
<div className="flex w-full items-center justify-end gap-2 sm:w-auto">
|
|
301
308
|
{hasChanges && (
|
|
302
309
|
<Button
|
|
303
310
|
size="sm"
|
|
304
311
|
variant="default"
|
|
305
|
-
className="gap-2"
|
|
312
|
+
className="gap-1 px-2 sm:gap-2 sm:px-3"
|
|
306
313
|
onClick={handleSaveLayout}
|
|
307
314
|
disabled={isSaving}
|
|
315
|
+
aria-label={isSaving ? t('saving') : t('saveLayout')}
|
|
308
316
|
>
|
|
309
317
|
<IconDeviceFloppy className="size-4" />
|
|
310
|
-
|
|
318
|
+
<span className="hidden sm:inline">
|
|
319
|
+
{isSaving ? t('saving') : t('saveLayout')}
|
|
320
|
+
</span>
|
|
311
321
|
</Button>
|
|
312
322
|
)}
|
|
313
323
|
<AddWidgetSelectorDialog
|
|
@@ -321,15 +331,16 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
|
|
|
321
331
|
</header>
|
|
322
332
|
<div className="flex flex-1 flex-col gap-4 overflow-auto p-4 pt-0">
|
|
323
333
|
{widgets.length > 0 ? (
|
|
324
|
-
<div className="min-h-[600px]">
|
|
334
|
+
<div className="min-h-[420px] sm:min-h-[520px] lg:min-h-[600px]">
|
|
325
335
|
<DraggableGrid
|
|
326
336
|
className="dashboard-grid"
|
|
327
337
|
layout={layout}
|
|
328
338
|
onLayoutChange={handleLayoutChange}
|
|
329
339
|
cols={12}
|
|
330
340
|
rowHeight={80}
|
|
331
|
-
|
|
332
|
-
|
|
341
|
+
preventCollision
|
|
342
|
+
isDraggable={!isMobile}
|
|
343
|
+
isResizable={!isMobile}
|
|
333
344
|
resizeHandles={['se']}
|
|
334
345
|
>
|
|
335
346
|
{widgets.map((widget) => (
|
|
@@ -36,6 +36,54 @@ interface DraggableGridProps {
|
|
|
36
36
|
resizeHandles?: Array<'s' | 'w' | 'e' | 'n' | 'sw' | 'nw' | 'se' | 'ne'>;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
const MOBILE_WIDTH = 640;
|
|
40
|
+
const TABLET_WIDTH = 1024;
|
|
41
|
+
|
|
42
|
+
const clamp = (value: number, min: number, max: number) =>
|
|
43
|
+
Math.max(min, Math.min(max, value));
|
|
44
|
+
|
|
45
|
+
const deriveResponsiveLayout = (
|
|
46
|
+
layout: Layout,
|
|
47
|
+
effectiveCols: number,
|
|
48
|
+
cols: number
|
|
49
|
+
): Layout => {
|
|
50
|
+
const sorted = [...layout].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
51
|
+
const ratio = effectiveCols / cols;
|
|
52
|
+
|
|
53
|
+
let cursorX = 0;
|
|
54
|
+
let cursorY = 0;
|
|
55
|
+
let rowHeight = 0;
|
|
56
|
+
|
|
57
|
+
return sorted.map((item) => {
|
|
58
|
+
const nextW =
|
|
59
|
+
effectiveCols === 1
|
|
60
|
+
? 1
|
|
61
|
+
: clamp(Math.round(item.w * ratio), 1, effectiveCols);
|
|
62
|
+
const nextH = Math.max(1, item.h || 1);
|
|
63
|
+
|
|
64
|
+
if (cursorX + nextW > effectiveCols) {
|
|
65
|
+
cursorY += Math.max(1, rowHeight);
|
|
66
|
+
cursorX = 0;
|
|
67
|
+
rowHeight = 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const mappedItem: LayoutItem = {
|
|
71
|
+
...item,
|
|
72
|
+
x: cursorX,
|
|
73
|
+
y: cursorY,
|
|
74
|
+
w: nextW,
|
|
75
|
+
h: nextH,
|
|
76
|
+
minW: clamp(Math.min(item.minW || 1, nextW), 1, nextW),
|
|
77
|
+
maxW: clamp(item.maxW || effectiveCols, nextW, effectiveCols),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
cursorX += nextW;
|
|
81
|
+
rowHeight = Math.max(rowHeight, nextH);
|
|
82
|
+
|
|
83
|
+
return mappedItem;
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
39
87
|
// Simple compaction function for grid layout
|
|
40
88
|
const compactLayout = (
|
|
41
89
|
layout: RGLLayout,
|
|
@@ -64,7 +112,7 @@ export function DraggableGrid({
|
|
|
64
112
|
isDraggable = true,
|
|
65
113
|
isResizable = true,
|
|
66
114
|
compactType = 'vertical',
|
|
67
|
-
preventCollision =
|
|
115
|
+
preventCollision = true,
|
|
68
116
|
margin = [16, 16],
|
|
69
117
|
containerPadding = [0, 0],
|
|
70
118
|
resizeHandles = ['se'],
|
|
@@ -72,6 +120,29 @@ export function DraggableGrid({
|
|
|
72
120
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
73
121
|
const [containerWidth, setContainerWidth] = useState(1200);
|
|
74
122
|
|
|
123
|
+
const effectiveCols =
|
|
124
|
+
containerWidth < MOBILE_WIDTH
|
|
125
|
+
? 1
|
|
126
|
+
: containerWidth < TABLET_WIDTH
|
|
127
|
+
? Math.min(6, cols)
|
|
128
|
+
: cols;
|
|
129
|
+
|
|
130
|
+
const effectiveRowHeight =
|
|
131
|
+
containerWidth < MOBILE_WIDTH ? Math.max(76, rowHeight - 4) : rowHeight;
|
|
132
|
+
|
|
133
|
+
const effectiveMargin: [number, number] =
|
|
134
|
+
containerWidth < MOBILE_WIDTH
|
|
135
|
+
? [Math.min(10, margin[0]), Math.min(10, margin[1])]
|
|
136
|
+
: margin;
|
|
137
|
+
|
|
138
|
+
const responsiveLayout = (() => {
|
|
139
|
+
if (effectiveCols === cols) {
|
|
140
|
+
return layout;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return deriveResponsiveLayout(layout, effectiveCols, cols);
|
|
144
|
+
})();
|
|
145
|
+
|
|
75
146
|
useEffect(() => {
|
|
76
147
|
const updateWidth = () => {
|
|
77
148
|
if (containerRef.current) {
|
|
@@ -91,6 +162,10 @@ export function DraggableGrid({
|
|
|
91
162
|
}, []);
|
|
92
163
|
|
|
93
164
|
const handleLayoutChange = (newLayout: RGLLayout) => {
|
|
165
|
+
if (effectiveCols !== cols) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
94
169
|
const layouts = Array.isArray(newLayout) ? newLayout : [newLayout];
|
|
95
170
|
const convertedLayout = layouts.map((item: any) => ({
|
|
96
171
|
i: item.i,
|
|
@@ -111,12 +186,12 @@ export function DraggableGrid({
|
|
|
111
186
|
<div ref={containerRef} className="w-full">
|
|
112
187
|
<GridLayout
|
|
113
188
|
className={`layout ${className}`}
|
|
114
|
-
layout={
|
|
189
|
+
layout={responsiveLayout}
|
|
115
190
|
gridConfig={{
|
|
116
|
-
cols,
|
|
117
|
-
rowHeight,
|
|
191
|
+
cols: effectiveCols,
|
|
192
|
+
rowHeight: effectiveRowHeight,
|
|
118
193
|
containerPadding,
|
|
119
|
-
margin,
|
|
194
|
+
margin: effectiveMargin,
|
|
120
195
|
}}
|
|
121
196
|
dragConfig={{
|
|
122
197
|
enabled: isDraggable,
|