diff --git a/console/app/components/extension-injector.hbs b/console/app/components/extension-injector.hbs new file mode 100644 index 00000000..fb5c4b15 --- /dev/null +++ b/console/app/components/extension-injector.hbs @@ -0,0 +1 @@ +{{yield}} \ No newline at end of file diff --git a/console/app/components/extension-injector.js b/console/app/components/extension-injector.js new file mode 100644 index 00000000..26ab0ed1 --- /dev/null +++ b/console/app/components/extension-injector.js @@ -0,0 +1,191 @@ +import Component from '@glimmer/component'; +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 { 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; + @service universe; + @tracked engines = []; + @tracked packages = []; + + constructor () { + super(...arguments); + this.loadInstalledEngines.perform(); + } + + @task *loadInstalledEngines () { + yield timeout(300); + + try { + const engines = yield this.fetch.get('load-installed-engines', {}, { namespace: '~registry/v1' }); + for (const id in engines) { + yield this.loadAndMountEngine.perform(id, engines[id]); + } + } catch (error) { + this.notifications.serverError(error); + } + } + + @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]); + } + } + + yield timeout(300); + this.registerEngine(enginePackage); + } + + registerEngine (enginePackage) { + const engineName = enginePackage.name; + const owner = getOwner(this); + const router = getOwner(this).lookup('router:main'); + + if (this.hasAssetManifest(engineName)) { + return this.universe.loadEngine(engineName).then(engineInstance => { + if (engineInstance.base && engineInstance.base.setupExtension) { + engineInstance.base.setupExtension(owner, engineInstance, this.universe); + } + }); + } + + 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); + } + } + + 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); + } + } + + 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' } + ); + } + } + + 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/routes/console/extensions.js b/console/app/routes/console/extensions.js deleted file mode 100644 index e50e79b7..00000000 --- a/console/app/routes/console/extensions.js +++ /dev/null @@ -1,3 +0,0 @@ -import Route from '@ember/routing/route'; - -export default class ConsoleExtensionsRoute extends Route {} 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/templates/console/extensions.hbs b/console/app/templates/console/extensions.hbs deleted file mode 100644 index 6d5436bb..00000000 --- a/console/app/templates/console/extensions.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{page-title "Extensions"}} - -
-
- -

{{t "console.extensions.title"}}

-

- {{t "console.extensions.message"}} -

-
-
-
\ 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/router.map.js b/console/router.map.js index d1a3a537..e28d87f2 100644 --- a/console/router.map.js +++ b/console/router.map.js @@ -23,7 +23,6 @@ Router.map(function () { }); this.route('console', { path: '/' }, function () { this.route('home', { path: '/' }); - this.route('extensions'); this.route('notifications'); this.route('account', function () { this.route('virtual', { path: '/:slug/:view' }); 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/routes/console/extensions-test.js b/console/tests/unit/routes/console/extensions-test.js deleted file mode 100644 index 7ccba108..00000000 --- a/console/tests/unit/routes/console/extensions-test.js +++ /dev/null @@ -1,11 +0,0 @@ -import { module, test } from 'qunit'; -import { setupTest } from '@fleetbase/console/tests/helpers'; - -module('Unit | Route | console/extensions', function (hooks) { - setupTest(hooks); - - test('it exists', function (assert) { - let route = this.owner.lookup('route:console/extensions'); - assert.ok(route); - }); -}); 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 4da1b914..3c58523d 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 ember-cli pnpm +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