@hed-hog/core 0.0.215 → 0.0.217
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/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +5 -5
- package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +1 -1
- package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +312 -0
- package/hedhog/frontend/app/dashboard/components/dashboard-grid.tsx.ejs +54 -0
- package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +132 -0
- package/hedhog/frontend/app/dashboard/components/dynamic-widget.tsx.ejs +88 -0
- package/hedhog/frontend/app/dashboard/components/index.ts.ejs +6 -0
- package/hedhog/frontend/app/dashboard/components/stats.tsx.ejs +93 -0
- package/hedhog/frontend/app/dashboard/components/widget-wrapper.tsx.ejs +150 -0
- package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +184 -0
- package/hedhog/frontend/app/dashboard/components/widgets/active-users-card.tsx.ejs +58 -0
- package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +219 -0
- package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +191 -0
- package/hedhog/frontend/app/dashboard/components/widgets/locale-config.tsx.ejs +309 -0
- package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +111 -0
- package/hedhog/frontend/app/dashboard/components/widgets/mail-config.tsx.ejs +445 -0
- package/hedhog/frontend/app/dashboard/components/widgets/mail-sent-card.tsx.ejs +58 -0
- package/hedhog/frontend/app/dashboard/components/widgets/mail-sent-chart.tsx.ejs +149 -0
- package/hedhog/frontend/app/dashboard/components/widgets/oauth-config.tsx.ejs +296 -0
- package/hedhog/frontend/app/dashboard/components/widgets/permissions-card.tsx.ejs +61 -0
- package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +152 -0
- package/hedhog/frontend/app/dashboard/components/widgets/profile-card.tsx.ejs +186 -0
- package/hedhog/frontend/app/dashboard/components/widgets/session-activity-chart.tsx.ejs +183 -0
- package/hedhog/frontend/app/dashboard/components/widgets/sessions-today-card.tsx.ejs +62 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +57 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +57 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +57 -0
- package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +57 -0
- package/hedhog/frontend/app/dashboard/components/widgets/storage-config.tsx.ejs +340 -0
- package/hedhog/frontend/app/dashboard/components/widgets/theme-config.tsx.ejs +275 -0
- package/hedhog/frontend/app/dashboard/components/widgets/user-growth-chart.tsx.ejs +210 -0
- package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +130 -0
- package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +233 -0
- package/hedhog/frontend/messages/en.json +143 -1
- package/hedhog/frontend/messages/pt.json +143 -1
- package/hedhog/table/dashboard_user.yaml +2 -10
- package/package.json +2 -2
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
AddWidgetSelectorDialog,
|
|
5
|
-
DraggableGrid,
|
|
6
|
-
LayoutItem,
|
|
7
|
-
} from '@/components/dashboard';
|
|
8
3
|
import {
|
|
9
4
|
Breadcrumb,
|
|
10
5
|
BreadcrumbItem,
|
|
@@ -22,6 +17,11 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|
|
22
17
|
import { useTranslations } from 'next-intl';
|
|
23
18
|
import { useRouter } from 'next/navigation';
|
|
24
19
|
import { useCallback, useEffect, useState } from 'react';
|
|
20
|
+
import {
|
|
21
|
+
AddWidgetSelectorDialog,
|
|
22
|
+
DraggableGrid,
|
|
23
|
+
LayoutItem,
|
|
24
|
+
} from '../components';
|
|
25
25
|
import '../dashboard.css';
|
|
26
26
|
import {
|
|
27
27
|
DashboardAccessResponse,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { DynamicWidget } from '@/components/dashboard';
|
|
4
3
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
5
4
|
import { useEffect, useState } from 'react';
|
|
5
|
+
import { DynamicWidget } from '../components';
|
|
6
6
|
import { WidgetRendererProps } from './types';
|
|
7
7
|
|
|
8
8
|
export const WidgetRenderer = ({ widget, onRemove }: WidgetRendererProps) => {
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
} from '@/components/ui/card';
|
|
10
|
+
import {
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogDescription,
|
|
14
|
+
DialogFooter,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
} from '@/components/ui/dialog';
|
|
18
|
+
import {
|
|
19
|
+
DropdownMenu,
|
|
20
|
+
DropdownMenuContent,
|
|
21
|
+
DropdownMenuItem,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from '@/components/ui/dropdown-menu';
|
|
24
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
25
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
26
|
+
import { cn } from '@/lib/utils';
|
|
27
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
28
|
+
import {
|
|
29
|
+
IconArrowsRightLeft,
|
|
30
|
+
IconPlus,
|
|
31
|
+
IconSettings,
|
|
32
|
+
} from '@tabler/icons-react';
|
|
33
|
+
import { useTranslations } from 'next-intl';
|
|
34
|
+
import { useRouter } from 'next/navigation';
|
|
35
|
+
import { useState } from 'react';
|
|
36
|
+
|
|
37
|
+
interface DashboardComponent {
|
|
38
|
+
id: number;
|
|
39
|
+
slug: string;
|
|
40
|
+
path: string;
|
|
41
|
+
min_width: number;
|
|
42
|
+
max_width?: number;
|
|
43
|
+
min_height: number;
|
|
44
|
+
max_height?: number;
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
is_resizable: boolean;
|
|
48
|
+
dashboard_component_locale?: Array<{
|
|
49
|
+
name: string;
|
|
50
|
+
locale: {
|
|
51
|
+
code: string;
|
|
52
|
+
};
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface Dashboard {
|
|
57
|
+
id: number;
|
|
58
|
+
slug: string;
|
|
59
|
+
dashboard_locale?: Array<{
|
|
60
|
+
name: string;
|
|
61
|
+
locale: {
|
|
62
|
+
code: string;
|
|
63
|
+
};
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface AddWidgetSelectorDialogProps {
|
|
68
|
+
availableComponents: DashboardComponent[];
|
|
69
|
+
isLoading: boolean;
|
|
70
|
+
onAdd: (slug: string) => void;
|
|
71
|
+
currentSlug?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function AddWidgetSelectorDialog({
|
|
75
|
+
availableComponents,
|
|
76
|
+
isLoading,
|
|
77
|
+
onAdd,
|
|
78
|
+
currentSlug = 'default',
|
|
79
|
+
}: AddWidgetSelectorDialogProps) {
|
|
80
|
+
const tWidget = useTranslations('core.AddWidgetDialog');
|
|
81
|
+
const tMenu = useTranslations('core.DashboardMenu');
|
|
82
|
+
const { request } = useApp();
|
|
83
|
+
const router = useRouter();
|
|
84
|
+
|
|
85
|
+
const [openWidgets, setOpenWidgets] = useState(false);
|
|
86
|
+
const [openDashboards, setOpenDashboards] = useState(false);
|
|
87
|
+
const [selectedWidget, setSelectedWidget] = useState<string | null>(null);
|
|
88
|
+
const [selectedDashboard, setSelectedDashboard] = useState<string | null>(
|
|
89
|
+
null
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Buscar dashboards disponíveis para o usuário
|
|
93
|
+
const { data: userDashboards, isLoading: isLoadingDashboards } = useQuery<
|
|
94
|
+
Dashboard[]
|
|
95
|
+
>({
|
|
96
|
+
queryKey: ['user-dashboards'],
|
|
97
|
+
queryFn: async () => {
|
|
98
|
+
const { data } = await request<Dashboard[]>({
|
|
99
|
+
url: '/dashboard-core/user-dashboards',
|
|
100
|
+
method: 'GET',
|
|
101
|
+
});
|
|
102
|
+
return data;
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const handleAdd = () => {
|
|
107
|
+
if (selectedWidget) {
|
|
108
|
+
onAdd(selectedWidget);
|
|
109
|
+
setSelectedWidget(null);
|
|
110
|
+
setOpenWidgets(false);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const handleSwitchDashboard = () => {
|
|
115
|
+
if (selectedDashboard && selectedDashboard !== currentSlug) {
|
|
116
|
+
router.push(`/core/dashboard/${selectedDashboard}`);
|
|
117
|
+
setOpenDashboards(false);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<>
|
|
123
|
+
<DropdownMenu>
|
|
124
|
+
<DropdownMenuTrigger asChild>
|
|
125
|
+
<Button size="sm" className="gap-2" variant="outline">
|
|
126
|
+
<IconSettings className="size-4" />
|
|
127
|
+
</Button>
|
|
128
|
+
</DropdownMenuTrigger>
|
|
129
|
+
<DropdownMenuContent align="end">
|
|
130
|
+
<DropdownMenuItem onClick={() => setOpenWidgets(true)}>
|
|
131
|
+
<IconPlus className="mr-2 size-4" />
|
|
132
|
+
{tMenu('addWidgets')}
|
|
133
|
+
</DropdownMenuItem>
|
|
134
|
+
<DropdownMenuItem onClick={() => setOpenDashboards(true)}>
|
|
135
|
+
<IconArrowsRightLeft className="mr-2 size-4" />
|
|
136
|
+
{tMenu('switchDashboard')}
|
|
137
|
+
</DropdownMenuItem>
|
|
138
|
+
</DropdownMenuContent>
|
|
139
|
+
</DropdownMenu>
|
|
140
|
+
|
|
141
|
+
{/* Diálogo de Adicionar Widgets */}
|
|
142
|
+
<Dialog open={openWidgets} onOpenChange={setOpenWidgets}>
|
|
143
|
+
<DialogContent className="sm:max-w-[600px]">
|
|
144
|
+
<DialogHeader>
|
|
145
|
+
<DialogTitle>{tWidget('title')}</DialogTitle>
|
|
146
|
+
<DialogDescription>{tWidget('description')}</DialogDescription>
|
|
147
|
+
</DialogHeader>
|
|
148
|
+
<ScrollArea className="max-h-[400px] pr-4">
|
|
149
|
+
{isLoading ? (
|
|
150
|
+
<div className="grid gap-3">
|
|
151
|
+
<Skeleton className="h-24 w-full" />
|
|
152
|
+
<Skeleton className="h-24 w-full" />
|
|
153
|
+
<Skeleton className="h-24 w-full" />
|
|
154
|
+
</div>
|
|
155
|
+
) : (
|
|
156
|
+
<div className="grid gap-3">
|
|
157
|
+
{availableComponents.length === 0 ? (
|
|
158
|
+
<div className="flex min-h-[200px] flex-col items-center justify-center text-center">
|
|
159
|
+
<p className="text-muted-foreground text-sm">
|
|
160
|
+
{tWidget('noComponentsAvailable')}
|
|
161
|
+
</p>
|
|
162
|
+
</div>
|
|
163
|
+
) : (
|
|
164
|
+
availableComponents.map((component) => {
|
|
165
|
+
const name =
|
|
166
|
+
component.dashboard_component_locale?.[0]?.name ||
|
|
167
|
+
component.slug;
|
|
168
|
+
return (
|
|
169
|
+
<Card
|
|
170
|
+
key={component.slug}
|
|
171
|
+
className={cn(
|
|
172
|
+
'cursor-pointer py-4 transition-colors hover:bg-accent',
|
|
173
|
+
selectedWidget === component.slug &&
|
|
174
|
+
'border-primary bg-accent'
|
|
175
|
+
)}
|
|
176
|
+
onClick={() => setSelectedWidget(component.slug)}
|
|
177
|
+
>
|
|
178
|
+
<CardHeader>
|
|
179
|
+
<div className="flex items-start gap-3">
|
|
180
|
+
<div className="bg-primary/10 flex size-10 items-center justify-center rounded-lg">
|
|
181
|
+
<IconPlus className="text-primary size-5" />
|
|
182
|
+
</div>
|
|
183
|
+
<div className="flex-1">
|
|
184
|
+
<CardTitle className="text-base">
|
|
185
|
+
{name}
|
|
186
|
+
</CardTitle>
|
|
187
|
+
<CardDescription className="text-sm">
|
|
188
|
+
{tWidget('dimensions')}: {component.width}x
|
|
189
|
+
{component.height} |
|
|
190
|
+
{component.is_resizable
|
|
191
|
+
? ` ${tWidget('resizable')}`
|
|
192
|
+
: ` ${tWidget('fixedSize')}`}
|
|
193
|
+
</CardDescription>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</CardHeader>
|
|
197
|
+
</Card>
|
|
198
|
+
);
|
|
199
|
+
})
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
</ScrollArea>
|
|
204
|
+
<DialogFooter>
|
|
205
|
+
<Button
|
|
206
|
+
type="button"
|
|
207
|
+
variant="outline"
|
|
208
|
+
onClick={() => setOpenWidgets(false)}
|
|
209
|
+
>
|
|
210
|
+
{tWidget('cancel')}
|
|
211
|
+
</Button>
|
|
212
|
+
<Button
|
|
213
|
+
type="button"
|
|
214
|
+
onClick={handleAdd}
|
|
215
|
+
disabled={!selectedWidget || isLoading}
|
|
216
|
+
>
|
|
217
|
+
{tWidget('add')}
|
|
218
|
+
</Button>
|
|
219
|
+
</DialogFooter>
|
|
220
|
+
</DialogContent>
|
|
221
|
+
</Dialog>
|
|
222
|
+
|
|
223
|
+
{/* Diálogo de Trocar Dashboard */}
|
|
224
|
+
<Dialog open={openDashboards} onOpenChange={setOpenDashboards}>
|
|
225
|
+
<DialogContent className="sm:max-w-[600px]">
|
|
226
|
+
<DialogHeader>
|
|
227
|
+
<DialogTitle>{tMenu('selectDashboardTitle')}</DialogTitle>
|
|
228
|
+
<DialogDescription>
|
|
229
|
+
{tMenu('selectDashboardDescription')}
|
|
230
|
+
</DialogDescription>
|
|
231
|
+
</DialogHeader>
|
|
232
|
+
<ScrollArea className="max-h-[400px] pr-4">
|
|
233
|
+
{isLoadingDashboards ? (
|
|
234
|
+
<div className="grid gap-3">
|
|
235
|
+
<Skeleton className="h-24 w-full" />
|
|
236
|
+
<Skeleton className="h-24 w-full" />
|
|
237
|
+
<Skeleton className="h-24 w-full" />
|
|
238
|
+
</div>
|
|
239
|
+
) : (
|
|
240
|
+
<div className="grid gap-3">
|
|
241
|
+
{!userDashboards || userDashboards.length === 0 ? (
|
|
242
|
+
<div className="flex min-h-[200px] flex-col items-center justify-center text-center">
|
|
243
|
+
<p className="text-muted-foreground text-sm">
|
|
244
|
+
{tMenu('noDashboardsAvailable')}
|
|
245
|
+
</p>
|
|
246
|
+
</div>
|
|
247
|
+
) : (
|
|
248
|
+
userDashboards.map((dashboard) => {
|
|
249
|
+
const name =
|
|
250
|
+
dashboard.dashboard_locale?.[0]?.name || dashboard.slug;
|
|
251
|
+
const isCurrent = dashboard.slug === currentSlug;
|
|
252
|
+
return (
|
|
253
|
+
<Card
|
|
254
|
+
key={dashboard.slug}
|
|
255
|
+
className={cn(
|
|
256
|
+
'cursor-pointer transition-colors py-4 hover:bg-accent',
|
|
257
|
+
selectedDashboard === dashboard.slug &&
|
|
258
|
+
'border-primary bg-accent',
|
|
259
|
+
isCurrent && 'opacity-50'
|
|
260
|
+
)}
|
|
261
|
+
onClick={() =>
|
|
262
|
+
!isCurrent && setSelectedDashboard(dashboard.slug)
|
|
263
|
+
}
|
|
264
|
+
>
|
|
265
|
+
<CardHeader>
|
|
266
|
+
<div className="flex items-start gap-3">
|
|
267
|
+
<div className="bg-primary/10 flex size-10 items-center justify-center rounded-lg">
|
|
268
|
+
<IconArrowsRightLeft className="text-primary size-5" />
|
|
269
|
+
</div>
|
|
270
|
+
<div className="flex-1">
|
|
271
|
+
<CardTitle className="text-base">
|
|
272
|
+
{name}
|
|
273
|
+
{isCurrent && ' (atual)'}
|
|
274
|
+
</CardTitle>
|
|
275
|
+
<CardDescription className="text-sm">
|
|
276
|
+
{dashboard.slug}
|
|
277
|
+
</CardDescription>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</CardHeader>
|
|
281
|
+
</Card>
|
|
282
|
+
);
|
|
283
|
+
})
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</ScrollArea>
|
|
288
|
+
<DialogFooter>
|
|
289
|
+
<Button
|
|
290
|
+
type="button"
|
|
291
|
+
variant="outline"
|
|
292
|
+
onClick={() => setOpenDashboards(false)}
|
|
293
|
+
>
|
|
294
|
+
{tWidget('cancel')}
|
|
295
|
+
</Button>
|
|
296
|
+
<Button
|
|
297
|
+
type="button"
|
|
298
|
+
onClick={handleSwitchDashboard}
|
|
299
|
+
disabled={
|
|
300
|
+
!selectedDashboard ||
|
|
301
|
+
isLoadingDashboards ||
|
|
302
|
+
selectedDashboard === currentSlug
|
|
303
|
+
}
|
|
304
|
+
>
|
|
305
|
+
{tMenu('switch')}
|
|
306
|
+
</Button>
|
|
307
|
+
</DialogFooter>
|
|
308
|
+
</DialogContent>
|
|
309
|
+
</Dialog>
|
|
310
|
+
</>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
interface DashboardGridProps {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DashboardGrid({ children, className }: DashboardGridProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
className={cn(
|
|
15
|
+
'grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
|
16
|
+
className
|
|
17
|
+
)}
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface DashboardGridItemProps {
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
className?: string;
|
|
27
|
+
colSpan?: 1 | 2 | 3 | 4;
|
|
28
|
+
rowSpan?: 1 | 2 | 3 | 4;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function DashboardGridItem({
|
|
32
|
+
children,
|
|
33
|
+
className,
|
|
34
|
+
colSpan = 1,
|
|
35
|
+
rowSpan = 1,
|
|
36
|
+
}: DashboardGridItemProps) {
|
|
37
|
+
const colSpanClass = {
|
|
38
|
+
1: 'col-span-1',
|
|
39
|
+
2: 'md:col-span-2',
|
|
40
|
+
3: 'lg:col-span-3',
|
|
41
|
+
4: 'xl:col-span-4',
|
|
42
|
+
}[colSpan];
|
|
43
|
+
|
|
44
|
+
const rowSpanClass = {
|
|
45
|
+
1: 'row-span-1',
|
|
46
|
+
2: 'md:row-span-2',
|
|
47
|
+
3: 'lg:row-span-3',
|
|
48
|
+
4: 'xl:row-span-4',
|
|
49
|
+
}[rowSpan];
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={cn(colSpanClass, rowSpanClass, className)}>{children}</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ReactNode, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import GridLayout, { Layout as RGLLayout } from 'react-grid-layout';
|
|
5
|
+
import 'react-grid-layout/css/styles.css';
|
|
6
|
+
import 'react-resizable/css/styles.css';
|
|
7
|
+
|
|
8
|
+
export interface LayoutItem {
|
|
9
|
+
i: string;
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
w: number;
|
|
13
|
+
h: number;
|
|
14
|
+
minW?: number;
|
|
15
|
+
maxW?: number;
|
|
16
|
+
minH?: number;
|
|
17
|
+
maxH?: number;
|
|
18
|
+
static?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type Layout = LayoutItem[];
|
|
22
|
+
|
|
23
|
+
interface DraggableGridProps {
|
|
24
|
+
layout: Layout;
|
|
25
|
+
onLayoutChange: (layout: Layout) => void;
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
className?: string;
|
|
28
|
+
cols?: number;
|
|
29
|
+
rowHeight?: number;
|
|
30
|
+
isDraggable?: boolean;
|
|
31
|
+
isResizable?: boolean;
|
|
32
|
+
compactType?: 'vertical' | 'horizontal' | null;
|
|
33
|
+
preventCollision?: boolean;
|
|
34
|
+
margin?: [number, number];
|
|
35
|
+
containerPadding?: [number, number];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function DraggableGrid({
|
|
39
|
+
layout,
|
|
40
|
+
onLayoutChange,
|
|
41
|
+
children,
|
|
42
|
+
className = '',
|
|
43
|
+
cols = 12,
|
|
44
|
+
rowHeight = 100,
|
|
45
|
+
isDraggable = true,
|
|
46
|
+
isResizable = true,
|
|
47
|
+
compactType = 'vertical',
|
|
48
|
+
preventCollision = false,
|
|
49
|
+
margin = [16, 16],
|
|
50
|
+
containerPadding = [0, 0],
|
|
51
|
+
}: DraggableGridProps) {
|
|
52
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
53
|
+
const [containerWidth, setContainerWidth] = useState(1200);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const updateWidth = () => {
|
|
57
|
+
if (containerRef.current) {
|
|
58
|
+
setContainerWidth(containerRef.current.offsetWidth);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
updateWidth();
|
|
63
|
+
const resizeObserver = new ResizeObserver(updateWidth);
|
|
64
|
+
if (containerRef.current) {
|
|
65
|
+
resizeObserver.observe(containerRef.current);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
resizeObserver.disconnect();
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const handleLayoutChange = (newLayout: RGLLayout) => {
|
|
74
|
+
const layouts = Array.isArray(newLayout) ? newLayout : [newLayout];
|
|
75
|
+
const convertedLayout = layouts.map((item: any) => ({
|
|
76
|
+
i: item.i,
|
|
77
|
+
x: item.x,
|
|
78
|
+
y: item.y,
|
|
79
|
+
w: item.w,
|
|
80
|
+
h: item.h,
|
|
81
|
+
minW: item.minW,
|
|
82
|
+
maxW: item.maxW,
|
|
83
|
+
minH: item.minH,
|
|
84
|
+
maxH: item.maxH,
|
|
85
|
+
static: item.static,
|
|
86
|
+
})) as LayoutItem[];
|
|
87
|
+
onLayoutChange(convertedLayout);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div ref={containerRef} className="w-full">
|
|
92
|
+
<GridLayout
|
|
93
|
+
className={`layout ${className}`}
|
|
94
|
+
layout={layout}
|
|
95
|
+
cols={cols}
|
|
96
|
+
rowHeight={rowHeight}
|
|
97
|
+
width={containerWidth}
|
|
98
|
+
isDraggable={isDraggable}
|
|
99
|
+
isResizable={isResizable}
|
|
100
|
+
compactType={compactType}
|
|
101
|
+
preventCollision={preventCollision}
|
|
102
|
+
margin={margin}
|
|
103
|
+
containerPadding={containerPadding}
|
|
104
|
+
onLayoutChange={handleLayoutChange}
|
|
105
|
+
useCSSTransforms={true}
|
|
106
|
+
draggableHandle=".drag-handle"
|
|
107
|
+
resizeHandles={['se', 's', 'e', 'sw', 'w', 'n', 'ne', 'nw']}
|
|
108
|
+
draggableCancel="button,.no-drag"
|
|
109
|
+
>
|
|
110
|
+
{children}
|
|
111
|
+
</GridLayout>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface DraggableGridItemProps {
|
|
117
|
+
id: string;
|
|
118
|
+
children: ReactNode;
|
|
119
|
+
className?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function DraggableGridItem({
|
|
123
|
+
id,
|
|
124
|
+
children,
|
|
125
|
+
className = '',
|
|
126
|
+
}: DraggableGridItemProps) {
|
|
127
|
+
return (
|
|
128
|
+
<div key={id} className={className} style={{ position: 'relative' }}>
|
|
129
|
+
{children}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
import * as TablerIcons from '@tabler/icons-react';
|
|
7
|
+
import { IconGripVertical, IconX } from '@tabler/icons-react';
|
|
8
|
+
import { ComponentType } from 'react';
|
|
9
|
+
|
|
10
|
+
interface DynamicWidgetProps {
|
|
11
|
+
title: string;
|
|
12
|
+
value: string | number;
|
|
13
|
+
description?: string;
|
|
14
|
+
iconName?: string;
|
|
15
|
+
color?: string;
|
|
16
|
+
draggable?: boolean;
|
|
17
|
+
onRemove?: () => void;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function DynamicWidget({
|
|
22
|
+
title,
|
|
23
|
+
value,
|
|
24
|
+
description,
|
|
25
|
+
iconName,
|
|
26
|
+
color,
|
|
27
|
+
draggable = false,
|
|
28
|
+
onRemove,
|
|
29
|
+
className,
|
|
30
|
+
}: DynamicWidgetProps) {
|
|
31
|
+
// Dynamically get icon from Tabler Icons
|
|
32
|
+
const Icon: ComponentType<{ className?: string }> | null = iconName
|
|
33
|
+
? (TablerIcons as any)[iconName] || null
|
|
34
|
+
: null;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Card className={cn('h-full flex flex-col', className)}>
|
|
38
|
+
<CardHeader
|
|
39
|
+
className={cn(
|
|
40
|
+
'flex flex-row items-center justify-between space-y-0 pb-2',
|
|
41
|
+
draggable && 'drag-handle'
|
|
42
|
+
)}
|
|
43
|
+
style={draggable ? { cursor: 'grab', userSelect: 'none' } : undefined}
|
|
44
|
+
onMouseDown={(e) =>
|
|
45
|
+
draggable && (e.currentTarget.style.cursor = 'grabbing')
|
|
46
|
+
}
|
|
47
|
+
onMouseUp={(e) => draggable && (e.currentTarget.style.cursor = 'grab')}
|
|
48
|
+
>
|
|
49
|
+
<div className="flex items-center gap-2">
|
|
50
|
+
{draggable && (
|
|
51
|
+
<IconGripVertical className="text-muted-foreground size-4 shrink-0" />
|
|
52
|
+
)}
|
|
53
|
+
<CardTitle className="text-md font-medium">{title}</CardTitle>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="flex items-center gap-2">
|
|
56
|
+
{onRemove && (
|
|
57
|
+
<Button
|
|
58
|
+
variant="ghost"
|
|
59
|
+
size="icon"
|
|
60
|
+
className="size-6 shrink-0 no-drag"
|
|
61
|
+
onClick={(e) => {
|
|
62
|
+
e.stopPropagation();
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
onRemove();
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<IconX className="size-3" />
|
|
68
|
+
</Button>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
</CardHeader>
|
|
72
|
+
<CardContent className="flex-1">
|
|
73
|
+
<div className="text-2xl font-bold">{value}</div>
|
|
74
|
+
{description && (
|
|
75
|
+
<p className="text-muted-foreground text-xs mt-1">{description}</p>
|
|
76
|
+
)}
|
|
77
|
+
{Icon && (
|
|
78
|
+
<Icon
|
|
79
|
+
className={cn(
|
|
80
|
+
'size-6 absolute bottom-4 right-4 shrink-0',
|
|
81
|
+
color ? `text-${color}` : 'text-muted-foreground'
|
|
82
|
+
)}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
</CardContent>
|
|
86
|
+
</Card>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { AddWidgetSelectorDialog } from './add-widget-selector-dialog';
|
|
2
|
+
export { DashboardGrid, DashboardGridItem } from './dashboard-grid';
|
|
3
|
+
export { DraggableGrid, DraggableGridItem } from './draggable-grid';
|
|
4
|
+
export type { Layout, LayoutItem } from './draggable-grid';
|
|
5
|
+
export { DynamicWidget } from './dynamic-widget';
|
|
6
|
+
export { WidgetWrapper } from './widget-wrapper';
|