@fleetbase/fleetops-engine 0.6.54 → 0.6.55

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.
Files changed (127) hide show
  1. package/addon/components/admin/routing-settings.hbs +2 -1
  2. package/addon/components/cell/attached-vehicle.hbs +10 -0
  3. package/addon/components/cell/attached-vehicle.js +58 -0
  4. package/addon/components/cell/device-identity.hbs +24 -0
  5. package/addon/components/cell/device-identity.js +129 -0
  6. package/addon/components/cell/driver-identity.hbs +33 -0
  7. package/addon/components/cell/driver-identity.js +143 -0
  8. package/addon/components/cell/equipment-identity.hbs +5 -0
  9. package/addon/components/cell/equipment-identity.js +48 -0
  10. package/addon/components/cell/part-identity.hbs +5 -0
  11. package/addon/components/cell/part-identity.js +63 -0
  12. package/addon/components/cell/telematic-device.hbs +38 -0
  13. package/addon/components/cell/telematic-device.js +44 -0
  14. package/addon/components/cell/telematic-provider.hbs +33 -16
  15. package/addon/components/cell/telematic-provider.js +50 -8
  16. package/addon/components/cell/vehicle-identity.hbs +33 -0
  17. package/addon/components/cell/vehicle-identity.js +138 -0
  18. package/addon/components/device/form.hbs +2 -2
  19. package/addon/components/device/form.js +7 -0
  20. package/addon/components/device/panel-header.hbs +45 -20
  21. package/addon/components/device/panel-header.js +49 -1
  22. package/addon/components/device/panel-tabs/events.hbs +30 -0
  23. package/addon/components/device/panel-tabs/events.js +128 -0
  24. package/addon/components/device/panel-tabs/sensors.hbs +30 -0
  25. package/addon/components/device/panel-tabs/sensors.js +93 -0
  26. package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
  27. package/addon/components/device/panel-tabs/vehicle.js +90 -0
  28. package/addon/components/equipment/form.js +8 -1
  29. package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
  30. package/addon/components/layout/fleet-ops-sidebar.js +8 -0
  31. package/addon/components/map/drawer/device-event-listing.js +2 -0
  32. package/addon/components/map/drawer/driver-listing.js +2 -0
  33. package/addon/components/map/drawer/place-listing.js +2 -0
  34. package/addon/components/map/drawer/position-listing.hbs +11 -6
  35. package/addon/components/map/drawer/position-listing.js +17 -4
  36. package/addon/components/map/drawer/vehicle-listing.js +2 -0
  37. package/addon/components/sensor/panel-header.hbs +47 -15
  38. package/addon/components/sensor/panel-header.js +59 -1
  39. package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
  40. package/addon/components/table/cell/dropdown/action-item.js +62 -0
  41. package/addon/components/table/cell/dropdown.hbs +28 -0
  42. package/addon/components/table/cell/dropdown.js +91 -0
  43. package/addon/components/vendor/panel-header.hbs +82 -0
  44. package/addon/components/vendor/panel-header.js +48 -0
  45. package/addon/controllers/connectivity/devices/index/details/vehicle.js +49 -1
  46. package/addon/controllers/connectivity/devices/index.js +144 -129
  47. package/addon/controllers/connectivity/events/details.js +3 -0
  48. package/addon/controllers/connectivity/events/index.js +187 -11
  49. package/addon/controllers/connectivity/sensors/index.js +156 -14
  50. package/addon/controllers/connectivity/telematics/details/devices.js +126 -134
  51. package/addon/controllers/maintenance/equipment/index.js +1 -2
  52. package/addon/controllers/maintenance/parts/index.js +1 -2
  53. package/addon/controllers/management/drivers/index.js +82 -42
  54. package/addon/controllers/management/fuel-reports/index.js +63 -4
  55. package/addon/controllers/management/issues/index.js +63 -4
  56. package/addon/controllers/management/vehicles/index.js +33 -6
  57. package/addon/routes/connectivity/devices/index/details/vehicle.js +24 -2
  58. package/addon/routes/connectivity/devices/index.js +7 -0
  59. package/addon/routes/connectivity/events/details.js +3 -0
  60. package/addon/routes/connectivity/events/index.js +6 -2
  61. package/addon/routes/connectivity/sensors/index.js +7 -2
  62. package/addon/routes/connectivity/telematics/details/devices.js +2 -0
  63. package/addon/services/device-actions.js +30 -0
  64. package/addon/services/device-event-actions.js +1 -1
  65. package/addon/services/equipment-actions.js +1 -0
  66. package/addon/services/sensor-actions.js +3 -0
  67. package/addon/services/service-rate-actions.js +39 -1
  68. package/addon/services/vehicle-actions.js +24 -12
  69. package/addon/services/vendor-actions.js +3 -0
  70. package/addon/styles/fleetops-engine.css +8 -0
  71. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +64 -1
  72. package/addon/templates/management/vendors/index/details.hbs +2 -2
  73. package/addon/templates/settings/notifications.hbs +2 -1
  74. package/addon/templates/settings/routing.hbs +1 -0
  75. package/addon/utils/device-table-columns.js +288 -0
  76. package/addon/utils/identity-cell-resource.js +20 -0
  77. package/addon/utils/map-drawer-dropdown-position.js +47 -0
  78. package/addon/utils/trackable-option.js +93 -0
  79. package/app/components/cell/attached-vehicle.js +1 -0
  80. package/app/components/cell/device-identity.js +1 -0
  81. package/app/components/cell/driver-identity.js +1 -0
  82. package/app/components/cell/equipment-identity.js +1 -0
  83. package/app/components/cell/part-identity.js +1 -0
  84. package/app/components/cell/telematic-device.js +1 -0
  85. package/app/components/cell/vehicle-identity.js +1 -0
  86. package/app/components/device/panel-tabs/events.hbs +30 -0
  87. package/app/components/device/panel-tabs/events.js +128 -0
  88. package/app/components/device/panel-tabs/sensors.hbs +30 -0
  89. package/app/components/device/panel-tabs/sensors.js +93 -0
  90. package/app/components/device/panel-tabs/vehicle.hbs +68 -0
  91. package/app/components/device/panel-tabs/vehicle.js +90 -0
  92. package/app/components/table/cell/dropdown/action-item.js +1 -0
  93. package/app/components/table/cell/dropdown.js +1 -0
  94. package/app/components/vendor/panel-header.js +1 -0
  95. package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
  96. package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
  97. package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
  98. package/app/utils/map-drawer-dropdown-position.js +1 -0
  99. package/app/utils/trackable-option.js +1 -0
  100. package/composer.json +1 -1
  101. package/extension.json +1 -1
  102. package/package.json +4 -4
  103. package/server/src/Console/Commands/SyncTelematics.php +4 -4
  104. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -1
  105. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +1 -1
  106. package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
  107. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
  108. package/server/src/Http/Filter/DeviceEventFilter.php +21 -0
  109. package/server/src/Http/Filter/DeviceFilter.php +24 -1
  110. package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
  111. package/server/src/Models/Device.php +2 -0
  112. package/server/src/Models/DeviceEvent.php +81 -0
  113. package/server/src/Models/Equipment.php +55 -0
  114. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +1 -0
  115. package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
  116. package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
  117. package/server/src/Support/Telematics/Providers/SafeeProvider.php +26 -2
  118. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
  119. package/server/src/routes.php +0 -1
  120. package/server/tests/DeviceFilterTest.php +9 -0
  121. package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
  122. package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
  123. package/server/tests/ServiceRateRouteValidationTest.php +101 -0
  124. package/server/tests/TelematicsHardeningTest.php +238 -7
  125. package/addon/controllers/connectivity/events/index/details.js +0 -3
  126. package/addon/routes/connectivity/events/index/details.js +0 -3
  127. /package/addon/templates/connectivity/events/{index/details.hbs → details.hbs} +0 -0
@@ -1,16 +1,33 @@
1
- <div ...attributes>
2
- <button
3
- type="button"
4
- disabled={{and @column.permission (cannot @column.permission)}}
5
- {{on "click" this.onClick}}
6
- class="flex min-w-0 max-w-md items-start gap-2 py-1 text-left"
7
- >
8
- <div class="pt-0.5">
9
- <Image src={{this.icon}} class="mr-3 h-8 w-8 flex-shrink-0 rounded-md" />
10
- </div>
11
- <div class="min-w-0 text-left">
12
- <div class="truncate text-sm font-semibold text-gray-900 hover:text-blue-600 dark:text-gray-100 dark:hover:text-blue-300">{{n-a this.name}}</div>
13
- <div class="mt-0.5 line-clamp-2 whitespace-normal text-left text-xs leading-4 text-gray-500 dark:text-gray-400">{{n-a this.description}}</div>
14
- </div>
15
- </button>
16
- </div>
1
+ {{#if this.telematic}}
2
+ {{#if this.compact}}
3
+ <button
4
+ type="button"
5
+ disabled={{and @column.permission (cannot @column.permission)}}
6
+ {{on "click" this.onClick}}
7
+ class="group flex w-full min-w-0 cursor-pointer items-center gap-2 py-0.5 text-left"
8
+ data-test-telematic-provider-compact
9
+ ...attributes
10
+ >
11
+ <Image src={{this.icon}} @fallbackSrc="/engines-dist/images/telematics/providers/default.webp" class="h-5 w-5 flex-shrink-0 rounded-md border border-gray-200 object-cover shadow-sm dark:border-gray-700" />
12
+ <div class="min-w-0 truncate text-sm leading-4 text-gray-900 group-hover:text-blue-600 dark:text-gray-200 dark:group-hover:text-blue-300">{{n-a this.name}}</div>
13
+ </button>
14
+ {{else}}
15
+ <button
16
+ type="button"
17
+ disabled={{and @column.permission (cannot @column.permission)}}
18
+ {{on "click" this.onClick}}
19
+ class="group flex w-full min-w-0 max-w-md cursor-pointer items-start gap-2 py-1 text-left"
20
+ ...attributes
21
+ >
22
+ <div class="pt-0.5">
23
+ <Image src={{this.icon}} @fallbackSrc="/engines-dist/images/telematics/providers/default.webp" class="mr-0.5 h-8 w-8 flex-shrink-0 rounded-md border border-gray-200 object-cover shadow-sm dark:border-gray-700" />
24
+ </div>
25
+ <div class="min-w-0 text-left">
26
+ <div class="truncate text-sm text-gray-900 group-hover:text-blue-600 dark:text-gray-100 dark:group-hover:text-blue-300">{{n-a this.name}}</div>
27
+ <div class="mt-0.5 max-w-[225px] truncate whitespace-nowrap text-left text-xs leading-4 text-gray-500 dark:text-gray-400">{{n-a this.description}}</div>
28
+ </div>
29
+ </button>
30
+ {{/if}}
31
+ {{else}}
32
+ <div class="py-1 text-xs text-gray-400 dark:text-gray-500" data-test-telematic-provider-empty-text ...attributes>{{this.emptyText}}</div>
33
+ {{/if}}
@@ -1,36 +1,78 @@
1
1
  import Component from '@glimmer/component';
2
- import { action } from '@ember/object';
2
+ import { action, get } from '@ember/object';
3
+
4
+ const DEFAULT_PROVIDER_ICON = '/engines-dist/images/telematics/providers/default.webp';
3
5
 
4
6
  export default class CellTelematicProviderComponent extends Component {
7
+ get telematic() {
8
+ const resourcePath = this.args.column?.resourcePath;
9
+
10
+ if (typeof resourcePath === 'function') {
11
+ const resource = resourcePath(this.args.row, this.args.value, this.args.column);
12
+
13
+ return resource ?? null;
14
+ }
15
+
16
+ if (typeof resourcePath === 'string') {
17
+ const resource = get(this.args.row, resourcePath);
18
+
19
+ return resource ?? null;
20
+ }
21
+
22
+ if (this.args.row?.telematic) {
23
+ return this.args.row.telematic;
24
+ }
25
+
26
+ if (this.args.row?.telematic_uuid || this.args.row?.telematic_name) {
27
+ return {
28
+ id: this.args.row.telematic_uuid,
29
+ name: this.args.row.telematic_name,
30
+ provider: this.args.row.provider,
31
+ provider_descriptor: this.args.row.provider_descriptor,
32
+ };
33
+ }
34
+
35
+ return this.args.row;
36
+ }
37
+
38
+ get emptyText() {
39
+ return this.args.column?.emptyText ?? '-';
40
+ }
41
+
42
+ get compact() {
43
+ return this.args.column?.compact ?? false;
44
+ }
45
+
5
46
  get descriptor() {
6
- return this.args.row?.provider_descriptor ?? {};
47
+ return this.telematic?.provider_descriptor ?? this.args.row?.provider_descriptor ?? {};
7
48
  }
8
49
 
9
50
  get name() {
10
- return this.args.row?.name ?? this.descriptor.label ?? this.args.row?.provider;
51
+ return this.telematic?.name ?? this.descriptor.label ?? this.telematic?.provider ?? this.args.row?.provider ?? this.args.row?.telematic_name;
11
52
  }
12
53
 
13
54
  get description() {
14
- return this.descriptor.description ?? this.args.row?.provider;
55
+ return this.descriptor.description ?? this.telematic?.provider ?? this.args.row?.provider;
15
56
  }
16
57
 
17
58
  get icon() {
18
- return this.descriptor.icon;
59
+ return this.descriptor.icon ?? DEFAULT_PROVIDER_ICON;
19
60
  }
20
61
 
21
62
  @action onClick(event) {
22
63
  const { row, column, onClick } = this.args;
64
+ const resource = this.telematic ?? row;
23
65
 
24
66
  if (typeof onClick === 'function') {
25
- onClick(row, event);
67
+ onClick(resource, event);
26
68
  }
27
69
 
28
70
  if (typeof column?.action === 'function') {
29
- column.action(row, event);
71
+ column.action(resource, event);
30
72
  }
31
73
 
32
74
  if (typeof column?.onClick === 'function') {
33
- column.onClick(row, event);
75
+ column.onClick(resource, event);
34
76
  }
35
77
  }
36
78
  }
@@ -0,0 +1,33 @@
1
+ {{#if this.resource}}
2
+ {{#if this.compact}}
3
+ <button
4
+ type="button"
5
+ disabled={{and @column.permission (cannot @column.permission)}}
6
+ {{on "click" this.onClick}}
7
+ class="group flex w-full min-w-0 cursor-pointer items-center gap-2 py-0.5 text-left"
8
+ data-test-vehicle-identity-compact
9
+ ...attributes
10
+ >
11
+ <div class="relative flex h-5 w-5 flex-shrink-0 items-center justify-center">
12
+ <Image src={{this.mediaUrl}} @fallbackSrc={{this.fallbackImage}} class="h-5 w-5 rounded-md border border-gray-200 object-cover shadow-sm dark:border-gray-700" />
13
+ {{#if this.hasCompactStatusDot}}
14
+ <FaIcon @icon="circle" @size="2xs" class="absolute left-0 top-0 -ml-1 -mt-1 {{this.compactStatusDotClass}}" data-test-resource-identity-status-dot />
15
+ {{/if}}
16
+ </div>
17
+ <div class="min-w-0 truncate text-sm leading-4 text-gray-900 group-hover:text-blue-600 dark:text-gray-200 dark:group-hover:text-blue-300">{{n-a this.label}}</div>
18
+ {{#if this.assignedDriverLabel}}
19
+ <span
20
+ data-test-resource-identity-meta-badge
21
+ class="inline-flex min-w-0 max-w-[12rem] flex-shrink items-center rounded-lg bg-gray-200 px-1 py-0 text-[10px] leading-4 text-gray-700 shadow-sm dark:bg-gray-800 dark:text-gray-300"
22
+ >
23
+ <FaIcon @icon="user" @prefix="fas" @size="xs" class="mr-1 flex-shrink-0" />
24
+ <span class="truncate">{{n-a this.assignedDriverLabel}}</span>
25
+ </span>
26
+ {{/if}}
27
+ </button>
28
+ {{else}}
29
+ <Table::Cell::ResourceIdentity @row={{this.resource}} @value={{@value}} @column={{this.column}} @onClick={{@onClick}} ...attributes />
30
+ {{/if}}
31
+ {{else}}
32
+ <div class="py-1 text-xs text-gray-400 dark:text-gray-500" data-test-identity-empty-text ...attributes>{{this.emptyText}}</div>
33
+ {{/if}}
@@ -0,0 +1,138 @@
1
+ import Component from '@glimmer/component';
2
+ import { action, get } from '@ember/object';
3
+ import config from 'ember-get-config';
4
+ import { resolveIdentityCellResource } from '../../utils/identity-cell-resource';
5
+
6
+ const DEFAULT_STATUS_TONES = {
7
+ available: 'text-green-500',
8
+ active: 'text-green-500',
9
+ in_service: 'text-green-500',
10
+ maintenance: 'text-yellow-500',
11
+ unavailable: 'text-gray-400',
12
+ inactive: 'text-gray-400',
13
+ out_of_service: 'text-red-500',
14
+ };
15
+
16
+ export default class CellVehicleIdentityComponent extends Component {
17
+ get resource() {
18
+ return resolveIdentityCellResource(this.args);
19
+ }
20
+
21
+ get emptyText() {
22
+ return this.args.column?.emptyText ?? '-';
23
+ }
24
+
25
+ get compact() {
26
+ return this.args.column?.compact ?? false;
27
+ }
28
+
29
+ get showStatus() {
30
+ return this.args.column?.showStatus ?? true;
31
+ }
32
+
33
+ get label() {
34
+ const vehicle = this.resource;
35
+
36
+ return get(vehicle, 'displayName') ?? get(vehicle, 'display_name') ?? get(vehicle, 'name');
37
+ }
38
+
39
+ get mediaUrl() {
40
+ return get(this.resource, 'photo_url');
41
+ }
42
+
43
+ get fallbackImage() {
44
+ return config?.defaultValues?.vehicleAvatar;
45
+ }
46
+
47
+ get hasCompactStatusDot() {
48
+ return this.args.column?.showStatusDot ?? this.args.column?.showOnlineIndicator ?? true;
49
+ }
50
+
51
+ get compactStatusValue() {
52
+ const vehicle = this.resource;
53
+
54
+ return get(vehicle, 'online') ?? get(vehicle, 'status');
55
+ }
56
+
57
+ get compactStatusToneClass() {
58
+ const value = this.compactStatusValue;
59
+ const statusToneMap = {
60
+ ...DEFAULT_STATUS_TONES,
61
+ ...(this.args.column?.statusToneMap ?? {}),
62
+ };
63
+
64
+ if (typeof this.args.column?.statusToneClass === 'function') {
65
+ return this.args.column.statusToneClass(value, this.resource, this.args.column);
66
+ }
67
+
68
+ if (typeof value === 'boolean') {
69
+ return value ? 'text-green-500' : 'text-yellow-200';
70
+ }
71
+
72
+ return statusToneMap[value] ?? statusToneMap[String(value ?? '').toLowerCase()] ?? 'text-gray-400';
73
+ }
74
+
75
+ get compactStatusDotClass() {
76
+ return this.compactStatusToneClass;
77
+ }
78
+
79
+ get assignedDriverLabel() {
80
+ const vehicle = this.resource;
81
+
82
+ return get(vehicle, 'driver.displayName') ?? get(vehicle, 'driver.display_name') ?? get(vehicle, 'driver.name') ?? get(vehicle, 'driver_name');
83
+ }
84
+
85
+ get column() {
86
+ return {
87
+ ...(this.args.column ?? {}),
88
+ labelPath: (vehicle) => get(vehicle, 'displayName') ?? get(vehicle, 'display_name') ?? get(vehicle, 'name'),
89
+ mediaPath: 'photo_url',
90
+ fallbackImage: config?.defaultValues?.vehicleAvatar,
91
+ statusPath: this.showStatus ? 'status' : undefined,
92
+ onlinePath: 'online',
93
+ showStatusBadge: this.showStatus ? (this.args.column?.showStatusBadge ?? true) : false,
94
+ statusBadgeSize: this.args.column?.statusBadgeSize ?? 'xxs',
95
+ statusBadgeWrapperClass: this.args.column?.statusBadgeWrapperClass ?? 'resource-identity-status-badge vehicle-identity-status-badge',
96
+ metaPaths: [
97
+ {
98
+ value: (vehicle) => get(vehicle, 'plate_number') ?? get(vehicle, 'call_sign') ?? get(vehicle, 'vehicle_number') ?? get(vehicle, 'public_id'),
99
+ icon: 'id-card',
100
+ style: 'badge',
101
+ class: 'max-w-[12rem]',
102
+ },
103
+ {
104
+ value: (vehicle) => get(vehicle, 'driver.displayName') ?? get(vehicle, 'driver.display_name') ?? get(vehicle, 'driver.name') ?? get(vehicle, 'driver_name'),
105
+ icon: 'user',
106
+ style: 'badge',
107
+ class: 'max-w-[12rem]',
108
+ },
109
+ ],
110
+ statusToneMap: {
111
+ available: 'text-green-500',
112
+ active: 'text-green-500',
113
+ in_service: 'text-green-500',
114
+ maintenance: 'text-yellow-500',
115
+ unavailable: 'text-gray-400',
116
+ inactive: 'text-gray-400',
117
+ out_of_service: 'text-red-500',
118
+ },
119
+ };
120
+ }
121
+
122
+ @action onClick(event) {
123
+ const { column, onClick } = this.args;
124
+ const resource = this.resource;
125
+
126
+ if (typeof onClick === 'function') {
127
+ onClick(resource, event);
128
+ }
129
+
130
+ if (typeof column?.onClick === 'function') {
131
+ column.onClick(resource, event);
132
+ }
133
+
134
+ if (typeof column?.action === 'function') {
135
+ column.action(resource, event);
136
+ }
137
+ }
138
+ }
@@ -10,7 +10,7 @@
10
10
  @infiniteScroll={{false}}
11
11
  @renderInPlace={{true}}
12
12
  @onChange={{this.selectTelematic}}
13
- @disabled={{cannot-write @resource}}
13
+ @disabled={{or (cannot-write @resource) this.isTelematicLocked}}
14
14
  as |model|
15
15
  >
16
16
  <div class="space-x-2 text-sm">
@@ -227,4 +227,4 @@
227
227
  <RegistryYield @registry="fleet-ops:component:device:form" as |RegistryComponent|>
228
228
  <RegistryComponent @resource={{@resource}} @controller={{@controller}} @permission={{get-write-permission @resource}} />
229
229
  </RegistryYield>
230
- </div>
230
+ </div>
@@ -8,6 +8,13 @@ export default class DeviceFormComponent extends Component {
8
8
  @service currentUser;
9
9
  @service notifications;
10
10
 
11
+ get isTelematicLocked() {
12
+ const resource = this.args.resource;
13
+ const hasTelematic = Boolean(resource?.telematic_uuid || resource?.telematic?.id);
14
+
15
+ return Boolean(hasTelematic && resource?.isNew === false);
16
+ }
17
+
11
18
  @action selectTelematic(telematic) {
12
19
  this.args.resource.setProperties({
13
20
  telematic,
@@ -1,35 +1,60 @@
1
- <div class="px-4 py-2">
2
- <div class="flex flex-1 flex-row items-start justify-between">
3
- <div class="flex flex-row space-x-3">
4
- <div class="flex items-start justify-start rounded-full">
1
+ <div class="px-4 py-3" ...attributes>
2
+ <div class="flex flex-1 flex-row items-start justify-between gap-4">
3
+ <div class="flex min-w-0 flex-row items-start gap-3">
4
+ <div class="relative flex flex-shrink-0 items-start justify-start">
5
5
  <Image
6
- src={{@resource.photo_url}}
6
+ src={{this.resource.photo_url}}
7
7
  @fallbackSrc={{config "defaultValues.placeholderImage"}}
8
- alt={{@resource.displayName}}
8
+ alt={{this.name}}
9
9
  height="48"
10
10
  width="48"
11
- class="h-14 w-14 rounded-full shadow-sm"
11
+ class="h-14 w-14 rounded-md border border-gray-200 object-cover shadow-sm dark:border-gray-700"
12
12
  />
13
13
  <Attach::Tooltip @class="clean" @animation="scale" @placement="top">
14
- <InputInfo @text={{@resource.displayName}} />
14
+ <InputInfo @text={{this.name}} />
15
15
  </Attach::Tooltip>
16
16
  </div>
17
- <div class="flex flex-col">
18
- <h1 class="text-gray-900 dark:text-white font-semibold text-lg">{{@resource.displayName}}</h1>
19
- <div>
20
- <div class="text-xs text-gray-400 dark:text-gray-500">{{concat
21
- (n-a (titleize @resource.provider))
22
- " "
23
- (n-a (get-fleet-ops-option-label "deviceTypes" @resource.type))
24
- }}</div>
25
- <div class="text-xs text-gray-400 dark:text-gray-500">{{n-a @resource.serial_number}}</div>
17
+ <div class="flex min-w-0 flex-col">
18
+ <div class="flex min-w-0 flex-wrap items-center gap-2">
19
+ <h1 class="truncate text-base font-semibold leading-5 text-gray-900 dark:text-gray-100">{{this.name}}</h1>
20
+ <Badge @status={{this.connectionStatus}} @size="xs">{{smart-humanize this.connectionStatus}}</Badge>
21
+ </div>
22
+
23
+ <div class="mt-1 flex max-w-2xl flex-wrap items-center gap-1.5">
24
+ {{#if this.provider}}
25
+ <span class="inline-flex max-w-[12rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
26
+ <FaIcon @icon="satellite-dish" class="text-gray-400 dark:text-gray-300" />
27
+ <span class="truncate">{{titleize this.provider}}</span>
28
+ </span>
29
+ {{/if}}
30
+ {{#if this.type}}
31
+ <span class="inline-flex max-w-[10rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
32
+ <FaIcon @icon="microchip" class="text-gray-400 dark:text-gray-300" />
33
+ <span class="truncate">{{n-a (get-fleet-ops-option-label "deviceTypes" this.type)}}</span>
34
+ </span>
35
+ {{/if}}
36
+ {{#if this.identifier}}
37
+ <span class="inline-flex max-w-[12rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
38
+ <FaIcon @icon="id-card" class="text-gray-400 dark:text-gray-300" />
39
+ <span class="truncate">{{this.identifier}}</span>
40
+ </span>
41
+ {{/if}}
42
+ {{#if this.attachedVehicle}}
43
+ <span class="inline-flex max-w-[12rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
44
+ <FaIcon @icon="car" class="text-gray-400 dark:text-gray-300" />
45
+ <span class="truncate">{{this.attachedVehicle}}</span>
46
+ </span>
47
+ {{/if}}
48
+ <span class="inline-flex max-w-[13rem] items-center gap-1 truncate rounded-md bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium leading-4 text-gray-600 dark:bg-gray-700 dark:text-gray-200">
49
+ <FaIcon @icon="clock" class="text-gray-400 dark:text-gray-300" />
50
+ <span class="truncate">{{#if this.lastOnlineAt}}{{format-date-fns this.lastOnlineAt "dd MMM yyyy, HH:mm"}}{{else}}No last online{{/if}}</span>
51
+ </span>
26
52
  </div>
27
- <Badge @status={{if @resource.is_online "online" "offline"}}>{{if @resource.is_online (t "common.online") (t "common.offline")}}</Badge>
28
53
  </div>
29
54
  </div>
30
- <div class="next-view-header-right">
55
+ <div class="next-view-header-right flex-shrink-0">
31
56
  <Layout::Resource::Panel::HeaderActions
32
- @resource={{@resource}}
57
+ @resource={{this.resource}}
33
58
  @saveTask={{@saveTask}}
34
59
  @saveOptions={{@saveOptions}}
35
60
  @saveDisabled={{@saveDisabled}}
@@ -1,3 +1,51 @@
1
1
  import Component from '@glimmer/component';
2
+ import { get } from '@ember/object';
2
3
 
3
- export default class DevicePanelHeaderComponent extends Component {}
4
+ export default class DevicePanelHeaderComponent extends Component {
5
+ get resource() {
6
+ return this.args.resource ?? {};
7
+ }
8
+
9
+ get name() {
10
+ return (
11
+ get(this.resource, 'displayName') ??
12
+ get(this.resource, 'display_name') ??
13
+ get(this.resource, 'name') ??
14
+ get(this.resource, 'device_id') ??
15
+ get(this.resource, 'imei') ??
16
+ get(this.resource, 'serial_number') ??
17
+ get(this.resource, 'public_id') ??
18
+ '-'
19
+ );
20
+ }
21
+
22
+ get connectionStatus() {
23
+ return get(this.resource, 'connection_status') ?? get(this.resource, 'status') ?? (get(this.resource, 'is_online') || get(this.resource, 'online') ? 'online' : 'offline');
24
+ }
25
+
26
+ get provider() {
27
+ return get(this.resource, 'telematic.name') ?? get(this.resource, 'telematic_name') ?? get(this.resource, 'provider');
28
+ }
29
+
30
+ get type() {
31
+ return get(this.resource, 'type');
32
+ }
33
+
34
+ get identifier() {
35
+ return get(this.resource, 'imei') ?? get(this.resource, 'serial_number') ?? get(this.resource, 'device_id') ?? get(this.resource, 'public_id');
36
+ }
37
+
38
+ get attachedVehicle() {
39
+ return (
40
+ get(this.resource, 'attached_to_name') ??
41
+ get(this.resource, 'attachable.displayName') ??
42
+ get(this.resource, 'attachable.display_name') ??
43
+ get(this.resource, 'attachable.name') ??
44
+ get(this.resource, 'attachable_uuid')
45
+ );
46
+ }
47
+
48
+ get lastOnlineAt() {
49
+ return get(this.resource, 'last_online_at') ?? get(this.resource, 'lastOnlineAt');
50
+ }
51
+ }
@@ -0,0 +1,30 @@
1
+ <div class="flex flex-col gap-3 p-3" ...attributes>
2
+ <div class="flex items-start justify-between gap-3">
3
+ <div class="min-w-0">
4
+ <h2 class="text-base font-bold text-gray-900 dark:text-gray-100">Telemetry Events</h2>
5
+ <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-300">Recent device events, warning signals, and processing state.</p>
6
+ </div>
7
+ <Button @icon="refresh" @size="xs" @onClick={{this.refreshEvents}} @isLoading={{this.loadEvents.isRunning}} />
8
+ </div>
9
+
10
+ <Layout::Resource::Tabular
11
+ @resource="device-event"
12
+ @title="Telemetry Events"
13
+ @data={{this.events}}
14
+ @columns={{this.columns}}
15
+ @controller={{this}}
16
+ @pagination={{false}}
17
+ @withoutHeader={{true}}
18
+ @tableWrapperClass="no-table-extra-spacing"
19
+ @emptyStateComponent={{component
20
+ "table/empty-state"
21
+ variant="compact"
22
+ icon="bolt"
23
+ title="No telemetry events"
24
+ description="Events appear after this device reports provider activity."
25
+ primaryText="Refresh"
26
+ primaryIcon="refresh"
27
+ primaryAction=this.refreshEvents
28
+ }}
29
+ />
30
+ </div>
@@ -0,0 +1,128 @@
1
+ import Component from '@glimmer/component';
2
+ import { action } from '@ember/object';
3
+ import { inject as service } from '@ember/service';
4
+ import { tracked } from '@glimmer/tracking';
5
+ import { task } from 'ember-concurrency';
6
+
7
+ const severityOptions = [
8
+ { label: 'Info', value: 'info' },
9
+ { label: 'Warning', value: 'warning' },
10
+ { label: 'Error', value: 'error' },
11
+ { label: 'Critical', value: 'critical' },
12
+ { label: 'High', value: 'high' },
13
+ ];
14
+
15
+ function toRecentList(records) {
16
+ const list = Array.from(records ?? []);
17
+
18
+ list.meta = {
19
+ current_page: 1,
20
+ last_page: 1,
21
+ per_page: list.length,
22
+ total: list.length,
23
+ from: list.length > 0 ? 1 : 0,
24
+ to: list.length,
25
+ };
26
+
27
+ return list;
28
+ }
29
+
30
+ export default class DevicePanelTabsEventsComponent extends Component {
31
+ @service deviceEventActions;
32
+ @service store;
33
+
34
+ @tracked events = toRecentList();
35
+
36
+ constructor() {
37
+ super(...arguments);
38
+ this.loadEvents.perform();
39
+ }
40
+
41
+ get device() {
42
+ return this.args.resource ?? this.args.model;
43
+ }
44
+
45
+ get columns() {
46
+ return [
47
+ {
48
+ sticky: true,
49
+ label: 'Event',
50
+ valuePath: 'event_type',
51
+ cellComponent: 'table/cell/anchor',
52
+ action: this.deviceEventActions.panel?.view ?? this.deviceEventActions.transition.view,
53
+ permission: 'fleet-ops view device-event',
54
+ resizable: true,
55
+ },
56
+ {
57
+ label: 'Severity',
58
+ valuePath: 'severity',
59
+ cellComponent: 'table/cell/status',
60
+ filterOptions: severityOptions,
61
+ resizable: true,
62
+ },
63
+ {
64
+ label: 'Message',
65
+ valuePath: 'message',
66
+ resizable: true,
67
+ },
68
+ {
69
+ label: 'Code',
70
+ valuePath: 'code',
71
+ resizable: true,
72
+ },
73
+ {
74
+ label: 'Processed',
75
+ valuePath: 'processedAt',
76
+ sortParam: 'processed_at',
77
+ resizable: true,
78
+ },
79
+ {
80
+ label: 'Occurred',
81
+ valuePath: 'occurredAt',
82
+ sortParam: 'occurred_at',
83
+ resizable: true,
84
+ },
85
+ {
86
+ label: '',
87
+ cellComponent: 'table/cell/dropdown',
88
+ ddButtonText: false,
89
+ ddButtonIcon: 'ellipsis-h',
90
+ ddButtonIconPrefix: 'fas',
91
+ wrapperClass: 'flex items-center justify-end mx-2',
92
+ sticky: 'right',
93
+ width: 60,
94
+ actions: [
95
+ {
96
+ label: 'View event',
97
+ fn: this.deviceEventActions.panel?.view ?? this.deviceEventActions.transition.view,
98
+ permission: 'fleet-ops view device-event',
99
+ },
100
+ {
101
+ label: 'Mark processed',
102
+ fn: this.markProcessed,
103
+ permission: 'fleet-ops update device-event',
104
+ },
105
+ ],
106
+ },
107
+ ];
108
+ }
109
+
110
+ @action async markProcessed(deviceEvent) {
111
+ await this.deviceEventActions.markProcessed(deviceEvent);
112
+ await this.loadEvents.perform();
113
+ }
114
+
115
+ @action refreshEvents() {
116
+ return this.loadEvents.perform();
117
+ }
118
+
119
+ @task *loadEvents() {
120
+ if (!this.device?.id) {
121
+ this.events = toRecentList();
122
+ return;
123
+ }
124
+
125
+ const events = yield this.store.query('device-event', { device_uuid: this.device.id, limit: 10, sort: '-created_at' });
126
+ this.events = toRecentList(events);
127
+ }
128
+ }
@@ -0,0 +1,30 @@
1
+ <div class="flex flex-col gap-3 p-3" ...attributes>
2
+ <div class="flex items-start justify-between gap-3">
3
+ <div class="min-w-0">
4
+ <h2 class="text-base font-bold text-gray-900 dark:text-gray-100">Sensor Inventory</h2>
5
+ <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-300">Recent sensor readings and health for this device.</p>
6
+ </div>
7
+ <Button @icon="refresh" @size="xs" @onClick={{this.refreshSensors}} @isLoading={{this.loadSensors.isRunning}} />
8
+ </div>
9
+
10
+ <Layout::Resource::Tabular
11
+ @resource="sensor"
12
+ @title="Device Sensors"
13
+ @data={{this.sensors}}
14
+ @columns={{this.columns}}
15
+ @controller={{this}}
16
+ @pagination={{false}}
17
+ @withoutHeader={{true}}
18
+ @tableWrapperClass="no-table-extra-spacing"
19
+ @emptyStateComponent={{component
20
+ "table/empty-state"
21
+ variant="compact"
22
+ icon="gauge-high"
23
+ title="No sensors connected"
24
+ description="Sensors appear here after provider telemetry links them to this device."
25
+ primaryText="Refresh"
26
+ primaryIcon="refresh"
27
+ primaryAction=this.refreshSensors
28
+ }}
29
+ />
30
+ </div>