@fleetbase/ember-core 0.3.11 → 0.3.13
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.
|
@@ -35,6 +35,16 @@ import isObject from '../utils/is-object';
|
|
|
35
35
|
* .onClick((menuItem, router) => {
|
|
36
36
|
* router.transitionTo('virtual', menuItem.slug);
|
|
37
37
|
* })
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Menu item with description and shortcuts (Phase 2)
|
|
41
|
+
* new MenuItem('Fleet-Ops', 'console.fleet-ops')
|
|
42
|
+
* .withIcon('route')
|
|
43
|
+
* .withDescription('Manage fleets, drivers, and live orders')
|
|
44
|
+
* .withShortcuts([
|
|
45
|
+
* { title: 'Scheduler', route: 'console.fleet-ops.scheduler', icon: 'calendar' },
|
|
46
|
+
* { title: 'Order Config', route: 'console.fleet-ops.order-configs', icon: 'gear' },
|
|
47
|
+
* ])
|
|
38
48
|
*/
|
|
39
49
|
export default class MenuItem extends BaseContract {
|
|
40
50
|
/**
|
|
@@ -102,6 +112,17 @@ export default class MenuItem extends BaseContract {
|
|
|
102
112
|
|
|
103
113
|
// Nested items
|
|
104
114
|
this.items = definition.items || null;
|
|
115
|
+
|
|
116
|
+
// ── Phase 2 additions ──────────────────────────────────────────
|
|
117
|
+
// A short human-readable description of the extension shown in the
|
|
118
|
+
// overflow dropdown. Optional – defaults to null.
|
|
119
|
+
this.description = definition.description || null;
|
|
120
|
+
|
|
121
|
+
// An array of shortcut items (sub-menu links) displayed beneath the
|
|
122
|
+
// extension name in the multi-column dropdown. Each shortcut is a
|
|
123
|
+
// plain object with at minimum { title, route } and optionally
|
|
124
|
+
// { icon, iconPrefix, id }. Optional – defaults to null.
|
|
125
|
+
this.shortcuts = definition.shortcuts || null;
|
|
105
126
|
} else {
|
|
106
127
|
// Handle string title with optional route (chaining pattern)
|
|
107
128
|
this.title = titleOrDefinition;
|
|
@@ -153,6 +174,10 @@ export default class MenuItem extends BaseContract {
|
|
|
153
174
|
|
|
154
175
|
// Nested items
|
|
155
176
|
this.items = null;
|
|
177
|
+
|
|
178
|
+
// ── Phase 2 additions ──────────────────────────────────────────
|
|
179
|
+
this.description = null;
|
|
180
|
+
this.shortcuts = null;
|
|
156
181
|
}
|
|
157
182
|
|
|
158
183
|
// Call setup() to trigger validation after properties are set
|
|
@@ -342,6 +367,75 @@ export default class MenuItem extends BaseContract {
|
|
|
342
367
|
return this;
|
|
343
368
|
}
|
|
344
369
|
|
|
370
|
+
// ── Phase 2 builder methods ────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Set a short human-readable description for the extension.
|
|
374
|
+
* Displayed beneath the extension title in the overflow dropdown.
|
|
375
|
+
*
|
|
376
|
+
* @method withDescription
|
|
377
|
+
* @param {String} description Short description text
|
|
378
|
+
* @returns {MenuItem} This instance for chaining
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* new MenuItem('Fleet-Ops', 'console.fleet-ops')
|
|
382
|
+
* .withDescription('Manage fleets, drivers, and live orders')
|
|
383
|
+
*/
|
|
384
|
+
withDescription(description) {
|
|
385
|
+
this.description = description;
|
|
386
|
+
this._options.description = description;
|
|
387
|
+
return this;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Set an array of shortcut items displayed beneath the extension in the
|
|
392
|
+
* multi-column overflow dropdown. Each shortcut is a plain object:
|
|
393
|
+
*
|
|
394
|
+
* { title, route, icon?, iconPrefix?, id? }
|
|
395
|
+
*
|
|
396
|
+
* Shortcuts are purely navigational – they do not support onClick handlers.
|
|
397
|
+
* They are rendered as compact links inside the extension card in the
|
|
398
|
+
* dropdown and can be individually pinned to the navigation bar.
|
|
399
|
+
*
|
|
400
|
+
* @method withShortcuts
|
|
401
|
+
* @param {Array<Object>} shortcuts Array of shortcut definition objects
|
|
402
|
+
* @returns {MenuItem} This instance for chaining
|
|
403
|
+
*
|
|
404
|
+
* @example
|
|
405
|
+
* new MenuItem('Fleet-Ops', 'console.fleet-ops')
|
|
406
|
+
* .withShortcuts([
|
|
407
|
+
* { title: 'Scheduler', route: 'console.fleet-ops.scheduler', icon: 'calendar' },
|
|
408
|
+
* { title: 'Order Config', route: 'console.fleet-ops.order-configs', icon: 'gear' },
|
|
409
|
+
* ])
|
|
410
|
+
*/
|
|
411
|
+
withShortcuts(shortcuts) {
|
|
412
|
+
this.shortcuts = Array.isArray(shortcuts) ? shortcuts : null;
|
|
413
|
+
this._options.shortcuts = this.shortcuts;
|
|
414
|
+
return this;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Add a single shortcut to the existing shortcuts array.
|
|
419
|
+
* Creates the array if it does not yet exist.
|
|
420
|
+
*
|
|
421
|
+
* @method addShortcut
|
|
422
|
+
* @param {Object} shortcut Shortcut definition object { title, route, icon?, iconPrefix?, id? }
|
|
423
|
+
* @returns {MenuItem} This instance for chaining
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* new MenuItem('Fleet-Ops', 'console.fleet-ops')
|
|
427
|
+
* .addShortcut({ title: 'Scheduler', route: 'console.fleet-ops.scheduler' })
|
|
428
|
+
* .addShortcut({ title: 'Order Config', route: 'console.fleet-ops.order-configs' })
|
|
429
|
+
*/
|
|
430
|
+
addShortcut(shortcut) {
|
|
431
|
+
if (!Array.isArray(this.shortcuts)) {
|
|
432
|
+
this.shortcuts = [];
|
|
433
|
+
}
|
|
434
|
+
this.shortcuts = [...this.shortcuts, shortcut];
|
|
435
|
+
this._options.shortcuts = this.shortcuts;
|
|
436
|
+
return this;
|
|
437
|
+
}
|
|
438
|
+
|
|
345
439
|
/**
|
|
346
440
|
* Get the plain object representation
|
|
347
441
|
*
|
|
@@ -401,6 +495,13 @@ export default class MenuItem extends BaseContract {
|
|
|
401
495
|
// Nested items
|
|
402
496
|
items: this.items,
|
|
403
497
|
|
|
498
|
+
// ── Phase 2 additions ──────────────────────────────────────────
|
|
499
|
+
// Optional short description shown in the overflow dropdown card
|
|
500
|
+
description: this.description,
|
|
501
|
+
|
|
502
|
+
// Optional array of shortcut sub-links shown inside the extension card
|
|
503
|
+
shortcuts: this.shortcuts,
|
|
504
|
+
|
|
404
505
|
// Indicator flag
|
|
405
506
|
_isMenuItem: true,
|
|
406
507
|
|
|
@@ -8,6 +8,7 @@ import { isBlank } from '@ember/utils';
|
|
|
8
8
|
import { alias } from '@ember/object/computed';
|
|
9
9
|
import { storageFor } from 'ember-local-storage';
|
|
10
10
|
import { debug } from '@ember/debug';
|
|
11
|
+
import lookupUserIp from '../utils/lookup-user-ip';
|
|
11
12
|
|
|
12
13
|
export default class CurrentUserService extends Service.extend(Evented) {
|
|
13
14
|
@service session;
|
|
@@ -139,23 +140,34 @@ export default class CurrentUserService extends Service.extend(Evented) {
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
async loadWhois() {
|
|
142
|
-
this.fetch.shouldResetCache();
|
|
143
|
-
|
|
144
143
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
);
|
|
144
|
+
// Use frontend IP lookup to get accurate user location
|
|
145
|
+
// This avoids the issue of server-side lookup returning server IP instead of user IP
|
|
146
|
+
const whois = await lookupUserIp({
|
|
147
|
+
timeout: 5000,
|
|
148
|
+
cache: true,
|
|
149
|
+
});
|
|
150
|
+
|
|
153
151
|
this.setOption('whois', whois);
|
|
154
152
|
this.whoisData = whois;
|
|
155
153
|
|
|
156
154
|
return whois;
|
|
157
155
|
} catch (error) {
|
|
158
|
-
|
|
156
|
+
console.error('[currentUser] Failed to load whois:', error);
|
|
157
|
+
this.notifications.warning('Unable to detect your location. Some features may use default settings.');
|
|
158
|
+
|
|
159
|
+
// Return fallback data with browser timezone
|
|
160
|
+
const fallback = {
|
|
161
|
+
city: null,
|
|
162
|
+
country_code: null,
|
|
163
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
164
|
+
_source: 'fallback',
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
this.setOption('whois', fallback);
|
|
168
|
+
this.whoisData = fallback;
|
|
169
|
+
|
|
170
|
+
return fallback;
|
|
159
171
|
}
|
|
160
172
|
}
|
|
161
173
|
|
|
@@ -33,7 +33,6 @@ export default class UniverseService extends Service.extend(Evented) {
|
|
|
33
33
|
@service router;
|
|
34
34
|
@service intl;
|
|
35
35
|
@service urlSearchParams;
|
|
36
|
-
|
|
37
36
|
@tracked applicationInstance;
|
|
38
37
|
@tracked initialLocation = { ...window.location };
|
|
39
38
|
@tracked bootCallbacks = A([]);
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lookup user's IP address and geolocation information using geoiplookup.io
|
|
3
|
+
*
|
|
4
|
+
* This method calls the geoiplookup.io API directly from the browser to get
|
|
5
|
+
* accurate user location data, avoiding the issue of server-side IP lookup
|
|
6
|
+
* returning the server's location instead of the user's location.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} options - Configuration options
|
|
9
|
+
* @param {number} options.timeout - Request timeout in milliseconds (default: 5000)
|
|
10
|
+
* @param {boolean} options.cache - Whether to cache the result (default: true)
|
|
11
|
+
* @returns {Promise<Object>} User's IP and geolocation data
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const whois = await lookupUserIp();
|
|
15
|
+
* console.log(whois.city); // "New York"
|
|
16
|
+
* console.log(whois.country_code); // "US"
|
|
17
|
+
*/
|
|
18
|
+
export default async function lookupUserIp(options = {}) {
|
|
19
|
+
const { timeout = 5000, cache = true } = options;
|
|
20
|
+
|
|
21
|
+
// Check cache first if enabled
|
|
22
|
+
if (cache) {
|
|
23
|
+
const cached = getCachedWhois();
|
|
24
|
+
if (cached) {
|
|
25
|
+
return cached;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Try multiple APIs with fallback
|
|
30
|
+
const apis = [
|
|
31
|
+
{
|
|
32
|
+
url: 'https://json.geoiplookup.io/',
|
|
33
|
+
normalize: normalizeGeoIPLookup,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
url: 'https://ipapi.co/json/',
|
|
37
|
+
normalize: normalizeIPApi,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
for (const api of apis) {
|
|
42
|
+
try {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
45
|
+
|
|
46
|
+
const response = await fetch(api.url, {
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
headers: {
|
|
49
|
+
Accept: 'application/json',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
clearTimeout(timeoutId);
|
|
54
|
+
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
console.warn(`[lookupUserIp] ${api.url} returned ${response.status}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const data = await response.json();
|
|
61
|
+
const normalized = api.normalize(data);
|
|
62
|
+
|
|
63
|
+
// Cache the result if enabled
|
|
64
|
+
if (cache) {
|
|
65
|
+
cacheWhois(normalized);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return normalized;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.warn(`[lookupUserIp] ${api.url} failed:`, error.message);
|
|
71
|
+
// Continue to next API
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// All APIs failed, return fallback
|
|
76
|
+
console.error('[lookupUserIp] All IP lookup APIs failed, using fallback data');
|
|
77
|
+
return getFallbackWhois();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Normalize geoiplookup.io response to match Fleetbase whois format
|
|
82
|
+
*/
|
|
83
|
+
function normalizeGeoIPLookup(data) {
|
|
84
|
+
return {
|
|
85
|
+
ip: data.ip,
|
|
86
|
+
city: data.city,
|
|
87
|
+
region: data.region,
|
|
88
|
+
country_code: data.country_code,
|
|
89
|
+
country_name: data.country_name,
|
|
90
|
+
continent_code: data.continent_code,
|
|
91
|
+
continent_name: data.continent_name,
|
|
92
|
+
latitude: data.latitude,
|
|
93
|
+
longitude: data.longitude,
|
|
94
|
+
postal_code: data.postal_code,
|
|
95
|
+
timezone: data.timezone_name,
|
|
96
|
+
currency: {
|
|
97
|
+
code: data.currency_code,
|
|
98
|
+
name: data.currency_name,
|
|
99
|
+
},
|
|
100
|
+
languages: [
|
|
101
|
+
{
|
|
102
|
+
code: data.language_code,
|
|
103
|
+
name: data.language_name,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
isp: data.isp,
|
|
107
|
+
org: data.org,
|
|
108
|
+
asn: data.asn,
|
|
109
|
+
connection_type: data.connection_type,
|
|
110
|
+
// Metadata
|
|
111
|
+
_source: 'geoiplookup.io',
|
|
112
|
+
_timestamp: Date.now(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Normalize ipapi.co response to match Fleetbase whois format
|
|
118
|
+
*/
|
|
119
|
+
function normalizeIPApi(data) {
|
|
120
|
+
return {
|
|
121
|
+
ip: data.ip,
|
|
122
|
+
city: data.city,
|
|
123
|
+
region: data.region,
|
|
124
|
+
country_code: data.country_code,
|
|
125
|
+
country_name: data.country_name,
|
|
126
|
+
continent_code: data.continent_code,
|
|
127
|
+
continent_name: null,
|
|
128
|
+
latitude: data.latitude,
|
|
129
|
+
longitude: data.longitude,
|
|
130
|
+
postal_code: data.postal,
|
|
131
|
+
timezone: data.timezone,
|
|
132
|
+
currency: {
|
|
133
|
+
code: data.currency,
|
|
134
|
+
name: data.currency_name,
|
|
135
|
+
},
|
|
136
|
+
languages: data.languages ? data.languages.split(',').map((lang) => ({ code: lang.trim(), name: lang.trim() })) : [],
|
|
137
|
+
isp: data.org,
|
|
138
|
+
org: data.org,
|
|
139
|
+
asn: data.asn,
|
|
140
|
+
connection_type: null,
|
|
141
|
+
// Metadata
|
|
142
|
+
_source: 'ipapi.co',
|
|
143
|
+
_timestamp: Date.now(),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get cached whois data from localStorage
|
|
149
|
+
*/
|
|
150
|
+
function getCachedWhois() {
|
|
151
|
+
try {
|
|
152
|
+
const cached = localStorage.getItem('fleetbase:whois');
|
|
153
|
+
if (!cached) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const data = JSON.parse(cached);
|
|
158
|
+
const age = Date.now() - data._timestamp;
|
|
159
|
+
const maxAge = 60 * 60 * 1000; // 1 hour
|
|
160
|
+
|
|
161
|
+
if (age > maxAge) {
|
|
162
|
+
localStorage.removeItem('fleetbase:whois');
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return data;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('[getCachedWhois] Error reading cache:', error);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Cache whois data to localStorage
|
|
175
|
+
*/
|
|
176
|
+
function cacheWhois(data) {
|
|
177
|
+
try {
|
|
178
|
+
localStorage.setItem('fleetbase:whois', JSON.stringify(data));
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error('[cacheWhois] Error writing cache:', error);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get fallback whois data when all APIs fail
|
|
186
|
+
*/
|
|
187
|
+
function getFallbackWhois() {
|
|
188
|
+
// Try to get browser language and timezone as fallback
|
|
189
|
+
const browserLang = navigator.language || 'en-US';
|
|
190
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
ip: null,
|
|
194
|
+
city: null,
|
|
195
|
+
region: null,
|
|
196
|
+
country_code: null,
|
|
197
|
+
country_name: null,
|
|
198
|
+
continent_code: null,
|
|
199
|
+
continent_name: null,
|
|
200
|
+
latitude: null,
|
|
201
|
+
longitude: null,
|
|
202
|
+
postal_code: null,
|
|
203
|
+
timezone: timezone,
|
|
204
|
+
currency: {
|
|
205
|
+
code: null,
|
|
206
|
+
name: null,
|
|
207
|
+
},
|
|
208
|
+
languages: [
|
|
209
|
+
{
|
|
210
|
+
code: browserLang.split('-')[0],
|
|
211
|
+
name: browserLang,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
isp: null,
|
|
215
|
+
org: null,
|
|
216
|
+
asn: null,
|
|
217
|
+
connection_type: null,
|
|
218
|
+
_source: 'fallback',
|
|
219
|
+
_timestamp: Date.now(),
|
|
220
|
+
};
|
|
221
|
+
}
|
package/package.json
CHANGED