@fleetbase/ember-ui 0.3.10 → 0.3.12
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/SCHEDULING_COMPONENTS.md +228 -0
- package/addon/components/availability-editor.js +81 -0
- package/addon/components/chart.js +10 -10
- package/addon/components/click-to-reveal.hbs +1 -1
- package/addon/components/click-to-reveal.js +5 -23
- package/addon/components/dashboard/create.hbs +5 -3
- package/addon/components/dashboard/widget-panel.hbs +17 -7
- package/addon/components/dashboard/widget-panel.js +61 -13
- package/addon/components/dashboard.js +0 -1
- package/addon/components/filter/multi-input.hbs +16 -0
- package/addon/components/filter/multi-input.js +68 -0
- package/addon/components/filter/range.hbs +52 -0
- package/addon/components/filter/range.js +74 -0
- package/addon/components/filters-picker.js +13 -6
- package/addon/components/layout/resource/tabular.hbs +2 -0
- package/addon/components/layout/resource/tabular.js +28 -0
- package/addon/components/layout/sidebar/item.hbs +2 -2
- package/addon/components/lazy-engine-component.hbs +8 -0
- package/addon/components/lazy-engine-component.js +136 -0
- package/addon/components/load-engine.hbs +1 -0
- package/addon/components/load-engine.js +82 -0
- package/addon/components/modals/query-builder-computed-column-editor.hbs +80 -0
- package/addon/components/modals/query-builder-computed-column-editor.js +251 -0
- package/addon/components/pill.js +0 -1
- package/addon/components/query-builder/computed-columns.hbs +59 -0
- package/addon/components/query-builder/computed-columns.js +104 -0
- package/addon/components/query-builder.hbs +10 -0
- package/addon/components/query-builder.js +5 -0
- package/addon/components/registry-yield.hbs +7 -1
- package/addon/components/registry-yield.js +107 -15
- package/addon/components/report-builder.hbs +1 -0
- package/addon/components/report-builder.js +2 -1
- package/addon/components/schedule-calendar.hbs +37 -0
- package/addon/components/schedule-calendar.js +166 -0
- package/addon/components/schedule-item-card.hbs +39 -0
- package/addon/components/schedule-item-card.js +74 -0
- package/addon/components/tab-navigation.hbs +78 -38
- package/addon/components/tab-navigation.js +22 -9
- package/addon/components/table/cell/dropdown.hbs +2 -0
- package/addon/components/table/cell/dropdown.js +11 -0
- package/addon/components/table/cell/resizer.js +1 -1
- package/addon/components/table/cell.hbs +15 -2
- package/addon/components/table/cell.js +24 -0
- package/addon/components/table/foot.js +1 -1
- package/addon/components/table/td.hbs +2 -2
- package/addon/components/table/td.js +66 -1
- package/addon/components/table/th.hbs +15 -1
- package/addon/components/table/th.js +84 -0
- package/addon/components/table.hbs +22 -15
- package/addon/components/table.js +288 -0
- package/addon/helpers/avatar-url.js +9 -0
- package/addon/helpers/lazy-engine-component.js +116 -0
- package/addon/helpers/point-coordinates.js +10 -0
- package/addon/helpers/point-to-coordinates.js +32 -0
- package/addon/helpers/resolve-component.js +18 -0
- package/addon/helpers/set-object-kv.js +9 -0
- package/addon/helpers/unwrap-coordinates.js +249 -0
- package/addon/services/dashboard.js +39 -17
- package/addon/styles/components/badge.css +8 -0
- package/addon/styles/components/input.css +8 -0
- package/addon/styles/components/table.css +183 -0
- package/addon/styles/layout/next.css +245 -28
- package/app/components/filter/multi-input.js +1 -0
- package/app/components/filter/range.js +1 -0
- package/app/components/lazy-engine-component.js +1 -0
- package/app/components/load-engine.js +1 -0
- package/app/components/modals/query-builder-computed-column-editor.js +1 -0
- package/app/components/query-builder/computed-columns.js +1 -0
- package/app/components/schedule-calendar.js +1 -0
- package/app/helpers/avatar-url.js +1 -0
- package/app/helpers/lazy-engine-component.js +1 -0
- package/app/helpers/point-coordinates.js +1 -0
- package/app/helpers/point-to-coordinates.js +1 -0
- package/app/helpers/set-object-kv.js +1 -0
- package/app/helpers/unwrap-coordinates.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Scheduling Components for Ember UI
|
|
2
|
+
|
|
3
|
+
This package provides reusable scheduling components for the Fleetbase platform.
|
|
4
|
+
|
|
5
|
+
## Components
|
|
6
|
+
|
|
7
|
+
### ScheduleCalendar
|
|
8
|
+
|
|
9
|
+
A full-featured calendar component for displaying and managing schedules.
|
|
10
|
+
|
|
11
|
+
**Usage:**
|
|
12
|
+
```handlebars
|
|
13
|
+
<ScheduleCalendar
|
|
14
|
+
@resources={{this.drivers}}
|
|
15
|
+
@items={{this.scheduleItems}}
|
|
16
|
+
@view="resourceTimeline"
|
|
17
|
+
@onItemClick={{this.handleItemClick}}
|
|
18
|
+
@onItemDrop={{this.handleItemDrop}}
|
|
19
|
+
@onDateClick={{this.handleDateClick}}
|
|
20
|
+
>
|
|
21
|
+
<:item as |item|>
|
|
22
|
+
<div class="custom-event-content">
|
|
23
|
+
{{item.title}}
|
|
24
|
+
</div>
|
|
25
|
+
</:item>
|
|
26
|
+
</ScheduleCalendar>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Arguments:**
|
|
30
|
+
- `@resources` - Array of resources (e.g., drivers, vehicles)
|
|
31
|
+
- `@items` - Array of schedule items to display
|
|
32
|
+
- `@view` - Calendar view type (default: 'resourceTimeline')
|
|
33
|
+
- `@editable` - Enable drag-and-drop editing (default: true)
|
|
34
|
+
- `@onItemClick` - Callback when an item is clicked
|
|
35
|
+
- `@onItemDrop` - Callback when an item is dragged and dropped
|
|
36
|
+
- `@onDateClick` - Callback when a date is clicked
|
|
37
|
+
|
|
38
|
+
**Named Blocks:**
|
|
39
|
+
- `:item` - Custom rendering for schedule items
|
|
40
|
+
- `:header` - Custom header content
|
|
41
|
+
- `:footer` - Custom footer content
|
|
42
|
+
|
|
43
|
+
### ScheduleItemCard
|
|
44
|
+
|
|
45
|
+
Displays a schedule item in a card format.
|
|
46
|
+
|
|
47
|
+
**Usage:**
|
|
48
|
+
```handlebars
|
|
49
|
+
<ScheduleItemCard @item={{this.scheduleItem}} @onClick={{this.handleClick}}>
|
|
50
|
+
<:content as |ctx|>
|
|
51
|
+
<div class="custom-content">
|
|
52
|
+
{{ctx.item.title}}
|
|
53
|
+
</div>
|
|
54
|
+
</:content>
|
|
55
|
+
<:actions as |ctx|>
|
|
56
|
+
<button {{on "click" (fn this.edit ctx.item)}}>Edit</button>
|
|
57
|
+
<button {{on "click" (fn this.delete ctx.item)}}>Delete</button>
|
|
58
|
+
</:actions>
|
|
59
|
+
</ScheduleItemCard>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Arguments:**
|
|
63
|
+
- `@item` - The schedule item to display
|
|
64
|
+
- `@onClick` - Callback when the card is clicked
|
|
65
|
+
|
|
66
|
+
**Named Blocks:**
|
|
67
|
+
- `:content` - Custom content rendering
|
|
68
|
+
- `:actions` - Custom action buttons
|
|
69
|
+
|
|
70
|
+
### AvailabilityEditor
|
|
71
|
+
|
|
72
|
+
Allows users to set and manage availability windows.
|
|
73
|
+
|
|
74
|
+
**Usage:**
|
|
75
|
+
```handlebars
|
|
76
|
+
<AvailabilityEditor
|
|
77
|
+
@subjectType="driver"
|
|
78
|
+
@subjectUuid={{@driver.id}}
|
|
79
|
+
@onSave={{this.handleAvailabilitySave}}
|
|
80
|
+
/>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Arguments:**
|
|
84
|
+
- `@subjectType` - Type of the subject (e.g., 'driver', 'vehicle')
|
|
85
|
+
- `@subjectUuid` - UUID of the subject
|
|
86
|
+
- `@onSave` - Callback when availability is saved
|
|
87
|
+
|
|
88
|
+
## Models
|
|
89
|
+
|
|
90
|
+
### Schedule
|
|
91
|
+
|
|
92
|
+
Represents a master schedule.
|
|
93
|
+
|
|
94
|
+
**Attributes:**
|
|
95
|
+
- `name` - Schedule name
|
|
96
|
+
- `description` - Schedule description
|
|
97
|
+
- `start_date` - Start date
|
|
98
|
+
- `end_date` - End date
|
|
99
|
+
- `timezone` - Timezone
|
|
100
|
+
- `status` - Status (draft, published, active, paused, archived)
|
|
101
|
+
- `subject_uuid` - UUID of the subject
|
|
102
|
+
- `subject_type` - Type of the subject
|
|
103
|
+
|
|
104
|
+
**Relationships:**
|
|
105
|
+
- `items` - hasMany schedule-item
|
|
106
|
+
- `company` - belongsTo company
|
|
107
|
+
|
|
108
|
+
### ScheduleItem
|
|
109
|
+
|
|
110
|
+
Represents an individual scheduled item.
|
|
111
|
+
|
|
112
|
+
**Attributes:**
|
|
113
|
+
- `start_at` - Start datetime
|
|
114
|
+
- `end_at` - End datetime
|
|
115
|
+
- `duration` - Duration in minutes
|
|
116
|
+
- `status` - Status (pending, confirmed, in_progress, completed, cancelled, no_show)
|
|
117
|
+
- `assignee_uuid` - UUID of the assignee
|
|
118
|
+
- `assignee_type` - Type of the assignee
|
|
119
|
+
- `resource_uuid` - UUID of the resource
|
|
120
|
+
- `resource_type` - Type of the resource
|
|
121
|
+
|
|
122
|
+
**Relationships:**
|
|
123
|
+
- `schedule` - belongsTo schedule
|
|
124
|
+
|
|
125
|
+
### ScheduleTemplate
|
|
126
|
+
|
|
127
|
+
Represents a reusable schedule template.
|
|
128
|
+
|
|
129
|
+
**Attributes:**
|
|
130
|
+
- `name` - Template name
|
|
131
|
+
- `description` - Template description
|
|
132
|
+
- `start_time` - Start time
|
|
133
|
+
- `end_time` - End time
|
|
134
|
+
- `duration` - Duration in minutes
|
|
135
|
+
- `break_duration` - Break duration in minutes
|
|
136
|
+
- `rrule` - RFC 5545 recurrence rule
|
|
137
|
+
|
|
138
|
+
### ScheduleAvailability
|
|
139
|
+
|
|
140
|
+
Represents availability windows for resources.
|
|
141
|
+
|
|
142
|
+
**Attributes:**
|
|
143
|
+
- `subject_uuid` - UUID of the subject
|
|
144
|
+
- `subject_type` - Type of the subject
|
|
145
|
+
- `start_at` - Start datetime
|
|
146
|
+
- `end_at` - End datetime
|
|
147
|
+
- `is_available` - Availability flag
|
|
148
|
+
- `preference_level` - Preference strength (1-5)
|
|
149
|
+
- `reason` - Reason for unavailability
|
|
150
|
+
- `notes` - Additional notes
|
|
151
|
+
- `rrule` - RFC 5545 recurrence rule
|
|
152
|
+
|
|
153
|
+
### ScheduleConstraint
|
|
154
|
+
|
|
155
|
+
Represents scheduling constraints.
|
|
156
|
+
|
|
157
|
+
**Attributes:**
|
|
158
|
+
- `name` - Constraint name
|
|
159
|
+
- `description` - Constraint description
|
|
160
|
+
- `type` - Constraint type (hos, labor, business, capacity)
|
|
161
|
+
- `category` - Constraint category (compliance, optimization)
|
|
162
|
+
- `constraint_key` - Constraint key
|
|
163
|
+
- `constraint_value` - Constraint value
|
|
164
|
+
- `jurisdiction` - Jurisdiction (e.g., US-Federal, US-CA)
|
|
165
|
+
- `priority` - Priority (higher = more important)
|
|
166
|
+
- `is_active` - Active flag
|
|
167
|
+
|
|
168
|
+
## Service
|
|
169
|
+
|
|
170
|
+
### Scheduling Service
|
|
171
|
+
|
|
172
|
+
Provides methods for interacting with the scheduling API.
|
|
173
|
+
|
|
174
|
+
**Methods:**
|
|
175
|
+
|
|
176
|
+
- `loadSchedule(scheduleId)` - Load a schedule by ID
|
|
177
|
+
- `createSchedule(data)` - Create a new schedule
|
|
178
|
+
- `createScheduleItem(data)` - Create a new schedule item
|
|
179
|
+
- `updateScheduleItem(item, data)` - Update a schedule item
|
|
180
|
+
- `deleteScheduleItem(item)` - Delete a schedule item
|
|
181
|
+
- `getScheduleItemsForAssignee(assigneeType, assigneeUuid, filters)` - Get items for an assignee
|
|
182
|
+
- `checkAvailability(subjectType, subjectUuid, startAt, endAt)` - Check availability
|
|
183
|
+
- `setAvailability(data)` - Set availability
|
|
184
|
+
- `loadConstraints(subjectType, subjectUuid)` - Load constraints
|
|
185
|
+
- `validateScheduleItem(item)` - Validate an item against constraints
|
|
186
|
+
|
|
187
|
+
**Usage:**
|
|
188
|
+
```javascript
|
|
189
|
+
import { inject as service } from '@ember/service';
|
|
190
|
+
|
|
191
|
+
export default class MyComponent extends Component {
|
|
192
|
+
@service scheduling;
|
|
193
|
+
|
|
194
|
+
async loadDriverSchedule(driverId) {
|
|
195
|
+
const items = await this.scheduling.getScheduleItemsForAssignee.perform(
|
|
196
|
+
'driver',
|
|
197
|
+
driverId,
|
|
198
|
+
{ start_at: '2025-11-15', end_at: '2025-11-22' }
|
|
199
|
+
);
|
|
200
|
+
return items;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Styling
|
|
206
|
+
|
|
207
|
+
All components follow Fleetbase UI styling standards:
|
|
208
|
+
- Minimal padding and spacing
|
|
209
|
+
- Tailwind CSS framework
|
|
210
|
+
- Dark mode support
|
|
211
|
+
- Consistent with existing ember-ui components
|
|
212
|
+
|
|
213
|
+
## Dependencies
|
|
214
|
+
|
|
215
|
+
The ScheduleCalendar component requires FullCalendar to be installed:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
pnpm add @fullcalendar/core @fullcalendar/resource-timeline @fullcalendar/interaction
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Future Enhancements
|
|
222
|
+
|
|
223
|
+
- TimeOffForm component for time-off requests
|
|
224
|
+
- ScheduleTemplateBuilder component for creating templates
|
|
225
|
+
- Conflict detection UI
|
|
226
|
+
- RRULE editor for recurring patterns
|
|
227
|
+
- Multi-timezone support improvements
|
|
228
|
+
- Real-time updates via WebSockets
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { action } from '@ember/object';
|
|
4
|
+
import { inject as service } from '@ember/service';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AvailabilityEditor Component
|
|
8
|
+
*
|
|
9
|
+
* Allows users to set and manage availability windows for resources.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <AvailabilityEditor
|
|
13
|
+
* @subjectType="driver"
|
|
14
|
+
* @subjectUuid={{@driver.id}}
|
|
15
|
+
* @onSave={{this.handleAvailabilitySave}}
|
|
16
|
+
* />
|
|
17
|
+
*/
|
|
18
|
+
export default class AvailabilityEditorComponent extends Component {
|
|
19
|
+
@service scheduling;
|
|
20
|
+
@service notifications;
|
|
21
|
+
|
|
22
|
+
@tracked startAt = null;
|
|
23
|
+
@tracked endAt = null;
|
|
24
|
+
@tracked isAvailable = true;
|
|
25
|
+
@tracked preferenceLevel = 3;
|
|
26
|
+
@tracked reason = '';
|
|
27
|
+
@tracked notes = '';
|
|
28
|
+
@tracked rrule = '';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Save availability
|
|
32
|
+
*/
|
|
33
|
+
@action
|
|
34
|
+
async saveAvailability() {
|
|
35
|
+
try {
|
|
36
|
+
const data = {
|
|
37
|
+
subject_type: this.args.subjectType,
|
|
38
|
+
subject_uuid: this.args.subjectUuid,
|
|
39
|
+
start_at: this.startAt,
|
|
40
|
+
end_at: this.endAt,
|
|
41
|
+
is_available: this.isAvailable,
|
|
42
|
+
preference_level: this.preferenceLevel,
|
|
43
|
+
reason: this.reason,
|
|
44
|
+
notes: this.notes,
|
|
45
|
+
rrule: this.rrule,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const availability = await this.scheduling.setAvailability.perform(data);
|
|
49
|
+
|
|
50
|
+
if (this.args.onSave) {
|
|
51
|
+
this.args.onSave(availability);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.resetForm();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Failed to save availability:', error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reset form
|
|
62
|
+
*/
|
|
63
|
+
@action
|
|
64
|
+
resetForm() {
|
|
65
|
+
this.startAt = null;
|
|
66
|
+
this.endAt = null;
|
|
67
|
+
this.isAvailable = true;
|
|
68
|
+
this.preferenceLevel = 3;
|
|
69
|
+
this.reason = '';
|
|
70
|
+
this.notes = '';
|
|
71
|
+
this.rrule = '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Update field
|
|
76
|
+
*/
|
|
77
|
+
@action
|
|
78
|
+
updateField(field, value) {
|
|
79
|
+
this[field] = value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Component from '@glimmer/component';
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
3
|
import { action } from '@ember/object';
|
|
4
|
+
import { debug } from '@ember/debug';
|
|
4
5
|
import Chart, { _adapters } from 'chart.js/auto';
|
|
5
6
|
import {
|
|
6
7
|
parse,
|
|
@@ -46,13 +47,7 @@ import {
|
|
|
46
47
|
|
|
47
48
|
export default class ChartComponent extends Component {
|
|
48
49
|
@tracked chart;
|
|
49
|
-
@tracked isLoading;
|
|
50
|
-
|
|
51
|
-
constructor() {
|
|
52
|
-
super(...arguments);
|
|
53
|
-
|
|
54
|
-
this.isLoading = this.args.isLoading === true;
|
|
55
|
-
}
|
|
50
|
+
@tracked isLoading = this.args.isLoading;
|
|
56
51
|
|
|
57
52
|
@action async renderChart(ctx) {
|
|
58
53
|
const options = {
|
|
@@ -65,9 +60,14 @@ export default class ChartComponent extends Component {
|
|
|
65
60
|
};
|
|
66
61
|
|
|
67
62
|
if (typeof options.data.datasets === 'function') {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
try {
|
|
64
|
+
this.isLoading = true;
|
|
65
|
+
options.data.datasets = await options.data.datasets();
|
|
66
|
+
} catch (err) {
|
|
67
|
+
debug('Error loading Chart dataset: ' + err.message);
|
|
68
|
+
} finally {
|
|
69
|
+
this.isLoading = false;
|
|
70
|
+
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
this.useDateFns();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<div class="relative click-to-reveal {{if this.isVisible 'is-visible' 'is-blurred'}}" {{on "click" (fn this.copy @value)}} ...attributes>
|
|
1
|
+
<div class="relative click-to-reveal {{this.wrapperClass}} {{if this.isVisible 'is-visible' 'is-blurred'}}" {{on "click" (fn this.copy @value)}} ...attributes>
|
|
2
2
|
<span class="click-to-reveal--hidden-value">
|
|
3
3
|
{{#if (has-block)}}
|
|
4
4
|
{{yield}}
|
|
@@ -4,33 +4,11 @@ import { action, computed } from '@ember/object';
|
|
|
4
4
|
import { later } from '@ember/runloop';
|
|
5
5
|
|
|
6
6
|
export default class ClickToRevealComponent extends ClickToCopyComponent {
|
|
7
|
-
/**
|
|
8
|
-
* The visiblity state of the value
|
|
9
|
-
*
|
|
10
|
-
* @var {Boolean}
|
|
11
|
-
*/
|
|
12
7
|
@tracked isVisible = false;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The loading state of the reveal process
|
|
16
|
-
*
|
|
17
|
-
* @var {Boolean}
|
|
18
|
-
*/
|
|
19
8
|
@tracked isLoading = false;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The loading timing
|
|
23
|
-
*
|
|
24
|
-
* @var {Integer}
|
|
25
|
-
*/
|
|
26
9
|
@tracked timeout = 600;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* If click to copy should be enabled.
|
|
30
|
-
*
|
|
31
|
-
* @var {Boolean}
|
|
32
|
-
*/
|
|
33
10
|
@tracked clickToCopy = false;
|
|
11
|
+
@tracked wrapperClass = this.args.wrapperClass ?? '';
|
|
34
12
|
|
|
35
13
|
/**
|
|
36
14
|
* Setup the component
|
|
@@ -47,6 +25,10 @@ export default class ClickToRevealComponent extends ClickToCopyComponent {
|
|
|
47
25
|
if (column && column.cellComponentArgs) {
|
|
48
26
|
this.clickToCopy = column.cellComponentArgs.clickToCopy ?? false;
|
|
49
27
|
}
|
|
28
|
+
|
|
29
|
+
if (column && column.cellComponentArgs) {
|
|
30
|
+
this.wrapperClass = column.cellComponentArgs.wrapperClass ?? '';
|
|
31
|
+
}
|
|
50
32
|
}
|
|
51
33
|
|
|
52
34
|
/**
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<div class="fleetbase-dashboard-grid" ...attributes>
|
|
2
2
|
<GridStack @options={{this.gridOptions}} @onChange={{this.onChangeGrid}}>
|
|
3
3
|
{{#each @dashboard.widgets as |widget|}}
|
|
4
|
-
{{#let (resolve-component widget.component) as |
|
|
5
|
-
{{#if
|
|
4
|
+
{{#let (resolve-component widget.component) as |componentDefinition|}}
|
|
5
|
+
{{#if componentDefinition}}
|
|
6
6
|
<GridStackItem id={{widget.id}} @options={{spread-widget-options (hash id=widget.id options=widget.grid_options)}} class="relative">
|
|
7
|
-
{{
|
|
7
|
+
<LazyEngineComponent @component={{componentDefinition}} as |resolvedComponent|>
|
|
8
|
+
{{component resolvedComponent widget=widget options=widget.options}}
|
|
9
|
+
</LazyEngineComponent>
|
|
8
10
|
|
|
9
11
|
{{#if @isEdit}}
|
|
10
12
|
<div class="absolute top-2 right-2">
|
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
<Overlay @isOpen={{@isOpen}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width={{or this.width @width "400px"}}>
|
|
2
|
-
<Overlay::Header @title={{t "
|
|
2
|
+
<Overlay::Header @title={{t "dashboard-widget-panel.select-widgets"}} @hideStatusDot={{true}} @titleWrapperClass="leading-5 text-gray-800 dark:text-white">
|
|
3
3
|
<:actions>
|
|
4
4
|
<div class="flex flex-1 justify-end">
|
|
5
|
-
<Button @type="default" @icon="times" @helpText={{t "
|
|
5
|
+
<Button @type="default" @icon="times" @helpText={{t "dashboard-widget-panel.close-and-save"}} @onClick={{this.onPressClose}} />
|
|
6
6
|
</div>
|
|
7
7
|
</:actions>
|
|
8
8
|
</Overlay::Header>
|
|
9
9
|
|
|
10
|
-
<Overlay::Body @wrapperClass="new-service-rate-overlay-body px-
|
|
10
|
+
<Overlay::Body @wrapperClass="new-service-rate-overlay-body px-1 space-y-4 pt-2">
|
|
11
|
+
<div>
|
|
12
|
+
<Input
|
|
13
|
+
@type="text"
|
|
14
|
+
@value={{this.searchQuery}}
|
|
15
|
+
placeholder={{or (t "dashboard-widget-panel.filter-widgets") "Filter widgets by keyword"}}
|
|
16
|
+
class="w-full form-input form-input-sm"
|
|
17
|
+
{{on "input" this.updateSearchQuery}}
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
11
21
|
<div class="grid grid-cols-1 gap-4 text-xs dark:text-gray-100">
|
|
12
22
|
{{#each this.availableWidgets as |widget|}}
|
|
13
23
|
<div
|
|
14
24
|
class="rounded-lg border border-gray-300 bg-white dark:border-gray-700 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-300 ease-in-out shadow-md px-4 py-2 cursor-pointer"
|
|
15
|
-
{{on "click" (
|
|
25
|
+
{{on "click" (perform this.addWidgetToDashboard widget)}}
|
|
16
26
|
>
|
|
17
|
-
<div class="flex flex-row items-center leading-
|
|
18
|
-
<div class="w-
|
|
27
|
+
<div class="flex flex-row items-center leading-7 mb-2">
|
|
28
|
+
<div class="w-7 flex items-center justify-start">
|
|
19
29
|
<FaIcon @icon={{widget.icon}} class="text-lg text-gray-600 dark:text-gray-300" />
|
|
20
30
|
</div>
|
|
21
31
|
<p class="text-sm truncate font-semibold dark:text-gray-100 text-gray-800">
|
|
22
|
-
{{t "
|
|
32
|
+
{{t "dashboard-widget-panel.widget-name" widgetName=widget.name}}
|
|
23
33
|
</p>
|
|
24
34
|
</div>
|
|
25
35
|
<div>
|
|
@@ -2,23 +2,72 @@ import Component from '@glimmer/component';
|
|
|
2
2
|
import { tracked } from '@glimmer/tracking';
|
|
3
3
|
import { inject as service } from '@ember/service';
|
|
4
4
|
import { action } from '@ember/object';
|
|
5
|
+
import { task } from 'ember-concurrency';
|
|
6
|
+
import { ExtensionComponent } from '@fleetbase/ember-core/contracts';
|
|
5
7
|
|
|
6
8
|
export default class DashboardWidgetPanelComponent extends Component {
|
|
7
|
-
@service
|
|
8
|
-
@
|
|
9
|
+
@service('universe/widget-service') widgetService;
|
|
10
|
+
@service notifications;
|
|
11
|
+
@tracked defaultDashboardId = this.args.defaultDashboardId ?? 'dashboard';
|
|
9
12
|
@tracked dashboard;
|
|
10
13
|
@tracked isOpen = true;
|
|
11
|
-
@
|
|
14
|
+
@tracked searchQuery = '';
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Constructs the component and applies initial state.
|
|
15
18
|
*/
|
|
16
19
|
constructor(owner, { dashboard, defaultDashboardId = 'dashboard' }) {
|
|
17
20
|
super(...arguments);
|
|
18
|
-
this.
|
|
21
|
+
this.defaultDashboardId = defaultDashboardId;
|
|
19
22
|
this.dashboard = dashboard;
|
|
20
23
|
}
|
|
21
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Gets available widgets for the dashboard.
|
|
27
|
+
* Computed as a getter to ensure reactivity when widgets are registered.
|
|
28
|
+
* @returns {Array} Available widgets
|
|
29
|
+
*/
|
|
30
|
+
get availableWidgets() {
|
|
31
|
+
const dashboardId = this.args.defaultDashboardId || this.defaultDashboardId || 'dashboard';
|
|
32
|
+
const widgets = this.widgetService.getWidgets(dashboardId);
|
|
33
|
+
|
|
34
|
+
// Filter widgets by search query
|
|
35
|
+
if (this.searchQuery && this.searchQuery.trim()) {
|
|
36
|
+
const query = this.searchQuery.toLowerCase().trim();
|
|
37
|
+
return widgets.filter((widget) => {
|
|
38
|
+
const name = (widget.name || '').toLowerCase();
|
|
39
|
+
const description = (widget.description || '').toLowerCase();
|
|
40
|
+
return name.includes(query) || description.includes(query);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return widgets;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Adds a new available widget to the current dashboard.
|
|
49
|
+
*
|
|
50
|
+
* @param {Component|Widget|string} widget
|
|
51
|
+
* @memberof DashboardWidgetPanelComponent
|
|
52
|
+
*/
|
|
53
|
+
@task *addWidgetToDashboard(widget) {
|
|
54
|
+
// If widget is a component definition/class
|
|
55
|
+
if (typeof widget.component === 'function') {
|
|
56
|
+
widget.component = widget.component.name;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If using extension component definition
|
|
60
|
+
if (widget.component instanceof ExtensionComponent) {
|
|
61
|
+
widget.component = widget.component.toString();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
yield this.dashboard.addWidget(widget);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
this.notifications.serverError(err);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
22
71
|
/**
|
|
23
72
|
* Sets the overlay context.
|
|
24
73
|
*
|
|
@@ -33,15 +82,14 @@ export default class DashboardWidgetPanelComponent extends Component {
|
|
|
33
82
|
}
|
|
34
83
|
}
|
|
35
84
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
});
|
|
85
|
+
/**
|
|
86
|
+
* Updates the search query
|
|
87
|
+
*
|
|
88
|
+
* @action
|
|
89
|
+
* @param {Event} event
|
|
90
|
+
*/
|
|
91
|
+
@action updateSearchQuery(event) {
|
|
92
|
+
this.searchQuery = event.target.value;
|
|
45
93
|
}
|
|
46
94
|
|
|
47
95
|
/**
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<div class="filter-multi-input" ...attributes>
|
|
2
|
+
<TagInput
|
|
3
|
+
class="form-input form-input-sm flex-1"
|
|
4
|
+
@placeholder={{or @placeholder "Add values..."}}
|
|
5
|
+
@allowSpacesInTags={{or @allowSpacesInTags true}}
|
|
6
|
+
@tags={{this.tags}}
|
|
7
|
+
@addTag={{this.addTag}}
|
|
8
|
+
@removeTagAtIndex={{this.removeTag}}
|
|
9
|
+
as |tag|
|
|
10
|
+
>
|
|
11
|
+
{{tag}}
|
|
12
|
+
</TagInput>
|
|
13
|
+
<button type="button" class="clear-button" disabled={{not this.tags.length}} alt="Clear" {{on "click" this.clear}}>
|
|
14
|
+
<FaIcon @icon="times" />
|
|
15
|
+
</button>
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import Component from '@glimmer/component';
|
|
2
|
+
import { tracked } from '@glimmer/tracking';
|
|
3
|
+
import { action } from '@ember/object';
|
|
4
|
+
import { isArray } from '@ember/array';
|
|
5
|
+
|
|
6
|
+
export default class FilterMultiInputComponent extends Component {
|
|
7
|
+
@tracked tags = [];
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.tags = this.parseValue(this.args.value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
parseValue(value) {
|
|
15
|
+
if (isArray(value)) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof value === 'string' && value.includes(',')) {
|
|
20
|
+
return value
|
|
21
|
+
.split(',')
|
|
22
|
+
.map((v) => v.trim())
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (value) {
|
|
27
|
+
return [value];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
buildValue() {
|
|
34
|
+
return this.tags.join(',');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@action addTag(tag) {
|
|
38
|
+
const { onChange, filter } = this.args;
|
|
39
|
+
|
|
40
|
+
this.tags.pushObject(tag);
|
|
41
|
+
const value = this.buildValue();
|
|
42
|
+
|
|
43
|
+
if (typeof onChange === 'function') {
|
|
44
|
+
onChange(filter, value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@action removeTag(index) {
|
|
49
|
+
const { onChange, filter } = this.args;
|
|
50
|
+
|
|
51
|
+
this.tags.removeAt(index);
|
|
52
|
+
const value = this.buildValue();
|
|
53
|
+
|
|
54
|
+
if (typeof onChange === 'function') {
|
|
55
|
+
onChange(filter, value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@action clear() {
|
|
60
|
+
const { onClear, filter } = this.args;
|
|
61
|
+
|
|
62
|
+
this.tags = [];
|
|
63
|
+
|
|
64
|
+
if (typeof onClear === 'function') {
|
|
65
|
+
onClear(filter);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|