diff --git a/console/app/components/extension-injector.js b/console/app/components/extension-injector.js index 2fb4e1b9..26ab0ed1 100644 --- a/console/app/components/extension-injector.js +++ b/console/app/components/extension-injector.js @@ -3,9 +3,19 @@ import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; import { isArray } from '@ember/array'; import { getOwner } from '@ember/application'; +import { later } from '@ember/runloop'; import { task, timeout } from 'ember-concurrency'; -import loadExtensions from '@fleetbase/ember-core/utils/load-extensions'; +import { injectAsset } from '../utils/asset-injector'; +import getMountedEngineRoutePrefix from '@fleetbase/ember-core/utils/get-mounted-engine-route-prefix'; +function removeTrailingDot (str) { + if (str.endsWith('.')) { + return str.slice(0, -1); + } + return str; +} + +window.exports = window.exports ?? {}; export default class ExtensionInjectorComponent extends Component { @service fetch; @service notifications; @@ -23,73 +33,159 @@ export default class ExtensionInjectorComponent extends Component { try { const engines = yield this.fetch.get('load-installed-engines', {}, { namespace: '~registry/v1' }); - if (isArray(engines)) { - engines.forEach(extensionId => { - this.loadExtensionFromManifest.perform(extensionId); - }); + for (const id in engines) { + yield this.loadAndMountEngine.perform(id, engines[id]); } } catch (error) { this.notifications.serverError(error); } } - @task *loadExtensionFromManifest (extensionId) { - try { - const assets = yield this.fetch.get(`load-engine-manifest/${extensionId}`, {}, { namespace: '~registry/v1' }); - if (isArray(assets)) { - assets.forEach(asset => { - this.inject(asset); - }); + @task *loadAndMountEngine (id, enginePackage) { + const engineName = enginePackage.name; + const assets = yield this.fetch.get(`load-engine-manifest/${id}`, {}, { namespace: '~registry/v1' }); + + if (isArray(assets)) { + for (const i in assets) { + injectAsset(assets[i]); } - } catch (error) { - this.notifications.serverError(error); } - this.initializeExtensions(); + yield timeout(300); + this.registerEngine(enginePackage); } - initializeExtensions () { + registerEngine (enginePackage) { + const engineName = enginePackage.name; const owner = getOwner(this); + const router = getOwner(this).lookup('router:main'); - this.packages.forEach(packageJson => { - this.universe.loadEngine(packageJson.name).then(engineInstance => { + if (this.hasAssetManifest(engineName)) { + return this.universe.loadEngine(engineName).then(engineInstance => { if (engineInstance.base && engineInstance.base.setupExtension) { engineInstance.base.setupExtension(owner, engineInstance, this.universe); } }); - }); - } + } - inject ({ type, content }) { - switch (type) { - case 'package': - this.packages.pushObject(content); - return; - case 'css': - return this.injectStylesheet(content); - case 'js': - default: - return this.injectScript(content); + try { + if (router._engineIsLoaded(engineName)) { + router._registerEngine(engineName); + + const instanceId = Object.values(router._engineInstances).length; + const mountPoint = removeTrailingDot(getMountedEngineRoutePrefix(engineName.replace('@fleetbase/', ''), enginePackage.fleetbase)); + this.universe.constructEngineInstance(engineName, instanceId, mountPoint).then(engineInstance => { + if (engineInstance) { + this.setupEngine(owner, engineInstance, enginePackage); + this.setEnginePromise(engineName, engineInstance); + this.addEngineRoutesToRouter(engineName, engineInstance, instanceId, mountPoint, router); + this.addEngineRoutesToRecognizer(engineName, router); + this.resolveEngineRegistrations(engineInstance); + + console.log(engineInstance, router, owner); + if (engineInstance.base && engineInstance.base.setupExtension) { + engineInstance.base.setupExtension(owner, engineInstance, this.universe); + } + } + }); + } + } catch (error) { + console.trace(error); } } - injectScript (content) { - const script = document.createElement('script'); - script.type = 'application/javascript'; - script.text = content; - document.head.appendChild(script); + setupEngine (appInstance, engineInstance, enginePackage) { + const engineName = enginePackage.name; + appInstance.application.engines[engineName] = engineInstance.dependencies ?? { externalRoutes: {}, services: {} }; + appInstance._dependenciesForChildEngines[engineName] = engineInstance.dependencies ?? { externalRoutes: {}, services: {} }; + if (isArray(appInstance.application.extensions)) { + appInstance.application.extensions.push(enginePackage); + } } - injectStylesheet (content) { - const style = document.createElement('style'); - style.type = 'text/css'; - if (style.styleSheet) { - // This is required for IE8 and below. - style.styleSheet.cssText = content; - } else { - // For most browsers - style.appendChild(document.createTextNode(content)); + addEngineRoutesToRecognizer (engineName, router) { + const recognizer = router._routerMicrolib.recognizer || router._router._routerMicrolib.recognizer; + if (recognizer) { + let routeName = `${engineName}.application`; + + recognizer.add( + [ + { + path: 'console.fleet-ops', + handler: 'console.fleet-ops', + }, + ], + { as: 'console.fleet-ops' } + ); } - document.head.appendChild(style); + } + + addEngineRoutesToRouter (engineName, engineInstance, instanceId, mountPoint, router) { + const getRouteInfo = routeName => { + const applicationRoute = routeName.replace(mountPoint, '') === ''; + return { + fullName: mountPoint, + instanceId, + localFullName: applicationRoute ? 'application' : routeName.replace(`${mountPoint}.`, ''), + mountPoint, + name: engineName, + }; + }; + + const routes = ['console.fleet-ops', 'console.fleet-ops.home']; + for (let i = 0; i < routes.length; i++) { + const routeName = routes[i]; + router._engineInfoByRoute[routeName] = getRouteInfo(routeName); + } + + // Reinitialize or refresh the router + router.setupRouter(); + } + + setEnginePromise (engineName, engineInstance) { + const router = getOwner(this).lookup('router:main'); + if (router) { + router._enginePromises[engineName] = { manual: engineInstance._bootPromise }; + } + } + + resolveEngineRegistrations (engineInstance) { + const owner = getOwner(this); + const registry = engineInstance.__registry__; + const registrations = registry.registrations; + const getOwnerSymbol = obj => { + const symbols = Object.getOwnPropertySymbols(obj); + const ownerSymbol = symbols.find(symbol => symbol.toString() === 'Symbol(OWNER)'); + return ownerSymbol; + }; + for (let registrationKey in registrations) { + const registrationInstance = registrations[registrationKey]; + if (typeof registrationInstance === 'string') { + // Try to resolve from owner + let resolvedRegistrationInstance = owner.lookup(registrationKey); + // Hack for host-router + if (registrationKey === 'service:host-router') { + resolvedRegistrationInstance = owner.lookup('service:router'); + } + if (resolvedRegistrationInstance) { + // Correct the owner + resolvedRegistrationInstance[getOwnerSymbol(resolvedRegistrationInstance)] = engineInstance; + // Resolve + registrations[registrationKey] = resolvedRegistrationInstance; + } + } + } + } + + hasAssetManifest (engineName) { + const router = getOwner(this).lookup('router:main'); + if (router._assetLoader) { + const manifest = router._assetLoader.getManifest(); + if (manifest && manifest.bundles) { + return manifest.bundles[engineName] !== undefined; + } + } + + return false; } } diff --git a/console/app/templates/console.hbs b/console/app/templates/console.hbs index 50b13ee8..8116d9e9 100644 --- a/console/app/templates/console.hbs +++ b/console/app/templates/console.hbs @@ -16,4 +16,3 @@ - diff --git a/console/app/templates/console/extension.hbs b/console/app/templates/console/extension.hbs new file mode 100644 index 00000000..e0dfda48 --- /dev/null +++ b/console/app/templates/console/extension.hbs @@ -0,0 +1,3 @@ +{{#if this.ready}} + {{mount "@fleetbase/fleetops-engine"}} +{{/if}} \ No newline at end of file diff --git a/console/app/utils/asset-injector.js b/console/app/utils/asset-injector.js new file mode 100644 index 00000000..458d9b19 --- /dev/null +++ b/console/app/utils/asset-injector.js @@ -0,0 +1,46 @@ +export function injectAsset ({ type, content, name, syntax }) { + switch (type) { + case 'css': + injectStylesheet(content, syntax); + break; + case 'meta': + injectMeta(name, content); + break; + case 'js': + default: + injectScript(content, syntax); + break; + } +} + +export function injectMeta (name, content) { + const meta = document.createElement('meta'); + meta.name = name; + meta.content = content; + document.head.appendChild(meta); +} + +export function injectScript (content, type = 'application/javascript') { + const script = document.createElement('script'); + script.type = type; + script.text = content; + document.head.appendChild(script); +} + +export function injectStylesheet (content, type = 'text/css') { + const style = document.createElement('style'); + style.type = type; + if (style.styleSheet) { + style.styleSheet.cssText = content; + } else { + style.appendChild(document.createTextNode(content)); + } + document.head.appendChild(style); +} + +export default { + injectAsset, + injectMeta, + injectScript, + injectStylesheet, +}; diff --git a/console/tests/integration/components/extension-injector-test.js b/console/tests/integration/components/extension-injector-test.js new file mode 100644 index 00000000..928bdac1 --- /dev/null +++ b/console/tests/integration/components/extension-injector-test.js @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from '@fleetbase/console/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | extension-injector', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + + await render(hbs``); + + assert.dom().hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom().hasText('template block text'); + }); +}); diff --git a/console/tests/unit/utils/asset-injector-test.js b/console/tests/unit/utils/asset-injector-test.js new file mode 100644 index 00000000..537a9847 --- /dev/null +++ b/console/tests/unit/utils/asset-injector-test.js @@ -0,0 +1,10 @@ +import assetInjector from '@fleetbase/console/utils/asset-injector'; +import { module, test } from 'qunit'; + +module('Unit | Utility | asset-injector', function () { + // TODO: Replace this with your real tests. + test('it works', function (assert) { + let result = assetInjector(); + assert.ok(result); + }); +}); diff --git a/docker/Dockerfile b/docker/Dockerfile index 4dd127c6..02e3d5f3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -31,7 +31,7 @@ RUN sed -e 's/^expose_php.*/expose_php = Off/' "$PHP_INI_DIR/php.ini-production" -e 's/^memory_limit.*/memory_limit = 600M/' "$PHP_INI_DIR/php.ini" # Install global node modules -RUN npm install -g chokidar +RUN npm install -g chokidar pnpm ember-cli # Install ssm-parent COPY --from=ghcr.io/springload/ssm-parent:1.8 /usr/bin/ssm-parent /sbin/ssm-parent