@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.
- package/addon/components/admin/routing-settings.hbs +2 -1
- package/addon/components/cell/attached-vehicle.hbs +10 -0
- package/addon/components/cell/attached-vehicle.js +58 -0
- package/addon/components/cell/device-identity.hbs +24 -0
- package/addon/components/cell/device-identity.js +129 -0
- package/addon/components/cell/driver-identity.hbs +33 -0
- package/addon/components/cell/driver-identity.js +143 -0
- package/addon/components/cell/equipment-identity.hbs +5 -0
- package/addon/components/cell/equipment-identity.js +48 -0
- package/addon/components/cell/part-identity.hbs +5 -0
- package/addon/components/cell/part-identity.js +63 -0
- package/addon/components/cell/telematic-device.hbs +38 -0
- package/addon/components/cell/telematic-device.js +44 -0
- package/addon/components/cell/telematic-provider.hbs +33 -16
- package/addon/components/cell/telematic-provider.js +50 -8
- package/addon/components/cell/vehicle-identity.hbs +33 -0
- package/addon/components/cell/vehicle-identity.js +138 -0
- package/addon/components/device/form.hbs +2 -2
- package/addon/components/device/form.js +7 -0
- package/addon/components/device/panel-header.hbs +45 -20
- package/addon/components/device/panel-header.js +49 -1
- package/addon/components/device/panel-tabs/events.hbs +30 -0
- package/addon/components/device/panel-tabs/events.js +128 -0
- package/addon/components/device/panel-tabs/sensors.hbs +30 -0
- package/addon/components/device/panel-tabs/sensors.js +93 -0
- package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
- package/addon/components/device/panel-tabs/vehicle.js +90 -0
- package/addon/components/equipment/form.js +8 -1
- package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
- package/addon/components/layout/fleet-ops-sidebar.js +8 -0
- package/addon/components/map/drawer/device-event-listing.js +2 -0
- package/addon/components/map/drawer/driver-listing.js +2 -0
- package/addon/components/map/drawer/place-listing.js +2 -0
- package/addon/components/map/drawer/position-listing.hbs +11 -6
- package/addon/components/map/drawer/position-listing.js +17 -4
- package/addon/components/map/drawer/vehicle-listing.js +2 -0
- package/addon/components/sensor/panel-header.hbs +47 -15
- package/addon/components/sensor/panel-header.js +59 -1
- package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
- package/addon/components/table/cell/dropdown/action-item.js +62 -0
- package/addon/components/table/cell/dropdown.hbs +28 -0
- package/addon/components/table/cell/dropdown.js +91 -0
- package/addon/components/vendor/panel-header.hbs +82 -0
- package/addon/components/vendor/panel-header.js +48 -0
- package/addon/controllers/connectivity/devices/index/details/vehicle.js +49 -1
- package/addon/controllers/connectivity/devices/index.js +144 -129
- package/addon/controllers/connectivity/events/details.js +3 -0
- package/addon/controllers/connectivity/events/index.js +187 -11
- package/addon/controllers/connectivity/sensors/index.js +156 -14
- package/addon/controllers/connectivity/telematics/details/devices.js +126 -134
- package/addon/controllers/maintenance/equipment/index.js +1 -2
- package/addon/controllers/maintenance/parts/index.js +1 -2
- package/addon/controllers/management/drivers/index.js +82 -42
- package/addon/controllers/management/fuel-reports/index.js +63 -4
- package/addon/controllers/management/issues/index.js +63 -4
- package/addon/controllers/management/vehicles/index.js +33 -6
- package/addon/routes/connectivity/devices/index/details/vehicle.js +24 -2
- package/addon/routes/connectivity/devices/index.js +7 -0
- package/addon/routes/connectivity/events/details.js +3 -0
- package/addon/routes/connectivity/events/index.js +6 -2
- package/addon/routes/connectivity/sensors/index.js +7 -2
- package/addon/routes/connectivity/telematics/details/devices.js +2 -0
- package/addon/services/device-actions.js +30 -0
- package/addon/services/device-event-actions.js +1 -1
- package/addon/services/equipment-actions.js +1 -0
- package/addon/services/sensor-actions.js +3 -0
- package/addon/services/service-rate-actions.js +39 -1
- package/addon/services/vehicle-actions.js +24 -12
- package/addon/services/vendor-actions.js +3 -0
- package/addon/styles/fleetops-engine.css +8 -0
- package/addon/templates/connectivity/devices/index/details/vehicle.hbs +64 -1
- package/addon/templates/management/vendors/index/details.hbs +2 -2
- package/addon/templates/settings/notifications.hbs +2 -1
- package/addon/templates/settings/routing.hbs +1 -0
- package/addon/utils/device-table-columns.js +288 -0
- package/addon/utils/identity-cell-resource.js +20 -0
- package/addon/utils/map-drawer-dropdown-position.js +47 -0
- package/addon/utils/trackable-option.js +93 -0
- package/app/components/cell/attached-vehicle.js +1 -0
- package/app/components/cell/device-identity.js +1 -0
- package/app/components/cell/driver-identity.js +1 -0
- package/app/components/cell/equipment-identity.js +1 -0
- package/app/components/cell/part-identity.js +1 -0
- package/app/components/cell/telematic-device.js +1 -0
- package/app/components/cell/vehicle-identity.js +1 -0
- package/app/components/device/panel-tabs/events.hbs +30 -0
- package/app/components/device/panel-tabs/events.js +128 -0
- package/app/components/device/panel-tabs/sensors.hbs +30 -0
- package/app/components/device/panel-tabs/sensors.js +93 -0
- package/app/components/device/panel-tabs/vehicle.hbs +68 -0
- package/app/components/device/panel-tabs/vehicle.js +90 -0
- package/app/components/table/cell/dropdown/action-item.js +1 -0
- package/app/components/table/cell/dropdown.js +1 -0
- package/app/components/vendor/panel-header.js +1 -0
- package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
- package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
- package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
- package/app/utils/map-drawer-dropdown-position.js +1 -0
- package/app/utils/trackable-option.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +4 -4
- package/server/src/Console/Commands/SyncTelematics.php +4 -4
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -1
- package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
- package/server/src/Http/Filter/DeviceEventFilter.php +21 -0
- package/server/src/Http/Filter/DeviceFilter.php +24 -1
- package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
- package/server/src/Models/Device.php +2 -0
- package/server/src/Models/DeviceEvent.php +81 -0
- package/server/src/Models/Equipment.php +55 -0
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +1 -0
- package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
- package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +26 -2
- package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
- package/server/src/routes.php +0 -1
- package/server/tests/DeviceFilterTest.php +9 -0
- package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
- package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
- package/server/tests/ServiceRateRouteValidationTest.php +101 -0
- package/server/tests/TelematicsHardeningTest.php +238 -7
- package/addon/controllers/connectivity/events/index/details.js +0 -3
- package/addon/routes/connectivity/events/index/details.js +0 -3
- /package/addon/templates/connectivity/events/{index/details.hbs → details.hbs} +0 -0
|
@@ -1,16 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<div class="truncate text-sm
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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.
|
|
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(
|
|
67
|
+
onClick(resource, event);
|
|
26
68
|
}
|
|
27
69
|
|
|
28
70
|
if (typeof column?.action === 'function') {
|
|
29
|
-
column.action(
|
|
71
|
+
column.action(resource, event);
|
|
30
72
|
}
|
|
31
73
|
|
|
32
74
|
if (typeof column?.onClick === 'function') {
|
|
33
|
-
column.onClick(
|
|
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
|
-
<div class="flex flex-1 flex-row items-start justify-between">
|
|
3
|
-
<div class="flex flex-row
|
|
4
|
-
<div class="flex items-start justify-start
|
|
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={{
|
|
6
|
+
src={{this.resource.photo_url}}
|
|
7
7
|
@fallbackSrc={{config "defaultValues.placeholderImage"}}
|
|
8
|
-
alt={{
|
|
8
|
+
alt={{this.name}}
|
|
9
9
|
height="48"
|
|
10
10
|
width="48"
|
|
11
|
-
class="h-14 w-14 rounded-
|
|
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={{
|
|
14
|
+
<InputInfo @text={{this.name}} />
|
|
15
15
|
</Attach::Tooltip>
|
|
16
16
|
</div>
|
|
17
|
-
<div class="flex flex-col">
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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={{
|
|
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>
|