@fleetbase/ember-core 0.3.9 → 0.3.11

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.
@@ -19,6 +19,7 @@ export const hostServices = [
19
19
  'sidebar',
20
20
  'dashboard',
21
21
  'universe',
22
+ 'events',
22
23
  'intl',
23
24
  'abilities',
24
25
  'language',
@@ -21,6 +21,7 @@ export const services = [
21
21
  'sidebar',
22
22
  'dashboard',
23
23
  'universe',
24
+ 'events',
24
25
  'intl',
25
26
  'abilities',
26
27
  'language',
@@ -242,7 +242,7 @@ export default class SubjectCustomFields {
242
242
  let existingMany = subject.hasMany?.('custom_field_values')?.value?.() ?? null;
243
243
 
244
244
  // If asked or not loaded, fetch from API (scoped to subject)
245
- if (reloadExisting || !existingMany) {
245
+ if ((reloadExisting || !existingMany) && subjectId) {
246
246
  try {
247
247
  existingMany = await this.store.query('custom-field-value', {
248
248
  subject_uuid: subjectId,
@@ -13,7 +13,8 @@ export default class AppCacheService extends Service {
13
13
 
14
14
  get cachePrefix() {
15
15
  const userId = this.currentUser.id ?? 'anon';
16
- return `${userId}:${this.currentUser.companyId}:`;
16
+ const companyId = this.currentUser.companyId ?? 'no-org';
17
+ return `${userId}:${companyId}:`;
17
18
  }
18
19
 
19
20
  @action setEmberData(key, value, except = []) {
@@ -17,6 +17,8 @@ export default class CrudService extends Service {
17
17
  @service notifications;
18
18
  @service store;
19
19
  @service currentUser;
20
+ @service universe;
21
+ @service events;
20
22
 
21
23
  /**
22
24
  * Generic deletion modal with options
@@ -43,6 +45,10 @@ export default class CrudService extends Service {
43
45
  try {
44
46
  const response = await model.destroyRecord();
45
47
  this.notifications.success(successNotification);
48
+
49
+ // Track deletion event
50
+ this.events.trackResourceDeleted(model);
51
+
46
52
  if (typeof options.onSuccess === 'function') {
47
53
  options.onSuccess(model);
48
54
  }
@@ -161,6 +167,10 @@ export default class CrudService extends Service {
161
167
  );
162
168
 
163
169
  this.notifications.success(response.message ?? successMessage);
170
+
171
+ // Track bulk action event
172
+ this.events.trackBulkAction(verb, selected);
173
+
164
174
  if (typeof options.onSuccess === 'function') {
165
175
  options.onSuccess(selected);
166
176
  }
@@ -224,6 +234,9 @@ export default class CrudService extends Service {
224
234
  }
225
235
  )
226
236
  .then(() => {
237
+ // Track export event
238
+ this.events.trackResourceExported(modelName, format, exportParams);
239
+
227
240
  later(
228
241
  this,
229
242
  () => {
@@ -248,6 +261,7 @@ export default class CrudService extends Service {
248
261
  * @param {Object} [options={}]
249
262
  * @memberof CrudService
250
263
  */
264
+
251
265
  @action import(modelName, options = {}) {
252
266
  // always lowercase modelname
253
267
  modelName = modelName.toLowerCase();
@@ -337,6 +351,11 @@ export default class CrudService extends Service {
337
351
 
338
352
  try {
339
353
  const response = await this.fetch.post(importEndpoint, { files }, fetchOptions);
354
+
355
+ // Track import event
356
+ const importCount = response?.imported?.length || response?.count || files.length;
357
+ this.events.trackResourceImported(modelName, importCount);
358
+
340
359
  if (typeof options.onImportCompleted === 'function') {
341
360
  options.onImportCompleted(response, files);
342
361
  }
@@ -16,6 +16,7 @@ export default class CurrentUserService extends Service.extend(Evented) {
16
16
  @service theme;
17
17
  @service notifications;
18
18
  @service intl;
19
+ @service events;
19
20
 
20
21
  @tracked user = { id: 'anon' };
21
22
  @tracked userSnapshot = { id: 'anon' };
@@ -65,14 +66,9 @@ export default class CurrentUserService extends Service.extend(Evented) {
65
66
  async load() {
66
67
  if (this.session.isAuthenticated) {
67
68
  const user = await this.store.findRecord('user', 'me');
68
- const snapshot = await this.getUserSnapshot(user);
69
69
 
70
- this.set('user', user);
71
- this.set('userSnapshot', snapshot);
72
- this.trigger('user.loaded', user);
73
-
74
- // Set permissions
75
- this.permissions = this.getUserPermissions(user);
70
+ // set user
71
+ this.setUser(user);
76
72
 
77
73
  // Load preferences
78
74
  await this.loadPreferences();
@@ -91,25 +87,9 @@ export default class CurrentUserService extends Service.extend(Evented) {
91
87
 
92
88
  try {
93
89
  const user = await this.store.queryRecord('user', { me: true });
94
- const snapshot = await this.getUserSnapshot(user);
95
-
96
- // Set current user
97
- this.set('user', user);
98
- this.set('userSnapshot', snapshot);
99
- this.trigger('user.loaded', user);
100
-
101
- // Set permissions
102
- this.permissions = this.getUserPermissions(user);
103
90
 
104
- // Set environment from user option
105
- this.theme.setEnvironment();
106
-
107
- // Set locale
108
- if (user.locale) {
109
- this.setLocale(user.locale);
110
- } else {
111
- await this.loadLocale();
112
- }
91
+ // set user
92
+ this.setUser(user);
113
93
 
114
94
  // Load user whois data
115
95
  await this.loadWhois();
@@ -309,4 +289,26 @@ export default class CurrentUserService extends Service.extend(Evented) {
309
289
 
310
290
  return defaultValue;
311
291
  }
292
+
293
+ async setUser(user) {
294
+ const snapshot = await this.getUserSnapshot(user);
295
+
296
+ // Set current user
297
+ this.set('user', user);
298
+ this.set('userSnapshot', snapshot);
299
+ this.trigger('user.loaded', user);
300
+
301
+ // Set permissions
302
+ this.permissions = this.getUserPermissions(user);
303
+
304
+ // Set environment from user option
305
+ this.theme.setEnvironment();
306
+
307
+ // Set locale
308
+ if (user.locale) {
309
+ this.setLocale(user.locale);
310
+ } else {
311
+ await this.loadLocale();
312
+ }
313
+ }
312
314
  }
@@ -0,0 +1,322 @@
1
+ import Service, { inject as service } from '@ember/service';
2
+ import Evented from '@ember/object/evented';
3
+ import config from 'ember-get-config';
4
+
5
+ /**
6
+ * Events Service
7
+ *
8
+ * Provides a centralized event tracking system for Fleetbase.
9
+ * This service emits standardized events on both its own event bus and the universe service,
10
+ * allowing components, services, and engines to subscribe and react to application events.
11
+ *
12
+ * @class EventsService
13
+ * @extends Service
14
+ */
15
+ export default class EventsService extends Service.extend(Evented) {
16
+ @service universe;
17
+ @service currentUser;
18
+
19
+ /**
20
+ * Tracks the creation of a resource
21
+ *
22
+ * @param {Object} resource - The created resource/model
23
+ * @param {Object} [props={}] - Additional properties to include
24
+ */
25
+ trackResourceCreated(resource, props = {}) {
26
+ const events = this.#getResourceEvents(resource, 'created');
27
+ const properties = this.#enrichProperties({
28
+ ...this.#getSafeProperties(resource),
29
+ ...props,
30
+ });
31
+
32
+ events.forEach((eventName) => {
33
+ this.#trigger(eventName, resource, properties);
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Tracks the update of a resource
39
+ *
40
+ * @param {Object} resource - The updated resource/model
41
+ * @param {Object} [props={}] - Additional properties to include
42
+ */
43
+ trackResourceUpdated(resource, props = {}) {
44
+ const events = this.#getResourceEvents(resource, 'updated');
45
+ const properties = this.#enrichProperties({
46
+ ...this.#getSafeProperties(resource),
47
+ ...props,
48
+ });
49
+
50
+ events.forEach((eventName) => {
51
+ this.#trigger(eventName, resource, properties);
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Tracks the deletion of a resource
57
+ *
58
+ * @param {Object} resource - The deleted resource/model
59
+ * @param {Object} [props={}] - Additional properties to include
60
+ */
61
+ trackResourceDeleted(resource, props = {}) {
62
+ const events = this.#getResourceEvents(resource, 'deleted');
63
+ const properties = this.#enrichProperties({
64
+ ...this.#getSafeProperties(resource),
65
+ ...props,
66
+ });
67
+
68
+ events.forEach((eventName) => {
69
+ this.#trigger(eventName, resource, properties);
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Tracks a bulk import of resources
75
+ *
76
+ * @param {String} modelName - The name of the model being imported
77
+ * @param {Number} count - Number of resources imported
78
+ * @param {Object} [props={}] - Additional properties to include
79
+ */
80
+ trackResourceImported(modelName, count, props = {}) {
81
+ const properties = this.#enrichProperties({
82
+ model_name: modelName,
83
+ count: count,
84
+ ...props,
85
+ });
86
+
87
+ this.#trigger('resource.imported', modelName, count, properties);
88
+ }
89
+
90
+ /**
91
+ * Tracks a resource export
92
+ *
93
+ * @param {String} modelName - The name of the model being exported
94
+ * @param {String} format - Export format (csv, xlsx, etc.)
95
+ * @param {Object} [params={}] - Export parameters/filters
96
+ * @param {Object} [props={}] - Additional properties to include
97
+ */
98
+ trackResourceExported(modelName, format, params = {}, props = {}) {
99
+ const properties = this.#enrichProperties({
100
+ model_name: modelName,
101
+ export_format: format,
102
+ has_filters: !!(params && Object.keys(params).length > 0),
103
+ ...props,
104
+ });
105
+
106
+ this.#trigger('resource.exported', modelName, format, params, properties);
107
+
108
+ // Also trigger model-specific export event
109
+ const specificEvent = `${modelName}.exported`;
110
+ this.#trigger(specificEvent, modelName, format, params, properties);
111
+ }
112
+
113
+ /**
114
+ * Tracks a bulk action on multiple resources
115
+ *
116
+ * @param {String} verb - The action verb (delete, archive, etc.)
117
+ * @param {Array} resources - Array of selected resources
118
+ * @param {Object} [props={}] - Additional properties to include
119
+ */
120
+ trackBulkAction(verb, resources, props = {}) {
121
+ const firstResource = resources && resources.length > 0 ? resources[0] : null;
122
+ const modelName = this.#getModelName(firstResource);
123
+
124
+ const properties = this.#enrichProperties({
125
+ action: verb,
126
+ count: resources?.length || 0,
127
+ model_name: modelName,
128
+ ...props,
129
+ });
130
+
131
+ this.#trigger('resource.bulk_action', verb, resources, firstResource, properties);
132
+ }
133
+
134
+ /**
135
+ * Tracks when the current user is loaded (session initialized)
136
+ *
137
+ * @param {Object} user - The user object
138
+ * @param {Object} organization - The organization object
139
+ * @param {Object} [props={}] - Additional properties to include
140
+ */
141
+ trackUserLoaded(user, organization, props = {}) {
142
+ const properties = this.#enrichProperties({
143
+ user_id: user?.id,
144
+ organization_id: organization?.id,
145
+ organization_name: organization?.name,
146
+ ...props,
147
+ });
148
+
149
+ this.#trigger('user.loaded', user, organization, properties);
150
+ }
151
+
152
+ /**
153
+ * Tracks when a user session is terminated
154
+ *
155
+ * @param {Number} duration - Session duration in seconds
156
+ * @param {Object} [props={}] - Additional properties to include
157
+ */
158
+ trackSessionTerminated(duration, props = {}) {
159
+ const properties = this.#enrichProperties({
160
+ session_duration: duration,
161
+ ...props,
162
+ });
163
+
164
+ this.#trigger('session.terminated', duration, properties);
165
+ }
166
+
167
+ /**
168
+ * Tracks a generic custom event
169
+ *
170
+ * @param {String} eventName - The event name (dot notation)
171
+ * @param {Object} [props={}] - Event properties
172
+ */
173
+ trackEvent(eventName, props = {}) {
174
+ const properties = this.#enrichProperties(props);
175
+ this.#trigger(eventName, properties);
176
+ }
177
+
178
+ /**
179
+ * Checks if event tracking is enabled
180
+ *
181
+ * @returns {Boolean}
182
+ */
183
+ isEnabled() {
184
+ const eventsConfig = config?.events || {};
185
+ return eventsConfig.enabled !== false; // Enabled by default
186
+ }
187
+
188
+ // =========================================================================
189
+ // Private Methods (using # syntax)
190
+ // =========================================================================
191
+
192
+ /**
193
+ * Triggers an event on both the events service and universe service
194
+ *
195
+ * This dual event system allows listeners to subscribe to events on either:
196
+ * - this.events.on('event.name', handler) - Local listeners
197
+ * - this.universe.on('event.name', handler) - Cross-engine listeners
198
+ *
199
+ * @private
200
+ * @param {String} eventName - The event name
201
+ * @param {...*} args - Arguments to pass to event listeners
202
+ */
203
+ #trigger(eventName, ...args) {
204
+ if (!this.isEnabled()) {
205
+ return;
206
+ }
207
+
208
+ // Debug logging if enabled
209
+ if (config?.events?.debug) {
210
+ console.log(`[Events] ${eventName}`, args);
211
+ }
212
+
213
+ // Trigger on events service (local listeners)
214
+ this.trigger(eventName, ...args);
215
+
216
+ // Trigger on universe service (cross-engine listeners)
217
+ if (this.universe) {
218
+ this.universe.trigger(eventName, ...args);
219
+ } else {
220
+ console.warn('[Events] Universe service not available');
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Generates both generic and specific event names for a resource action
226
+ *
227
+ * @private
228
+ * @param {Object} resource - The resource/model
229
+ * @param {String} action - The action (created, updated, deleted)
230
+ * @returns {Array<String>} Array of event names
231
+ */
232
+ #getResourceEvents(resource, action) {
233
+ const modelName = this.#getModelName(resource);
234
+ return [`resource.${action}`, `${modelName}.${action}`];
235
+ }
236
+
237
+ /**
238
+ * Extracts safe, serializable properties from a resource
239
+ *
240
+ * @private
241
+ * @param {Object} resource - The resource/model
242
+ * @returns {Object} Safe properties object
243
+ */
244
+ #getSafeProperties(resource) {
245
+ if (!resource) {
246
+ return {};
247
+ }
248
+
249
+ const props = {
250
+ id: resource.id,
251
+ model_name: this.#getModelName(resource),
252
+ };
253
+
254
+ // Add common properties if available
255
+ const commonProps = ['name', 'status', 'type', 'slug', 'public_id'];
256
+ commonProps.forEach((prop) => {
257
+ if (resource[prop] !== undefined && resource[prop] !== null) {
258
+ props[prop] = resource[prop];
259
+ }
260
+ });
261
+
262
+ return props;
263
+ }
264
+
265
+ /**
266
+ * Enriches properties with user, organization, and timestamp context
267
+ *
268
+ * @private
269
+ * @param {Object} props - Base properties
270
+ * @returns {Object} Enriched properties
271
+ */
272
+ #enrichProperties(props = {}) {
273
+ const eventsConfig = config?.events || {};
274
+ const enrichConfig = eventsConfig.enrich || {};
275
+ const enriched = { ...props };
276
+
277
+ // Add user context if enabled
278
+ if (enrichConfig.user !== false && this.currentUser?.user) {
279
+ enriched.user_id = this.currentUser.user.id;
280
+ }
281
+
282
+ // Add organization context if enabled
283
+ if (enrichConfig.organization !== false && this.currentUser?.organization) {
284
+ enriched.organization_id = this.currentUser.organization.id;
285
+ }
286
+
287
+ // Add timestamp if enabled
288
+ if (enrichConfig.timestamp !== false) {
289
+ enriched.timestamp = new Date().toISOString();
290
+ }
291
+
292
+ return enriched;
293
+ }
294
+
295
+ /**
296
+ * Safely extracts the model name from a resource
297
+ *
298
+ * @private
299
+ * @param {Object} resource - The resource/model
300
+ * @returns {String} Model name or 'unknown'
301
+ */
302
+ #getModelName(resource) {
303
+ if (!resource) {
304
+ return 'unknown';
305
+ }
306
+
307
+ // Try multiple ways to get model name
308
+ if (resource.constructor?.modelName) {
309
+ return resource.constructor.modelName;
310
+ }
311
+
312
+ if (resource._internalModel?.modelName) {
313
+ return resource._internalModel.modelName;
314
+ }
315
+
316
+ if (resource.modelName) {
317
+ return resource.modelName;
318
+ }
319
+
320
+ return 'unknown';
321
+ }
322
+ }
@@ -155,7 +155,6 @@ export default class LegacyUniverseService extends Service.extend(Evented) {
155
155
  * @return {void}
156
156
  */
157
157
  setApplicationInstance(instance) {
158
- window.Fleetbase = instance;
159
158
  this.applicationInstance = instance;
160
159
  }
161
160
 
@@ -29,6 +29,8 @@ export default class ResourceActionService extends Service {
29
29
  @service abilities;
30
30
  @service tableContext;
31
31
  @service resourceContextPanel;
32
+ @service universe;
33
+ @service events;
32
34
 
33
35
  /**
34
36
  * Getter for router, attempt to use hostRouter if from engine
@@ -299,6 +301,9 @@ export default class ResourceActionService extends Service {
299
301
  })
300
302
  );
301
303
 
304
+ // Track creation event
305
+ this.events.trackResourceCreated(record);
306
+
302
307
  if (options.refresh) {
303
308
  this.refresh();
304
309
  }
@@ -329,6 +334,9 @@ export default class ResourceActionService extends Service {
329
334
  })
330
335
  );
331
336
 
337
+ // Track update event
338
+ this.events.trackResourceUpdated(record);
339
+
332
340
  if (options.refresh) {
333
341
  this.refresh();
334
342
  }
@@ -362,6 +370,13 @@ export default class ResourceActionService extends Service {
362
370
  })
363
371
  );
364
372
 
373
+ // Track save event (create or update)
374
+ if (isNew) {
375
+ this.events.trackResourceCreated(record);
376
+ } else {
377
+ this.events.trackResourceUpdated(record);
378
+ }
379
+
365
380
  if (options.refresh) {
366
381
  this.refresh();
367
382
  }
@@ -409,6 +424,9 @@ export default class ResourceActionService extends Service {
409
424
  })
410
425
  );
411
426
 
427
+ // Track deletion event
428
+ this.events.trackResourceDeleted(record);
429
+
412
430
  if (options.refresh) {
413
431
  this.refresh();
414
432
  }
@@ -1,4 +1,4 @@
1
- import Service from '@ember/service';
1
+ import Service, { inject as service } from '@ember/service';
2
2
  import Evented from '@ember/object/evented';
3
3
  import { tracked } from '@glimmer/tracking';
4
4
  import { getOwner } from '@ember/application';
@@ -22,6 +22,7 @@ import ExtensionBootState from '../../contracts/extension-boot-state';
22
22
  * @extends Service
23
23
  */
24
24
  export default class ExtensionManagerService extends Service.extend(Evented) {
25
+ @service universe;
25
26
  /**
26
27
  * Reference to the root Ember Application Instance.
27
28
  * Used for registering components/services to the application container
@@ -84,14 +85,14 @@ export default class ExtensionManagerService extends Service.extend(Evented) {
84
85
  * @returns {Application}
85
86
  */
86
87
  #getApplication() {
87
- // First priority: use applicationInstance if set
88
- if (this.applicationInstance) {
89
- return this.applicationInstance;
88
+ // First priority use the universe application instance
89
+ if (this.universe.applicationInstance) {
90
+ return this.universe.applicationInstance;
90
91
  }
91
92
 
92
- // Second priority: window.Fleetbase
93
- if (typeof window !== 'undefined' && window.Fleetbase) {
94
- return window.Fleetbase;
93
+ // Second priority: use applicationInstance if set
94
+ if (this.applicationInstance) {
95
+ return this.applicationInstance;
95
96
  }
96
97
 
97
98
  // Third priority: try to get application from owner
@@ -1,4 +1,4 @@
1
- import Service from '@ember/service';
1
+ import Service, { inject as service } from '@ember/service';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { getOwner } from '@ember/application';
4
4
  import Hook from '../../contracts/hook';
@@ -14,6 +14,8 @@ import HookRegistry from '../../contracts/hook-registry';
14
14
  * @extends Service
15
15
  */
16
16
  export default class HookService extends Service {
17
+ @service universe;
18
+
17
19
  /**
18
20
  * Reference to the root Ember Application Instance.
19
21
  * Used for registering components/services to the application container
@@ -69,14 +71,14 @@ export default class HookService extends Service {
69
71
  * @returns {Application}
70
72
  */
71
73
  #getApplication() {
72
- // First priority: use applicationInstance if set
73
- if (this.applicationInstance) {
74
- return this.applicationInstance;
74
+ // First priority use the universe application instance
75
+ if (this.universe.applicationInstance) {
76
+ return this.universe.applicationInstance;
75
77
  }
76
78
 
77
- // Second priority: window.Fleetbase
78
- if (typeof window !== 'undefined' && window.Fleetbase) {
79
- return window.Fleetbase;
79
+ // Second priority: use applicationInstance if set
80
+ if (this.applicationInstance) {
81
+ return this.applicationInstance;
80
82
  }
81
83
 
82
84
  // Third priority: try to get application from owner
@@ -82,21 +82,16 @@ export default class RegistryService extends Service {
82
82
  let application = this.applicationInstance;
83
83
 
84
84
  if (!application) {
85
- // Second priority: window.Fleetbase
86
- if (typeof window !== 'undefined' && window.Fleetbase) {
87
- application = window.Fleetbase;
85
+ // Second priority: try to get from owner
86
+ const owner = getOwner(this);
87
+ if (owner && owner.application) {
88
+ application = owner.application;
88
89
  } else {
89
- // Third priority: try to get from owner
90
- const owner = getOwner(this);
91
- if (owner && owner.application) {
92
- application = owner.application;
93
- } else {
94
- warn('[RegistryService] Could not find application instance for registry initialization', {
95
- id: 'registry-service.no-application',
96
- });
97
- // Return a new instance as fallback (won't be shared)
98
- return new UniverseRegistry();
99
- }
90
+ warn('[RegistryService] Could not find application instance for registry initialization', {
91
+ id: 'registry-service.no-application',
92
+ });
93
+ // Return a new instance as fallback (won't be shared)
94
+ return new UniverseRegistry();
100
95
  }
101
96
  }
102
97
 
@@ -489,7 +484,7 @@ export default class RegistryService extends Service {
489
484
  * );
490
485
  */
491
486
  async registerHelper(helperName, helperClassOrTemplateHelper, options = {}) {
492
- const owner = this.applicationInstance || (typeof window !== 'undefined' && window.Fleetbase) || getOwner(this);
487
+ const owner = this.applicationInstance || getOwner(this);
493
488
 
494
489
  if (!owner) {
495
490
  warn('No owner available for helper registration. Cannot register helper.', {
@@ -539,7 +534,7 @@ export default class RegistryService extends Service {
539
534
  * @returns {Promise<Function|Class|null>} The loaded helper or null if failed
540
535
  */
541
536
  async #loadHelperFromEngine(templateHelper) {
542
- const owner = this.applicationInstance || (typeof window !== 'undefined' && window.Fleetbase) || getOwner(this);
537
+ const owner = this.applicationInstance || getOwner(this);
543
538
 
544
539
  if (!owner) {
545
540
  return null;