@ttoss/react-dashboard 0.1.1
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/LICENSE +21 -0
- package/README.md +539 -0
- package/dist/esm/index.js +611 -0
- package/dist/index.d.ts +116 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Terezinha Tech Operations (ttoss)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
# @ttoss/react-dashboard
|
|
2
|
+
|
|
3
|
+
## About
|
|
4
|
+
|
|
5
|
+
A comprehensive React dashboard module that provides fully customizable dashboard functionality with filters, templates, and responsive grid layouts. This module enables you to create data-rich dashboards with support for multiple card types, filter systems, and template management.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
pnpm add @ttoss/react-dashboard @ttoss/components @ttoss/react-icons @ttoss/ui @emotion/react react-day-picker react-grid-layout
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Getting Started
|
|
14
|
+
|
|
15
|
+
### Provider Setup
|
|
16
|
+
|
|
17
|
+
If you're using the `Dashboard` component directly, you don't need to set up the provider separately as it wraps the `DashboardProvider` internally. However, if you need to use the `useDashboard` hook in other components, you can wrap your application with the `DashboardProvider`:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { DashboardProvider } from '@ttoss/react-dashboard';
|
|
21
|
+
import { ThemeProvider } from '@ttoss/ui';
|
|
22
|
+
import type {
|
|
23
|
+
DashboardTemplate,
|
|
24
|
+
DashboardFilter,
|
|
25
|
+
} from '@ttoss/react-dashboard';
|
|
26
|
+
|
|
27
|
+
const templates: DashboardTemplate[] = [];
|
|
28
|
+
const filters: DashboardFilter[] = [];
|
|
29
|
+
|
|
30
|
+
ReactDOM.render(
|
|
31
|
+
<React.StrictMode>
|
|
32
|
+
<ThemeProvider>
|
|
33
|
+
<DashboardProvider filters={filters} templates={templates}>
|
|
34
|
+
<App />
|
|
35
|
+
</DashboardProvider>
|
|
36
|
+
</ThemeProvider>
|
|
37
|
+
</React.StrictMode>,
|
|
38
|
+
document.getElementById('root')
|
|
39
|
+
);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Basic Dashboard Usage
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { Dashboard } from '@ttoss/react-dashboard';
|
|
46
|
+
import type {
|
|
47
|
+
DashboardTemplate,
|
|
48
|
+
DashboardFilter,
|
|
49
|
+
} from '@ttoss/react-dashboard';
|
|
50
|
+
import { DashboardFilterType } from '@ttoss/react-dashboard';
|
|
51
|
+
|
|
52
|
+
const MyDashboard = () => {
|
|
53
|
+
const templates: DashboardTemplate[] = [
|
|
54
|
+
{
|
|
55
|
+
id: 'default',
|
|
56
|
+
name: 'Default Template',
|
|
57
|
+
description: 'My default dashboard layout',
|
|
58
|
+
grid: [
|
|
59
|
+
{
|
|
60
|
+
i: 'card-1',
|
|
61
|
+
x: 0,
|
|
62
|
+
y: 0,
|
|
63
|
+
w: 4,
|
|
64
|
+
h: 4,
|
|
65
|
+
card: {
|
|
66
|
+
title: 'Total Revenue',
|
|
67
|
+
numberType: 'currency',
|
|
68
|
+
type: 'bigNumber',
|
|
69
|
+
sourceType: [{ source: 'api' }],
|
|
70
|
+
data: {
|
|
71
|
+
api: { total: 150000 },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const filters: DashboardFilter[] = [
|
|
80
|
+
{
|
|
81
|
+
key: 'date-range',
|
|
82
|
+
type: DashboardFilterType.DATE_RANGE,
|
|
83
|
+
label: 'Date Range',
|
|
84
|
+
value: { from: new Date(), to: new Date() },
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
return <Dashboard templates={templates} filters={filters} loading={false} />;
|
|
89
|
+
};
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Components
|
|
93
|
+
|
|
94
|
+
### Dashboard
|
|
95
|
+
|
|
96
|
+
The main dashboard component that orchestrates the entire dashboard experience.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { Dashboard } from '@ttoss/react-dashboard';
|
|
100
|
+
|
|
101
|
+
<Dashboard
|
|
102
|
+
templates={templates}
|
|
103
|
+
filters={filters}
|
|
104
|
+
loading={false}
|
|
105
|
+
headerChildren={<CustomHeaderContent />}
|
|
106
|
+
onFiltersChange={(filters) => {
|
|
107
|
+
// Handle filter changes
|
|
108
|
+
}}
|
|
109
|
+
/>;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Props:**
|
|
113
|
+
|
|
114
|
+
| Prop | Type | Default | Description |
|
|
115
|
+
| ----------------- | -------------------------------------- | ------- | ------------------------------------------ |
|
|
116
|
+
| `templates` | `DashboardTemplate[]` | `[]` | Array of dashboard templates |
|
|
117
|
+
| `filters` | `DashboardFilter[]` | `[]` | Array of dashboard filters |
|
|
118
|
+
| `loading` | `boolean` | `false` | Loading state for the dashboard |
|
|
119
|
+
| `headerChildren` | `React.ReactNode` | - | Additional content to render in the header |
|
|
120
|
+
| `onFiltersChange` | `(filters: DashboardFilter[]) => void` | - | Callback when filters change |
|
|
121
|
+
|
|
122
|
+
### DashboardProvider
|
|
123
|
+
|
|
124
|
+
Context provider that manages dashboard state (filters and templates).
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { DashboardProvider } from '@ttoss/react-dashboard';
|
|
128
|
+
|
|
129
|
+
<DashboardProvider
|
|
130
|
+
filters={filters}
|
|
131
|
+
templates={templates}
|
|
132
|
+
onFiltersChange={handleFiltersChange}
|
|
133
|
+
>
|
|
134
|
+
{children}
|
|
135
|
+
</DashboardProvider>;
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Props:**
|
|
139
|
+
|
|
140
|
+
| Prop | Type | Default | Description |
|
|
141
|
+
| ----------------- | -------------------------------------- | ------- | ---------------------------- |
|
|
142
|
+
| `children` | `React.ReactNode` | - | Child components |
|
|
143
|
+
| `filters` | `DashboardFilter[]` | `[]` | Filter state |
|
|
144
|
+
| `templates` | `DashboardTemplate[]` | `[]` | Template state |
|
|
145
|
+
| `onFiltersChange` | `(filters: DashboardFilter[]) => void` | - | Callback when filters change |
|
|
146
|
+
|
|
147
|
+
### useDashboard Hook
|
|
148
|
+
|
|
149
|
+
Hook to access and modify dashboard state.
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { useDashboard } from '@ttoss/react-dashboard';
|
|
153
|
+
|
|
154
|
+
const MyComponent = () => {
|
|
155
|
+
const { filters, updateFilter, templates, selectedTemplate } = useDashboard();
|
|
156
|
+
|
|
157
|
+
// Use dashboard state
|
|
158
|
+
const handleFilterChange = (key: string, value: DashboardFilterValue) => {
|
|
159
|
+
updateFilter(key, value);
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Returns:**
|
|
165
|
+
|
|
166
|
+
| Property | Type | Description |
|
|
167
|
+
| ------------------ | ---------------------------------------------------- | -------------------------------------------- |
|
|
168
|
+
| `filters` | `DashboardFilter[]` | Current filter state |
|
|
169
|
+
| `updateFilter` | `(key: string, value: DashboardFilterValue) => void` | Function to update a specific filter by key |
|
|
170
|
+
| `templates` | `DashboardTemplate[]` | Current template state |
|
|
171
|
+
| `selectedTemplate` | `DashboardTemplate \| undefined` | Currently selected template based on filters |
|
|
172
|
+
|
|
173
|
+
### DashboardCard
|
|
174
|
+
|
|
175
|
+
Component for rendering individual dashboard cards. Currently supports `bigNumber` type.
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
import { DashboardCard } from '@ttoss/react-dashboard';
|
|
179
|
+
|
|
180
|
+
<DashboardCard
|
|
181
|
+
title="Total Revenue"
|
|
182
|
+
description="Revenue from all sources"
|
|
183
|
+
numberType="currency"
|
|
184
|
+
type="bigNumber"
|
|
185
|
+
sourceType={[{ source: 'api' }]}
|
|
186
|
+
data={{
|
|
187
|
+
api: { total: 150000 },
|
|
188
|
+
}}
|
|
189
|
+
trend={{
|
|
190
|
+
value: 15.5,
|
|
191
|
+
status: 'positive',
|
|
192
|
+
}}
|
|
193
|
+
/>;
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Props:**
|
|
197
|
+
|
|
198
|
+
| Prop | Type | Default | Description |
|
|
199
|
+
| ---------------- | ------------------------- | ------- | ----------------------------------------------------------------------------------------- |
|
|
200
|
+
| `title` | `string` | - | Card title |
|
|
201
|
+
| `description` | `string` | - | Optional card description |
|
|
202
|
+
| `icon` | `string` | - | Optional icon name |
|
|
203
|
+
| `color` | `string` | - | Optional color for the card |
|
|
204
|
+
| `variant` | `CardVariant` | - | Card variant (`'default' \| 'dark' \| 'light-green'`) |
|
|
205
|
+
| `numberType` | `CardNumberType` | - | Number formatting type (`'number' \| 'percentage' \| 'currency'`) |
|
|
206
|
+
| `type` | `DashboardCardType` | - | Card type (`'bigNumber' \| 'pieChart' \| 'barChart' \| 'lineChart' \| 'table' \| 'list'`) |
|
|
207
|
+
| `sourceType` | `CardSourceType[]` | - | Data source configuration |
|
|
208
|
+
| `labels` | `Array<string \| number>` | - | Optional labels for the card |
|
|
209
|
+
| `data` | `DashboardCardData` | - | Card data from various sources |
|
|
210
|
+
| `trend` | `TrendIndicator` | - | Optional trend indicator |
|
|
211
|
+
| `additionalInfo` | `string` | - | Optional additional information text |
|
|
212
|
+
| `status` | `StatusIndicator` | - | Optional status indicator |
|
|
213
|
+
|
|
214
|
+
### DashboardFilters
|
|
215
|
+
|
|
216
|
+
Component that automatically renders filters based on the dashboard state.
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
import { DashboardFilters } from '@ttoss/react-dashboard';
|
|
220
|
+
|
|
221
|
+
// Automatically renders filters from DashboardProvider context
|
|
222
|
+
<DashboardFilters />;
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
This component reads filters from the `DashboardProvider` context and renders the appropriate filter component for each filter type.
|
|
226
|
+
|
|
227
|
+
### DashboardHeader
|
|
228
|
+
|
|
229
|
+
Header component that displays filters and optional custom content.
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
import { DashboardHeader } from '@ttoss/react-dashboard';
|
|
233
|
+
|
|
234
|
+
<DashboardHeader>
|
|
235
|
+
<CustomActionButtons />
|
|
236
|
+
</DashboardHeader>;
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Props:**
|
|
240
|
+
|
|
241
|
+
| Prop | Type | Description |
|
|
242
|
+
| ---------- | ----------------- | ------------------------------------ |
|
|
243
|
+
| `children` | `React.ReactNode` | Optional content to render in header |
|
|
244
|
+
|
|
245
|
+
### DashboardGrid
|
|
246
|
+
|
|
247
|
+
Responsive grid layout component that displays dashboard cards using `react-grid-layout`.
|
|
248
|
+
|
|
249
|
+
```tsx
|
|
250
|
+
import { DashboardGrid } from '@ttoss/react-dashboard';
|
|
251
|
+
|
|
252
|
+
<DashboardGrid loading={false} />;
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Props:**
|
|
256
|
+
|
|
257
|
+
| Prop | Type | Default | Description |
|
|
258
|
+
| --------- | --------- | ------- | ----------------------------- |
|
|
259
|
+
| `loading` | `boolean` | - | Shows loading spinner if true |
|
|
260
|
+
|
|
261
|
+
## Filter Types
|
|
262
|
+
|
|
263
|
+
### Text Filter
|
|
264
|
+
|
|
265
|
+
A text input filter for string values.
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
import { DashboardFilterType } from '@ttoss/react-dashboard';
|
|
269
|
+
|
|
270
|
+
const textFilter: DashboardFilter = {
|
|
271
|
+
key: 'search',
|
|
272
|
+
type: DashboardFilterType.TEXT,
|
|
273
|
+
label: 'Search',
|
|
274
|
+
placeholder: 'Enter search term...',
|
|
275
|
+
value: '',
|
|
276
|
+
onChange: (value) => {
|
|
277
|
+
console.log('Search:', value);
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Select Filter
|
|
283
|
+
|
|
284
|
+
A dropdown select filter for predefined options.
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
const selectFilter: DashboardFilter = {
|
|
288
|
+
key: 'status',
|
|
289
|
+
type: DashboardFilterType.SELECT,
|
|
290
|
+
label: 'Status',
|
|
291
|
+
value: 'active',
|
|
292
|
+
options: [
|
|
293
|
+
{ label: 'Active', value: 'active' },
|
|
294
|
+
{ label: 'Inactive', value: 'inactive' },
|
|
295
|
+
],
|
|
296
|
+
onChange: (value) => {
|
|
297
|
+
console.log('Status:', value);
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Date Range Filter
|
|
303
|
+
|
|
304
|
+
A date range picker with optional presets.
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
const dateRangeFilter: DashboardFilter = {
|
|
308
|
+
key: 'date-range',
|
|
309
|
+
type: DashboardFilterType.DATE_RANGE,
|
|
310
|
+
label: 'Date Range',
|
|
311
|
+
value: { from: new Date(), to: new Date() },
|
|
312
|
+
presets: [
|
|
313
|
+
{
|
|
314
|
+
label: 'Last 7 days',
|
|
315
|
+
getValue: () => ({
|
|
316
|
+
from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
|
317
|
+
to: new Date(),
|
|
318
|
+
}),
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
label: 'Last 30 days',
|
|
322
|
+
getValue: () => ({
|
|
323
|
+
from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
|
324
|
+
to: new Date(),
|
|
325
|
+
}),
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
onChange: (value) => {
|
|
329
|
+
console.log('Date range:', value);
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Types
|
|
335
|
+
|
|
336
|
+
### DashboardTemplate
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
interface DashboardTemplate {
|
|
340
|
+
id: string;
|
|
341
|
+
name: string;
|
|
342
|
+
description?: string;
|
|
343
|
+
grid: DashboardGridItem[];
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### DashboardGridItem
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
interface DashboardGridItem extends ReactGridLayout.Layout {
|
|
351
|
+
card: DashboardCard;
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### DashboardFilter
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
interface DashboardFilter {
|
|
359
|
+
key: string;
|
|
360
|
+
type: DashboardFilterType;
|
|
361
|
+
label: string;
|
|
362
|
+
placeholder?: string;
|
|
363
|
+
value: DashboardFilterValue;
|
|
364
|
+
onChange?: (value: DashboardFilterValue) => void;
|
|
365
|
+
options?: { label: string; value: string | number | boolean }[];
|
|
366
|
+
presets?: { label: string; getValue: () => DateRange }[];
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### DashboardFilterValue
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
type DashboardFilterValue =
|
|
374
|
+
| string
|
|
375
|
+
| number
|
|
376
|
+
| boolean
|
|
377
|
+
| { from: Date; to: Date };
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### DashboardFilterType
|
|
381
|
+
|
|
382
|
+
```tsx
|
|
383
|
+
enum DashboardFilterType {
|
|
384
|
+
TEXT = 'text',
|
|
385
|
+
SELECT = 'select',
|
|
386
|
+
DATE_RANGE = 'date-range',
|
|
387
|
+
NUMBER = 'number',
|
|
388
|
+
BOOLEAN = 'boolean',
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### DashboardCard
|
|
393
|
+
|
|
394
|
+
See [DashboardCard](#dashboardcard) section for full interface.
|
|
395
|
+
|
|
396
|
+
### CardNumberType
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
type CardNumberType = 'number' | 'percentage' | 'currency';
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### CardSourceType
|
|
403
|
+
|
|
404
|
+
```tsx
|
|
405
|
+
type CardSourceType = {
|
|
406
|
+
source: 'meta' | 'oneclickads' | 'api';
|
|
407
|
+
level?: 'adAccount' | 'campaign' | 'adSet' | 'ad';
|
|
408
|
+
};
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### DashboardCardType
|
|
412
|
+
|
|
413
|
+
```tsx
|
|
414
|
+
type DashboardCardType =
|
|
415
|
+
| 'bigNumber'
|
|
416
|
+
| 'pieChart'
|
|
417
|
+
| 'barChart'
|
|
418
|
+
| 'lineChart'
|
|
419
|
+
| 'table'
|
|
420
|
+
| 'list';
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### CardVariant
|
|
424
|
+
|
|
425
|
+
```tsx
|
|
426
|
+
type CardVariant = 'default' | 'dark' | 'light-green';
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## Complete Example
|
|
430
|
+
|
|
431
|
+
```tsx
|
|
432
|
+
import { Dashboard } from '@ttoss/react-dashboard';
|
|
433
|
+
import type {
|
|
434
|
+
DashboardTemplate,
|
|
435
|
+
DashboardFilter,
|
|
436
|
+
} from '@ttoss/react-dashboard';
|
|
437
|
+
import { DashboardFilterType } from '@ttoss/react-dashboard';
|
|
438
|
+
|
|
439
|
+
const App = () => {
|
|
440
|
+
const templates: DashboardTemplate[] = [
|
|
441
|
+
{
|
|
442
|
+
id: 'default',
|
|
443
|
+
name: 'Default Dashboard',
|
|
444
|
+
grid: [
|
|
445
|
+
{
|
|
446
|
+
i: 'revenue',
|
|
447
|
+
x: 0,
|
|
448
|
+
y: 0,
|
|
449
|
+
w: 4,
|
|
450
|
+
h: 4,
|
|
451
|
+
card: {
|
|
452
|
+
title: 'Total Revenue',
|
|
453
|
+
numberType: 'currency',
|
|
454
|
+
type: 'bigNumber',
|
|
455
|
+
sourceType: [{ source: 'api' }],
|
|
456
|
+
data: {
|
|
457
|
+
api: { total: 150000 },
|
|
458
|
+
},
|
|
459
|
+
trend: {
|
|
460
|
+
value: 15.5,
|
|
461
|
+
status: 'positive',
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
i: 'roas',
|
|
467
|
+
x: 4,
|
|
468
|
+
y: 0,
|
|
469
|
+
w: 4,
|
|
470
|
+
h: 4,
|
|
471
|
+
card: {
|
|
472
|
+
title: 'ROAS',
|
|
473
|
+
numberType: 'number',
|
|
474
|
+
type: 'bigNumber',
|
|
475
|
+
sourceType: [{ source: 'api' }],
|
|
476
|
+
data: {
|
|
477
|
+
api: { total: 3.5 },
|
|
478
|
+
},
|
|
479
|
+
variant: 'light-green',
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
},
|
|
484
|
+
];
|
|
485
|
+
|
|
486
|
+
const filters: DashboardFilter[] = [
|
|
487
|
+
{
|
|
488
|
+
key: 'template',
|
|
489
|
+
type: DashboardFilterType.SELECT,
|
|
490
|
+
label: 'Template',
|
|
491
|
+
value: 'default',
|
|
492
|
+
options: [{ label: 'Default Dashboard', value: 'default' }],
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
key: 'date-range',
|
|
496
|
+
type: DashboardFilterType.DATE_RANGE,
|
|
497
|
+
label: 'Date Range',
|
|
498
|
+
value: {
|
|
499
|
+
from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
|
500
|
+
to: new Date(),
|
|
501
|
+
},
|
|
502
|
+
presets: [
|
|
503
|
+
{
|
|
504
|
+
label: 'Last 7 days',
|
|
505
|
+
getValue: () => ({
|
|
506
|
+
from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
|
507
|
+
to: new Date(),
|
|
508
|
+
}),
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
label: 'Last 30 days',
|
|
512
|
+
getValue: () => ({
|
|
513
|
+
from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
|
514
|
+
to: new Date(),
|
|
515
|
+
}),
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
},
|
|
519
|
+
];
|
|
520
|
+
|
|
521
|
+
const handleFiltersChange = (updatedFilters: DashboardFilter[]) => {
|
|
522
|
+
console.log('Filters changed:', updatedFilters);
|
|
523
|
+
// Update your data fetching logic here
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<Dashboard
|
|
528
|
+
templates={templates}
|
|
529
|
+
filters={filters}
|
|
530
|
+
loading={false}
|
|
531
|
+
onFiltersChange={handleFiltersChange}
|
|
532
|
+
/>
|
|
533
|
+
);
|
|
534
|
+
};
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
## License
|
|
538
|
+
|
|
539
|
+
[MIT](https://github.com/ttoss/ttoss/blob/main/LICENSE)
|
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __name = (target, value) => __defProp(target, "name", {
|
|
5
|
+
value,
|
|
6
|
+
configurable: true
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/Dashboard.tsx
|
|
10
|
+
import { Divider, Flex as Flex6 } from "@ttoss/ui";
|
|
11
|
+
import * as React8 from "react";
|
|
12
|
+
|
|
13
|
+
// src/DashboardGrid.tsx
|
|
14
|
+
import "react-grid-layout/css/styles.css";
|
|
15
|
+
import { Box as Box3, Flex as Flex3, Spinner } from "@ttoss/ui";
|
|
16
|
+
import { Responsive, WidthProvider } from "react-grid-layout";
|
|
17
|
+
|
|
18
|
+
// src/Cards/BigNumber.tsx
|
|
19
|
+
import { Icon } from "@ttoss/react-icons";
|
|
20
|
+
import { Box as Box2, Flex as Flex2, Text as Text2 } from "@ttoss/ui";
|
|
21
|
+
|
|
22
|
+
// src/Cards/Wrapper.tsx
|
|
23
|
+
import { Box, Flex, Text, TooltipIcon } from "@ttoss/ui";
|
|
24
|
+
var CardWrapper = /* @__PURE__ */__name(({
|
|
25
|
+
title,
|
|
26
|
+
children,
|
|
27
|
+
description,
|
|
28
|
+
variant = "default"
|
|
29
|
+
}) => {
|
|
30
|
+
const getBackgroundColor = /* @__PURE__ */__name(() => {
|
|
31
|
+
switch (variant) {
|
|
32
|
+
case "dark":
|
|
33
|
+
return "input.background.secondary.default";
|
|
34
|
+
case "light-green":
|
|
35
|
+
return "feedback.background.positive.default";
|
|
36
|
+
case "default":
|
|
37
|
+
default:
|
|
38
|
+
return "display.background.primary.default";
|
|
39
|
+
}
|
|
40
|
+
}, "getBackgroundColor");
|
|
41
|
+
const getTitleColor = /* @__PURE__ */__name(() => {
|
|
42
|
+
switch (variant) {
|
|
43
|
+
case "dark":
|
|
44
|
+
return "action.text.primary.default";
|
|
45
|
+
case "light-green":
|
|
46
|
+
return "feedback.text.positive.default";
|
|
47
|
+
case "default":
|
|
48
|
+
default:
|
|
49
|
+
return "display.text.primary.default";
|
|
50
|
+
}
|
|
51
|
+
}, "getTitleColor");
|
|
52
|
+
return /* @__PURE__ */React.createElement(Flex, {
|
|
53
|
+
sx: {
|
|
54
|
+
flexDirection: "column",
|
|
55
|
+
gap: "3",
|
|
56
|
+
backgroundColor: getBackgroundColor(),
|
|
57
|
+
borderRadius: "lg",
|
|
58
|
+
padding: "4",
|
|
59
|
+
width: "100%",
|
|
60
|
+
height: "100%",
|
|
61
|
+
boxShadow: "0px 2px 8px rgba(0, 0, 0, 0.15)"
|
|
62
|
+
}
|
|
63
|
+
}, /* @__PURE__ */React.createElement(Box, {
|
|
64
|
+
sx: {
|
|
65
|
+
alignItems: "center",
|
|
66
|
+
gap: "2"
|
|
67
|
+
}
|
|
68
|
+
}, /* @__PURE__ */React.createElement(Text, {
|
|
69
|
+
variant: "h5",
|
|
70
|
+
sx: {
|
|
71
|
+
color: getTitleColor(),
|
|
72
|
+
flex: 1,
|
|
73
|
+
minWidth: 0
|
|
74
|
+
}
|
|
75
|
+
}, title), description && /* @__PURE__ */React.createElement(TooltipIcon, {
|
|
76
|
+
variant: "info",
|
|
77
|
+
icon: "ant-design:info-circle-outlined",
|
|
78
|
+
tooltip: {
|
|
79
|
+
children: description
|
|
80
|
+
},
|
|
81
|
+
sx: {
|
|
82
|
+
marginLeft: "2",
|
|
83
|
+
flexShrink: 0
|
|
84
|
+
}
|
|
85
|
+
})), /* @__PURE__ */React.createElement(Flex, {
|
|
86
|
+
sx: {
|
|
87
|
+
flexDirection: "column",
|
|
88
|
+
flex: "auto",
|
|
89
|
+
height: "100%",
|
|
90
|
+
justifyContent: "center"
|
|
91
|
+
}
|
|
92
|
+
}, children));
|
|
93
|
+
}, "CardWrapper");
|
|
94
|
+
|
|
95
|
+
// src/Cards/BigNumber.tsx
|
|
96
|
+
var formatNumber = /* @__PURE__ */__name((value, type) => {
|
|
97
|
+
if (value === void 0 || value === null) {
|
|
98
|
+
return "-";
|
|
99
|
+
}
|
|
100
|
+
switch (type) {
|
|
101
|
+
case "currency":
|
|
102
|
+
return new Intl.NumberFormat("pt-BR", {
|
|
103
|
+
style: "currency",
|
|
104
|
+
currency: "BRL"
|
|
105
|
+
}).format(value);
|
|
106
|
+
case "percentage":
|
|
107
|
+
return `${value.toFixed(2)}%`;
|
|
108
|
+
case "number":
|
|
109
|
+
default:
|
|
110
|
+
{
|
|
111
|
+
const formatted = new Intl.NumberFormat("pt-BR", {
|
|
112
|
+
minimumFractionDigits: 2,
|
|
113
|
+
maximumFractionDigits: 2
|
|
114
|
+
}).format(value);
|
|
115
|
+
return formatted;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}, "formatNumber");
|
|
119
|
+
var getValueColor = /* @__PURE__ */__name((color, variant) => {
|
|
120
|
+
if (variant === "dark") {
|
|
121
|
+
return "action.text.primary.default";
|
|
122
|
+
}
|
|
123
|
+
if (variant === "light-green") {
|
|
124
|
+
return "feedback.text.positive.default";
|
|
125
|
+
}
|
|
126
|
+
if (!color) {
|
|
127
|
+
return "display.text.primary.default";
|
|
128
|
+
}
|
|
129
|
+
if (["green", "accent", "positive"].includes(color.toLowerCase())) {
|
|
130
|
+
return "display.text.accent.default";
|
|
131
|
+
}
|
|
132
|
+
return "display.text.primary.default";
|
|
133
|
+
}, "getValueColor");
|
|
134
|
+
var getTrendColor = /* @__PURE__ */__name(status => {
|
|
135
|
+
if (status === "positive") {
|
|
136
|
+
return "display.text.accent.default";
|
|
137
|
+
}
|
|
138
|
+
if (status === "negative") {
|
|
139
|
+
return "display.text.negative.default";
|
|
140
|
+
}
|
|
141
|
+
return "display.text.primary.default";
|
|
142
|
+
}, "getTrendColor");
|
|
143
|
+
var BigNumber = /* @__PURE__ */__name(props => {
|
|
144
|
+
const total = props.data.api?.total ?? props.data.meta?.total ?? void 0;
|
|
145
|
+
const formattedValue = formatNumber(total, props.numberType);
|
|
146
|
+
const displayValue = props.numberType === "number" && props.title.toLowerCase().includes("roas") ? `${formattedValue}x` : formattedValue;
|
|
147
|
+
const valueColor = getValueColor(props.color, props.variant);
|
|
148
|
+
const variant = props.variant || "default";
|
|
149
|
+
return /* @__PURE__ */React.createElement(CardWrapper, {
|
|
150
|
+
title: props.title,
|
|
151
|
+
description: props.description || "",
|
|
152
|
+
variant
|
|
153
|
+
}, /* @__PURE__ */React.createElement(Flex2, {
|
|
154
|
+
sx: {
|
|
155
|
+
flexDirection: "column",
|
|
156
|
+
gap: "2"
|
|
157
|
+
}
|
|
158
|
+
}, /* @__PURE__ */React.createElement(Text2, {
|
|
159
|
+
sx: {
|
|
160
|
+
color: valueColor,
|
|
161
|
+
fontSize: "2xl",
|
|
162
|
+
fontWeight: "bold",
|
|
163
|
+
lineHeight: "1.2"
|
|
164
|
+
}
|
|
165
|
+
}, displayValue), props.trend && /* @__PURE__ */React.createElement(Flex2, {
|
|
166
|
+
sx: {
|
|
167
|
+
alignItems: "center",
|
|
168
|
+
gap: "1"
|
|
169
|
+
}
|
|
170
|
+
}, /* @__PURE__ */React.createElement(Box2, {
|
|
171
|
+
sx: {
|
|
172
|
+
color: getTrendColor(props.trend.status),
|
|
173
|
+
display: "flex",
|
|
174
|
+
alignItems: "center"
|
|
175
|
+
}
|
|
176
|
+
}, props.trend.status === "positive" ? /* @__PURE__ */React.createElement(Icon, {
|
|
177
|
+
icon: "mdi:arrow-up",
|
|
178
|
+
width: 16
|
|
179
|
+
}) : props.trend.status === "negative" ? /* @__PURE__ */React.createElement(Icon, {
|
|
180
|
+
icon: "mdi:arrow-down",
|
|
181
|
+
width: 16
|
|
182
|
+
}) : null, props.trend.status === "neutral" ? /* @__PURE__ */React.createElement(Icon, {
|
|
183
|
+
icon: "mdi:arrow-right",
|
|
184
|
+
width: 16
|
|
185
|
+
}) : null), /* @__PURE__ */React.createElement(Text2, {
|
|
186
|
+
sx: {
|
|
187
|
+
color: getTrendColor(props.trend.status),
|
|
188
|
+
fontSize: "sm",
|
|
189
|
+
fontWeight: "medium"
|
|
190
|
+
}
|
|
191
|
+
}, props.trend.status === "positive" ? "+" : "", props.trend.status === "negative" ? "-" : "", props.trend.status === "neutral" ? "" : "", props.trend.value.toFixed(1), "%", " ", props.trend ? "vs. per\xEDodo anterior" : "")), props.additionalInfo && /* @__PURE__ */React.createElement(Text2, {
|
|
192
|
+
sx: {
|
|
193
|
+
color: getValueColor(props.color, props.variant),
|
|
194
|
+
fontSize: "sm",
|
|
195
|
+
mt: "1"
|
|
196
|
+
}
|
|
197
|
+
}, props.additionalInfo), props.status && /* @__PURE__ */React.createElement(Flex2, {
|
|
198
|
+
sx: {
|
|
199
|
+
alignItems: "center",
|
|
200
|
+
gap: "1",
|
|
201
|
+
mt: props.trend || props.additionalInfo ? "2" : "1"
|
|
202
|
+
}
|
|
203
|
+
}, props.status.icon && /* @__PURE__ */React.createElement(Box2, {
|
|
204
|
+
sx: {
|
|
205
|
+
color: "feedback.text.positive.default",
|
|
206
|
+
display: "flex",
|
|
207
|
+
alignItems: "center"
|
|
208
|
+
}
|
|
209
|
+
}, /* @__PURE__ */React.createElement(Icon, {
|
|
210
|
+
icon: props.status.icon,
|
|
211
|
+
width: 16
|
|
212
|
+
})), /* @__PURE__ */React.createElement(Text2, {
|
|
213
|
+
sx: {
|
|
214
|
+
color: "feedback.text.positive.default",
|
|
215
|
+
fontSize: "sm",
|
|
216
|
+
fontWeight: "medium"
|
|
217
|
+
}
|
|
218
|
+
}, props.status.text))));
|
|
219
|
+
}, "BigNumber");
|
|
220
|
+
|
|
221
|
+
// src/DashboardCard.tsx
|
|
222
|
+
var DashboardCard = /* @__PURE__ */__name(props => {
|
|
223
|
+
switch (props.type) {
|
|
224
|
+
case "bigNumber":
|
|
225
|
+
return /* @__PURE__ */React.createElement(BigNumber, props);
|
|
226
|
+
default:
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}, "DashboardCard");
|
|
230
|
+
|
|
231
|
+
// src/DashboardGrid.tsx
|
|
232
|
+
var ResponsiveGridLayout = WidthProvider(Responsive);
|
|
233
|
+
var DashboardGrid = /* @__PURE__ */__name(({
|
|
234
|
+
loading,
|
|
235
|
+
selectedTemplate
|
|
236
|
+
}) => {
|
|
237
|
+
if (!selectedTemplate) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const breakpoints = {
|
|
241
|
+
xs: 0,
|
|
242
|
+
sm: 480,
|
|
243
|
+
md: 768,
|
|
244
|
+
lg: 1024,
|
|
245
|
+
xl: 1280,
|
|
246
|
+
"2xl": 1536
|
|
247
|
+
};
|
|
248
|
+
const cols = {
|
|
249
|
+
xs: 2,
|
|
250
|
+
sm: 2,
|
|
251
|
+
md: 12,
|
|
252
|
+
lg: 12,
|
|
253
|
+
xl: 12,
|
|
254
|
+
"2xl": 12
|
|
255
|
+
};
|
|
256
|
+
const baseLayout = selectedTemplate.grid.map(item => {
|
|
257
|
+
const {
|
|
258
|
+
card,
|
|
259
|
+
...layout
|
|
260
|
+
} = item;
|
|
261
|
+
return layout;
|
|
262
|
+
});
|
|
263
|
+
const layouts = {
|
|
264
|
+
xs: baseLayout,
|
|
265
|
+
sm: baseLayout,
|
|
266
|
+
md: baseLayout,
|
|
267
|
+
lg: baseLayout,
|
|
268
|
+
xl: baseLayout,
|
|
269
|
+
"2xl": baseLayout
|
|
270
|
+
};
|
|
271
|
+
return /* @__PURE__ */React.createElement(Box3, {
|
|
272
|
+
sx: {
|
|
273
|
+
width: "100%",
|
|
274
|
+
height: "full"
|
|
275
|
+
}
|
|
276
|
+
}, loading ? /* @__PURE__ */React.createElement(Flex3, {
|
|
277
|
+
sx: {
|
|
278
|
+
width: "100%",
|
|
279
|
+
height: "full",
|
|
280
|
+
justifyContent: "center",
|
|
281
|
+
alignItems: "center"
|
|
282
|
+
}
|
|
283
|
+
}, /* @__PURE__ */React.createElement(Spinner, null)) : /* @__PURE__ */React.createElement(ResponsiveGridLayout, {
|
|
284
|
+
className: "layout",
|
|
285
|
+
layouts,
|
|
286
|
+
breakpoints,
|
|
287
|
+
cols,
|
|
288
|
+
rowHeight: 30,
|
|
289
|
+
margin: [10, 10],
|
|
290
|
+
containerPadding: [0, 0]
|
|
291
|
+
}, selectedTemplate.grid.map(item => {
|
|
292
|
+
return /* @__PURE__ */React.createElement("div", {
|
|
293
|
+
key: item.i
|
|
294
|
+
}, /* @__PURE__ */React.createElement(DashboardCard, item.card));
|
|
295
|
+
})));
|
|
296
|
+
}, "DashboardGrid");
|
|
297
|
+
|
|
298
|
+
// src/DashboardHeader.tsx
|
|
299
|
+
import { Flex as Flex5 } from "@ttoss/ui";
|
|
300
|
+
import * as React7 from "react";
|
|
301
|
+
|
|
302
|
+
// src/DashboardFilters.tsx
|
|
303
|
+
import { Flex as Flex4 } from "@ttoss/ui";
|
|
304
|
+
import * as React6 from "react";
|
|
305
|
+
|
|
306
|
+
// src/DashboardProvider.tsx
|
|
307
|
+
import * as React2 from "react";
|
|
308
|
+
var DashboardContext = /* @__PURE__ */React2.createContext({
|
|
309
|
+
filters: [],
|
|
310
|
+
updateFilter: /* @__PURE__ */__name(() => {}, "updateFilter"),
|
|
311
|
+
templates: [],
|
|
312
|
+
selectedTemplate: void 0
|
|
313
|
+
});
|
|
314
|
+
var DashboardProvider = /* @__PURE__ */__name(props => {
|
|
315
|
+
const {
|
|
316
|
+
filters: externalFilters,
|
|
317
|
+
templates: externalTemplates,
|
|
318
|
+
onFiltersChange
|
|
319
|
+
} = props;
|
|
320
|
+
const onFiltersChangeRef = React2.useRef(onFiltersChange);
|
|
321
|
+
const filtersRef = React2.useRef(externalFilters);
|
|
322
|
+
React2.useEffect(() => {
|
|
323
|
+
onFiltersChangeRef.current = onFiltersChange;
|
|
324
|
+
}, [onFiltersChange]);
|
|
325
|
+
React2.useEffect(() => {
|
|
326
|
+
filtersRef.current = externalFilters;
|
|
327
|
+
}, [externalFilters]);
|
|
328
|
+
const updateFilter = React2.useCallback((key, value) => {
|
|
329
|
+
const updatedFilters = filtersRef.current.map(filter => {
|
|
330
|
+
return filter.key === key ? {
|
|
331
|
+
...filter,
|
|
332
|
+
value
|
|
333
|
+
} : filter;
|
|
334
|
+
});
|
|
335
|
+
onFiltersChangeRef.current?.(updatedFilters);
|
|
336
|
+
}, []
|
|
337
|
+
// Empty deps - we use refs for current values
|
|
338
|
+
);
|
|
339
|
+
const selectedTemplate = React2.useMemo(() => {
|
|
340
|
+
const templateId = externalFilters.find(filter => {
|
|
341
|
+
return filter.key === "template";
|
|
342
|
+
})?.value;
|
|
343
|
+
return externalTemplates.find(template => {
|
|
344
|
+
return template.id === templateId;
|
|
345
|
+
});
|
|
346
|
+
}, [externalFilters, externalTemplates]);
|
|
347
|
+
return /* @__PURE__ */React2.createElement(DashboardContext.Provider, {
|
|
348
|
+
value: {
|
|
349
|
+
filters: externalFilters,
|
|
350
|
+
updateFilter,
|
|
351
|
+
templates: externalTemplates,
|
|
352
|
+
selectedTemplate
|
|
353
|
+
}
|
|
354
|
+
}, props.children);
|
|
355
|
+
}, "DashboardProvider");
|
|
356
|
+
var useDashboard = /* @__PURE__ */__name(() => {
|
|
357
|
+
const context = React2.useContext(DashboardContext);
|
|
358
|
+
if (!context) {
|
|
359
|
+
throw new Error("useDashboard must be used within a DashboardProvider");
|
|
360
|
+
}
|
|
361
|
+
return context;
|
|
362
|
+
}, "useDashboard");
|
|
363
|
+
|
|
364
|
+
// src/Filters/DateRangeFilter.tsx
|
|
365
|
+
import { FormFieldDatePicker } from "@ttoss/forms";
|
|
366
|
+
import * as React3 from "react";
|
|
367
|
+
import { FormProvider, useForm } from "react-hook-form";
|
|
368
|
+
var DateRangeFilter = /* @__PURE__ */__name(({
|
|
369
|
+
label,
|
|
370
|
+
value,
|
|
371
|
+
presets,
|
|
372
|
+
onChange
|
|
373
|
+
}) => {
|
|
374
|
+
const formMethods = useForm({
|
|
375
|
+
defaultValues: {
|
|
376
|
+
dateRange: value
|
|
377
|
+
},
|
|
378
|
+
mode: "onChange"
|
|
379
|
+
});
|
|
380
|
+
React3.useEffect(() => {
|
|
381
|
+
if (value !== void 0) {
|
|
382
|
+
formMethods.setValue("dateRange", value, {
|
|
383
|
+
shouldDirty: false
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}, [value, formMethods]);
|
|
387
|
+
const currentValue = formMethods.watch("dateRange");
|
|
388
|
+
const dateRangesEqual = React3.useCallback((a, b) => {
|
|
389
|
+
if (a === b) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
if (!a || !b) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
const aFrom = a.from?.getTime();
|
|
396
|
+
const aTo = a.to?.getTime();
|
|
397
|
+
const bFrom = b.from?.getTime();
|
|
398
|
+
const bTo = b.to?.getTime();
|
|
399
|
+
return aFrom === bFrom && aTo === bTo;
|
|
400
|
+
}, []);
|
|
401
|
+
React3.useEffect(() => {
|
|
402
|
+
if (currentValue !== void 0 && !dateRangesEqual(currentValue, value)) {
|
|
403
|
+
onChange?.(currentValue);
|
|
404
|
+
}
|
|
405
|
+
}, [currentValue, value, onChange, dateRangesEqual]);
|
|
406
|
+
return /* @__PURE__ */React3.createElement(FormProvider, formMethods, /* @__PURE__ */React3.createElement(FormFieldDatePicker, {
|
|
407
|
+
name: "dateRange",
|
|
408
|
+
label,
|
|
409
|
+
presets
|
|
410
|
+
}));
|
|
411
|
+
}, "DateRangeFilter");
|
|
412
|
+
|
|
413
|
+
// src/Filters/SelectFilter.tsx
|
|
414
|
+
import { FormFieldSelect } from "@ttoss/forms";
|
|
415
|
+
import * as React4 from "react";
|
|
416
|
+
import { FormProvider as FormProvider2, useForm as useForm2 } from "react-hook-form";
|
|
417
|
+
var SelectFilter = /* @__PURE__ */__name(props => {
|
|
418
|
+
const {
|
|
419
|
+
value,
|
|
420
|
+
onChange,
|
|
421
|
+
label,
|
|
422
|
+
options
|
|
423
|
+
} = props;
|
|
424
|
+
const formMethods = useForm2({
|
|
425
|
+
defaultValues: {
|
|
426
|
+
value
|
|
427
|
+
},
|
|
428
|
+
mode: "onChange"
|
|
429
|
+
});
|
|
430
|
+
React4.useEffect(() => {
|
|
431
|
+
const propValue = value;
|
|
432
|
+
formMethods.setValue("value", propValue, {
|
|
433
|
+
shouldDirty: false
|
|
434
|
+
});
|
|
435
|
+
}, [value, formMethods]);
|
|
436
|
+
const currentValue = formMethods.watch("value");
|
|
437
|
+
React4.useEffect(() => {
|
|
438
|
+
if (currentValue !== void 0 && currentValue !== value) {
|
|
439
|
+
onChange(currentValue);
|
|
440
|
+
}
|
|
441
|
+
}, [currentValue, value, onChange]);
|
|
442
|
+
return /* @__PURE__ */React4.createElement(FormProvider2, formMethods, /* @__PURE__ */React4.createElement(FormFieldSelect, {
|
|
443
|
+
name: "value",
|
|
444
|
+
label,
|
|
445
|
+
options,
|
|
446
|
+
sx: {
|
|
447
|
+
fontSize: "sm"
|
|
448
|
+
}
|
|
449
|
+
}));
|
|
450
|
+
}, "SelectFilter");
|
|
451
|
+
|
|
452
|
+
// src/Filters/TextFilter.tsx
|
|
453
|
+
import { FormFieldInput } from "@ttoss/forms";
|
|
454
|
+
import * as React5 from "react";
|
|
455
|
+
import { FormProvider as FormProvider3, useForm as useForm3 } from "react-hook-form";
|
|
456
|
+
var TextFilter = /* @__PURE__ */__name(props => {
|
|
457
|
+
const {
|
|
458
|
+
value,
|
|
459
|
+
onChange,
|
|
460
|
+
label,
|
|
461
|
+
placeholder
|
|
462
|
+
} = props;
|
|
463
|
+
const formMethods = useForm3({
|
|
464
|
+
defaultValues: {
|
|
465
|
+
value: value.toString()
|
|
466
|
+
},
|
|
467
|
+
mode: "onChange"
|
|
468
|
+
});
|
|
469
|
+
React5.useEffect(() => {
|
|
470
|
+
const stringValue = value.toString();
|
|
471
|
+
formMethods.setValue("value", stringValue, {
|
|
472
|
+
shouldDirty: false
|
|
473
|
+
});
|
|
474
|
+
}, [value, formMethods]);
|
|
475
|
+
const currentValue = formMethods.watch("value");
|
|
476
|
+
React5.useEffect(() => {
|
|
477
|
+
if (currentValue !== void 0 && currentValue !== value.toString()) {
|
|
478
|
+
onChange(currentValue);
|
|
479
|
+
}
|
|
480
|
+
}, [currentValue, value, onChange]);
|
|
481
|
+
return /* @__PURE__ */React5.createElement(FormProvider3, formMethods, /* @__PURE__ */React5.createElement(FormFieldInput, {
|
|
482
|
+
name: "value",
|
|
483
|
+
label,
|
|
484
|
+
placeholder,
|
|
485
|
+
sx: {
|
|
486
|
+
fontSize: "sm"
|
|
487
|
+
}
|
|
488
|
+
}));
|
|
489
|
+
}, "TextFilter");
|
|
490
|
+
|
|
491
|
+
// src/DashboardFilters.tsx
|
|
492
|
+
var DashboardFilterType = /* @__PURE__ */function (DashboardFilterType2) {
|
|
493
|
+
DashboardFilterType2["TEXT"] = "text";
|
|
494
|
+
DashboardFilterType2["SELECT"] = "select";
|
|
495
|
+
DashboardFilterType2["DATE_RANGE"] = "date-range";
|
|
496
|
+
DashboardFilterType2["NUMBER"] = "number";
|
|
497
|
+
DashboardFilterType2["BOOLEAN"] = "boolean";
|
|
498
|
+
return DashboardFilterType2;
|
|
499
|
+
}({});
|
|
500
|
+
var DashboardFilters = /* @__PURE__ */__name(() => {
|
|
501
|
+
const {
|
|
502
|
+
filters,
|
|
503
|
+
updateFilter
|
|
504
|
+
} = useDashboard();
|
|
505
|
+
const onChangeHandlers = React6.useMemo(() => {
|
|
506
|
+
const handlers = /* @__PURE__ */new Map();
|
|
507
|
+
for (const filter of filters) {
|
|
508
|
+
handlers.set(filter.key, value => {
|
|
509
|
+
updateFilter(filter.key, value);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return handlers;
|
|
513
|
+
}, [filters, updateFilter]);
|
|
514
|
+
return /* @__PURE__ */React6.createElement(Flex4, {
|
|
515
|
+
sx: {
|
|
516
|
+
gap: "2",
|
|
517
|
+
flexDirection: "row",
|
|
518
|
+
"@media (max-width: 768px)": {
|
|
519
|
+
flexWrap: "wrap"
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}, filters.map(filter => {
|
|
523
|
+
const onChange = onChangeHandlers.get(filter.key);
|
|
524
|
+
if (!onChange) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
switch (filter.type) {
|
|
528
|
+
case "text":
|
|
529
|
+
return /* @__PURE__ */React6.createElement(TextFilter, {
|
|
530
|
+
key: filter.key,
|
|
531
|
+
name: filter.key,
|
|
532
|
+
label: filter.label,
|
|
533
|
+
value: filter.value,
|
|
534
|
+
placeholder: filter.placeholder,
|
|
535
|
+
onChange
|
|
536
|
+
});
|
|
537
|
+
case "select":
|
|
538
|
+
return /* @__PURE__ */React6.createElement(SelectFilter, {
|
|
539
|
+
key: filter.key,
|
|
540
|
+
name: filter.key,
|
|
541
|
+
label: filter.label,
|
|
542
|
+
value: filter.value,
|
|
543
|
+
options: filter.options ?? [],
|
|
544
|
+
onChange: /* @__PURE__ */__name(value => {
|
|
545
|
+
onChange(value);
|
|
546
|
+
}, "onChange")
|
|
547
|
+
});
|
|
548
|
+
case "date-range":
|
|
549
|
+
return /* @__PURE__ */React6.createElement(DateRangeFilter, {
|
|
550
|
+
label: filter.label,
|
|
551
|
+
key: filter.key,
|
|
552
|
+
value: filter.value,
|
|
553
|
+
presets: filter.presets,
|
|
554
|
+
onChange: /* @__PURE__ */__name(range => {
|
|
555
|
+
onChange(range);
|
|
556
|
+
}, "onChange")
|
|
557
|
+
});
|
|
558
|
+
default:
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}));
|
|
562
|
+
}, "DashboardFilters");
|
|
563
|
+
|
|
564
|
+
// src/DashboardHeader.tsx
|
|
565
|
+
var DashboardHeader = /* @__PURE__ */__name(({
|
|
566
|
+
children
|
|
567
|
+
}) => {
|
|
568
|
+
return /* @__PURE__ */React7.createElement(Flex5, {
|
|
569
|
+
sx: {
|
|
570
|
+
padding: "2"
|
|
571
|
+
}
|
|
572
|
+
}, /* @__PURE__ */React7.createElement(DashboardFilters, null), children);
|
|
573
|
+
}, "DashboardHeader");
|
|
574
|
+
|
|
575
|
+
// src/Dashboard.tsx
|
|
576
|
+
var DashboardContent = /* @__PURE__ */__name(({
|
|
577
|
+
loading = false,
|
|
578
|
+
headerChildren
|
|
579
|
+
}) => {
|
|
580
|
+
const {
|
|
581
|
+
selectedTemplate
|
|
582
|
+
} = useDashboard();
|
|
583
|
+
return /* @__PURE__ */React8.createElement(Flex6, {
|
|
584
|
+
sx: {
|
|
585
|
+
flexDirection: "column",
|
|
586
|
+
gap: "5",
|
|
587
|
+
padding: "2",
|
|
588
|
+
width: "100%"
|
|
589
|
+
}
|
|
590
|
+
}, /* @__PURE__ */React8.createElement(DashboardHeader, null, headerChildren), /* @__PURE__ */React8.createElement(Divider, null), /* @__PURE__ */React8.createElement(DashboardGrid, {
|
|
591
|
+
loading,
|
|
592
|
+
selectedTemplate
|
|
593
|
+
}));
|
|
594
|
+
}, "DashboardContent");
|
|
595
|
+
var Dashboard = /* @__PURE__ */__name(({
|
|
596
|
+
loading = false,
|
|
597
|
+
templates = [],
|
|
598
|
+
filters = [],
|
|
599
|
+
headerChildren,
|
|
600
|
+
onFiltersChange
|
|
601
|
+
}) => {
|
|
602
|
+
return /* @__PURE__ */React8.createElement(DashboardProvider, {
|
|
603
|
+
filters,
|
|
604
|
+
templates,
|
|
605
|
+
onFiltersChange
|
|
606
|
+
}, /* @__PURE__ */React8.createElement(DashboardContent, {
|
|
607
|
+
loading,
|
|
608
|
+
headerChildren
|
|
609
|
+
}));
|
|
610
|
+
}, "Dashboard");
|
|
611
|
+
export { Dashboard, DashboardCard, DashboardFilterType, DashboardFilters, DashboardGrid, DashboardHeader, DashboardProvider, useDashboard };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import ReactGridLayout from 'react-grid-layout';
|
|
4
|
+
import { DateRange } from '@ttoss/components/DatePicker';
|
|
5
|
+
|
|
6
|
+
type CardNumberType = 'number' | 'percentage' | 'currency';
|
|
7
|
+
type CardSourceType = {
|
|
8
|
+
source: 'meta' | 'oneclickads' | 'api';
|
|
9
|
+
level?: 'adAccount' | 'campaign' | 'adSet' | 'ad';
|
|
10
|
+
};
|
|
11
|
+
type DashboardCardType = 'bigNumber' | 'pieChart' | 'barChart' | 'lineChart' | 'table' | 'list';
|
|
12
|
+
type DashboardCardData = {
|
|
13
|
+
meta?: {
|
|
14
|
+
total?: number;
|
|
15
|
+
daily?: number[];
|
|
16
|
+
};
|
|
17
|
+
api?: {
|
|
18
|
+
total?: number;
|
|
19
|
+
daily?: number[];
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
type CardVariant = 'default' | 'dark' | 'light-green';
|
|
23
|
+
type TrendIndicator = {
|
|
24
|
+
value: number;
|
|
25
|
+
status?: 'positive' | 'negative' | 'neutral';
|
|
26
|
+
};
|
|
27
|
+
type StatusIndicator = {
|
|
28
|
+
text: string;
|
|
29
|
+
icon?: string;
|
|
30
|
+
};
|
|
31
|
+
interface DashboardCard {
|
|
32
|
+
title: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
icon?: string;
|
|
35
|
+
color?: string;
|
|
36
|
+
variant?: CardVariant;
|
|
37
|
+
numberType: CardNumberType;
|
|
38
|
+
type: DashboardCardType;
|
|
39
|
+
sourceType: CardSourceType[];
|
|
40
|
+
labels?: Array<string | number>;
|
|
41
|
+
data: DashboardCardData;
|
|
42
|
+
trend?: TrendIndicator;
|
|
43
|
+
additionalInfo?: string;
|
|
44
|
+
status?: StatusIndicator;
|
|
45
|
+
}
|
|
46
|
+
declare const DashboardCard: (props: DashboardCard) => react_jsx_runtime.JSX.Element | null;
|
|
47
|
+
|
|
48
|
+
type DashboardFilterValue = string | number | boolean | {
|
|
49
|
+
from: Date;
|
|
50
|
+
to: Date;
|
|
51
|
+
};
|
|
52
|
+
declare enum DashboardFilterType {
|
|
53
|
+
TEXT = "text",
|
|
54
|
+
SELECT = "select",
|
|
55
|
+
DATE_RANGE = "date-range",
|
|
56
|
+
NUMBER = "number",
|
|
57
|
+
BOOLEAN = "boolean"
|
|
58
|
+
}
|
|
59
|
+
type DashboardFilter = {
|
|
60
|
+
key: string;
|
|
61
|
+
type: DashboardFilterType;
|
|
62
|
+
label: string;
|
|
63
|
+
placeholder?: string;
|
|
64
|
+
value: DashboardFilterValue;
|
|
65
|
+
onChange?: (value: DashboardFilterValue) => void;
|
|
66
|
+
options?: {
|
|
67
|
+
label: string;
|
|
68
|
+
value: string | number | boolean;
|
|
69
|
+
}[];
|
|
70
|
+
presets?: {
|
|
71
|
+
label: string;
|
|
72
|
+
getValue: () => DateRange;
|
|
73
|
+
}[];
|
|
74
|
+
};
|
|
75
|
+
declare const DashboardFilters: () => react_jsx_runtime.JSX.Element;
|
|
76
|
+
|
|
77
|
+
type DashboardGridItem = ReactGridLayout.Layout & {
|
|
78
|
+
card: DashboardCard;
|
|
79
|
+
};
|
|
80
|
+
interface DashboardTemplate {
|
|
81
|
+
id: string;
|
|
82
|
+
name: string;
|
|
83
|
+
description?: string;
|
|
84
|
+
grid: DashboardGridItem[];
|
|
85
|
+
}
|
|
86
|
+
declare const Dashboard: ({ loading, templates, filters, headerChildren, onFiltersChange, }: {
|
|
87
|
+
loading?: boolean;
|
|
88
|
+
headerChildren?: React.ReactNode;
|
|
89
|
+
templates?: DashboardTemplate[];
|
|
90
|
+
filters?: DashboardFilter[];
|
|
91
|
+
onFiltersChange?: (filters: DashboardFilter[]) => void;
|
|
92
|
+
}) => react_jsx_runtime.JSX.Element;
|
|
93
|
+
|
|
94
|
+
declare const DashboardGrid: ({ loading, selectedTemplate, }: {
|
|
95
|
+
loading: boolean;
|
|
96
|
+
selectedTemplate?: DashboardTemplate;
|
|
97
|
+
}) => react_jsx_runtime.JSX.Element | null;
|
|
98
|
+
|
|
99
|
+
declare const DashboardHeader: ({ children, }: {
|
|
100
|
+
children?: React.ReactNode;
|
|
101
|
+
}) => react_jsx_runtime.JSX.Element;
|
|
102
|
+
|
|
103
|
+
declare const DashboardProvider: (props: {
|
|
104
|
+
children: React.ReactNode;
|
|
105
|
+
filters: DashboardFilter[];
|
|
106
|
+
templates: DashboardTemplate[];
|
|
107
|
+
onFiltersChange?: (filters: DashboardFilter[]) => void;
|
|
108
|
+
}) => react_jsx_runtime.JSX.Element;
|
|
109
|
+
declare const useDashboard: () => {
|
|
110
|
+
filters: DashboardFilter[];
|
|
111
|
+
updateFilter: (key: string, value: DashboardFilterValue) => void;
|
|
112
|
+
templates: DashboardTemplate[];
|
|
113
|
+
selectedTemplate: DashboardTemplate | undefined;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export { Dashboard, DashboardCard, type DashboardFilter, DashboardFilterType, type DashboardFilterValue, DashboardFilters, DashboardGrid, type DashboardGridItem, DashboardHeader, DashboardProvider, type DashboardTemplate, useDashboard };
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ttoss/react-dashboard",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "ttoss dashboard module for React apps.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "ttoss",
|
|
7
|
+
"contributors": [
|
|
8
|
+
"Pedro Arantes <pedro@arantespp.com> (https://arantespp.com/contact)"
|
|
9
|
+
],
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/ttoss/ttoss.git",
|
|
13
|
+
"directory": "packages/react-dashboard"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/esm/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": ">=16.8.0",
|
|
28
|
+
"react-day-picker": "^9.11.2",
|
|
29
|
+
"react-grid-layout": "^1.5.2",
|
|
30
|
+
"react-hook-form": "^7.66.1",
|
|
31
|
+
"@ttoss/components": "^2.10.1",
|
|
32
|
+
"@ttoss/forms": "^0.35.2",
|
|
33
|
+
"@ttoss/config": "^1.35.12",
|
|
34
|
+
"@ttoss/test-utils": "^4.0.1",
|
|
35
|
+
"@ttoss/react-icons": "^0.5.5",
|
|
36
|
+
"@ttoss/ui": "^6.0.4"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/react": "^19.2.6",
|
|
40
|
+
"@types/react-grid-layout": "^1.3.6",
|
|
41
|
+
"jest": "^30.2.0",
|
|
42
|
+
"react-hook-form": "^7.66.1",
|
|
43
|
+
"tsup": "^8.5.1",
|
|
44
|
+
"@ttoss/components": "^2.10.1",
|
|
45
|
+
"@ttoss/forms": "^0.35.2"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"React",
|
|
49
|
+
"dashboard"
|
|
50
|
+
],
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public",
|
|
53
|
+
"provenance": true
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsup",
|
|
57
|
+
"test": "jest --projects tests/unit"
|
|
58
|
+
}
|
|
59
|
+
}
|