@tenorlab/react-dashboard 1.4.2 → 1.4.4
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 +346 -22
- package/dist/core.d.ts +2 -1
- package/dist/core.es.js +69 -67
- package/dist/react-dashboard.d.ts +50 -0
- package/dist/react-dashboard.es.js +880 -874
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,49 +1,373 @@
|
|
|
1
1
|
# @tenorlab/react-dashboard
|
|
2
2
|
|
|
3
|
-
Foundation components for creating user-configurable, high-performance dashboards in React.
|
|
3
|
+
Foundation components for creating user-configurable, high-performance dashboards in React. Built on top of **@tenorlab/dashboard-core**.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 🏗 Relationship to Core
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
**Tenorlab** is the specialized software foundry for the [@builtwithjavascript](https://github.com/builtwithjavascript) ecosystem, focusing on modular, type-safe utilities and UI kits.
|
|
7
|
+
This package extends **@tenorlab/dashboard-core**. It provides the React implementation of the core logic, including specialized hooks, state management via **Zustand**, and a suite of UI components.
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
> **Note**: This package re-exports all types and utilities from `@tenorlab/dashboard-core`. You do not need to install the core package separately.
|
|
10
|
+
|
|
11
|
+
## ✨ Features
|
|
12
|
+
|
|
13
|
+
- **Type-Safe:** Deep integration with TypeScript 5.8+ for full IDE support.
|
|
14
|
+
- **State Management:** Built-in `useDashboardStore` (Zustand) and `useDashboardUndoService`.
|
|
15
|
+
- **User Configurable:** Ready-to-use components for adding, removing, and dragging widgets.
|
|
16
|
+
- **Themeable:** Native support for CSS Variables and Tailwind CSS.
|
|
17
|
+
- **Vite Optimized:** Full ESM support and tree-shakeable.
|
|
11
18
|
|
|
12
19
|
## 🚀 Quick Start
|
|
13
20
|
|
|
14
21
|
### Installation
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
Bash
|
|
17
24
|
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
```
|
|
26
|
+
# with npm
|
|
27
|
+
npm i @tenorlab/react-dashboard
|
|
28
|
+
|
|
29
|
+
# with pnpm
|
|
21
30
|
pnpm add @tenorlab/react-dashboard
|
|
22
31
|
```
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
### 1. Global Styles
|
|
34
|
+
|
|
35
|
+
Import the base styles in your entry file (e.g., `main.tsx`):
|
|
36
|
+
|
|
37
|
+
TypeScript
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
import '@tenorlab/react-dashboard/dist/style.css'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
------
|
|
44
|
+
|
|
45
|
+
## 🛠 Developer Guide
|
|
46
|
+
|
|
47
|
+
### 1. Creating a Widget
|
|
48
|
+
|
|
49
|
+
Widgets should be organized by their loading strategy.
|
|
50
|
+
|
|
51
|
+
- **Bundled Widgets**: Place in `src/bundled-widgets/` (loaded immediately).
|
|
52
|
+
- **Async Widgets**: Place in `src/async-widgets/` (lazy-loaded).
|
|
53
|
+
|
|
54
|
+
Each widget requires a sub-directory using the `widget-name-here` convention.
|
|
55
|
+
|
|
56
|
+
#### Example: `WidgetTotalOrders`
|
|
57
|
+
|
|
58
|
+
TypeScript
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
// file: src/bundled-widgets/widget-total-orders/WidgetTotalOrders.tsx
|
|
62
|
+
import {
|
|
63
|
+
IDashboardWidget,
|
|
64
|
+
IDashboardWidgetProps,
|
|
65
|
+
TDashboardWidgetKey,
|
|
66
|
+
DashboardWidgetBase,
|
|
67
|
+
WrapperColumnContent
|
|
68
|
+
} from '@tenorlab/react-dashboard'
|
|
69
|
+
|
|
70
|
+
const WidgetKey: TDashboardWidgetKey = 'WidgetTotalOrders'
|
|
71
|
+
|
|
72
|
+
export function WidgetTotalOrders(props: IDashboardWidgetProps): IDashboardWidget {
|
|
73
|
+
return (
|
|
74
|
+
<DashboardWidgetBase
|
|
75
|
+
widgetKey={WidgetKey}
|
|
76
|
+
title="Total Orders"
|
|
77
|
+
parentWidgetKey={props.parentWidgetKey}
|
|
78
|
+
index={props.index}
|
|
79
|
+
maxIndex={props.maxIndex}
|
|
80
|
+
isEditing={props.isEditing}
|
|
81
|
+
onRemoveClick={props.onRemoveClick}
|
|
82
|
+
onMoveClick={props.onMoveClick}
|
|
83
|
+
>
|
|
84
|
+
<WrapperColumnContent>
|
|
85
|
+
<div className="dashboard-number number-xl text-primary">1,250</div>
|
|
86
|
+
<div className="text-sm">Orders this month</div>
|
|
87
|
+
</WrapperColumnContent>
|
|
88
|
+
</DashboardWidgetBase>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
TypeScript
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
// file: src/bundled-widgets/widget-total-orders/meta.ts
|
|
97
|
+
import type { TWidgetMetaInfo } from '@tenorlab/react-dashboard'
|
|
98
|
+
import { MonitorIcon as ComponentIcon } from '@tenorlab/react-dashboard'
|
|
99
|
+
|
|
100
|
+
export const WidgetTotalOrdersMeta: TWidgetMetaInfo = {
|
|
101
|
+
name: 'Total Orders',
|
|
102
|
+
categories: ['Widget'],
|
|
103
|
+
icon: ComponentIcon,
|
|
104
|
+
noDuplicatedWidgets: true,
|
|
105
|
+
description: 'Displays information about your total orders.',
|
|
106
|
+
externalDependencies: []
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
TypeScript
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
// file: src/bundled-widgets/widget-total-orders/index.ts
|
|
114
|
+
import { WidgetTotalOrders } from './WidgetTotalOrders'
|
|
115
|
+
export default WidgetTotalOrders
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 2. Creating the Widgets Catalog
|
|
119
|
+
|
|
120
|
+
Create `src/widgets-catalog.tsx` in your project root. This file manages how widgets are discovered (locally via Vite's `import.meta.glob` or remotely via CDN).
|
|
121
|
+
|
|
122
|
+
TypeScript
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
// file: src/widgets-catalog.tsx
|
|
126
|
+
import {
|
|
127
|
+
IDynamicWidgetCatalogEntry,
|
|
128
|
+
TDashboardWidgetCatalog,
|
|
129
|
+
TWidgetMetaInfoBase,
|
|
130
|
+
WidgetContainerColumn,
|
|
131
|
+
WidgetContainerLarge,
|
|
132
|
+
WidgetContainerRow,
|
|
133
|
+
TWidgetFactory,
|
|
134
|
+
} from '@tenorlab/react-dashboard'
|
|
135
|
+
import {
|
|
136
|
+
createStaticEntry,
|
|
137
|
+
localWidgetDiscovery,
|
|
138
|
+
remoteWidgetDiscovery,
|
|
139
|
+
} from '@tenorlab/react-dashboard/core'
|
|
140
|
+
|
|
141
|
+
import { WidgetRecentPaymentInfo } from './other-widgets/WidgetRecentPaymentInfo'
|
|
142
|
+
//import { getWidgetsManifestUrl } from '@/utils'
|
|
143
|
+
|
|
144
|
+
const bundledWidgetsSrcPath = '/src/bundled-widgets'
|
|
145
|
+
const asyncWidgetsSrcPath = '/src/async-widgets'
|
|
146
|
+
|
|
147
|
+
type TGlobModuleMap = Record<string, TWidgetFactory>
|
|
148
|
+
|
|
149
|
+
const bundledWidgetModules = import.meta.glob('/src/bundled-widgets/*/index.ts', {
|
|
150
|
+
eager: true
|
|
151
|
+
}) as TGlobModuleMap
|
|
152
|
+
|
|
153
|
+
const asyncWidgetModules = import.meta.glob('/src/async-widgets/*/index.ts') as TGlobModuleMap
|
|
154
|
+
|
|
155
|
+
const allMetaModules = import.meta.glob('/src/**/widget-*/meta.ts', {
|
|
156
|
+
eager: true,
|
|
157
|
+
}) as Record<string, Record<string, TWidgetMetaInfoBase>>
|
|
158
|
+
|
|
159
|
+
const hasPermission = (_user_: any, _permission: string) => true
|
|
160
|
+
|
|
161
|
+
export const getWidgetCatalog = async (user: any | null): Promise<TDashboardWidgetCatalog> => {
|
|
162
|
+
const catalogMapEntries: [string, IDynamicWidgetCatalogEntry][] = [
|
|
163
|
+
createStaticEntry('WidgetContainer', WidgetContainerColumn),
|
|
164
|
+
createStaticEntry('WidgetContainerRow', WidgetContainerRow),
|
|
165
|
+
createStaticEntry('WidgetContainerLarge', WidgetContainerLarge),
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
if (hasPermission(user, 'some-permission')) {
|
|
169
|
+
catalogMapEntries.push(createStaticEntry('WidgetRecentPaymentInfo', WidgetRecentPaymentInfo))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
catalogMapEntries.push(...localWidgetDiscovery(
|
|
173
|
+
bundledWidgetsSrcPath,
|
|
174
|
+
bundledWidgetModules,
|
|
175
|
+
allMetaModules,
|
|
176
|
+
false
|
|
177
|
+
))
|
|
178
|
+
|
|
179
|
+
catalogMapEntries.push(...localWidgetDiscovery(
|
|
180
|
+
asyncWidgetsSrcPath,
|
|
181
|
+
asyncWidgetModules,
|
|
182
|
+
allMetaModules,
|
|
183
|
+
true
|
|
184
|
+
))
|
|
185
|
+
|
|
186
|
+
// Optional: Remote discovery of -pre-built widgets hosted on a CDN
|
|
187
|
+
/*const manifestUrl = getWidgetsManifestUrl()
|
|
188
|
+
if (manifestUrl.length > 0) {
|
|
189
|
+
const remoteResponse = await remoteWidgetDiscovery(manifestUrl)
|
|
190
|
+
if (!remoteResponse.message) {
|
|
191
|
+
catalogMapEntries.push(...(remoteResponse.entries || []))
|
|
192
|
+
}
|
|
193
|
+
}*/
|
|
194
|
+
|
|
195
|
+
return new Map(catalogMapEntries)
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 3. Defining Dashboard Defaults
|
|
200
|
+
|
|
201
|
+
Use a `dashboard-defaults.ts` file to define initial layouts based on user roles.
|
|
202
|
+
|
|
203
|
+
TypeScript
|
|
204
|
+
|
|
28
205
|
```
|
|
206
|
+
// file: src/dashboard-defaults.ts
|
|
207
|
+
import {
|
|
208
|
+
TDashboardWidgetKey,
|
|
209
|
+
IChildWidgetConfigEntry,
|
|
210
|
+
IDashboardConfig,
|
|
211
|
+
TDashboardWidgetCatalog,
|
|
212
|
+
} from '@tenorlab/react-dashboard'
|
|
213
|
+
import { blankDashboardConfig, cssSettingsCatalog } from '@tenorlab/react-dashboard/core'
|
|
214
|
+
import { getWidgetCatalog } from './widgets-catalog'
|
|
215
|
+
|
|
216
|
+
const DEFAULT_DASHBOARD_ID = 'default' as const
|
|
217
|
+
const DEFAULT_DASHBOARD_NAME = 'Default' as const
|
|
218
|
+
|
|
219
|
+
const getDefaultDashboardForCustomerUser = (
|
|
220
|
+
user: any,
|
|
221
|
+
clientAppKey: string,
|
|
222
|
+
availableWidgetKeys: TDashboardWidgetKey[]
|
|
223
|
+
): IDashboardConfig => {
|
|
224
|
+
const userID = user.userID || 0
|
|
225
|
+
return {
|
|
226
|
+
userID,
|
|
227
|
+
clientAppKey,
|
|
228
|
+
dashboardId: DEFAULT_DASHBOARD_ID,
|
|
229
|
+
dashboardName: DEFAULT_DASHBOARD_NAME,
|
|
230
|
+
zoomScale: 1,
|
|
231
|
+
responsiveGrid: false,
|
|
232
|
+
widgets: ['WidgetContainer_container1'],
|
|
233
|
+
childWidgetsConfig: [
|
|
234
|
+
{ parentWidgetKey: 'WidgetContainer_container1', widgetKey: 'WidgetRecentPaymentInfo' }
|
|
235
|
+
],
|
|
236
|
+
cssSettings: [...cssSettingsCatalog]
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export const getDashboardDefaults = async (
|
|
241
|
+
user: any | null,
|
|
242
|
+
clientAppKey: string
|
|
243
|
+
): Promise<{
|
|
244
|
+
dashboardConfig: IDashboardConfig
|
|
245
|
+
widgetsCatalog: TDashboardWidgetCatalog
|
|
246
|
+
}> => {
|
|
247
|
+
const widgetsCatalog = await getWidgetCatalog(user)
|
|
248
|
+
|
|
249
|
+
if (!user) return { dashboardConfig: blankDashboardConfig, widgetsCatalog }
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
dashboardConfig: getDefaultDashboardForCustomerUser(user, clientAppKey, [...widgetsCatalog.keys()]),
|
|
253
|
+
widgetsCatalog
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### 4. Implementation Example: Read-Only Dashboard
|
|
259
|
+
|
|
260
|
+
Use this for a simplified, non-editable view of the dashboard.
|
|
261
|
+
|
|
262
|
+
TypeScript
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
// file: src/views/DashboardReadonly.tsx
|
|
266
|
+
import { useEffect, useState } from 'react'
|
|
267
|
+
import {
|
|
268
|
+
IDashboardConfig,
|
|
269
|
+
TDashboardWidgetCatalog,
|
|
270
|
+
useDashboardStore,
|
|
271
|
+
DynamicWidgetLoader,
|
|
272
|
+
DashboardGrid
|
|
273
|
+
} from '@tenorlab/react-dashboard'
|
|
274
|
+
import {
|
|
275
|
+
blankDashboardConfig,
|
|
276
|
+
cssVarsUtils,
|
|
277
|
+
useDashboardStorageService,
|
|
278
|
+
} from '@tenorlab/react-dashboard/core'
|
|
279
|
+
import { getDashboardDefaults } from '../dashboard-defaults'
|
|
280
|
+
|
|
281
|
+
export function DashboardReadonly() {
|
|
282
|
+
const clientAppKey = 'myclientapp'
|
|
283
|
+
const user = { id: 1234 }
|
|
284
|
+
const dashboardStore = useDashboardStore()
|
|
285
|
+
const dashboardStorageService = useDashboardStorageService()
|
|
286
|
+
const { isLoading, currentDashboardConfig } = dashboardStore
|
|
287
|
+
|
|
288
|
+
const [widgetsCatalog, setWidgetsCatalog] = useState<TDashboardWidgetCatalog>(new Map())
|
|
289
|
+
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
async function initDashboard() {
|
|
292
|
+
dashboardStore.setIsLoading(true)
|
|
293
|
+
try {
|
|
294
|
+
const defaults = await getDashboardDefaults(user, clientAppKey)
|
|
295
|
+
const savedConfigs = await dashboardStorageService.getSavedDashboards(
|
|
296
|
+
user.id,
|
|
297
|
+
clientAppKey,
|
|
298
|
+
defaults.widgetsCatalog,
|
|
299
|
+
defaults.dashboardConfig
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
dashboardStore.setAllDashboardConfigs(savedConfigs)
|
|
303
|
+
const activeConfig = savedConfigs[0] || defaults.dashboardConfig
|
|
304
|
+
dashboardStore.setCurrentDashboardConfig(activeConfig)
|
|
305
|
+
setWidgetsCatalog(defaults.widgetsCatalog)
|
|
306
|
+
cssVarsUtils.restoreCssVarsFromSettings(activeConfig.cssSettings || [])
|
|
307
|
+
} finally {
|
|
308
|
+
dashboardStore.setIsLoading(false)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
initDashboard()
|
|
312
|
+
}, [])
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<div className="relative flex flex-col h-full">
|
|
316
|
+
{isLoading ? <div>Loading</div> : (
|
|
317
|
+
<DashboardGrid
|
|
318
|
+
isEditing={false}
|
|
319
|
+
zoomScale={Number(currentDashboardConfig.zoomScale)}
|
|
320
|
+
responsiveGrid={currentDashboardConfig.responsiveGrid}
|
|
321
|
+
>
|
|
322
|
+
{currentDashboardConfig.widgets.map((widgetKey, index) => (
|
|
323
|
+
<DynamicWidgetLoader
|
|
324
|
+
key={`${widgetKey}_${index}`}
|
|
325
|
+
widgetKey={widgetKey}
|
|
326
|
+
index={index}
|
|
327
|
+
maxIndex={currentDashboardConfig.widgets.length - 1}
|
|
328
|
+
childWidgetsConfig={currentDashboardConfig.childWidgetsConfig}
|
|
329
|
+
widgetCatalog={widgetsCatalog as any}
|
|
330
|
+
isEditing={false}
|
|
331
|
+
/>
|
|
332
|
+
))}
|
|
333
|
+
</DashboardGrid>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
#### 5. Full Editable Dashboard
|
|
343
|
+
|
|
344
|
+
For a complete example including **Undo/Redo**, **Zooming**, **Catalog Flyouts**, and **Multiple Dashboards**, please refer to the [Full Implementation Example](https://github.com/tenorlab/react-dashboard-sample/blob/main/views/DashboardFullExample.tsx).
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
------
|
|
29
349
|
|
|
30
|
-
|
|
350
|
+
## 🧩 Components & Services
|
|
31
351
|
|
|
352
|
+
### UI Components
|
|
32
353
|
|
|
33
|
-
|
|
354
|
+
- **`DashboardGrid`**: The main layout engine for positioning widgets.
|
|
355
|
+
- **`WidgetContainer`**: Wrapper providing common widget UI (headers, actions, loading states).
|
|
356
|
+
- **`WidgetsCatalogFlyout`**: A slide-out panel for users to browse and add new widgets.
|
|
357
|
+
- **`DynamicWidgetLoader`**: Lazy-loading utility for high-performance dashboards.
|
|
34
358
|
|
|
35
|
-
|
|
36
|
-
- **Configurable:** Allow end-users to add/remove widgets.
|
|
37
|
-
- **Vite Optimized:** Tree-shakeable and lightweight for modern build pipelines.
|
|
38
|
-
- **Themeable:** Easy integration with Tailwind CSS or CSS Variables.
|
|
359
|
+
### Hooks & State
|
|
39
360
|
|
|
361
|
+
- **`useDashboardStore`**: Access the underlying Zustand store to manage widget state, layout, and configuration.
|
|
362
|
+
- **`useDashboardUndoService`**: Provides `undo` and `redo` functionality for user layout changes.
|
|
40
363
|
|
|
364
|
+
------
|
|
41
365
|
|
|
42
|
-
## Licensing
|
|
366
|
+
## ⚖️ Licensing
|
|
43
367
|
|
|
44
368
|
This project is dual-licensed:
|
|
45
369
|
|
|
46
|
-
1. **Non-Commercial / Personal Use:** Licensed under the [Polyform Non-Commercial 1.0.0](https://polyformproject.org/licenses/non-commercial/1.0.0/). Free
|
|
370
|
+
1. **Non-Commercial / Personal Use:** Licensed under the [Polyform Non-Commercial 1.0.0](https://polyformproject.org/licenses/non-commercial/1.0.0/). Free for students, hobbyists, and open-source projects.
|
|
47
371
|
2. **Commercial Use:** Requires a **Tenorlab Commercial License**.
|
|
48
372
|
|
|
49
|
-
If you are using this library to build a product
|
|
373
|
+
If you are using this library to build a revenue-generating product or within a commercial entity, please visit [tenorlab.com/license](https://tenorlab.com/license).
|
package/dist/core.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare const blankDashboardConfig: IDashboardConfig;
|
|
|
5
5
|
* Helper function to create dynamic entries
|
|
6
6
|
* This helps keep the catalog registration clean
|
|
7
7
|
*/
|
|
8
|
-
export declare const createDynamicEntry: (key: string, loader: TWidgetFactoryBase, metaData: TWidgetMetaInfoBase) => [string, IDynamicWidgetCatalogEntryBase];
|
|
8
|
+
export declare const createDynamicEntry: (key: string, loader: TWidgetFactoryBase, isRemote: boolean, metaData: TWidgetMetaInfoBase) => [string, IDynamicWidgetCatalogEntryBase];
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* @name createStaticEntry
|
|
@@ -185,6 +185,7 @@ export declare interface IDynamicWidgetCatalogEntryBase<TFrameworkElementType =
|
|
|
185
185
|
key: TDashboardWidgetKey;
|
|
186
186
|
title: string;
|
|
187
187
|
isContainer?: boolean;
|
|
188
|
+
isRemote?: boolean;
|
|
188
189
|
meta?: TWidgetMetaInfoBase<TFrameworkElementType>;
|
|
189
190
|
component?: TFrameworkComponentType;
|
|
190
191
|
loader?: TWidgetFactoryBase<TFrameworkComponentType>;
|