mirror of
https://github.com/fleetbase/fleetbase.git
synced 2026-01-08 07:16:49 +00:00
Compare commits
7 Commits
v0.4.27
...
feature-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21ecfb5d93 | ||
|
|
579a369888 | ||
|
|
b06830990c | ||
|
|
a7c4aba512 | ||
|
|
a625f37e14 | ||
|
|
cce1699a75 | ||
|
|
6338820372 |
2
.github/workflows/cd.yml
vendored
2
.github/workflows/cd.yml
vendored
@@ -111,7 +111,7 @@ jobs:
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
|
||||
@@ -13,15 +13,15 @@ We use github to host code, to track issues and feature requests, as well as acc
|
||||
## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests
|
||||
Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests:
|
||||
|
||||
1. Fork the repo and create your branch from `master`.
|
||||
1. Fork the repo and create your branch from `main`.
|
||||
2. If you've added code that should be tested, add tests.
|
||||
3. If you've changed APIs, update the documentation.
|
||||
4. Ensure the test suite passes.
|
||||
5. Make sure your code lints.
|
||||
6. Issue that pull request!
|
||||
|
||||
## Any contributions you make will be under the MIT Software License
|
||||
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
## Any contributions you make will be under the AGPL v3 Software License
|
||||
In short, when you submit code changes, your submissions are understood to be under the same [AGPL v3](https://choosealicense.com/licenses/agpl-3.0/) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
|
||||
## Report bugs using Github's [issues](https://github.com/fleetbase/fleetbase/issues)
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/fleetbase/fleetbase/issues), it's that easy!
|
||||
@@ -41,7 +41,7 @@ We use GitHub issues to track public bugs. Report a bug by [opening a new issue]
|
||||
People *love* thorough bug reports. I'm not even kidding.
|
||||
|
||||
## License
|
||||
By contributing, you agree that your contributions will be licensed under its MIT License.
|
||||
By contributing, you agree that your contributions will be licensed under its AGPL v3 Software License.
|
||||
|
||||
## References
|
||||
This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md)
|
||||
|
||||
@@ -10,3 +10,10 @@ http://:8000 {
|
||||
resolve_root_symlink
|
||||
}
|
||||
}
|
||||
|
||||
http://:4201 {
|
||||
root * /fleetbase/console/dist
|
||||
try_files {path} /
|
||||
encode zstd gzip
|
||||
file_server
|
||||
}
|
||||
|
||||
@@ -163,4 +163,4 @@ Get updates on Fleetbase's development and chat with the project maintainers and
|
||||
|
||||
# License & Copyright
|
||||
|
||||
Code and documentation copyright 2018–2023 the <a href="https://github.com/fleetbase/fleetbase/graphs/contributors">Fleetbase Authors</a>. Code released under the <a href="https://github.com/fleetbase/storefront-app/blob/main/LICENSE.md">MIT License</a>.
|
||||
Fleetbase is made available under the terms of the <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank">GNU Affero General Public License 3.0 (AGPL 3.0)</a>. For other licenses <a href="mailto:hello@fleetbase.io" target="_blank">contact us</a>.
|
||||
|
||||
1
console/app/components/extension-injector.hbs
Normal file
1
console/app/components/extension-injector.hbs
Normal file
@@ -0,0 +1 @@
|
||||
{{yield}}
|
||||
191
console/app/components/extension-injector.js
Normal file
191
console/app/components/extension-injector.js
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,8 @@
|
||||
import loadExtensions from '@fleetbase/ember-core/utils/load-extensions';
|
||||
|
||||
export function initialize(owner) {
|
||||
export function initialize (owner) {
|
||||
const universe = owner.lookup('service:universe');
|
||||
|
||||
loadExtensions().then((extensions) => {
|
||||
extensions.forEach((extension) => {
|
||||
universe.loadEngine(extension.name).then((engineInstance) => {
|
||||
if (engineInstance.base && engineInstance.base.setupExtension) {
|
||||
engineInstance.base.setupExtension(owner, engineInstance, universe);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
if (universe) {
|
||||
universe.bootEngines(owner);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ConsoleExtensionsRoute extends Route {}
|
||||
3
console/app/templates/console/extension.hbs
Normal file
3
console/app/templates/console/extension.hbs
Normal file
@@ -0,0 +1,3 @@
|
||||
{{#if this.ready}}
|
||||
{{mount "@fleetbase/fleetops-engine"}}
|
||||
{{/if}}
|
||||
@@ -1,12 +0,0 @@
|
||||
{{page-title "Extensions"}}
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full pt-6">
|
||||
<div class="container mx-auto h-screen space-y-4">
|
||||
<div class="flex flex-col items-center justify-center pt-14 px-40">
|
||||
<FaIcon @icon="shapes" @size="4x" class="mb-6 text-blue-500" />
|
||||
<h1 class="dark:text-gray-100 text-black text-4xl font-bold mb-4">{{t "console.extensions.title"}}</h1>
|
||||
<p class="dark:text-gray-300 text-black text-lg">
|
||||
{{t "console.extensions.message"}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout::Section::Body>
|
||||
46
console/app/utils/asset-injector.js
Normal file
46
console/app/utils/asset-injector.js
Normal file
@@ -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,
|
||||
};
|
||||
@@ -40,6 +40,10 @@ module.exports = function (environment) {
|
||||
port: getenv('SOCKETCLUSTER_PORT', 38000),
|
||||
},
|
||||
|
||||
stripe: {
|
||||
publishableKey: getenv('STRIPE_KEY')
|
||||
},
|
||||
|
||||
defaultValues: {
|
||||
categoryImage: getenv('DEFAULT_CATEGORY_IMAGE', 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/images/fallback-placeholder-1.png'),
|
||||
placeholderImage: getenv('DEFAULT_PLACEHOLDER_IMAGE', 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/images/fallback-placeholder-2.png'),
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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`<ExtensionInjector />`);
|
||||
|
||||
assert.dom().hasText('');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
<ExtensionInjector>
|
||||
template block text
|
||||
</ExtensionInjector>
|
||||
`);
|
||||
|
||||
assert.dom().hasText('template block text');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
10
console/tests/unit/utils/asset-injector-test.js
Normal file
10
console/tests/unit/utils/asset-injector-test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -38,18 +38,9 @@ services:
|
||||
CACHE_URL: tcp://cache
|
||||
REDIS_URL: tcp://cache
|
||||
|
||||
console:
|
||||
ports:
|
||||
- "4200:4200"
|
||||
volumes:
|
||||
- ./console:/app/console
|
||||
build:
|
||||
context: .
|
||||
dockerfile: console/Dockerfile
|
||||
args:
|
||||
ENVIRONMENT: development
|
||||
|
||||
application:
|
||||
ports:
|
||||
- "4201:4201"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
FROM dunglas/frankenphp:1.1.0-php8.2-bookworm as base
|
||||
|
||||
# Install packages
|
||||
RUN apt-get update && apt-get install -y git bind9-utils mycli nodejs npm \
|
||||
RUN apt-get update && apt-get install -y git bind9-utils mycli nodejs npm nano \
|
||||
&& mkdir -p /root/.ssh \
|
||||
&& ssh-keyscan github.com >> /root/.ssh/known_hosts
|
||||
|
||||
@@ -31,17 +31,25 @@ 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
|
||||
|
||||
# # Create the pnpm directory and set the PNPM_HOME environment variable
|
||||
# RUN mkdir -p ~/.pnpm
|
||||
# ENV PNPM_HOME /root/.pnpm
|
||||
|
||||
# # Add the pnpm global bin to the PATH
|
||||
# ENV PATH /root/.pnpm/bin:$PATH
|
||||
|
||||
# Set some build ENV variables
|
||||
ENV LOG_CHANNEL=stdout
|
||||
ENV CACHE_DRIVER=null
|
||||
ENV BROADCAST_DRIVER=socketcluster
|
||||
ENV QUEUE_CONNECTION=redis
|
||||
ENV CADDYFILE_PATH=/fleetbase/Caddyfile
|
||||
ENV CONSOLE_PATH=/fleetbase/console
|
||||
ENV OCTANE_SERVER=frankenphp
|
||||
|
||||
# Set environment
|
||||
@@ -57,17 +65,46 @@ COPY --chown=www-data:www-data ./Caddyfile $CADDYFILE_PATH
|
||||
# Create /fleetbase directory and set correct permissions
|
||||
RUN mkdir -p /fleetbase/api && chown -R www-data:www-data /fleetbase
|
||||
|
||||
## -- Start Console Setup --
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /fleetbase/console
|
||||
|
||||
# TEMPORARILY ADD REGISTRY BRIDGE AND CORE API
|
||||
COPY ./packages/registry-bridge /fleetbase/packages/registry-bridge
|
||||
COPY ./packages/core-api /fleetbase/packages/core-api
|
||||
|
||||
# Copy pnpm-lock.yaml (or package.json) into the directory /app in the container
|
||||
COPY ./console/package.json ./console/pnpm-lock.yaml /fleetbase/console/
|
||||
|
||||
# Copy over .npmrc if applicable
|
||||
COPY ./console/.npmr[c] /fleetbase/console/
|
||||
|
||||
# Install app dependencies
|
||||
# RUN pnpm install
|
||||
|
||||
# Copy the console directory contents into the container at /app
|
||||
COPY ./console /fleetbase/console/
|
||||
|
||||
# Build the application
|
||||
# RUN pnpm build --environment $ENVIRONMENT
|
||||
|
||||
## -- End Console Setup --
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /fleetbase/api
|
||||
|
||||
# If GITHUB_AUTH_KEY is provided, create auth.json with it
|
||||
RUN if [ -n "$GITHUB_AUTH_KEY" ]; then echo "{\"github-oauth\": {\"github.com\": \"$GITHUB_AUTH_KEY\"}}" > auth.json; fi
|
||||
|
||||
# Prepare composer cache directory
|
||||
RUN mkdir -p /var/www/.cache/composer && chown -R www-data:www-data /var/www/.cache/composer
|
||||
|
||||
# Optimize Composer Dependency Installation
|
||||
COPY --chown=www-data:www-data ./api/composer.json ./api/composer.lock /fleetbase/api/
|
||||
|
||||
# Pre-install Composer dependencies
|
||||
RUN su www-data -s /bin/sh -c "composer install --no-scripts --optimize-autoloader --no-dev"
|
||||
RUN su www-data -s /bin/sh -c "composer install --no-scripts --optimize-autoloader --no-dev --no-cache"
|
||||
|
||||
# Setup application
|
||||
COPY --chown=www-data:www-data ./api /fleetbase/api
|
||||
|
||||
Submodule packages/core-api updated: 4567b4a193...fb2e615b50
Submodule packages/ember-core updated: 82d2e57938...db536b414c
Reference in New Issue
Block a user