@tenorlab/react-dashboard 1.6.0 → 1.6.2
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 +179 -127
- package/dist/core.d.ts +3 -0
- package/dist/core.es.js +5 -15
- package/dist/react-dashboard.d.ts +3 -0
- package/dist/react-dashboard.es.js +769 -766
- package/dist/styles.css +1 -1
- package/package.json +4 -6
package/README.md
CHANGED
|
@@ -6,35 +6,34 @@
|
|
|
6
6
|
|
|
7
7
|
Foundation components for creating user-configurable, high-performance dashboards in React.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Relationship to Core
|
|
10
10
|
|
|
11
11
|
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.
|
|
12
12
|
|
|
13
13
|
> **Note**: This package re-exports all types and utilities from `@tenorlab/dashboard-core`. You do not need to install the core package separately.
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
## Demos
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
## Pro Template Demos
|
|
17
|
+
- [React Demo](https://react.tenorlab.com) (built with @tenorlab/react-dashboard)
|
|
18
|
+
- [Vue Demo](https://vue.tenorlab.com) (built with @tenorlab/vue-dashboard)
|
|
19
|
+
- [Nuxt Demo](https://nuxt.tenorlab.com) (built with @tenorlab/vue-dashboard)
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
## ✨ Features
|
|
23
23
|
|
|
24
|
-
- **Type-Safe:** Deep integration with TypeScript 5.8+ for full IDE support.
|
|
25
|
-
- **State Management:** Built-in `useDashboardStore` and `useDashboardUndoService`.
|
|
26
|
-
- **User Configurable:** Ready-to-use components for adding, removing, and dragging widgets.
|
|
27
|
-
- **Themeable:** Native support for CSS Variables and Tailwind CSS.
|
|
28
|
-
- **Vite Optimized:** Full ESM support and tree-shakeable.
|
|
24
|
+
- **Type-Safe:** Deep integration with TypeScript 5.8+ for full IDE support.
|
|
25
|
+
- **State Management:** Built-in `useDashboardStore` and `useDashboardUndoService`.
|
|
26
|
+
- **User Configurable:** Ready-to-use components for adding, removing, and dragging widgets.
|
|
27
|
+
- **Themeable:** Native support for CSS Variables and Tailwind CSS.
|
|
28
|
+
- **Vite Optimized:** Full ESM support and tree-shakeable.
|
|
29
29
|
|
|
30
30
|
## 🚀 Quick Start
|
|
31
31
|
|
|
32
32
|
### Installation
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
### Installation
|
|
35
35
|
|
|
36
|
-
```
|
|
37
|
-
# with npm
|
|
36
|
+
```bash
|
|
38
37
|
npm i @tenorlab/react-dashboard
|
|
39
38
|
|
|
40
39
|
# with pnpm
|
|
@@ -45,9 +44,7 @@ pnpm add @tenorlab/react-dashboard
|
|
|
45
44
|
|
|
46
45
|
Import the base styles in your entry file (e.g., `main.tsx`):
|
|
47
46
|
|
|
48
|
-
TypeScript
|
|
49
|
-
|
|
50
|
-
```
|
|
47
|
+
```TypeScript
|
|
51
48
|
import '@tenorlab/react-dashboard/styles.css'
|
|
52
49
|
```
|
|
53
50
|
|
|
@@ -59,62 +56,57 @@ import '@tenorlab/react-dashboard/styles.css'
|
|
|
59
56
|
|
|
60
57
|
Widgets should be organized by their loading strategy.
|
|
61
58
|
|
|
62
|
-
- **Bundled Widgets**: Place in `src/bundled-widgets/` (loaded immediately).
|
|
63
|
-
- **Async Widgets**: Place in `src/async-widgets/` (lazy-loaded).
|
|
59
|
+
- **Bundled Widgets**: Place in `src/bundled-widgets/` (loaded immediately).
|
|
60
|
+
- **Async Widgets**: Place in `src/async-widgets/` (lazy-loaded).
|
|
61
|
+
|
|
62
|
+
*(NOTE: These directory names are suggestions; you can use different names, or put the widgets under src/components if you prefer)*
|
|
63
|
+
|
|
64
64
|
|
|
65
65
|
Each widget requires a sub-directory using the `widget-name-here` convention.
|
|
66
66
|
|
|
67
67
|
#### Example: `WidgetTotalOrders`
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
Directory name `widget-total-orders`, files:
|
|
70
|
+
- WidgetTotalOrders.tsx
|
|
71
|
+
- meta.ts
|
|
72
|
+
- index.ts
|
|
70
73
|
|
|
71
|
-
```
|
|
72
|
-
// file: src/bundled-widgets/widget-total-orders/WidgetTotalOrders.tsx
|
|
73
|
-
import {
|
|
74
|
-
IDashboardWidget,
|
|
75
|
-
IDashboardWidgetProps,
|
|
76
|
-
TDashboardWidgetKey,
|
|
77
|
-
DashboardWidgetBase,
|
|
78
|
-
WrapperColumnContent
|
|
79
|
-
} from '@tenorlab/react-dashboard'
|
|
80
74
|
|
|
81
|
-
|
|
75
|
+
File: src/bundled-widgets/widget-total-orders/WidgetTotalOrders.tsx:
|
|
76
|
+
```react
|
|
77
|
+
import { IDashboardWidgetProps } from '@tenorlab/react-dashboard'
|
|
78
|
+
import { DashboardWidgetBase } from '@tenorlab/react-dashboard'
|
|
79
|
+
|
|
80
|
+
export function WidgetTotalOrders(props: IDashboardWidgetProps) {
|
|
82
81
|
return (
|
|
83
|
-
<DashboardWidgetBase
|
|
84
|
-
|
|
85
|
-
title="Total Orders"
|
|
86
|
-
{...props}
|
|
87
|
-
>
|
|
88
|
-
<WrapperColumnContent>
|
|
82
|
+
<DashboardWidgetBase {...props}>
|
|
83
|
+
<div className="w-full flex flex-col gap-2 items-end">
|
|
89
84
|
<div className="dashboard-number number-xl text-primary">1,250</div>
|
|
90
85
|
<div className="text-sm">Orders this month</div>
|
|
91
|
-
</
|
|
86
|
+
</div>
|
|
92
87
|
</DashboardWidgetBase>
|
|
93
88
|
)
|
|
94
89
|
}
|
|
95
90
|
```
|
|
96
91
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
```
|
|
100
|
-
// file: src/bundled-widgets/widget-total-orders/meta.ts
|
|
92
|
+
File: src/bundled-widgets/widget-total-orders/meta.ts:
|
|
93
|
+
```typescript
|
|
101
94
|
import type { TWidgetMetaInfo } from '@tenorlab/react-dashboard'
|
|
102
|
-
import {
|
|
95
|
+
import { ReceiptIcon as ComponentIcon } from 'lucide-react'
|
|
103
96
|
|
|
97
|
+
// Define the metadata object for the plugin
|
|
104
98
|
export const WidgetTotalOrdersMeta: TWidgetMetaInfo = {
|
|
105
99
|
name: 'Total Orders',
|
|
106
100
|
categories: ['Widget'],
|
|
107
101
|
icon: ComponentIcon,
|
|
108
102
|
noDuplicatedWidgets: true,
|
|
109
103
|
description: 'Displays information about your total orders.',
|
|
110
|
-
externalDependencies: []
|
|
104
|
+
externalDependencies: [],
|
|
111
105
|
}
|
|
112
106
|
```
|
|
113
107
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
```
|
|
117
|
-
// file: src/bundled-widgets/widget-total-orders/index.ts
|
|
108
|
+
File: src/bundled-widgets/widget-total-orders/index.ts:
|
|
109
|
+
```typescript
|
|
118
110
|
import { WidgetTotalOrders } from './WidgetTotalOrders'
|
|
119
111
|
export default WidgetTotalOrders
|
|
120
112
|
```
|
|
@@ -123,39 +115,44 @@ export default WidgetTotalOrders
|
|
|
123
115
|
|
|
124
116
|
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).
|
|
125
117
|
|
|
126
|
-
|
|
118
|
+
File: src/widgets-catalog.ts:
|
|
119
|
+
```typescript
|
|
120
|
+
import { WidgetContainerColumn, WidgetContainerLarge, WidgetContainerRow } from '@tenorlab/react-dashboard'
|
|
121
|
+
import {
|
|
122
|
+
createStaticEntry,
|
|
123
|
+
localWidgetDiscovery,
|
|
124
|
+
remoteWidgetDiscovery,
|
|
125
|
+
} from '@tenorlab/react-dashboard/core'
|
|
127
126
|
|
|
128
|
-
|
|
129
|
-
// file: src/widgets-catalog.tsx
|
|
127
|
+
// other static widgets
|
|
130
128
|
import {
|
|
129
|
+
WidgetSmallCardSample,
|
|
130
|
+
} from './other-widgets/other-widgets'
|
|
131
|
+
import { otherWidgetsMetaMap } from './other-widgets/other-widgets-meta'
|
|
132
|
+
import type {
|
|
131
133
|
IDynamicWidgetCatalogEntry,
|
|
132
134
|
TDashboardWidgetCatalog,
|
|
133
135
|
TWidgetMetaInfoBase,
|
|
134
|
-
WidgetContainerColumn,
|
|
135
|
-
WidgetContainerLarge,
|
|
136
|
-
WidgetContainerRow,
|
|
137
136
|
TWidgetFactory,
|
|
138
137
|
} from '@tenorlab/react-dashboard'
|
|
139
|
-
import {
|
|
140
|
-
createStaticEntry,
|
|
141
|
-
localWidgetDiscovery,
|
|
142
|
-
remoteWidgetDiscovery,
|
|
143
|
-
} from '@tenorlab/react-dashboard/core'
|
|
144
|
-
|
|
145
|
-
import { WidgetRecentPaymentInfo } from './other-widgets/WidgetRecentPaymentInfo'
|
|
146
|
-
//import { getWidgetsManifestUrl } from '@/utils'
|
|
147
138
|
|
|
148
139
|
const bundledWidgetsSrcPath = '/src/bundled-widgets'
|
|
149
140
|
const asyncWidgetsSrcPath = '/src/async-widgets'
|
|
150
141
|
|
|
142
|
+
// Use Vite's Glob Import
|
|
143
|
+
// This creates an object where the keys are file paths, and the values are the TWidgetFactory functions.
|
|
144
|
+
// We target the 'index.ts' files within the widgets subdirectories.
|
|
151
145
|
type TGlobModuleMap = Record<string, TWidgetFactory>
|
|
152
146
|
|
|
147
|
+
// Eagerly loaded (Non-lazy / Bundled):
|
|
153
148
|
const bundledWidgetModules = import.meta.glob('/src/bundled-widgets/*/index.ts', {
|
|
154
|
-
eager: true
|
|
149
|
+
eager: true /* we load this immediately */,
|
|
155
150
|
}) as TGlobModuleMap
|
|
156
151
|
|
|
152
|
+
// Lazy loaded (Code-split / Plugins):
|
|
157
153
|
const asyncWidgetModules = import.meta.glob('/src/async-widgets/*/index.ts') as TGlobModuleMap
|
|
158
154
|
|
|
155
|
+
// Meta modules (Always eager so titles/icons are available immediately)
|
|
159
156
|
const allMetaModules = import.meta.glob('/src/**/widget-*/meta.ts', {
|
|
160
157
|
eager: true,
|
|
161
158
|
}) as Record<string, Record<string, TWidgetMetaInfoBase>>
|
|
@@ -163,33 +160,61 @@ const allMetaModules = import.meta.glob('/src/**/widget-*/meta.ts', {
|
|
|
163
160
|
const hasPermission = (_user_: any, _permission: string) => true
|
|
164
161
|
|
|
165
162
|
export const getWidgetCatalog = async (user: any | null): Promise<TDashboardWidgetCatalog> => {
|
|
163
|
+
// A. Register Static Core Components
|
|
166
164
|
const catalogMapEntries: [string, IDynamicWidgetCatalogEntry][] = [
|
|
167
|
-
|
|
168
|
-
createStaticEntry('
|
|
169
|
-
createStaticEntry('
|
|
165
|
+
// everyone has access to the containers:
|
|
166
|
+
createStaticEntry('WidgetContainer', WidgetContainerColumn, otherWidgetsMetaMap['WidgetContainer']),
|
|
167
|
+
createStaticEntry('WidgetContainerRow', WidgetContainerRow, otherWidgetsMetaMap['WidgetContainerRow']),
|
|
168
|
+
createStaticEntry(
|
|
169
|
+
'WidgetContainerLarge',
|
|
170
|
+
WidgetContainerLarge,
|
|
171
|
+
otherWidgetsMetaMap['WidgetContainerLarge'],
|
|
172
|
+
),
|
|
170
173
|
]
|
|
171
174
|
|
|
175
|
+
// B. Optional: Register Business Static Widgets manually:
|
|
176
|
+
// we could filter further by permissions and user type if needed
|
|
172
177
|
if (hasPermission(user, 'some-permission')) {
|
|
173
|
-
|
|
178
|
+
// i.e.:
|
|
179
|
+
// catalogMapEntries.push(
|
|
180
|
+
// createStaticEntry(
|
|
181
|
+
// 'WidgetThatRequiresPermissions',
|
|
182
|
+
// WidgetThatRequiresPermissions,
|
|
183
|
+
// otherWidgetsMetaMap['WidgetThatRequiresPermissions'],
|
|
184
|
+
// ),
|
|
185
|
+
// )
|
|
186
|
+
catalogMapEntries.push(
|
|
187
|
+
createStaticEntry(
|
|
188
|
+
'WidgetSmallCardSample',
|
|
189
|
+
WidgetSmallCardSample,
|
|
190
|
+
otherWidgetsMetaMap['WidgetSmallCardSample'],
|
|
191
|
+
),
|
|
192
|
+
)
|
|
174
193
|
}
|
|
175
194
|
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
195
|
+
// C. Register widgets automatically with the localWidgetDiscovery helper:
|
|
196
|
+
// (bundled widgets are included always, non-lazy)
|
|
197
|
+
catalogMapEntries.push(
|
|
198
|
+
...localWidgetDiscovery(
|
|
199
|
+
bundledWidgetsSrcPath,
|
|
200
|
+
bundledWidgetModules,
|
|
201
|
+
allMetaModules,
|
|
202
|
+
false, // lazy: false
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
// D. Register "lazy" widgets automatically with the localWidgetDiscovery helper:
|
|
207
|
+
// (async widgets are not incuded, they are lazy loaded at run time)
|
|
208
|
+
catalogMapEntries.push(
|
|
209
|
+
...localWidgetDiscovery(
|
|
210
|
+
asyncWidgetsSrcPath,
|
|
211
|
+
asyncWidgetModules,
|
|
212
|
+
allMetaModules,
|
|
213
|
+
true, // lazy: true
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
// E. Optional: Remote discovery of -pre-built widgets hosted on a CDN (requires advance importMaps setup and other configuration)
|
|
193
218
|
/*const manifestUrl = getWidgetsManifestUrl()
|
|
194
219
|
if (manifestUrl.length > 0) {
|
|
195
220
|
const remoteResponse = await remoteWidgetDiscovery(manifestUrl)
|
|
@@ -206,26 +231,26 @@ export const getWidgetCatalog = async (user: any | null): Promise<TDashboardWidg
|
|
|
206
231
|
|
|
207
232
|
Use a `dashboard-defaults.ts` file to define initial layouts based on user roles.
|
|
208
233
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
import {
|
|
234
|
+
File: src/dashboard-defaults.ts:
|
|
235
|
+
```typescript
|
|
236
|
+
import { blankDashboardConfig, cssSettingsCatalog } from '@tenorlab/react-dashboard/core'
|
|
237
|
+
import { getWidgetCatalog } from './widgets-catalog'
|
|
238
|
+
import type {
|
|
214
239
|
TDashboardWidgetKey,
|
|
215
240
|
IChildWidgetConfigEntry,
|
|
216
241
|
IDashboardConfig,
|
|
217
242
|
TDashboardWidgetCatalog,
|
|
218
243
|
} from '@tenorlab/react-dashboard'
|
|
219
|
-
import { blankDashboardConfig, cssSettingsCatalog } from '@tenorlab/react-dashboard/core'
|
|
220
|
-
import { getWidgetCatalog } from './widgets-catalog'
|
|
221
244
|
|
|
245
|
+
// reserved identifier to be used only for the default dashboard
|
|
222
246
|
const DEFAULT_DASHBOARD_ID = 'default' as const
|
|
223
247
|
const DEFAULT_DASHBOARD_NAME = 'Default' as const
|
|
224
248
|
|
|
225
|
-
|
|
249
|
+
// default dashboard config for Regular user type
|
|
250
|
+
const getDefaultDashboardForRegularUser = (
|
|
226
251
|
user: any,
|
|
227
252
|
clientAppKey: string,
|
|
228
|
-
availableWidgetKeys: TDashboardWidgetKey[]
|
|
253
|
+
availableWidgetKeys: TDashboardWidgetKey[],
|
|
229
254
|
): IDashboardConfig => {
|
|
230
255
|
const userID = user.userID || 0
|
|
231
256
|
return {
|
|
@@ -234,10 +259,21 @@ const getDefaultDashboardForCustomerUser = (
|
|
|
234
259
|
dashboardId: DEFAULT_DASHBOARD_ID,
|
|
235
260
|
dashboardName: DEFAULT_DASHBOARD_NAME,
|
|
236
261
|
zoomScale: 1,
|
|
237
|
-
responsiveGrid:
|
|
238
|
-
widgets: [
|
|
262
|
+
responsiveGrid: true,
|
|
263
|
+
widgets: [
|
|
264
|
+
'WidgetContainer_container1', // will contain other widgets specified in the childWidgetsConfig secitno below
|
|
265
|
+
'WidgetBarGradients',
|
|
266
|
+
],
|
|
239
267
|
childWidgetsConfig: [
|
|
240
|
-
|
|
268
|
+
// two widgets go into container1:
|
|
269
|
+
{
|
|
270
|
+
parentWidgetKey: 'WidgetContainer_container1',
|
|
271
|
+
widgetKey: 'WidgetTotalOrders'
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
parentWidgetKey: 'WidgetContainer_container1',
|
|
275
|
+
widgetKey: 'WidgetTotalOrders'
|
|
276
|
+
}
|
|
241
277
|
],
|
|
242
278
|
cssSettings: [...cssSettingsCatalog]
|
|
243
279
|
}
|
|
@@ -252,10 +288,16 @@ export const getDashboardDefaults = async (
|
|
|
252
288
|
}> => {
|
|
253
289
|
const widgetsCatalog = await getWidgetCatalog(user)
|
|
254
290
|
|
|
255
|
-
if (!user)
|
|
291
|
+
if (!user) {
|
|
292
|
+
return {
|
|
293
|
+
dashboardConfig: blankDashboardConfig,
|
|
294
|
+
widgetsCatalog,
|
|
295
|
+
}
|
|
296
|
+
}
|
|
256
297
|
|
|
257
298
|
return {
|
|
258
|
-
|
|
299
|
+
// Optional, you could use different routines depending on user role:
|
|
300
|
+
dashboardConfig: getDefaultDashboardForRegularUser(user, clientAppKey, [...widgetsCatalog.keys()]),
|
|
259
301
|
widgetsCatalog
|
|
260
302
|
}
|
|
261
303
|
}
|
|
@@ -265,32 +307,33 @@ export const getDashboardDefaults = async (
|
|
|
265
307
|
|
|
266
308
|
Use this for a simplified, non-editable view of the dashboard.
|
|
267
309
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
```
|
|
271
|
-
// file: src/views/DashboardReadonly.tsx
|
|
310
|
+
File: src/views/DashboardReadonly.tsx:
|
|
311
|
+
```react
|
|
272
312
|
import { useEffect, useState } from 'react'
|
|
273
|
-
import {
|
|
274
|
-
IDashboardConfig,
|
|
275
|
-
TDashboardWidgetCatalog,
|
|
276
|
-
useDashboardStore,
|
|
277
|
-
DynamicWidgetLoader,
|
|
278
|
-
DashboardGrid
|
|
279
|
-
} from '@tenorlab/react-dashboard'
|
|
313
|
+
import { useDashboardStore } from '@tenorlab/react-dashboard'
|
|
280
314
|
import {
|
|
281
315
|
blankDashboardConfig,
|
|
282
316
|
cssVarsUtils,
|
|
283
317
|
useDashboardStorageService,
|
|
284
318
|
} from '@tenorlab/react-dashboard/core'
|
|
319
|
+
import { DynamicWidgetLoader, DashboardGrid } from '@tenorlab/react-dashboard'
|
|
285
320
|
import { getDashboardDefaults } from '../dashboard-defaults'
|
|
321
|
+
import type { IDashboardConfig, TDashboardWidgetCatalog } from '@tenorlab/react-dashboard'
|
|
322
|
+
|
|
286
323
|
|
|
287
324
|
export function DashboardReadonly() {
|
|
288
325
|
const clientAppKey = 'myclientapp'
|
|
289
326
|
const user = { id: 1234 }
|
|
327
|
+
const userId = user.id
|
|
290
328
|
const dashboardStore = useDashboardStore()
|
|
291
329
|
const dashboardStorageService = useDashboardStorageService()
|
|
330
|
+
|
|
292
331
|
const { isLoading, currentDashboardConfig } = dashboardStore
|
|
332
|
+
const getTargetContainerKey = () => dashboardStore.targetContainerKey
|
|
293
333
|
|
|
334
|
+
// default dashboard config
|
|
335
|
+
const [_defaultDashboardConfig, setDefaultDashboardConfig] =
|
|
336
|
+
useState<IDashboardConfig>(blankDashboardConfig)
|
|
294
337
|
const [widgetsCatalog, setWidgetsCatalog] = useState<TDashboardWidgetCatalog>(new Map())
|
|
295
338
|
|
|
296
339
|
useEffect(() => {
|
|
@@ -306,9 +349,11 @@ export function DashboardReadonly() {
|
|
|
306
349
|
)
|
|
307
350
|
|
|
308
351
|
dashboardStore.setAllDashboardConfigs(savedConfigs)
|
|
352
|
+
// show default dashboard or first dashboard
|
|
309
353
|
const activeConfig = savedConfigs[0] || defaults.dashboardConfig
|
|
310
354
|
dashboardStore.setCurrentDashboardConfig(activeConfig)
|
|
311
355
|
setWidgetsCatalog(defaults.widgetsCatalog)
|
|
356
|
+
setDefaultDashboardConfig(defaults.dashboardConfig)
|
|
312
357
|
cssVarsUtils.restoreCssVarsFromSettings(activeConfig.cssSettings || [])
|
|
313
358
|
} finally {
|
|
314
359
|
dashboardStore.setIsLoading(false)
|
|
@@ -319,7 +364,8 @@ export function DashboardReadonly() {
|
|
|
319
364
|
|
|
320
365
|
return (
|
|
321
366
|
<div className="relative flex flex-col h-full">
|
|
322
|
-
{isLoading
|
|
367
|
+
{isLoading && <div>Loading</div>}
|
|
368
|
+
{!isLoading && (
|
|
323
369
|
<DashboardGrid
|
|
324
370
|
isEditing={false}
|
|
325
371
|
zoomScale={Number(currentDashboardConfig.zoomScale)}
|
|
@@ -329,11 +375,16 @@ export function DashboardReadonly() {
|
|
|
329
375
|
<DynamicWidgetLoader
|
|
330
376
|
key={`${widgetKey}_${index}`}
|
|
331
377
|
widgetKey={widgetKey}
|
|
378
|
+
parentWidgetKey={undefined}
|
|
379
|
+
targetContainerKey={getTargetContainerKey()}
|
|
332
380
|
index={index}
|
|
333
381
|
maxIndex={currentDashboardConfig.widgets.length - 1}
|
|
334
382
|
childWidgetsConfig={currentDashboardConfig.childWidgetsConfig}
|
|
335
383
|
widgetCatalog={widgetsCatalog as any}
|
|
336
384
|
isEditing={false}
|
|
385
|
+
onRemoveClick={() => {}}
|
|
386
|
+
onMoveClick={() => {}}
|
|
387
|
+
selectContainer={() => {}}
|
|
337
388
|
/>
|
|
338
389
|
))}
|
|
339
390
|
</DashboardGrid>
|
|
@@ -344,11 +395,9 @@ export function DashboardReadonly() {
|
|
|
344
395
|
```
|
|
345
396
|
|
|
346
397
|
|
|
347
|
-
|
|
348
398
|
#### 5. Full Editable Dashboard
|
|
349
399
|
|
|
350
|
-
For
|
|
351
|
-
|
|
400
|
+
For editable dashboard examples, including **Undo/Redo**, **Zooming**, **Catalog Flyouts**, and **Multiple Dashboards**, please refer to the [Pro Template](https://www.tenorlab.com).
|
|
352
401
|
|
|
353
402
|
|
|
354
403
|
------
|
|
@@ -357,15 +406,15 @@ For a complete example including **Undo/Redo**, **Zooming**, **Catalog Flyouts**
|
|
|
357
406
|
|
|
358
407
|
### UI Components
|
|
359
408
|
|
|
360
|
-
- **`DashboardGrid`**: The main layout
|
|
361
|
-
- **`WidgetContainer`**:
|
|
362
|
-
- **`WidgetsCatalogFlyout`**: A
|
|
363
|
-
- **`DynamicWidgetLoader`**: The core widget loader
|
|
409
|
+
- **`DashboardGrid`**: The main dashboard layout that position widgets within a responsive grid.
|
|
410
|
+
- **`WidgetContainer`**: A special "widget" that is a container for other widgets.
|
|
411
|
+
- **`WidgetsCatalogFlyout`**: A slide-out panel for users to browse and add new widgets on editable dashboards.
|
|
412
|
+
- **`DynamicWidgetLoader`**: The core lazy-loading widget loader that renders the widgets within the grid.
|
|
364
413
|
|
|
365
414
|
### Hooks & State
|
|
366
415
|
|
|
367
|
-
- **`useDashboardStore`**: Access the underlying
|
|
368
|
-
- **`useDashboardUndoService`**: Provides `undo` and `redo` functionality for user layout changes.
|
|
416
|
+
- **`useDashboardStore`**: Access the underlying reactive store to manage widget state, layout, and configuration.
|
|
417
|
+
- **`useDashboardUndoService`**: Provides `undo` and `redo` functionality for user layout changes in editable dashboard (optional).
|
|
369
418
|
|
|
370
419
|
|
|
371
420
|
------
|
|
@@ -373,13 +422,19 @@ For a complete example including **Undo/Redo**, **Zooming**, **Catalog Flyouts**
|
|
|
373
422
|
|
|
374
423
|
## Links
|
|
375
424
|
|
|
425
|
+
### Open source core packages
|
|
376
426
|
- [@tenorlab/react-dashboard](https://www.npmjs.com/package/@tenorlab/react-dashboard): React-specific components
|
|
377
427
|
- [@tenorlab/vue-dashboard](https://www.npmjs.com/package/@tenorlab/vue-dashboard): Vue-specific components
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
- [
|
|
381
|
-
- [
|
|
428
|
+
|
|
429
|
+
### Pro Template Demos
|
|
430
|
+
- [React Demo](https://react.tenorlab.com) (built with @tenorlab/react-dashboard)
|
|
431
|
+
- [Vue Demo](https://vue.tenorlab.com) (built with @tenorlab/vue-dashboard)
|
|
432
|
+
- [Nuxt Demo](https://nuxt.tenorlab.com) (built with @tenorlab/vue-dashboard)
|
|
433
|
+
|
|
434
|
+
### Others
|
|
435
|
+
- [Buy a License](https://payhip.com/b/gPBpo)
|
|
382
436
|
- [Follow on BlueSky](https://bsky.app/profile/tenorlab.bsky.social)
|
|
437
|
+
- [Official Website](https://www.tenorlab.com)
|
|
383
438
|
|
|
384
439
|
|
|
385
440
|
------
|
|
@@ -387,7 +442,7 @@ For a complete example including **Undo/Redo**, **Zooming**, **Catalog Flyouts**
|
|
|
387
442
|
|
|
388
443
|
## ⚖️ Licensing & Usage
|
|
389
444
|
|
|
390
|
-
**@tenorlab/
|
|
445
|
+
**@tenorlab/vue-dashboard** is [MIT licensed](https://opensource.org/licenses/MIT).
|
|
391
446
|
|
|
392
447
|
It provides the foundational components and logic for building dashboards. You are free to use it in any project, personal or commercial.
|
|
393
448
|
|
|
@@ -395,10 +450,7 @@ It provides the foundational components and logic for building dashboards. You a
|
|
|
395
450
|
|
|
396
451
|
A commercial license for a full-blown professional app template is available for purchase [**here**](https://www.tenorlab.com) and comes with:
|
|
397
452
|
|
|
398
|
-
* **Full Application Shell:** A clean, optimized Vite + TypeScript project structure (with either React or
|
|
453
|
+
* **Full Application Shell:** A clean, optimized Vite + TypeScript project structure (with either React, Vue or Nuxt).
|
|
399
454
|
* **Dashboard Management:** Production-ready logic for creating, listing, renaming, and deleting multiple user-defined dashboards.
|
|
400
|
-
* **Implementation Examples:**
|
|
455
|
+
* **Implementation Examples:** Best patterns for both "Read-Only" (Analyst view) and "User-Editable" (Admin view) dashboard modes, a dynamic dashboard menu, etc.
|
|
401
456
|
* **Tenorlab Theme Engine:** A sophisticated Tailwind-based system supporting multiple custom themes (not just Light/Dark mode).
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
[**Live React Demo**](https://react.tenorlab.com), [**Live Vue Demo**](https://vue.tenorlab.com)
|
package/dist/core.d.ts
CHANGED
|
@@ -275,6 +275,9 @@ export declare interface IDashboardWidgetPropsBase<TExtraProps = any> {
|
|
|
275
275
|
size?: TWidgetSize;
|
|
276
276
|
borderCssClasses?: string;
|
|
277
277
|
backgroundCssClasses?: string;
|
|
278
|
+
addCssClasses?: string;
|
|
279
|
+
overrideCssClasses?: string;
|
|
280
|
+
tags?: string[];
|
|
278
281
|
hideTitle?: boolean;
|
|
279
282
|
noShadow?: boolean;
|
|
280
283
|
noBorder?: boolean;
|
package/dist/core.es.js
CHANGED
|
@@ -106,16 +106,12 @@ const W = [
|
|
|
106
106
|
else {
|
|
107
107
|
l.forEach((d) => {
|
|
108
108
|
d.value = (d.value || "").replace(/NaN/g, "");
|
|
109
|
-
const c = a.cssSettings.find(
|
|
110
|
-
(g) => g.key === d.key
|
|
111
|
-
);
|
|
109
|
+
const c = a.cssSettings.find((g) => g.key === d.key);
|
|
112
110
|
c && (Object.keys(c).forEach((g) => {
|
|
113
111
|
g in d || (d[g] = c[g]);
|
|
114
112
|
}), d.step = c.step, d.minValue = c.minValue, d.defaultValue = c.defaultValue, d.defaultUnit = c.defaultUnit, /\d+/g.test(d.value) === !1 && (d.value = c ? c.value : "1.0rem"));
|
|
115
113
|
});
|
|
116
|
-
const o = a.cssSettings.filter((d) => !l.some(
|
|
117
|
-
(c) => c.key === d.key
|
|
118
|
-
));
|
|
114
|
+
const o = a.cssSettings.filter((d) => !l.some((c) => c.key === d.key));
|
|
119
115
|
s.cssSettings = [...l, ...o];
|
|
120
116
|
}
|
|
121
117
|
s.widgets = s.widgets.filter(
|
|
@@ -172,16 +168,12 @@ const W = [
|
|
|
172
168
|
(r) => r.parentWidgetKey === i
|
|
173
169
|
);
|
|
174
170
|
if (!a || a.length === 0)
|
|
175
|
-
return t.widgets = t.widgets.filter(
|
|
176
|
-
(r) => r !== i
|
|
177
|
-
), !1;
|
|
171
|
+
return t.widgets = t.widgets.filter((r) => r !== i), !1;
|
|
178
172
|
}
|
|
179
173
|
return !0;
|
|
180
174
|
}), t;
|
|
181
175
|
}, x = (e) => {
|
|
182
|
-
const t = e.widgets.filter(
|
|
183
|
-
(a) => a.includes("WidgetContainer")
|
|
184
|
-
), i = {};
|
|
176
|
+
const t = e.widgets.filter((a) => a.includes("WidgetContainer")), i = {};
|
|
185
177
|
return t.forEach((a, r) => {
|
|
186
178
|
const n = `${a.split("_container")[0]}_container${r + 1}`;
|
|
187
179
|
i[a] = n;
|
|
@@ -394,9 +386,7 @@ const W = [
|
|
|
394
386
|
updatedDashboardConfig: o
|
|
395
387
|
};
|
|
396
388
|
} else {
|
|
397
|
-
const n = (e.widgets || []).filter(
|
|
398
|
-
(l) => `${l}`.trim().toLowerCase() !== a
|
|
399
|
-
), s = e.childWidgetsConfig.filter(
|
|
389
|
+
const n = (e.widgets || []).filter((l) => `${l}`.trim().toLowerCase() !== a), s = e.childWidgetsConfig.filter(
|
|
400
390
|
(l) => `${l.parentWidgetKey}`.trim().toLowerCase() !== a
|
|
401
391
|
);
|
|
402
392
|
return {
|
|
@@ -214,6 +214,9 @@ export declare interface IDashboardWidgetPropsBase<TExtraProps = any> {
|
|
|
214
214
|
size?: TWidgetSize;
|
|
215
215
|
borderCssClasses?: string;
|
|
216
216
|
backgroundCssClasses?: string;
|
|
217
|
+
addCssClasses?: string;
|
|
218
|
+
overrideCssClasses?: string;
|
|
219
|
+
tags?: string[];
|
|
217
220
|
hideTitle?: boolean;
|
|
218
221
|
noShadow?: boolean;
|
|
219
222
|
noBorder?: boolean;
|