@trackunit/iris-app 1.4.9 → 1.4.10
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/CHANGELOG.md +10 -0
- package/package.json +6 -6
- package/src/generators/preset/files/.cursor/rules/irisx-app-sdk-customfields.mdc +311 -0
- package/src/generators/preset/files/.cursor/rules/irisx-app-sdk.mdc +27 -0
- package/src/generators/preset/files/.cursor/rules/react-core-hooks.mdc +160 -0
- package/src/generators/preset/files/.cursor/rules/widget-extensions.mdc +325 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 1.4.10 (2025-07-11)
|
|
2
|
+
|
|
3
|
+
### 🧱 Updated Dependencies
|
|
4
|
+
|
|
5
|
+
- Updated iris-app-build-utilities to 1.4.10
|
|
6
|
+
- Updated iris-app-webpack-plugin to 1.4.10
|
|
7
|
+
- Updated iris-app-api to 1.4.10
|
|
8
|
+
- Updated react-test-setup to 1.1.10
|
|
9
|
+
- Updated shared-utils to 1.6.10
|
|
10
|
+
|
|
1
11
|
## 1.4.9 (2025-07-11)
|
|
2
12
|
|
|
3
13
|
### 🧱 Updated Dependencies
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/iris-app",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.10",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"generators": "./generators.json",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"@npmcli/arborist": "^7.3.1",
|
|
33
33
|
"webpack-bundle-analyzer": "^4.8.0",
|
|
34
34
|
"win-ca": "^3.5.1",
|
|
35
|
-
"@trackunit/iris-app-build-utilities": "1.4.
|
|
36
|
-
"@trackunit/shared-utils": "1.6.
|
|
37
|
-
"@trackunit/iris-app-api": "1.4.
|
|
38
|
-
"@trackunit/iris-app-webpack-plugin": "1.4.
|
|
35
|
+
"@trackunit/iris-app-build-utilities": "1.4.10",
|
|
36
|
+
"@trackunit/shared-utils": "1.6.10",
|
|
37
|
+
"@trackunit/iris-app-api": "1.4.10",
|
|
38
|
+
"@trackunit/iris-app-webpack-plugin": "1.4.10",
|
|
39
39
|
"tslib": "^2.6.2",
|
|
40
|
-
"@trackunit/react-test-setup": "1.1.
|
|
40
|
+
"@trackunit/react-test-setup": "1.1.10"
|
|
41
41
|
},
|
|
42
42
|
"types": "./src/index.d.ts",
|
|
43
43
|
"type": "commonjs"
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: CustomFields are the primary mechanism for storing data from IrisX applications. They allow IrisX App developers to extend the Trackunit data model by storing additional information on entities like assets, accounts, groups, and sites.
|
|
3
|
+
globs:
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# CustomFields Rule for Trackunit IrisX App SDK
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
**CustomFields are the primary mechanism for storing data from IrisX applications.** They allow IrisX App developers to extend the Trackunit data model by storing additional information on entities like assets, accounts, groups, and sites.
|
|
11
|
+
|
|
12
|
+
### Key Use Cases:
|
|
13
|
+
- **Extending the data model**: Add new fields and attributes to existing Trackunit entities
|
|
14
|
+
- **Storing user input**: Capture and persist form data, user preferences, and settings
|
|
15
|
+
- **Storing metadata**: Keep application-specific information, configuration data, and operational metrics
|
|
16
|
+
- **Fleet-specific data**: Custom attributes unique to specific fleet management needs
|
|
17
|
+
- **Performance metrics**: Custom KPIs and measurement data not available in standard Trackunit data
|
|
18
|
+
- **Business logic data**: Information required for app-specific workflows and processes
|
|
19
|
+
|
|
20
|
+
**Important**: CustomFields are the recommended and supported way to persist any data your IrisX app needs to store. Do not attempt to use external databases or storage solutions - use CustomFields for all persistent data requirements.
|
|
21
|
+
|
|
22
|
+
## 1. Defining Custom Fields
|
|
23
|
+
|
|
24
|
+
### Manifest Configuration
|
|
25
|
+
|
|
26
|
+
Custom fields must be defined in your IrisX App manifest using the `customFieldDefinitions` array:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// In your app manifest
|
|
30
|
+
customFieldDefinitions: [
|
|
31
|
+
{
|
|
32
|
+
type: 'STRING',
|
|
33
|
+
entityType: 'ASSET',
|
|
34
|
+
key: 'myCustomField',
|
|
35
|
+
translations: [{
|
|
36
|
+
language: 'en',
|
|
37
|
+
title: 'My Custom Field',
|
|
38
|
+
description: 'Description of what this field stores',
|
|
39
|
+
}],
|
|
40
|
+
uiEditable: true,
|
|
41
|
+
uiVisible: true,
|
|
42
|
+
scopeType: ScopeType.ACCOUNT
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Required Properties
|
|
48
|
+
|
|
49
|
+
- **`type`**: Field data type (see supported types below)
|
|
50
|
+
- **`entityType`**: Entity the field applies to (`ACCOUNT`, `ASSET`, `GROUP`, `SITE`)
|
|
51
|
+
- **`key`**: Programmatic identifier (cannot be changed after creation)
|
|
52
|
+
- **`translations`**: UI labels and descriptions for each supported language
|
|
53
|
+
- **`scopeType`**: Data access scope (see scope types below)
|
|
54
|
+
|
|
55
|
+
### Optional Properties
|
|
56
|
+
|
|
57
|
+
- **`uiEditable`**: Whether field can be edited in Manager UI (default: true)
|
|
58
|
+
- **`uiVisible`**: Whether field is visible in Manager UI (default: true)
|
|
59
|
+
|
|
60
|
+
## 2. Supported Field Types
|
|
61
|
+
|
|
62
|
+
| Type | Description | Extra Properties |
|
|
63
|
+
|------|-------------|------------------|
|
|
64
|
+
| `BOOLEAN` | True/false values | - |
|
|
65
|
+
| `DATE` | ISO8601 formatted dates | - |
|
|
66
|
+
| `DROPDOWN` | Predefined option lists | `allValues`, `multiSelect`, `dropDownValueReplacements` |
|
|
67
|
+
| `EMAIL` | Email addresses with mailto links | - |
|
|
68
|
+
| `NUMBER` | Numeric values | `minimum`, `maximum`, `isInteger`, `unitSi`, `unitUs` |
|
|
69
|
+
| `PHONE_NUMBER` | E.164 formatted phone numbers | - |
|
|
70
|
+
| `STRING` | Free text | `minimumLength`, `maximumLength`, `pattern` |
|
|
71
|
+
| `WEB_ADDRESS` | URLs as clickable links | - |
|
|
72
|
+
| `JSON` | JSON objects | - |
|
|
73
|
+
| `MONETARY` | Currency values (ISO 4217) | `minimum`, `maximum`, `currency` |
|
|
74
|
+
|
|
75
|
+
### Example Field Definitions
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// String field with validation
|
|
79
|
+
{
|
|
80
|
+
type: 'STRING',
|
|
81
|
+
entityType: 'ASSET',
|
|
82
|
+
key: 'serialNumber',
|
|
83
|
+
translations: [{ language: 'en', title: 'Serial Number', description: 'Asset serial number' }],
|
|
84
|
+
minimumLength: 5,
|
|
85
|
+
maximumLength: 20,
|
|
86
|
+
pattern: '^[A-Z0-9]+$'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Number field with units
|
|
90
|
+
{
|
|
91
|
+
type: 'NUMBER',
|
|
92
|
+
entityType: 'ASSET',
|
|
93
|
+
key: 'fuelCapacity',
|
|
94
|
+
translations: [{ language: 'en', title: 'Fuel Capacity', description: 'Tank capacity' }],
|
|
95
|
+
minimum: 0,
|
|
96
|
+
maximum: 1000,
|
|
97
|
+
unitSi: 'L',
|
|
98
|
+
unitUs: 'gal'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Dropdown with multiple selection
|
|
102
|
+
{
|
|
103
|
+
type: 'DROPDOWN',
|
|
104
|
+
entityType: 'ASSET',
|
|
105
|
+
key: 'features',
|
|
106
|
+
translations: [{ language: 'en', title: 'Features', description: 'Available features' }],
|
|
107
|
+
allValues: ['GPS', 'Camera', 'Bluetooth', 'WiFi'],
|
|
108
|
+
multiSelect: true
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 3. Scope Types
|
|
113
|
+
|
|
114
|
+
| Scope | Description |
|
|
115
|
+
|-------|-------------|
|
|
116
|
+
| `ACCOUNT` | Values shared within a single account only |
|
|
117
|
+
| `ACCOUNT_WRITE_GLOBAL_READ` | Account can write, all accounts can read |
|
|
118
|
+
| `GLOBAL` | Values shared between all accounts with entity access |
|
|
119
|
+
|
|
120
|
+
## 4. Using Custom Fields in React Components
|
|
121
|
+
|
|
122
|
+
### Required Hooks
|
|
123
|
+
|
|
124
|
+
Use these hooks to interact with custom fields:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import {
|
|
128
|
+
useCustomFieldsValueAndDefinition,
|
|
129
|
+
useUpdateCustomFieldValues,
|
|
130
|
+
useCurrentUserSystemOfMeasurement
|
|
131
|
+
} from '@trackunit/react-core-contexts-host';
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Basic Implementation Pattern
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import React from 'react';
|
|
138
|
+
import { useForm } from 'react-hook-form';
|
|
139
|
+
import { Card, CardBody, CardHeader, CardFooter, Button, Spinner } from '@trackunit/react-components';
|
|
140
|
+
import { CustomField } from '@trackunit/react-core-contexts-host';
|
|
141
|
+
|
|
142
|
+
const CustomFieldsForm = ({ assetId }: { assetId: string }) => {
|
|
143
|
+
const { register, handleSubmit, formState, setValue, control } = useForm({
|
|
144
|
+
shouldUnregister: false,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const { systemOfMeasurement } = useCurrentUserSystemOfMeasurement();
|
|
148
|
+
|
|
149
|
+
const { fields, loading } = useCustomFieldsValueAndDefinition({
|
|
150
|
+
entityType: "ASSET",
|
|
151
|
+
entityId: assetId,
|
|
152
|
+
systemOfMeasurement: systemOfMeasurement ?? "SI",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const { updateCustomFields, inProgress } = useUpdateCustomFieldValues(
|
|
156
|
+
assetId,
|
|
157
|
+
"ASSET",
|
|
158
|
+
fields,
|
|
159
|
+
systemOfMeasurement ?? "SI"
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (loading) {
|
|
163
|
+
return <Spinner />;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<Card>
|
|
168
|
+
<CardHeader heading="Custom Fields" />
|
|
169
|
+
<CardBody>
|
|
170
|
+
{fields.map(field => {
|
|
171
|
+
const isEditable = field.valueEditable && (field.definition.uiEditable ?? true);
|
|
172
|
+
return (
|
|
173
|
+
<CustomField
|
|
174
|
+
key={field.definition.key}
|
|
175
|
+
control={control}
|
|
176
|
+
field={field}
|
|
177
|
+
formState={formState}
|
|
178
|
+
isEditable={isEditable}
|
|
179
|
+
register={register}
|
|
180
|
+
setValue={setValue}
|
|
181
|
+
unitPreference={systemOfMeasurement}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
})}
|
|
185
|
+
</CardBody>
|
|
186
|
+
<CardFooter>
|
|
187
|
+
<Button
|
|
188
|
+
loading={inProgress}
|
|
189
|
+
onClick={handleSubmit(updateCustomFields)}
|
|
190
|
+
>
|
|
191
|
+
{inProgress ? "Saving..." : "Save Changes"}
|
|
192
|
+
</Button>
|
|
193
|
+
</CardFooter>
|
|
194
|
+
</Card>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## 5. Best Practices
|
|
200
|
+
|
|
201
|
+
### Field Design
|
|
202
|
+
- **Use descriptive keys**: Choose clear, descriptive field keys that won't need changing
|
|
203
|
+
- **Add proper translations**: Always provide meaningful titles and descriptions
|
|
204
|
+
- **Set appropriate constraints**: Use min/max values, length limits, and patterns for validation
|
|
205
|
+
- **Consider data types**: Choose the most appropriate type for your data
|
|
206
|
+
|
|
207
|
+
### Performance
|
|
208
|
+
- **Batch updates**: Use `useUpdateCustomFieldValues` to update multiple fields at once
|
|
209
|
+
- **Handle loading states**: Always show loading indicators while fetching data
|
|
210
|
+
- **Error handling**: Implement proper error handling for failed updates
|
|
211
|
+
|
|
212
|
+
### Data Management
|
|
213
|
+
- **Plan for migration**: Consider existing data when changing field definitions
|
|
214
|
+
- **Use appropriate scopes**: Choose the right scope type for your data sharing needs
|
|
215
|
+
- **Validate user input**: Implement client-side validation matching your field constraints
|
|
216
|
+
|
|
217
|
+
## 6. Common Use Cases
|
|
218
|
+
|
|
219
|
+
### Fleet Management
|
|
220
|
+
```typescript
|
|
221
|
+
// Store vehicle-specific information
|
|
222
|
+
{
|
|
223
|
+
type: 'NUMBER',
|
|
224
|
+
entityType: 'ASSET',
|
|
225
|
+
key: 'fuelEfficiency',
|
|
226
|
+
translations: [{ language: 'en', title: 'Fuel Efficiency', description: 'Miles per gallon' }],
|
|
227
|
+
unitSi: 'L/100km',
|
|
228
|
+
unitUs: 'mpg'
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Asset Utilization
|
|
233
|
+
```typescript
|
|
234
|
+
// Track custom performance metrics
|
|
235
|
+
{
|
|
236
|
+
type: 'JSON',
|
|
237
|
+
entityType: 'ASSET',
|
|
238
|
+
key: 'performanceMetrics',
|
|
239
|
+
translations: [{ language: 'en', title: 'Performance Data', description: 'Custom metrics' }]
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Driver Safety
|
|
244
|
+
```typescript
|
|
245
|
+
// Store safety-related information
|
|
246
|
+
{
|
|
247
|
+
type: 'DROPDOWN',
|
|
248
|
+
entityType: 'ASSET',
|
|
249
|
+
key: 'safetyRating',
|
|
250
|
+
translations: [{ language: 'en', title: 'Safety Rating', description: 'Driver safety score' }],
|
|
251
|
+
allValues: ['Excellent', 'Good', 'Fair', 'Poor']
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## 7. Local Development
|
|
256
|
+
|
|
257
|
+
- Custom fields are available immediately in local development
|
|
258
|
+
- Changes to field definitions require app redeployment
|
|
259
|
+
- Test with various data types and edge cases
|
|
260
|
+
- Verify validation rules work as expected
|
|
261
|
+
|
|
262
|
+
## 8. Deployment Considerations
|
|
263
|
+
|
|
264
|
+
- **Field keys are immutable**: Cannot be changed after creation
|
|
265
|
+
- **Definition updates**: Changing field definitions affects all existing data
|
|
266
|
+
- **Migration planning**: Consider data migration when modifying existing fields
|
|
267
|
+
- **Approval process**: Custom field changes require app approval and marketplace submission
|
|
268
|
+
|
|
269
|
+
## 9. Error Handling
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
const CustomFieldsComponent = () => {
|
|
273
|
+
const { fields, loading, error } = useCustomFieldsValueAndDefinition({
|
|
274
|
+
entityType: "ASSET",
|
|
275
|
+
entityId: assetId,
|
|
276
|
+
systemOfMeasurement: systemOfMeasurement ?? "SI",
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (loading) return <Spinner />;
|
|
280
|
+
if (error) return <ErrorState message="Failed to load custom fields" />;
|
|
281
|
+
|
|
282
|
+
// Render fields...
|
|
283
|
+
};
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## 10. Integration with Trackunit Components
|
|
287
|
+
|
|
288
|
+
Always use the provided `CustomField` component for consistent UI:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import { CustomField } from '@trackunit/react-core-contexts-host';
|
|
292
|
+
|
|
293
|
+
// This handles all field types automatically
|
|
294
|
+
<CustomField
|
|
295
|
+
control={control}
|
|
296
|
+
field={field}
|
|
297
|
+
formState={formState}
|
|
298
|
+
isEditable={isEditable}
|
|
299
|
+
register={register}
|
|
300
|
+
setValue={setValue}
|
|
301
|
+
unitPreference={systemOfMeasurement}
|
|
302
|
+
/>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## References
|
|
306
|
+
|
|
307
|
+
- [Save Data from Your App](https://developers.trackunit.com/docs/save-data-from-your-app)
|
|
308
|
+
- [Defining a Custom Field](https://developers.trackunit.com/docs/defining-a-custom-field)
|
|
309
|
+
- [Using a Custom Field](https://developers.trackunit.com/docs/using-a-custom-field)
|
|
310
|
+
- [Local Development](https://developers.trackunit.com/docs/local-development)
|
|
311
|
+
- [Deploying a New Custom Field](https://developers.trackunit.com/docs/deploying-a-new-custom-field)
|
|
@@ -5,6 +5,23 @@ alwaysApply: true
|
|
|
5
5
|
---
|
|
6
6
|
You are currently in an IrisX App workspace. Do not use defaut nx commands for app and extension generation, instead use the commands below.
|
|
7
7
|
|
|
8
|
+
## Building Real Applications
|
|
9
|
+
|
|
10
|
+
You are building real applications that work with actual Trackunit data. Key principles:
|
|
11
|
+
|
|
12
|
+
- Never mock data unless explicitly requested by the user
|
|
13
|
+
- Always use Trackunit's GraphQL API to fetch real data
|
|
14
|
+
- If data is not available via GraphQL, ask the user how they want to obtain the data
|
|
15
|
+
- Only use mock data if the user explicitly requests it for testing purposes
|
|
16
|
+
|
|
17
|
+
When building any feature:
|
|
18
|
+
1. Identify what data you need
|
|
19
|
+
2. Check if it's available via Trackunit's GraphQL API
|
|
20
|
+
3. If available, use the GraphQL API (see GraphQL rules for implementation)
|
|
21
|
+
4. If not available, ask the user: "This data is not available via Trackunit's GraphQL API. How would you like to obtain [specific data description]?"
|
|
22
|
+
|
|
23
|
+
## IrisX App Development Guidelines
|
|
24
|
+
|
|
8
25
|
Key Points:
|
|
9
26
|
1. An IrisX App is a container that packages multiple extensions with marketplace metadata and app manifest
|
|
10
27
|
2. Extensions are React applications that integrate into specific points in Trackunit Manager
|
|
@@ -48,6 +65,16 @@ cspHeader: {
|
|
|
48
65
|
|
|
49
66
|
For more information: [Embedding IrisX Dashboards in Apps](mdc:https:/developers.trackunit.com/docs/analytics-dashboards-in-apps#troubleshooting)
|
|
50
67
|
|
|
68
|
+
|
|
69
|
+
## Runtime Hooks Available
|
|
70
|
+
|
|
71
|
+
IrisX Apps have access to runtime hooks from `@trackunit/react-core-hooks` that provide context-specific data:
|
|
72
|
+
- `useAssetRuntime()` for asset information in asset-scoped extensions
|
|
73
|
+
- `useCustomerRuntime()` for customer data
|
|
74
|
+
- `useEventRuntime()` for event details
|
|
75
|
+
- `useSiteRuntime()` for site information
|
|
76
|
+
- Plus navigation, user context, and utility hooks (see react-core-hooks rule for full list)
|
|
77
|
+
|
|
51
78
|
DO NOT use generic React/Node.js commands for project creation or management - always use the Trackunit IrisX App SDK commands.
|
|
52
79
|
|
|
53
80
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Use these rules whne building App SDK specific React Hook, Runtime Hooks
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
---
|
|
5
|
+
## React Core Hooks Library
|
|
6
|
+
|
|
7
|
+
Available hooks from `@trackunit/react-core-hooks` for IrisX App development.
|
|
8
|
+
|
|
9
|
+
### Runtime Hooks
|
|
10
|
+
|
|
11
|
+
**`useAssetRuntime()`** - Access asset information in asset-scoped extensions
|
|
12
|
+
```typescript
|
|
13
|
+
const { assetInfo, loading, error } = useAssetRuntime();
|
|
14
|
+
// assetInfo.assetId, assetInfo.name, etc.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**`useCustomerRuntime()`** - Access customer information
|
|
18
|
+
```typescript
|
|
19
|
+
const { customerInfo, loading, error } = useCustomerRuntime();
|
|
20
|
+
// customerInfo.customerId, customerInfo.name, etc.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**`useEventRuntime()`** - Access event information in event-scoped extensions
|
|
24
|
+
```typescript
|
|
25
|
+
const { eventInfo, loading, error } = useEventRuntime();
|
|
26
|
+
// eventInfo.eventId, eventInfo.type, etc.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**`useSiteRuntime()`** - Access site information in site-scoped extensions
|
|
30
|
+
```typescript
|
|
31
|
+
const { siteInfo, loading, error } = useSiteRuntime();
|
|
32
|
+
// siteInfo.siteId, siteInfo.name, etc.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**`useIrisAppName()`** - Get current app name
|
|
36
|
+
```typescript
|
|
37
|
+
const { appName, loading, error } = useIrisAppName();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**`useIrisAppId()`** - Get current app ID
|
|
41
|
+
```typescript
|
|
42
|
+
const { irisAppId, loading, error } = useIrisAppId();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Navigation & User Context
|
|
46
|
+
|
|
47
|
+
**`useNavigateInHost()`** - Navigate within Trackunit Manager
|
|
48
|
+
```typescript
|
|
49
|
+
const { navigateToAsset, navigateToSite } = useNavigateInHost();
|
|
50
|
+
navigateToAsset({ assetId: "123" });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**`useHasAccessTo()`** - Check user permissions
|
|
54
|
+
```typescript
|
|
55
|
+
const hasAccess = useHasAccessTo({ permission: "assets.read" });
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**`useCurrentUser()`** - Access current user information
|
|
59
|
+
```typescript
|
|
60
|
+
const { user } = useCurrentUser();
|
|
61
|
+
// user.id, user.email, etc.
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**`useCurrentUserLanguage()`** - Get/set user language preference
|
|
65
|
+
```typescript
|
|
66
|
+
const { language, setLanguage } = useCurrentUserLanguage();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**`useCurrentUserTimeZonePreference()`** - Get user timezone
|
|
70
|
+
```typescript
|
|
71
|
+
const { timeZone } = useCurrentUserTimeZonePreference();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**`useToken()`** - Access authentication token
|
|
75
|
+
```typescript
|
|
76
|
+
const { token } = useToken();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### UI & Interaction
|
|
80
|
+
|
|
81
|
+
**`useToast()`** - Show toast notifications
|
|
82
|
+
```typescript
|
|
83
|
+
const { showToast } = useToast();
|
|
84
|
+
showToast({ message: "Success!", type: "success" });
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**`useConfirmationDialog()`** - Show confirmation dialogs
|
|
88
|
+
```typescript
|
|
89
|
+
const { showConfirmationDialog } = useConfirmationDialog();
|
|
90
|
+
const confirmed = await showConfirmationDialog({ message: "Are you sure?" });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**`useModalDialogContext()`** - Manage modal dialogs
|
|
94
|
+
```typescript
|
|
95
|
+
const { openModal, closeModal } = useModalDialogContext();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**`useErrorHandler()`** - Handle errors consistently
|
|
99
|
+
```typescript
|
|
100
|
+
const { handleError } = useErrorHandler();
|
|
101
|
+
handleError(new Error("Something went wrong"));
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Data & State Management
|
|
105
|
+
|
|
106
|
+
**`useLocalStorage()`** - Persist state in localStorage
|
|
107
|
+
```typescript
|
|
108
|
+
const [value, setValue, reset] = useLocalStorage({
|
|
109
|
+
key: "myKey",
|
|
110
|
+
defaultState: { count: 0 }
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**`useLocalStorageReducer()`** - localStorage with reducer pattern
|
|
115
|
+
```typescript
|
|
116
|
+
const [state, dispatch] = useLocalStorageReducer({
|
|
117
|
+
key: "counterState",
|
|
118
|
+
defaultState: { count: 0 },
|
|
119
|
+
reducer: (state, action) => { /* reducer logic */ }
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**`useAssetSorting()`** - Asset sorting functionality
|
|
124
|
+
```typescript
|
|
125
|
+
const { sortField, sortDirection, setSorting } = useAssetSorting();
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**`useFilterBarContext()`** - Filter bar state management
|
|
129
|
+
```typescript
|
|
130
|
+
const { filters, setFilters, clearFilters } = useFilterBarContext();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**`useTimeRange()`** - Time range selection
|
|
134
|
+
```typescript
|
|
135
|
+
const { timeRange, setTimeRange } = useTimeRange();
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
### Widget Configuration
|
|
141
|
+
|
|
142
|
+
**`useWidgetConfig()`** - Access widget configuration (synchronous)
|
|
143
|
+
```typescript
|
|
144
|
+
const config = useWidgetConfig();
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**`useWidgetConfigAsync()`** - Access widget configuration (asynchronous)
|
|
148
|
+
```typescript
|
|
149
|
+
const { getConfig, setConfig } = useWidgetConfigAsync();
|
|
150
|
+
const config = await getConfig();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
All hooks require their respective providers to be set up in your app's component tree.
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Use these rules when building Widget Extensions for an Iris App.
|
|
3
|
+
globs:
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Widget Extensions
|
|
8
|
+
|
|
9
|
+
Widget extensions are specialized components that integrate into Trackunit Manager's dashboard systems, providing customizable UI elements for data visualization and user interaction.
|
|
10
|
+
|
|
11
|
+
## Key Concepts
|
|
12
|
+
|
|
13
|
+
1. **Widget Extensions** are React applications that render within widget containers on various pages
|
|
14
|
+
2. **Supported Locations** define where widgets can be placed in Trackunit Manager
|
|
15
|
+
3. **Widget Types** categorize widgets by their primary purpose and visual style
|
|
16
|
+
4. **Filter Support** enables widgets to respond to user-applied filters
|
|
17
|
+
5. **Configuration** allows widgets to store and manage their own settings
|
|
18
|
+
|
|
19
|
+
## Creating Widget Extensions
|
|
20
|
+
|
|
21
|
+
### Generate a new widget extension:
|
|
22
|
+
```bash
|
|
23
|
+
npx nx g @trackunit/iris-app:extend --name=[my-widget-name] --directory=[feature] --type=WIDGET_EXTENSION
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This creates:
|
|
27
|
+
- `extension-manifest.ts` - Widget configuration and metadata
|
|
28
|
+
- `src/app.tsx` - Main widget component
|
|
29
|
+
- `src/editDialog.tsx` - Configuration dialog (if `hasEditDialog: true`)
|
|
30
|
+
|
|
31
|
+
## Widget Extension Manifest
|
|
32
|
+
|
|
33
|
+
The manifest defines widget metadata, appearance, and behavior:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { WidgetExtensionManifest } from '@trackunit/iris-app-api';
|
|
37
|
+
|
|
38
|
+
const widgetExtensionManifest: WidgetExtensionManifest = {
|
|
39
|
+
id: "my-widget-id",
|
|
40
|
+
type: "WIDGET_EXTENSION",
|
|
41
|
+
sourceRoot: "libs/[feature]/[widget-name]/src",
|
|
42
|
+
|
|
43
|
+
widgetType: "CHART", // Required: Widget classification
|
|
44
|
+
|
|
45
|
+
size: {
|
|
46
|
+
default: {
|
|
47
|
+
width: 2, // 1-6 grid units
|
|
48
|
+
height: 2, // 1-6 grid units
|
|
49
|
+
},
|
|
50
|
+
allowFullWidth: false, // Optional: Allow full-width expansion
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
header: {
|
|
54
|
+
name: "My Widget", // Display name
|
|
55
|
+
image: { name: "ChartBar" }, // Icon (IconByName or IconByPath)
|
|
56
|
+
hasEditDialog: true, // Optional: Enable edit button
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
footer: { // Optional: Footer with link
|
|
60
|
+
linkDescription: "View details",
|
|
61
|
+
link: "/my-app/details",
|
|
62
|
+
poweredByImage: { path: "/assets/logo.svg" },
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
preview: { // Optional: Description for widget drawer
|
|
66
|
+
description: "Displays chart data for analysis",
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
supportedLocations: ["MY_HOME", "SITE_HOME"],
|
|
70
|
+
|
|
71
|
+
supportedFilters: { // Optional: Enable filter support
|
|
72
|
+
TIME_RANGE: { include: ["ALL"] },
|
|
73
|
+
ASSETS: { include: ["assetType", "brands", "models"] },
|
|
74
|
+
CUSTOMERS: { include: ["customerType", "criticality"] },
|
|
75
|
+
SITES: { include: ["siteType", "location"] },
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default widgetExtensionManifest;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Widget Types
|
|
83
|
+
|
|
84
|
+
Available widget types that determine visual presentation:
|
|
85
|
+
|
|
86
|
+
- **`"KPI"`** - Single metric displays (typically 1x1 size)
|
|
87
|
+
- **`"CHART"`** - Data visualization widgets (charts, graphs)
|
|
88
|
+
- **`"LIST"`** - Tabular or list-based data presentation
|
|
89
|
+
- **`"MAP"`** - Geographic or spatial data visualization
|
|
90
|
+
- **`"OTHER"`** - Custom or specialized widgets
|
|
91
|
+
|
|
92
|
+
## Supported Locations
|
|
93
|
+
|
|
94
|
+
Widgets can be placed in these locations:
|
|
95
|
+
|
|
96
|
+
- **`"MY_HOME"`** - User's personal dashboard
|
|
97
|
+
- **`"SITE_HOME"`** - Site-specific dashboards
|
|
98
|
+
|
|
99
|
+
## Widget Development
|
|
100
|
+
|
|
101
|
+
### Basic Widget Component
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { useWidgetConfig } from '@trackunit/react-core-hooks';
|
|
105
|
+
|
|
106
|
+
export const App = () => {
|
|
107
|
+
const {
|
|
108
|
+
data,
|
|
109
|
+
setData,
|
|
110
|
+
dataVersion,
|
|
111
|
+
title,
|
|
112
|
+
setTitle,
|
|
113
|
+
filters,
|
|
114
|
+
timeRange,
|
|
115
|
+
isEditMode,
|
|
116
|
+
openEditMode,
|
|
117
|
+
closeEditMode
|
|
118
|
+
} = useWidgetConfig();
|
|
119
|
+
|
|
120
|
+
if (isEditMode) {
|
|
121
|
+
return <EditDialog />;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div>
|
|
126
|
+
<h2>{title}</h2>
|
|
127
|
+
{/* Widget content */}
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Widget Configuration Hook
|
|
134
|
+
|
|
135
|
+
The `useWidgetConfig()` hook provides:
|
|
136
|
+
|
|
137
|
+
**Data Management:**
|
|
138
|
+
- `data: Record<string, unknown>` - Widget configuration data
|
|
139
|
+
- `setData: (data: Record<string, unknown>, version: number) => Promise<void>` - Update widget data
|
|
140
|
+
- `dataVersion: number` - Current data version for migrations
|
|
141
|
+
|
|
142
|
+
**Title Management:**
|
|
143
|
+
- `title: string` - Widget title
|
|
144
|
+
- `setTitle: (title: string) => Promise<void>` - Update widget title
|
|
145
|
+
|
|
146
|
+
**Context Information:**
|
|
147
|
+
- `filters: { assetsFilterBarValues, customersFilterBarValues, sitesFilterBarValues }` - Applied filters
|
|
148
|
+
- `timeRange: { startMsInEpoch, endMsInEpoch }` - Selected time range
|
|
149
|
+
- `isEditMode: boolean` - Whether widget is in edit mode
|
|
150
|
+
|
|
151
|
+
**Edit Mode Control:**
|
|
152
|
+
- `openEditMode: () => Promise<void>` - Enter edit mode
|
|
153
|
+
- `closeEditMode: () => Promise<void>` - Exit edit mode
|
|
154
|
+
|
|
155
|
+
### Widget with Edit Dialog
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { useWidgetConfig } from '@trackunit/react-core-hooks';
|
|
159
|
+
import { WidgetEditBody } from '@trackunit/react-widgets';
|
|
160
|
+
|
|
161
|
+
export const App = () => {
|
|
162
|
+
const { isEditMode, data, setData, closeEditMode } = useWidgetConfig();
|
|
163
|
+
|
|
164
|
+
if (isEditMode) {
|
|
165
|
+
return (
|
|
166
|
+
<WidgetEditBody
|
|
167
|
+
onSave={(newData) => {
|
|
168
|
+
setData(newData, 1);
|
|
169
|
+
closeEditMode();
|
|
170
|
+
}}
|
|
171
|
+
onCancel={closeEditMode}
|
|
172
|
+
initialData={data}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return <MainWidgetContent />;
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Filter Support
|
|
182
|
+
|
|
183
|
+
### Supported Filter Types
|
|
184
|
+
|
|
185
|
+
Configure which filters your widget responds to:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
supportedFilters: {
|
|
189
|
+
// Time range filtering
|
|
190
|
+
TIME_RANGE: { include: ["ALL"] },
|
|
191
|
+
|
|
192
|
+
// Asset filtering
|
|
193
|
+
ASSETS: {
|
|
194
|
+
include: [
|
|
195
|
+
"assetType", "brands", "models", "types",
|
|
196
|
+
"activity", "criticality", "lastSeen", "groups",
|
|
197
|
+
"siteIds", "customerIds", "ownerAccountIds",
|
|
198
|
+
// Add custom fields support
|
|
199
|
+
...allAssetFilterKeysWithCustomFields
|
|
200
|
+
]
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// Customer filtering
|
|
204
|
+
CUSTOMERS: {
|
|
205
|
+
include: [
|
|
206
|
+
"customerType", "criticality", "servicePlan",
|
|
207
|
+
...allCustomerFilterKeysWithCustomFields
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
// Site filtering
|
|
212
|
+
SITES: {
|
|
213
|
+
include: [
|
|
214
|
+
"siteType", "location", "area",
|
|
215
|
+
...allSiteFilterKeysWithCustomFields
|
|
216
|
+
]
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Using Filters in Widgets
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
const { filters, timeRange } = useWidgetConfig();
|
|
225
|
+
|
|
226
|
+
// Access filter values
|
|
227
|
+
const selectedAssets = filters.assetsFilterBarValues?.assetType;
|
|
228
|
+
const selectedCustomers = filters.customersFilterBarValues?.customerType;
|
|
229
|
+
const selectedSites = filters.sitesFilterBarValues?.siteType;
|
|
230
|
+
|
|
231
|
+
// Use time range
|
|
232
|
+
const startTime = timeRange?.startMsInEpoch;
|
|
233
|
+
const endTime = timeRange?.endMsInEpoch;
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Widget Lifecycle
|
|
237
|
+
|
|
238
|
+
### 1. Widget Creation
|
|
239
|
+
- Generated via `@trackunit/iris-app:extend` command
|
|
240
|
+
- Manifest defines widget metadata and capabilities
|
|
241
|
+
- React component handles rendering and user interaction
|
|
242
|
+
|
|
243
|
+
### 2. Widget Registration
|
|
244
|
+
- Widget appears in the widget drawer based on `supportedLocations`
|
|
245
|
+
- Users can add widgets to their dashboards
|
|
246
|
+
- Widget configuration is persisted automatically
|
|
247
|
+
|
|
248
|
+
### 3. Widget Runtime
|
|
249
|
+
- Widget receives context (filters, time range, etc.)
|
|
250
|
+
- Widget can store and retrieve configuration data
|
|
251
|
+
- Widget responds to user interactions and filter changes
|
|
252
|
+
|
|
253
|
+
## Best Practices
|
|
254
|
+
|
|
255
|
+
### Widget Design
|
|
256
|
+
1. **Responsive Layout** - Design for different grid sizes
|
|
257
|
+
2. **Loading States** - Show loading indicators for async operations
|
|
258
|
+
3. **Error Handling** - Gracefully handle data loading errors
|
|
259
|
+
4. **Empty States** - Provide meaningful empty state messages
|
|
260
|
+
|
|
261
|
+
### Data Management
|
|
262
|
+
1. **Use Real Data** - Always fetch from Trackunit GraphQL API
|
|
263
|
+
2. **Respect Filters** - Apply user-selected filters to your data queries
|
|
264
|
+
3. **Efficient Updates** - Only update widget data when necessary
|
|
265
|
+
4. **Version Control** - Use `dataVersion` for configuration migrations
|
|
266
|
+
|
|
267
|
+
### Configuration
|
|
268
|
+
1. **Meaningful Defaults** - Provide sensible default configurations
|
|
269
|
+
2. **Validation** - Validate user input in edit dialogs
|
|
270
|
+
3. **Persistence** - Use `setData()` to save configuration changes
|
|
271
|
+
4. **User Experience** - Keep configuration UI simple and intuitive
|
|
272
|
+
|
|
273
|
+
## Example: KPI Widget
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { useWidgetConfig } from '@trackunit/react-core-hooks';
|
|
277
|
+
import { useQuery } from '@apollo/client';
|
|
278
|
+
|
|
279
|
+
export const App = () => {
|
|
280
|
+
const { filters, timeRange, data } = useWidgetConfig();
|
|
281
|
+
|
|
282
|
+
const { data: kpiData, loading } = useQuery(MY_KPI_QUERY, {
|
|
283
|
+
variables: {
|
|
284
|
+
filters: {
|
|
285
|
+
assetIds: filters.assetsFilterBarValues?.assetIds,
|
|
286
|
+
timeRange: {
|
|
287
|
+
start: timeRange?.startMsInEpoch,
|
|
288
|
+
end: timeRange?.endMsInEpoch,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (loading) return <Spinner />;
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div className="kpi-widget">
|
|
298
|
+
<div className="kpi-value">{kpiData?.totalCount}</div>
|
|
299
|
+
<div className="kpi-label">Total Assets</div>
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
};
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Common Patterns
|
|
306
|
+
|
|
307
|
+
### Chart Widget
|
|
308
|
+
- Use `widgetType: "CHART"`
|
|
309
|
+
- Implement responsive chart sizing
|
|
310
|
+
- Support time range filtering
|
|
311
|
+
- Show data trends and comparisons
|
|
312
|
+
|
|
313
|
+
### List Widget
|
|
314
|
+
- Use `widgetType: "LIST"`
|
|
315
|
+
- Implement pagination for large datasets
|
|
316
|
+
- Support asset/customer filtering
|
|
317
|
+
- Provide links to detailed views
|
|
318
|
+
|
|
319
|
+
### Map Widget
|
|
320
|
+
- Use `widgetType: "MAP"`
|
|
321
|
+
- Set `allowFullWidth: true` for better visibility
|
|
322
|
+
- Support location-based filtering
|
|
323
|
+
- Show asset positions and movement
|
|
324
|
+
|
|
325
|
+
DO NOT use generic React widget libraries - always use the Trackunit IrisX App SDK widget patterns and components.
|