UNPKG

@fleetbase/ember-core

Version:

Provides all the core services, decorators and utilities for building a Fleetbase extension for the Console.

1,299 lines (1,151 loc) 77.5 kB
import Service from '@ember/service'; import Evented from '@ember/object/evented'; import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; import { computed, action } from '@ember/object'; import { isBlank } from '@ember/utils'; import { A, isArray } from '@ember/array'; import { later } from '@ember/runloop'; import { dasherize, camelize } from '@ember/string'; import { pluralize } from 'ember-inflector'; import { getOwner } from '@ember/application'; import { assert, debug, warn } from '@ember/debug'; import RSVP from 'rsvp'; import loadInstalledExtensions from '../utils/load-installed-extensions'; import loadExtensions from '../utils/load-extensions'; import getWithDefault from '../utils/get-with-default'; import config from 'ember-get-config'; export default class UniverseService extends Service.extend(Evented) { @service router; @service intl; @service urlSearchParams; @tracked applicationInstance; @tracked enginesBooted = false; @tracked bootedExtensions = A([]); @tracked headerMenuItems = A([]); @tracked organizationMenuItems = A([]); @tracked userMenuItems = A([]); @tracked consoleAdminRegistry = { menuItems: A([]), menuPanels: A([]), }; @tracked consoleAccountRegistry = { menuItems: A([]), menuPanels: A([]), }; @tracked consoleSettingsRegistry = { menuItems: A([]), menuPanels: A([]), }; @tracked dashboardWidgets = { defaultWidgets: A([]), widgets: A([]), }; @tracked hooks = {}; @tracked bootCallbacks = A([]); @tracked initialLocation = { ...window.location }; /** * Computed property that returns all administrative menu items. * * @computed adminMenuItems * @public * @readonly * @memberof UniverseService * @returns {Array} Array of administrative menu items */ @computed('consoleAdminRegistry.menuItems.[]') get adminMenuItems() { return this.consoleAdminRegistry.menuItems; } /** * Computed property that returns all administrative menu panels. * * @computed adminMenuPanels * @public * @readonly * @memberof UniverseService * @returns {Array} Array of administrative menu panels */ @computed('consoleAdminRegistry.menuPanels.[]') get adminMenuPanels() { return this.consoleAdminRegistry.menuPanels; } /** * Computed property that returns all settings menu items. * * @computed settingsMenuItems * @public * @readonly * @memberof UniverseService * @returns {Array} Array of administrative menu items */ @computed('consoleSettingsRegistry.menuItems.[]') get settingsMenuItems() { return this.consoleSettingsRegistry.menuItems; } /** * Computed property that returns all settings menu panels. * * @computed settingsMenuPanels * @public * @readonly * @memberof UniverseService * @returns {Array} Array of administrative menu panels */ @computed('consoleSettingsRegistry.menuPanels.[]') get settingsMenuPanels() { return this.consoleSettingsRegistry.menuPanels; } /** * Transitions to a given route within a specified Ember engine. * * This action dynamically retrieves the specified engine's instance and its configuration to prepend the * engine's route prefix to the provided route. If the engine instance or its route prefix is not found, * it falls back to transitioning to the route without the prefix. * * @param {string} engineName - The name of the Ember engine. * @param {string} route - The route to transition to within the engine. * @param {...any} args - Additional arguments to pass to the router's transitionTo method. * @returns {Promise} A Promise that resolves with the result of the router's transitionTo method. * * @example * // Transitions to the 'management.fleets.index.new' route within the '@fleetbase/fleet-ops' engine. * this.transitionToEngineRoute('@fleetbase/fleet-ops', 'management.fleets.index.new'); */ @action transitionToEngineRoute(engineName, route, ...args) { const engineInstance = this.getEngineInstance(engineName); if (engineInstance) { const config = engineInstance.resolveRegistration('config:environment'); if (config) { let mountedEngineRoutePrefix = config.mountedEngineRoutePrefix; if (!mountedEngineRoutePrefix) { mountedEngineRoutePrefix = this._mountPathFromEngineName(engineName); } if (!mountedEngineRoutePrefix.endsWith('.')) { mountedEngineRoutePrefix = mountedEngineRoutePrefix + '.'; } return this.router.transitionTo(`${mountedEngineRoutePrefix}${route}`, ...args); } } return this.router.transitionTo(route, ...args); } /** * Initialize the universe service. * * @memberof UniverseService */ initialize() { this.initialLocation = { ...window.location }; this.trigger('init', this); } /** * Sets the application instance. * * @param {ApplicationInstance} - The application instance object. * @return {void} */ setApplicationInstance(instance) { window.Fleetbase = instance; this.applicationInstance = instance; } /** * Retrieves the application instance. * * @returns {ApplicationInstance} - The application instance object. */ getApplicationInstance() { return this.applicationInstance; } /** * Retrieves the mount point of a specified engine by its name. * @param {string} engineName - The name of the engine for which to get the mount point. * @returns {string|null} The mount point of the engine or null if not found. */ getEngineMountPoint(engineName) { const engineInstance = this.getEngineInstance(engineName); return this._getMountPointFromEngineInstance(engineInstance); } /** * Determines the mount point from an engine instance by reading its configuration. * @param {object} engineInstance - The instance of the engine. * @returns {string|null} The resolved mount point or null if the instance is undefined or the configuration is not set. * @private */ _getMountPointFromEngineInstance(engineInstance) { if (engineInstance) { const config = engineInstance.resolveRegistration('config:environment'); if (config) { let engineName = config.modulePrefix; let mountedEngineRoutePrefix = config.mountedEngineRoutePrefix; if (!mountedEngineRoutePrefix) { mountedEngineRoutePrefix = this._mountPathFromEngineName(engineName); } if (!mountedEngineRoutePrefix.endsWith('.')) { mountedEngineRoutePrefix = mountedEngineRoutePrefix + '.'; } return mountedEngineRoutePrefix; } } return null; } /** * Extracts and formats the mount path from a given engine name. * * This function takes an engine name in the format '@scope/engine-name', * extracts the 'engine-name' part, removes the '-engine' suffix if present, * and formats it into a string that represents a console path. * * @param {string} engineName - The full name of the engine, typically in the format '@scope/engine-name'. * @returns {string} A string representing the console path derived from the engine name. * @example * // returns 'console.some' * _mountPathFromEngineName('@fleetbase/some-engine'); */ _mountPathFromEngineName(engineName) { let engineNameSegments = engineName.split('/'); let mountName = engineNameSegments[1]; if (typeof mountName !== 'string') { mountName = engineNameSegments[0]; } const mountPath = mountName.replace('-engine', ''); return `console.${mountPath}`; } /** * Refreshes the current route. * * This action is a simple wrapper around the router's refresh method. It can be used to re-run the * model hooks and reset the controller properties on the current route, effectively reloading the route. * This is particularly useful in scenarios where the route needs to be reloaded due to changes in * state or data. * * @returns {Promise} A Promise that resolves with the result of the router's refresh method. * * @example * // To refresh the current route * this.refreshRoute(); */ @action refreshRoute() { return this.router.refresh(); } /** * Action to transition to a specified route based on the provided menu item. * * The route transition will include the 'slug' as a dynamic segment, and * the 'view' as an optional dynamic segment if it is defined. * * @action * @memberof UniverseService * @param {string} route - The target route to transition to. * @param {Object} menuItem - The menu item containing the transition parameters. * @param {string} menuItem.slug - The 'slug' dynamic segment for the route. * @param {string} [menuItem.view] - The 'view' dynamic segment for the route, if applicable. * * @returns {Transition} Returns a Transition object representing the transition to the route. */ @action transitionMenuItem(route, menuItem) { const { slug, view, section } = menuItem; if (section && slug && view) { return this.router.transitionTo(route, section, slug, { queryParams: { view } }); } if (section && slug) { return this.router.transitionTo(route, section, slug); } if (slug && view) { return this.router.transitionTo(route, slug, { queryParams: { view } }); } return this.router.transitionTo(route, slug); } /** * Redirects to a virtual route if a corresponding menu item exists based on the current URL slug. * * This asynchronous function checks whether a virtual route exists by extracting the slug from the current * window's pathname and looking up a matching menu item in a specified registry. If a matching menu item * is found, it initiates a transition to the given route associated with that menu item and returns the * transition promise. * * @async * * @param {Object} transition - The current transition object from the router. * Used to retrieve additional information required for the menu item lookup. * @param {string} registryName - The name of the registry to search for the menu item. * This registry should contain menu items mapped by their slugs. * @param {string} route - The name of the route to transition to if the menu item is found. * This is typically the route associated with displaying the menu item's content. * * @returns {Promise|undefined} - Returns a promise that resolves when the route transition completes * if a matching menu item is found. If no matching menu item is found, the function returns undefined. * */ async virtualRouteRedirect(transition, registryName, route, options = {}) { const view = this.getViewFromTransition(transition); const slug = window.location.pathname.replace('/', ''); const queryParams = this.urlSearchParams.all(); const menuItem = await this.lookupMenuItemFromRegistry(registryName, slug, view); if (menuItem && transition.from === null) { return this.transitionMenuItem(route, menuItem, { queryParams }).then((transition) => { if (options && options.restoreQueryParams === true) { this.urlSearchParams.setParamsToCurrentUrl(queryParams); } return transition; }); } } /** * @action * Creates a new registry with the given name and options. * @memberof UniverseService * @param {string} registryName - The name of the registry to create. * @param {Object} [options={}] - Optional settings for the registry. * @param {Array} [options.menuItems=[]] - An array of menu items for the registry. * @param {Array} [options.menuPanel=[]] - An array of menu panels for the registry. * * @fires registry.created - Event triggered when a new registry is created. * * @returns {UniverseService} Returns the current UniverseService for chaining. * * @example * createRegistry('myRegistry', { menuItems: ['item1', 'item2'], menuPanel: ['panel1', 'panel2'] }); */ @action createRegistry(registryName, options = {}) { const internalRegistryName = this.createInternalRegistryName(registryName); if (this[internalRegistryName] == undefined) { this[internalRegistryName] = { name: registryName, menuItems: [], menuPanels: [], renderableComponents: [], ...options, }; } else { this[internalRegistryName] = { ...this[internalRegistryName], ...options, }; } // trigger registry created event this.trigger('registry.created', this[internalRegistryName]); return this; } /** * Creates multiple registries from a given array of registries. Each registry can be either a string or an array. * If a registry is an array, it expects two elements: the registry name (string) and registry options (object). * If a registry is a string, only the registry name is needed. * * The function iterates over each element in the `registries` array and creates a registry using the `createRegistry` method. * It supports two types of registry definitions: * 1. Array format: [registryName, registryOptions] - where registryOptions is an optional object. * 2. String format: "registryName" - in this case, only the name is provided and the registry is created with default options. * * @param {Array} registries - An array of registries to be created. Each element can be either a string or an array. * @action * @memberof YourComponentOrClassName */ @action createRegistries(registries = []) { if (!isArray(registries)) { throw new Error('`createRegistries()` method must take an array.'); } for (let i = 0; i < registries.length; i++) { const registry = registries[i]; if (isArray(registry) && registry.length === 2) { let registryName = registry[0]; let registryOptions = registry[1] ?? {}; this.createRegistry(registryName, registryOptions); continue; } if (typeof registry === 'string') { this.createRegistry(registry); } } } /** * Triggers an event on for a universe registry. * * @memberof UniverseService * @method createRegistryEvent * @param {string} registryName - The name of the registry to trigger the event on. * @param {string} event - The name of the event to trigger. * @param {...*} params - Additional parameters to pass to the event handler. */ @action createRegistryEvent(registryName, event, ...params) { this.trigger(`${registryName}.${event}`, ...params); } /** * @action * Retrieves the entire registry with the given name. * * @memberof UniverseService * @param {string} registryName - The name of the registry to retrieve. * * @returns {Object|null} Returns the registry object if it exists; otherwise, returns null. * * @example * const myRegistry = getRegistry('myRegistry'); */ @action getRegistry(registryName) { const internalRegistryName = this.createInternalRegistryName(registryName); const registry = this[internalRegistryName]; if (!isBlank(registry)) { return registry; } return null; } /** * Looks up a registry by its name and returns it as a Promise. * * @memberof UniverseService * @param {string} registryName - The name of the registry to look up. * * @returns {Promise<Object|null>} A Promise that resolves to the registry object if it exists; otherwise, rejects with null. * * @example * lookupRegistry('myRegistry') * .then((registry) => { * // Do something with the registry * }) * .catch((error) => { * // Handle the error or absence of the registry * }); */ lookupRegistry(registryName) { const internalRegistryName = this.createInternalRegistryName(registryName); const registry = this[internalRegistryName]; return new Promise((resolve, reject) => { if (!isBlank(registry)) { return resolve(registry); } later( this, () => { if (!isBlank(registry)) { return resolve(registry); } }, 100 ); reject(null); }); } /** * @action * Retrieves the menu items from a registry with the given name. * * @memberof UniverseService * @param {string} registryName - The name of the registry to retrieve menu items from. * * @returns {Array} Returns an array of menu items if the registry exists and has menu items; otherwise, returns an empty array. * * @example * const items = getMenuItemsFromRegistry('myRegistry'); */ @action getMenuItemsFromRegistry(registryName) { const internalRegistryName = this.createInternalRegistryName(registryName); const registry = this[internalRegistryName]; if (!isBlank(registry) && isArray(registry.menuItems)) { return registry.menuItems; } return []; } /** * @action * Retrieves the menu panels from a registry with the given name. * * @memberof UniverseService * @param {string} registryName - The name of the registry to retrieve menu panels from. * * @returns {Array} Returns an array of menu panels if the registry exists and has menu panels; otherwise, returns an empty array. * * @example * const panels = getMenuPanelsFromRegistry('myRegistry'); */ @action getMenuPanelsFromRegistry(registryName) { const internalRegistryName = this.createInternalRegistryName(registryName); const registry = this[internalRegistryName]; if (!isBlank(registry) && isArray(registry.menuPanels)) { return registry.menuPanels; } return []; } /** * Retrieves renderable components from a specified registry. * This action checks the internal registry, identified by the given registry name, * and returns the 'renderableComponents' if they are present and are an array. * * @action * @param {string} registryName - The name of the registry to retrieve components from. * @returns {Array} An array of renderable components from the specified registry, or an empty array if none found. */ @action getRenderableComponentsFromRegistry(registryName) { const internalRegistryName = this.createInternalRegistryName(registryName); const registry = this[internalRegistryName]; if (!isBlank(registry) && isArray(registry.renderableComponents)) { return registry.renderableComponents; } return []; } /** * Loads a component from the specified registry based on a given slug and view. * * @param {string} registryName - The name of the registry where the component is located. * @param {string} slug - The slug of the menu item. * @param {string} [view=null] - The view of the menu item, if applicable. * * @returns {Promise} Returns a Promise that resolves with the component if it is found, or null. */ loadComponentFromRegistry(registryName, slug, view = null) { const internalRegistryName = this.createInternalRegistryName(registryName); const registry = this[internalRegistryName]; return new Promise((resolve) => { let component = null; if (isBlank(registry)) { return resolve(component); } // check menu items first for (let i = 0; i < registry.menuItems.length; i++) { const menuItem = registry.menuItems[i]; // no view hack if (menuItem && menuItem.slug === slug && menuItem.view === null && view === 'index') { component = menuItem.component; break; } if (menuItem && menuItem.slug === slug && menuItem.view === view) { component = menuItem.component; break; } } // check menu panels for (let i = 0; i < registry.menuPanels.length; i++) { const menuPanel = registry.menuPanels[i]; if (menuPanel && isArray(menuPanel.items)) { for (let j = 0; j < menuPanel.items.length; j++) { const menuItem = menuPanel.items[j]; // no view hack if (menuItem && menuItem.slug === slug && menuItem.view === null && view === 'index') { component = menuItem.component; break; } if (menuItem && menuItem.slug === slug && menuItem.view === view) { component = menuItem.component; break; } } } } resolve(component); }); } /** * Looks up a menu item from the specified registry based on a given slug and view. * * @param {string} registryName - The name of the registry where the menu item is located. * @param {string} slug - The slug of the menu item. * @param {string} [view=null] - The view of the menu item, if applicable. * @param {string} [section=null] - The section of the menu item, if applicable. * * @returns {Promise} Returns a Promise that resolves with the menu item if it is found, or null. */ lookupMenuItemFromRegistry(registryName, slug, view = null, section = null) { const internalRegistryName = this.createInternalRegistryName(registryName); const registry = this[internalRegistryName]; return new Promise((resolve) => { let foundMenuItem = null; if (isBlank(registry)) { return resolve(foundMenuItem); } // check menu items first for (let i = 0; i < registry.menuItems.length; i++) { const menuItem = registry.menuItems[i]; if (menuItem && menuItem.slug === slug && menuItem.section === section && menuItem.view === view) { foundMenuItem = menuItem; break; } if (menuItem && menuItem.slug === slug && menuItem.view === view) { foundMenuItem = menuItem; break; } } // check menu panels for (let i = 0; i < registry.menuPanels.length; i++) { const menuPanel = registry.menuPanels[i]; if (menuPanel && isArray(menuPanel.items)) { for (let j = 0; j < menuPanel.items.length; j++) { const menuItem = menuPanel.items[j]; if (menuItem && menuItem.slug === slug && menuItem.section === section && menuItem.view === view) { foundMenuItem = menuItem; break; } if (menuItem && menuItem.slug === slug && menuItem.view === view) { foundMenuItem = menuItem; break; } } } } resolve(foundMenuItem); }); } /** * Gets the view param from the transition object. * * @param {Transition} transition * @return {String|Null} * @memberof UniverseService */ getViewFromTransition(transition) { const queryParams = transition.to.queryParams ?? { view: null }; return queryParams.view; } /** * Creates an internal registry name for hooks based on a given registry name. * The registry name is transformed to camel case and appended with 'Hooks'. * Non-alphanumeric characters are replaced with hyphens. * * @param {string} registryName - The name of the registry for which to create an internal hook registry name. * @returns {string} - The internal hook registry name, formatted as camel case with 'Hooks' appended. */ createInternalHookRegistryName(registryName) { return `${camelize(registryName.replace(/[^a-zA-Z0-9]/g, '-'))}Hooks`; } /** * Registers a hook function under a specified registry name. * The hook is stored in an internal registry, and its hash is computed for identification. * If the hook is already registered, it is appended to the existing list of hooks. * * @param {string} registryName - The name of the registry where the hook should be registered. * @param {Function} hook - The hook function to be registered. */ registerHook(registryName, hook) { if (typeof hook !== 'function') { throw new Error('The hook must be a function.'); } // no duplicate hooks if (this.didRegisterHook(registryName, hook)) { return; } const internalHookRegistryName = this.createInternalHookRegistryName(registryName); const hookRegistry = this.hooks[internalHookRegistryName] || []; hookRegistry.pushObject({ id: this._createHashFromFunctionDefinition(hook), hook }); this.hooks[internalHookRegistryName] = hookRegistry; } /** * Checks if a hook was registered already. * * @param {String} registryName * @param {Function} hook * @return {Boolean} * @memberof UniverseService */ didRegisterHook(registryName, hook) { const hooks = this.getHooks(registryName); const hookId = this._createHashFromFunctionDefinition(hook); return isArray(hooks) && hooks.some((h) => h.id === hookId); } /** * Retrieves the list of hooks registered under a specified registry name. * If no hooks are registered, returns an empty array. * * @param {string} registryName - The name of the registry for which to retrieve hooks. * @returns {Array<Object>} - An array of hook objects registered under the specified registry name. * Each object contains an `id` and a `hook` function. */ getHooks(registryName) { const internalHookRegistryName = this.createInternalHookRegistryName(registryName); return this.hooks[internalHookRegistryName] ?? []; } /** * Executes all hooks registered under a specified registry name with the given parameters. * Each hook is called with the provided parameters. * * @param {string} registryName - The name of the registry under which hooks should be executed. * @param {...*} params - The parameters to pass to each hook function. */ executeHooks(registryName, ...params) { const hooks = this.getHooks(registryName); hooks.forEach(({ hook }) => { try { hook(...params); } catch (error) { debug(`Error executing hook: ${error}`); } }); } /** * Calls all hooks registered under a specified registry name with the given parameters. * This is an alias for `executeHooks` for consistency in naming. * * @param {string} registryName - The name of the registry under which hooks should be called. * @param {...*} params - The parameters to pass to each hook function. */ callHooks(registryName, ...params) { this.executeHooks(registryName, ...params); } /** * Calls a specific hook identified by its ID under a specified registry name with the given parameters. * Only the hook with the matching ID is executed. * * @param {string} registryName - The name of the registry where the hook is registered. * @param {string} hookId - The unique identifier of the hook to be called. * @param {...*} params - The parameters to pass to the hook function. */ callHook(registryName, hookId, ...params) { const hooks = this.getHooks(registryName); const hook = hooks.find((h) => h.id === hookId); if (hook) { try { hook.hook(...params); } catch (error) { debug(`Error executing hook: ${error}`); } } else { warn(`Hook with ID ${hookId} not found.`); } } /** * Registers a renderable component or an array of components into a specified registry. * If a single component is provided, it is registered directly. * If an array of components is provided, each component in the array is registered individually. * The component is also registered into the specified engine. * * @param {string} engineName - The name of the engine to register the component(s) into. * @param {string} registryName - The registry name where the component(s) should be registered. * @param {Object|Array} component - The component or array of components to register. */ registerRenderableComponent(engineName, registryName, component) { if (isArray(component)) { component.forEach((_) => this.registerRenderableComponent(registryName, _)); return; } // register component to engine this.registerComponentInEngine(engineName, component); // register to registry const internalRegistryName = this.createInternalRegistryName(registryName); if (!isBlank(this[internalRegistryName])) { if (isArray(this[internalRegistryName].renderableComponents)) { this[internalRegistryName].renderableComponents.pushObject(component); } else { this[internalRegistryName].renderableComponents = [component]; } } else { this.createRegistry(registryName); return this.registerRenderableComponent(...arguments); } } /** * Registers a new menu panel in a registry. * * @method registerMenuPanel * @public * @memberof UniverseService * @param {String} registryName The name of the registry to use * @param {String} title The title of the panel * @param {Array} items The items of the panel * @param {Object} options Additional options for the panel */ registerMenuPanel(registryName, title, items = [], options = {}) { const internalRegistryName = this.createInternalRegistryName(registryName); const intl = this._getOption(options, 'intl', null); const open = this._getOption(options, 'open', true); const slug = this._getOption(options, 'slug', dasherize(title)); const menuPanel = { intl, title, open, items: items.map(({ title, route, ...options }) => { options.slug = slug; options.view = dasherize(title); return this._createMenuItem(title, route, options); }), }; // register menu panel this[internalRegistryName].menuPanels.pushObject(menuPanel); // trigger menu panel registered event this.trigger('menuPanel.registered', menuPanel, this[internalRegistryName]); } /** * Registers a new menu item in a registry. * * @method registerMenuItem * @public * @memberof UniverseService * @param {String} registryName The name of the registry to use * @param {String} title The title of the item * @param {String} route The route of the item * @param {Object} options Additional options for the item */ registerMenuItem(registryName, title, options = {}) { const internalRegistryName = this.createInternalRegistryName(registryName); const route = this._getOption(options, 'route', `console.${dasherize(registryName)}.virtual`); options.slug = this._getOption(options, 'slug', '~'); options.view = this._getOption(options, 'view', dasherize(title)); // not really a fan of assumptions, but will do this for the timebeing till anyone complains if (options.slug === options.view) { options.view = null; } // register component if applicable this.registerMenuItemComponentToEngine(options); // create menu item const menuItem = this._createMenuItem(title, route, options); // register menu item if (!this[internalRegistryName]) { this[internalRegistryName] = { menuItems: [], menuPanels: [], }; } // register menu item this[internalRegistryName].menuItems.pushObject(menuItem); // trigger menu panel registered event this.trigger('menuItem.registered', menuItem, this[internalRegistryName]); } /** * Register multiple menu items to a registry. * * @param {String} registryName * @param {Array} [menuItems=[]] * @memberof UniverseService */ registerMenuItems(registryName, menuItems = []) { for (let i = 0; i < menuItems.length; i++) { const menuItem = menuItems[i]; if (menuItem && menuItem.title) { if (menuItem.options) { this.registerMenuItem(registryName, menuItem.title, menuItem.options); } else { this.registerMenuItem(registryName, menuItem.title, menuItem); } } } } /** * Registers a menu item's component to one or multiple engines. * * @method registerMenuItemComponentToEngine * @public * @memberof UniverseService * @param {Object} options - An object containing the following properties: * - `registerComponentToEngine`: A string or an array of strings representing the engine names where the component should be registered. * - `component`: The component class to register, which should have a 'name' property. */ registerMenuItemComponentToEngine(options) { // Register component if applicable if (typeof options.registerComponentToEngine === 'string') { this.registerComponentInEngine(options.registerComponentToEngine, options.component); } // register to multiple engines if (isArray(options.registerComponentToEngine)) { for (let i = 0; i < options.registerComponentInEngine.length; i++) { const engineName = options.registerComponentInEngine.objectAt(i); if (typeof engineName === 'string') { this.registerComponentInEngine(engineName, options.component); } } } } /** * Registers a new administrative menu panel. * * @method registerAdminMenuPanel * @public * @memberof UniverseService * @param {String} title The title of the panel * @param {Array} items The items of the panel * @param {Object} options Additional options for the panel */ registerAdminMenuPanel(title, items = [], options = {}) { options.section = this._getOption(options, 'section', 'admin'); this.registerMenuPanel('console:admin', title, items, options); } /** * Registers a new administrative menu item. * * @method registerAdminMenuItem * @public * @memberof UniverseService * @param {String} title The title of the item * @param {Object} options Additional options for the item */ registerAdminMenuItem(title, options = {}) { this.registerMenuItem('console:admin', title, options); } /** * Registers a new settings menu panel. * * @method registerSettingsMenuPanel * @public * @memberof UniverseService * @param {String} title The title of the panel * @param {Array} items The items of the panel * @param {Object} options Additional options for the panel */ registerSettingsMenuPanel(title, items = [], options = {}) { this.registerMenuPanel('console:settings', title, items, options); } /** * Registers a new settings menu item. * * @method registerSettingsMenuItem * @public * @memberof UniverseService * @param {String} title The title of the item * @param {Object} options Additional options for the item */ registerSettingsMenuItem(title, options = {}) { this.registerMenuItem('console:settings', title, options); } /** * Registers a new account menu panel. * * @method registerAccountMenuPanel * @public * @memberof UniverseService * @param {String} title The title of the panel * @param {Array} items The items of the panel * @param {Object} options Additional options for the panel */ registerAccountMenuPanel(title, items = [], options = {}) { this.registerMenuPanel('console:account', title, items, options); } /** * Registers a new account menu item. * * @method registerAccountMenuItem * @public * @memberof UniverseService * @param {String} title The title of the item * @param {Object} options Additional options for the item */ registerAccountMenuItem(title, options = {}) { this.registerMenuItem('console:account', title, options); } /** * Registers a new dashboard with the given name. * Initializes the dashboard with empty arrays for default widgets and widgets. * * @param {string} dashboardName - The name of the dashboard to register. * @returns {void} */ registerDashboard(dashboardName) { const internalDashboardRegistryName = this.createInternalDashboardName(dashboardName); if (this[internalDashboardRegistryName] !== undefined) { return; } this[internalDashboardRegistryName] = { defaultWidgets: A([]), widgets: A([]), }; this.trigger('dashboard.registered', this[internalDashboardRegistryName]); } /** * Retrieves the registry for a specific dashboard. * * @param {string} dashboardName - The name of the dashboard to get the registry for. * @returns {Object} - The registry object for the specified dashboard, including default and registered widgets. */ getDashboardRegistry(dashboardName) { const internalDashboardRegistryName = this.createInternalDashboardName(dashboardName); return this[internalDashboardRegistryName]; } /** * Checks if a dashboard has been registered. * * @param {String} dashboardName * @return {Boolean} * @memberof UniverseService */ didRegisterDashboard(dashboardName) { const internalDashboardRegistryName = this.createInternalDashboardName(dashboardName); return this[internalDashboardRegistryName] !== undefined; } /** * Retrieves the widget registry for a specific dashboard and type. * * @param {string} dashboardName - The name of the dashboard to get the widget registry for. * @param {string} [type='widgets'] - The type of widget registry to retrieve (e.g., 'widgets', 'defaultWidgets'). * @returns {Array} - An array of widget objects for the specified dashboard and type. */ getWidgetRegistry(dashboardName, type = 'widgets') { const internalDashboardRegistryName = this.createInternalDashboardName(dashboardName); const typeKey = pluralize(type); return isArray(this[internalDashboardRegistryName][typeKey]) ? this[internalDashboardRegistryName][typeKey] : []; } /** * Registers widgets for a specific dashboard. * Supports registering multiple widgets and different types of widget collections. * * @param {string} dashboardName - The name of the dashboard to register widgets for. * @param {Array|Object} widgets - An array of widget objects or a single widget object to register. * @param {string} [type='widgets'] - The type of widgets to register (e.g., 'widgets', 'defaultWidgets'). * @returns {void} */ registerWidgets(dashboardName, widgets = [], type = 'widgets') { const internalDashboardRegistryName = this.createInternalDashboardName(dashboardName); if (isArray(widgets)) { widgets.forEach((w) => this.registerWidgets(dashboardName, w, type)); return; } const typeKey = pluralize(type); const newWidget = this._createDashboardWidget(widgets); const widgetRegistry = this.getWidgetRegistry(dashboardName, type); if (this.widgetRegistryHasWidget(widgetRegistry, newWidget)) { return; } this[internalDashboardRegistryName][typeKey] = [...widgetRegistry, newWidget]; this.trigger('widget.registered', newWidget); } /** * Checks if a widget with the same ID as the pending widget is already registered in the specified dashboard and type. * * @param {string} dashboardName - The name of the dashboard to check. * @param {Object} widgetPendingRegistry - The widget to check for in the registry. * @param {string} [type='widgets'] - The type of widget registry to check (e.g., 'widgets', 'defaultWidgets'). * @returns {boolean} - `true` if a widget with the same ID is found in the registry; otherwise, `false`. */ didRegisterWidget(dashboardName, widgetPendingRegistry, type = 'widgets') { const widgetRegistry = this.getWidgetRegistry(dashboardName, type); return widgetRegistry.includes((widget) => widget.widgetId === widgetPendingRegistry.widgetId); } /** * Checks if a widget with the same ID as the pending widget exists in the provided widget registry instance. * * @param {Array} [widgetRegistryInstance=[]] - An array of widget objects to check. * @param {Object} widgetPendingRegistry - The widget to check for in the registry. * @returns {boolean} - `true` if a widget with the same ID is found in the registry; otherwise, `false`. */ widgetRegistryHasWidget(widgetRegistryInstance = [], widgetPendingRegistry) { return widgetRegistryInstance.includes((widget) => widget.widgetId === widgetPendingRegistry.widgetId); } /** * Registers widgets for the default 'dashboard' dashboard. * * @param {Array} [widgets=[]] - An array of widget objects to register. * @returns {void} */ registerDashboardWidgets(widgets = []) { this.registerWidgets('dashboard', widgets); } /** * Registers default widgets for the default 'dashboard' dashboard. * * @param {Array} [widgets=[]] - An array of default widget objects to register. * @returns {void} */ registerDefaultDashboardWidgets(widgets = []) { this.registerWidgets('dashboard', widgets, 'defaultWidgets'); } /** * Registers default widgets for a specified dashboard. * * @param {String} dashboardName * @param {Array} [widgets=[]] - An array of default widget objects to register. * @returns {void} */ registerDefaultWidgets(dashboardName, widgets = []) { this.registerWidgets(dashboardName, widgets, 'defaultWidgets'); } /** * Retrieves widgets for a specific dashboard. * * @param {string} dashboardName - The name of the dashboard to retrieve widgets for. * @param {string} [type='widgets'] - The type of widgets to retrieve (e.g., 'widgets', 'defaultWidgets'). * @returns {Array} - An array of widgets for the specified dashboard and type. */ getWidgets(dashboardName, type = 'widgets') { const typeKey = pluralize(type); const internalDashboardRegistryName = this.createInternalDashboardName(dashboardName); return isArray(this[internalDashboardRegistryName][typeKey]) ? this[internalDashboardRegistryName][typeKey] : []; } /** * Retrieves default widgets for a specific dashboard. * * @param {string} dashboardName - The name of the dashboard to retrieve default widgets for. * @returns {Array} - An array of default widgets for the specified dashboard. */ getDefaultWidgets(dashboardName) { return this.getWidgets(dashboardName, 'defaultWidgets'); } /** * Retrieves widgets for the default 'dashboard' dashboard. * * @returns {Array} - An array of widgets for the default 'dashboard' dashboard. */ getDashboardWidgets() { return this.getWidgets('dashboard'); } /** * Retrieves default widgets for the default 'dashboard' dashboard. * * @returns {Array} - An array of default widgets for the default 'dashboard' dashboard. */ getDefaultDashboardWidgets() { return this.getWidgets('dashboard', 'defaultWidgets'); } /** * Creates an internal name for a dashboard based on its given name. * * @param {string} dashboardName - The name of the dashboard. * @returns {string} - The internal name for the dashboard, formatted as `${dashboardName}Widgets`. */ createInternalDashboardName(dashboardName) { return `${camelize(dashboardName.replace(/[^a-zA-Z0-9]/g, '-'))}Widgets`; } /** * Creates a new widget object from a widget definition. * If the component is a function, it is registered with the host application. * * @param {Object} widget - The widget definition. * @param {string} widget.widgetId - The unique ID of the widget. * @param {string} widget.name - The name of the widget. * @param {string} [widget.description] - A description of the widget. * @param {string} [widget.icon] - An icon for the widget. * @param {Function|string} [widget.component] - A component definition or name for the widget. * @param {Object} [widget.grid_options] - Grid options for the widget. * @param {Object} [widget.options] - Additional options for the widget. * @returns {Object} - The newly created widget object. */ _createDashboardWidget(widget) { // Extract properties from the widget object let { widgetId, name, description, icon, component, grid_options, options } = widget; // If component is a definition register to host application if (typeof component === 'function') { const owner = getOwner(this); widgetId = component.widgetId || widgetId || this._createUniqueWidgetHashFromDefinition(component); if (owner) { owner.register(`component:${widgetId}`, component); // Update component name component = widgetId; } } // Create a new widget object with the extracted properties const newWidget = { widgetId, name, description, icon, component, grid_options, options, }; return newWidget; } /** * Generates a unique hash for a widget component based on its function definition. * This method delegates the hash creation to the `_createHashFromFunctionDefinition` method. * * @param {Function} component - The function representing the widget component. * @returns {string} - The unique hash representing the widget component. */ _createUniqueWidgetHashFromDefinition(component) { return this._createHashFromFunctionDefinition(component); } /** * Creates a hash value from a function definition. The hash is generated based on the function's string representation. * If the function has a name, it returns that name. Otherwise, it converts the function's string representation * into a hash value. This is done by iterating over the characters of the string and performing a simple hash calculation. * * @param {Function} func - The function whose definition will be hashed. * @returns {string} - The hash value derived from the function's definition. If the function has a name, it is returned directly. */ _createHashFromFunctionDefinition(func) { if (func.name) { return func.name; } if (typeof func.toString === 'function') { let definition = func.toString(); let hash = 0; for (let i = 0; i < definition.length; i++) { const char = definition.cha