@fleetbase/ember-core 0.1.1 → 0.1.3

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.
@@ -3,6 +3,7 @@ import { inject as service } from '@ember/service';
3
3
  import { action } from '@ember/object';
4
4
  import { isArray } from '@ember/array';
5
5
  import { dasherize } from '@ember/string';
6
+ import { later } from '@ember/runloop';
6
7
  import { pluralize } from 'ember-inflector';
7
8
  import { format as formatDate } from 'date-fns';
8
9
  import getModelName from '../utils/get-model-name';
@@ -193,6 +194,7 @@ export default class CrudService extends Service {
193
194
  this.modalsManager.show('modals/export-form', {
194
195
  title: `Export ${pluralize(modelName)}`,
195
196
  acceptButtonText: 'Download',
197
+ modalClass: 'modal-sm',
196
198
  formatOptions: ['csv', 'xlsx', 'xls', 'html', 'pdf'],
197
199
  setFormat: ({ target }) => {
198
200
  this.modalsManager.setOption('format', target.value || null);
@@ -211,9 +213,13 @@ export default class CrudService extends Service {
211
213
  }
212
214
  )
213
215
  .then(() => {
214
- setTimeout(() => {
215
- return done();
216
- }, 600);
216
+ later(
217
+ this,
218
+ () => {
219
+ return done();
220
+ },
221
+ 600
222
+ );
217
223
  })
218
224
  .catch((error) => {
219
225
  modal.stopLoading();
@@ -538,7 +538,10 @@ export default class FetchService extends Service {
538
538
  withCredentials: true,
539
539
  headers,
540
540
  })
541
- .then((response) => response.json());
541
+ .then((response) => response.json())
542
+ .catch((error) => {
543
+ this.notifications.serverError(error, `Upload failed.`);
544
+ });
542
545
 
543
546
  const model = this.store.push(this.store.normalize('file', upload.file));
544
547
  set(file, 'model', model);
@@ -1,22 +1,19 @@
1
1
  import EmberNotificationsService from 'ember-cli-notifications/services/notifications';
2
2
  import { isArray } from '@ember/array';
3
+ import getWithDefault from '../utils/get-with-default';
3
4
 
4
5
  export default class NotificationsService extends EmberNotificationsService {
5
- /**
6
- * Handles errors from the server
7
- *
8
- * @param {Error} error
9
- * @void
10
- */
11
- serverError(error, fallbackMessage = 'Oops! Something went wrong with your request.', options) {
6
+ serverError(error, fallbackMessage = 'Oops! Something went wrong with your request.', options = {}) {
12
7
  if (isArray(error.errors)) {
13
- const errorMessage = error.errors.firstObject;
8
+ const errors = getWithDefault(error, 'errors');
9
+ const errorMessage = getWithDefault(errors, '0', fallbackMessage);
14
10
 
15
- return this.error(errorMessage ?? fallbackMessage, options);
11
+ return this.error(errorMessage, options);
16
12
  }
17
13
 
18
14
  if (error instanceof Error) {
19
- return this.error(error.message ?? fallbackMessage, options);
15
+ const errorMessage = getWithDefault(error, 'message', fallbackMessage);
16
+ return this.error(errorMessage, options);
20
17
  }
21
18
 
22
19
  return this.error(error ?? fallbackMessage, options);
@@ -31,18 +31,20 @@ export default class SocketService extends Service {
31
31
  async listen(channelId, callback) {
32
32
  const channel = this.socket.subscribe(channelId);
33
33
 
34
- // track channel
34
+ // Track channel
35
35
  this.channels.pushObject(channel);
36
36
 
37
- // listen to channel for events
37
+ // Listen to channel for events
38
38
  await channel.listener('subscribe').once();
39
39
 
40
- // get incoming data and console out
41
- for await (let output of channel) {
42
- if (typeof callback === 'function') {
43
- callback(output);
40
+ // Listen for channel subscription
41
+ (async () => {
42
+ for await (let output of channel) {
43
+ if (typeof callback === 'function') {
44
+ callback(output);
45
+ }
44
46
  }
45
- }
47
+ })();
46
48
  }
47
49
 
48
50
  closeChannels() {
@@ -5,7 +5,8 @@ import { inject as service } from '@ember/service';
5
5
  import { computed, action } from '@ember/object';
6
6
  import { isBlank } from '@ember/utils';
7
7
  import { isArray } from '@ember/array';
8
- import { dasherize } from '@ember/string';
8
+ import { later } from '@ember/runloop';
9
+ import { dasherize, camelize } from '@ember/string';
9
10
  import { getOwner } from '@ember/application';
10
11
  import { assert } from '@ember/debug';
11
12
  import RSVP from 'rsvp';
@@ -106,6 +107,148 @@ export default class UniverseService extends Service.extend(Evented) {
106
107
  return this.router.transitionTo(route, slug, 'index');
107
108
  }
108
109
 
110
+ /**
111
+ * @action
112
+ * Creates a new registry with the given name and options.
113
+
114
+ * @memberof UniverseService
115
+ * @param {string} registryName - The name of the registry to create.
116
+ * @param {Object} [options={}] - Optional settings for the registry.
117
+ * @param {Array} [options.menuItems=[]] - An array of menu items for the registry.
118
+ * @param {Array} [options.menuPanel=[]] - An array of menu panels for the registry.
119
+ *
120
+ * @fires registry.created - Event triggered when a new registry is created.
121
+ *
122
+ * @returns {UniverseService} Returns the current UniverseService for chaining.
123
+ *
124
+ * @example
125
+ * createRegistry('myRegistry', { menuItems: ['item1', 'item2'], menuPanel: ['panel1', 'panel2'] });
126
+ */
127
+ @action createRegistry(registryName, options = {}) {
128
+ const internalRegistryName = this.createInternalRegistryName(registryName);
129
+
130
+ this[internalRegistryName] = {
131
+ name: registryName,
132
+ menuItems: [],
133
+ menuPanels: [],
134
+ ...options,
135
+ };
136
+
137
+ // trigger registry created event
138
+ this.trigger('registry.created', this[internalRegistryName]);
139
+
140
+ return this;
141
+ }
142
+
143
+ /**
144
+ * @action
145
+ * Retrieves the entire registry with the given name.
146
+ *
147
+ * @memberof UniverseService
148
+ * @param {string} registryName - The name of the registry to retrieve.
149
+ *
150
+ * @returns {Object|null} Returns the registry object if it exists; otherwise, returns null.
151
+ *
152
+ * @example
153
+ * const myRegistry = getRegistry('myRegistry');
154
+ */
155
+ @action getRegistry(registryName) {
156
+ const internalRegistryName = this.createInternalRegistryName(registryName);
157
+ const registry = this[internalRegistryName];
158
+
159
+ if (!isBlank(registry)) {
160
+ return registry;
161
+ }
162
+
163
+ return null;
164
+ }
165
+
166
+ /**
167
+ * Looks up a registry by its name and returns it as a Promise.
168
+ *
169
+ * @memberof UniverseService
170
+ * @param {string} registryName - The name of the registry to look up.
171
+ *
172
+ * @returns {Promise<Object|null>} A Promise that resolves to the registry object if it exists; otherwise, rejects with null.
173
+ *
174
+ * @example
175
+ * lookupRegistry('myRegistry')
176
+ * .then((registry) => {
177
+ * // Do something with the registry
178
+ * })
179
+ * .catch((error) => {
180
+ * // Handle the error or absence of the registry
181
+ * });
182
+ */
183
+ lookupRegistry(registryName) {
184
+ const internalRegistryName = this.createInternalRegistryName(registryName);
185
+ const registry = this[internalRegistryName];
186
+
187
+ return new Promise((resolve, reject) => {
188
+ if (!isBlank(registry)) {
189
+ return resolve(registry);
190
+ }
191
+
192
+ later(
193
+ this,
194
+ () => {
195
+ if (!isBlank(registry)) {
196
+ return resolve(registry);
197
+ }
198
+ },
199
+ 100
200
+ );
201
+
202
+ reject(null);
203
+ });
204
+ }
205
+
206
+ /**
207
+ * @action
208
+ * Retrieves the menu items from a registry with the given name.
209
+ *
210
+ * @memberof UniverseService
211
+ * @param {string} registryName - The name of the registry to retrieve menu items from.
212
+ *
213
+ * @returns {Array} Returns an array of menu items if the registry exists and has menu items; otherwise, returns an empty array.
214
+ *
215
+ * @example
216
+ * const items = getMenuItemsFromRegistry('myRegistry');
217
+ */
218
+ @action getMenuItemsFromRegistry(registryName) {
219
+ const internalRegistryName = this.createInternalRegistryName(registryName);
220
+ const registry = this[internalRegistryName];
221
+
222
+ if (!isBlank(registry) && isArray(registry.menuItems)) {
223
+ return registry.menuItems;
224
+ }
225
+
226
+ return [];
227
+ }
228
+
229
+ /**
230
+ * @action
231
+ * Retrieves the menu panels from a registry with the given name.
232
+ *
233
+ * @memberof UniverseService
234
+ * @param {string} registryName - The name of the registry to retrieve menu panels from.
235
+ *
236
+ * @returns {Array} Returns an array of menu panels if the registry exists and has menu panels; otherwise, returns an empty array.
237
+ *
238
+ * @example
239
+ * const panels = getMenuPanelsFromRegistry('myRegistry');
240
+ */
241
+ @action getMenuPanelsFromRegistry(registryName) {
242
+ const internalRegistryName = this.createInternalRegistryName(registryName);
243
+ const registry = this[internalRegistryName];
244
+
245
+ if (!isBlank(registry) && isArray(registry.menuPanels)) {
246
+ return registry.menuPanels;
247
+ }
248
+
249
+ return [];
250
+ }
251
+
109
252
  /**
110
253
  * Loads a component from the specified registry based on a given slug and view.
111
254
  *
@@ -116,15 +259,16 @@ export default class UniverseService extends Service.extend(Evented) {
116
259
  * @returns {Promise} Returns a Promise that resolves with the component if it is found, or null.
117
260
  */
118
261
  loadComponentFromRegistry(registryName, slug, view = null) {
119
- const registry = this[`${registryName}Registry`];
120
-
121
- if (isBlank(registry)) {
122
- return null;
123
- }
262
+ const internalRegistryName = this.createInternalRegistryName(registryName);
263
+ const registry = this[internalRegistryName];
124
264
 
125
265
  return new Promise((resolve) => {
126
266
  let component = null;
127
267
 
268
+ if (isBlank(registry)) {
269
+ return resolve(component);
270
+ }
271
+
128
272
  // check menu items first
129
273
  for (let i = 0; i < registry.menuItems.length; i++) {
130
274
  const menuItem = registry.menuItems[i];
@@ -177,15 +321,16 @@ export default class UniverseService extends Service.extend(Evented) {
177
321
  * @returns {Promise} Returns a Promise that resolves with the menu item if it is found, or null.
178
322
  */
179
323
  lookupMenuItemFromRegistry(registryName, slug, view = null) {
180
- const registry = this[`${registryName}Registry`];
181
-
182
- if (isBlank(registry)) {
183
- return null;
184
- }
324
+ const internalRegistryName = this.createInternalRegistryName(registryName);
325
+ const registry = this[internalRegistryName];
185
326
 
186
327
  return new Promise((resolve) => {
187
328
  let foundMenuItem = null;
188
329
 
330
+ if (isBlank(registry)) {
331
+ return resolve(foundMenuItem);
332
+ }
333
+
189
334
  // check menu items first
190
335
  for (let i = 0; i < registry.menuItems.length; i++) {
191
336
  const menuItem = registry.menuItems[i];
@@ -240,10 +385,10 @@ export default class UniverseService extends Service.extend(Evented) {
240
385
  * @param {Object} options Additional options for the panel
241
386
  */
242
387
  registerMenuPanel(registryName, title, items = [], options = {}) {
388
+ const internalRegistryName = this.createInternalRegistryName(registryName);
243
389
  const open = this._getOption(options, 'open', true);
244
390
  const slug = this._getOption(options, 'slug', dasherize(title));
245
-
246
- this[`${registryName}Registry`].menuPanels.pushObject({
391
+ const menuPanel = {
247
392
  title,
248
393
  open,
249
394
  items: items.map(({ title, route, ...options }) => {
@@ -252,7 +397,13 @@ export default class UniverseService extends Service.extend(Evented) {
252
397
 
253
398
  return this._createMenuItem(title, route, options);
254
399
  }),
255
- });
400
+ };
401
+
402
+ // register menu panel
403
+ this[internalRegistryName].menuPanels.pushObject(menuPanel);
404
+
405
+ // trigger menu panel registered event
406
+ this.trigger('menuPanel.registered', menuPanel, this[internalRegistryName]);
256
407
  }
257
408
 
258
409
  /**
@@ -267,7 +418,8 @@ export default class UniverseService extends Service.extend(Evented) {
267
418
  * @param {Object} options Additional options for the item
268
419
  */
269
420
  registerMenuItem(registryName, title, options = {}) {
270
- const route = this._getOption(options, 'route', `console.${registryName}.virtual`);
421
+ const internalRegistryName = this.createInternalRegistryName(registryName);
422
+ const route = this._getOption(options, 'route', `console.${dasherize(registryName)}.virtual`);
271
423
  options.slug = this._getOption(options, 'slug', '~');
272
424
  options.view = this._getOption(options, 'view', dasherize(title));
273
425
 
@@ -276,7 +428,45 @@ export default class UniverseService extends Service.extend(Evented) {
276
428
  options.view = null;
277
429
  }
278
430
 
279
- this[`${registryName}Registry`].menuItems.pushObject(this._createMenuItem(title, route, options));
431
+ // register component if applicable
432
+ this.registerMenuItemComponentToEngine(options);
433
+
434
+ // create menu item
435
+ const menuItem = this._createMenuItem(title, route, options);
436
+
437
+ // register menu item
438
+ this[internalRegistryName].menuItems.pushObject(menuItem);
439
+
440
+ // trigger menu panel registered event
441
+ this.trigger('menuItem.registered', menuItem, this[internalRegistryName]);
442
+ }
443
+
444
+ /**
445
+ * Registers a menu item's component to one or multiple engines.
446
+ *
447
+ * @method registerMenuItemComponentToEngine
448
+ * @public
449
+ * @memberof UniverseService
450
+ * @param {Object} options - An object containing the following properties:
451
+ * - `registerComponentToEngine`: A string or an array of strings representing the engine names where the component should be registered.
452
+ * - `component`: The component class to register, which should have a 'name' property.
453
+ */
454
+ registerMenuItemComponentToEngine(options) {
455
+ // Register component if applicable
456
+ if (typeof options.registerComponentToEngine === 'string') {
457
+ this.registerComponentInEngine(options.registerComponentToEngine, options.component);
458
+ }
459
+
460
+ // register to multiple engines
461
+ if (isArray(options.registerComponentToEngine)) {
462
+ for (let i = 0; i < options.registerComponentInEngine.length; i++) {
463
+ const engineName = options.registerComponentInEngine.objectAt(i);
464
+
465
+ if (typeof engineName === 'string') {
466
+ this.registerComponentInEngine(engineName, options.component);
467
+ }
468
+ }
469
+ }
280
470
  }
281
471
 
282
472
  /**
@@ -424,6 +614,14 @@ export default class UniverseService extends Service.extend(Evented) {
424
614
  const onClick = this._getOption(options, 'onClick', null);
425
615
  const section = this._getOption(options, 'section', null);
426
616
 
617
+ // dasherize route segments
618
+ if (typeof route === 'string') {
619
+ route = route
620
+ .split('.')
621
+ .map((segment) => dasherize(segment))
622
+ .join('.');
623
+ }
624
+
427
625
  // todo: create menu item class
428
626
  const menuItem = {
429
627
  title,
@@ -450,6 +648,35 @@ export default class UniverseService extends Service.extend(Evented) {
450
648
  return menuItem;
451
649
  }
452
650
 
651
+ /**
652
+ * Creates an internal registry name by camelizing the provided registry name and appending "Registry" to it.
653
+ *
654
+ * @method createInternalRegistryName
655
+ * @public
656
+ * @memberof UniverseService
657
+ * @param {String} registryName - The name of the registry to be camelized and formatted.
658
+ * @returns {String} The formatted internal registry name.
659
+ */
660
+ createInternalRegistryName(registryName) {
661
+ return `${camelize(registryName.replace(/[^a-zA-Z0-9]/g, '-'))}Registry`;
662
+ }
663
+
664
+ /**
665
+ * Manually registers a component in a specified engine.
666
+ *
667
+ * @method registerComponentInEngine
668
+ * @public
669
+ * @memberof UniverseService
670
+ * @param {String} engineName - The name of the engine where the component should be registered.
671
+ * @param {Object} componentClass - The component class to register, which should have a 'name' property.
672
+ */
673
+ registerComponentInEngine(engineName, componentClass) {
674
+ const engineInstance = this.getEngineInstance(engineName);
675
+ if (engineInstance && !isBlank(componentClass) && typeof componentClass.name === 'string') {
676
+ engineInstance.register(`component:${componentClass.name}`, componentClass);
677
+ }
678
+ }
679
+
453
680
  /**
454
681
  * Load the specified engine. If it is not loaded yet, it will use assetLoader
455
682
  * to load it and then register it to the router.
@@ -529,6 +756,28 @@ export default class UniverseService extends Service.extend(Evented) {
529
756
  });
530
757
  }
531
758
 
759
+ /**
760
+ * Retrieve an existing engine instance by its name and instanceId.
761
+ *
762
+ * @method getEngineInstance
763
+ * @public
764
+ * @memberof UniverseService
765
+ * @param {String} name The name of the engine
766
+ * @param {String} [instanceId='manual'] The id of the engine instance (defaults to 'manual')
767
+ * @returns {Object|null} The engine instance if it exists, otherwise null
768
+ */
769
+ getEngineInstance(name, instanceId = 'manual') {
770
+ const owner = getOwner(this);
771
+ const router = owner.lookup('router:main');
772
+ const engineInstances = router._engineInstances;
773
+
774
+ if (engineInstances && engineInstances[name]) {
775
+ return engineInstances[name][instanceId] || null;
776
+ }
777
+
778
+ return null;
779
+ }
780
+
532
781
  /**
533
782
  * Alias for intl service `t`
534
783
  *
@@ -2,55 +2,39 @@ import config from '@fleetbase/console/config/environment';
2
2
  import { isBlank } from '@ember/utils';
3
3
 
4
4
  const isDevelopment = ['local', 'development'].includes(config.environment);
5
- const isProduction = ['production'].includes(config.environment);
6
5
 
7
- function queryString(params) {
6
+ export function queryString(params) {
8
7
  return Object.keys(params)
9
- .map((key) => `${key}=${params[key]}`)
8
+ .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
10
9
  .join('&');
11
10
  }
12
11
 
13
- function extractHostAndPort(url) {
12
+ export function extractHostAndPort(url) {
14
13
  try {
15
- const parsedUrl = new URL(url);
16
- const host = parsedUrl.hostname;
17
- const port = parsedUrl.port;
18
-
19
- return {
20
- host: host,
21
- port: port || null,
22
- };
14
+ const { hostname: host, port = null } = new URL(url);
15
+ return { host, port };
23
16
  } catch (error) {
24
- console.error('Invalid URL:', error);
25
- return null;
17
+ return { host: null, port: null };
26
18
  }
27
19
  }
28
20
 
29
- export default function consoleUrl(path = '', queryParams = {}, subdomain = 'console', host = 'fleetbase.io') {
30
- let parsedHost = extractHostAndPort(host);
31
- let url = isDevelopment ? 'http://' : 'https://';
32
-
33
- if (subdomain) {
34
- url += subdomain + '.';
35
- }
36
-
37
- let urlParams = !isBlank(queryParams) ? queryString(queryParams) : '';
38
-
39
- if (!isProduction && !isDevelopment) {
40
- url += `${config.environment}.`;
21
+ export default function consoleUrl(path = '', queryParams = {}, subdomain = null, host = null) {
22
+ if (subdomain === null || host === null) {
23
+ const { hostname, host: currentHost } = window.location;
24
+ if (subdomain === null) {
25
+ const parts = hostname.split('.');
26
+ subdomain = parts.length > 2 ? parts[0] : null;
27
+ }
28
+ if (host === null) {
29
+ host = currentHost;
30
+ }
41
31
  }
42
32
 
43
- url += parsedHost.host;
44
-
45
- if (parsedHost.port) {
46
- url += `:${parsedHost.port}`;
47
- }
48
-
49
- url += `/${path}`;
50
-
51
- if (urlParams) {
52
- url += `?${urlParams}`;
53
- }
33
+ const { host: parsedHost, port } = extractHostAndPort(host);
34
+ const protocol = isDevelopment ? 'http://' : 'https://';
35
+ const urlParams = !isBlank(queryParams) ? queryString(queryParams) : '';
36
+ const portSegment = port ? `:${port}` : '';
37
+ const pathSegment = path.startsWith('/') ? path : `/${path}`;
54
38
 
55
- return url;
39
+ return `${protocol}${subdomain ? subdomain + '.' : ''}${parsedHost}${portSegment}${pathSegment}${urlParams ? '?' + urlParams : ''}`;
56
40
  }
@@ -1,3 +1,5 @@
1
+ import { later } from '@ember/runloop';
2
+
1
3
  function corslite(url, callback, cors) {
2
4
  var sent = false;
3
5
 
@@ -28,11 +30,13 @@ function corslite(url, callback, cors) {
28
30
  if (sent) {
29
31
  original.apply(this, arguments);
30
32
  } else {
31
- var that = this,
32
- args = arguments;
33
- setTimeout(function () {
34
- original.apply(that, args);
35
- }, 0);
33
+ later(
34
+ this,
35
+ function () {
36
+ original.apply(this, arguments);
37
+ },
38
+ 0
39
+ );
36
40
  }
37
41
  };
38
42
  }
@@ -1,3 +1,5 @@
1
+ import { later } from '@ember/runloop';
2
+
1
3
  export default function download(data, strFileName, strMimeType) {
2
4
  var self = window, // this script is only for browsers anyway...
3
5
  defaultMime = 'application/octet-stream', // this default mime also triggers iframe downloads
@@ -33,9 +35,13 @@ export default function download(data, strFileName, strMimeType) {
33
35
  ajax.onload = function (e) {
34
36
  download(e.target.response, fileName, defaultMime);
35
37
  };
36
- setTimeout(function () {
37
- ajax.send();
38
- }, 0); // allows setting custom ajax headers using the return:
38
+ later(
39
+ this,
40
+ function () {
41
+ ajax.send();
42
+ },
43
+ 0
44
+ ); // allows setting custom ajax headers using the return:
39
45
  return ajax;
40
46
  } // end if valid url?
41
47
  } // end if url?
@@ -90,15 +96,23 @@ export default function download(data, strFileName, strMimeType) {
90
96
  this.removeEventListener('click', arguments.callee);
91
97
  });
92
98
  document.body.appendChild(anchor);
93
- setTimeout(function () {
94
- anchor.click();
95
- document.body.removeChild(anchor);
96
- if (winMode === true) {
97
- setTimeout(function () {
98
- self.URL.revokeObjectURL(anchor.href);
99
- }, 250);
100
- }
101
- }, 66);
99
+ later(
100
+ this,
101
+ function () {
102
+ anchor.click();
103
+ document.body.removeChild(anchor);
104
+ if (winMode === true) {
105
+ later(
106
+ this,
107
+ function () {
108
+ self.URL.revokeObjectURL(anchor.href);
109
+ },
110
+ 250
111
+ );
112
+ }
113
+ },
114
+ 66
115
+ );
102
116
  return true;
103
117
  }
104
118
 
@@ -123,9 +137,13 @@ export default function download(data, strFileName, strMimeType) {
123
137
  url = 'data:' + url.replace(/^data:([\w\/\-\+]+)/, defaultMime);
124
138
  }
125
139
  f.src = url;
126
- setTimeout(function () {
127
- document.body.removeChild(f);
128
- }, 333);
140
+ later(
141
+ this,
142
+ function () {
143
+ document.body.removeChild(f);
144
+ },
145
+ 333
146
+ );
129
147
  } //end saver
130
148
 
131
149
  if (navigator.msSaveBlob) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/ember-core",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Provides all the core services, decorators and utilities for building a Fleetbase extension for the Console.",
5
5
  "keywords": [
6
6
  "fleetbase-core",