refactor of universe and lazy loading fix contd.

This commit is contained in:
Ronald A. Richardson
2025-12-03 09:55:12 +08:00
parent cb7a2fb05b
commit 9653cfcaf0
28 changed files with 138 additions and 104 deletions

View File

@@ -8,7 +8,7 @@
</a>
</div>
<div class="px-4 py-2.5">
{{#if this.isLoading}}
{{#if this.loadBlogPosts.isRunning}}
<Spinner />
{{else}}
<ul class="space-y-2">

View File

@@ -1,28 +1,42 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { isArray } from '@ember/array';
import { storageFor } from 'ember-local-storage';
import { add, isPast } from 'date-fns';
import { task } from 'ember-concurrency';
export default class FleetbaseBlogComponent extends Component {
@storageFor('local-cache') localCache;
@service fetch;
@tracked posts = [];
@tracked isLoading = false;
constructor() {
super(...arguments);
this.loadBlogPosts();
this.loadBlogPosts.perform();
}
@action loadBlogPosts() {
this.isLoading = true;
@task *loadBlogPosts() {
// Check if cached data and expiration are available
const cachedData = this.localCache.get('fleetbase-blog-data');
const expiration = this.localCache.get('fleetbase-blog-data-expiration');
return this.fetch
.get('lookup/fleetbase-blog')
.then((response) => {
this.posts = response;
})
.finally(() => {
this.isLoading = false;
});
// Check if the cached data is still valid
if (cachedData && isArray(cachedData) && expiration && !isPast(new Date(expiration))) {
// Use cached data
this.posts = cachedData;
} else {
// Fetch new data
try {
const data = yield this.fetch.get('lookup/fleetbase-blog');
this.posts = isArray(data) ? data : [];
if (data) {
this.localCache.set('fleetbase-blog-data', data);
this.localCache.set('fleetbase-blog-data-expiration', add(new Date(), { hours: 6 }));
}
} catch (err) {
debug('Failed to load blog: ' + err.message);
}
}
}
}

View File

@@ -52,7 +52,7 @@ export default class GithubCardComponent extends Component {
this.data = cachedData;
} else {
// Fetch new data
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase');
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase', { cache: 'default' });
if (response.ok) {
this.data = yield response.json();
this.localCache.set('fleetbase-github-data', this.data);
@@ -72,7 +72,7 @@ export default class GithubCardComponent extends Component {
this.tags = cachedTags;
} else {
// Fetch new tags
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase/tags');
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase/tags', { cache: 'default' });
if (response.ok) {
this.tags = yield response.json();
this.localCache.set('fleetbase-github-tags', this.tags);

View File

@@ -0,0 +1,7 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class VirtualController extends Controller {
@tracked view;
queryParams = ['view'];
}

View File

@@ -40,5 +40,5 @@ export function initialize(application) {
export default {
name: 'load-intl-polyfills',
initialize
initialize,
};

View File

@@ -14,17 +14,18 @@ import { debug } from '@ember/debug';
*/
export function initialize(application) {
const startTime = performance.now();
debug('[Initializer:load-runtime-config] Loading runtime configuration...');
debug('[Runtime Config] Loading runtime configuration...');
// Defer readiness until config is loaded
application.deferReadiness();
(async () => {
try {
await loadRuntimeConfig();
debug(`[Initializer:load-runtime-config] Runtime config loaded in ${(endTime - startTime).toFixed(2)}ms`);
const endTime = performance.now();
debug(`[Runtime Config] Runtime config loaded in ${(endTime - startTime).toFixed(2)}ms`);
application.advanceReadiness();
} catch (error) {
console.error('[Initializer:load-runtime-config] Failed to load runtime config:', error);
console.error('[Runtime Config] Failed to load runtime config:', error);
// Still advance readiness to prevent hanging
application.advanceReadiness();
}

View File

@@ -3,31 +3,31 @@ import { debug } from '@ember/debug';
/**
* Apply Router Fix Instance Initializer
*
*
* Applies the Fleetbase router refresh bug fix patch.
* This patches the Ember router to handle dynamic segments correctly
* when refreshing routes with query parameters.
*
*
* Runs as an instance-initializer because it needs access to the
* application instance and router service.
*
*
* Bug: https://github.com/emberjs/ember.js/issues/19260
*
*
* @export
* @param {ApplicationInstance} appInstance
*/
export function initialize(appInstance) {
const startTime = performance.now();
debug('[InstanceInitializer:apply-router-fix] Applying router refresh bug fix...');
debug('[Initializing Router Patch] Applying router refresh bug fix...');
try {
const application = appInstance.application;
applyRouterFix(application);
const endTime = performance.now();
debug(`[InstanceInitializer:apply-router-fix] Router fix applied in ${(endTime - startTime).toFixed(2)}ms`);
debug(`[Initializing Router Patch] Router fix applied in ${(endTime - startTime).toFixed(2)}ms`);
} catch (error) {
console.error('[InstanceInitializer:apply-router-fix] Failed to apply router fix:', error);
console.error('[Initializing Router Patch] Failed to apply router fix:', error);
}
}
@@ -35,5 +35,5 @@ export default {
name: 'apply-router-fix',
initialize,
// Run before extension loading to ensure router is patched early
before: 'load-extensions'
before: 'load-extensions',
};

View File

@@ -1,3 +1,5 @@
import { debug } from '@ember/debug';
/**
* Create console-specific registries
* Runs after extensions are loaded
@@ -5,8 +7,8 @@
export function initialize(appInstance) {
const registryService = appInstance.lookup('service:universe/registry-service');
console.log('[initialize-registries] Creating console registries...');
debug('[Initializing Registries] Creating console registries...');
// Create console-specific registries
registryService.createRegistries(['@fleetbase/console', 'auth:login']);
}
@@ -14,5 +16,5 @@ export function initialize(appInstance) {
export default {
name: 'initialize-registries',
after: 'load-extensions',
initialize
initialize,
};

View File

@@ -1,5 +1,6 @@
import { Widget } from '@fleetbase/ember-core/contracts';
import { faGithub } from '@fortawesome/free-brands-svg-icons';
import { debug } from '@ember/debug';
/**
* Register dashboard and widgets for FleetbaseConsole
@@ -8,7 +9,7 @@ import { faGithub } from '@fortawesome/free-brands-svg-icons';
export function initialize(appInstance) {
const widgetService = appInstance.lookup('service:universe/widget-service');
console.log('[initialize-widgets] Registering console dashboard and widgets...');
debug('[Initializing Widgets] Registering console dashboard and widgets...');
// Register the console dashboard
widgetService.registerDashboard('dashboard');

View File

@@ -17,6 +17,5 @@ export async function initialize(appInstance) {
export default {
name: 'load-extensions',
initialize
initialize,
};

View File

@@ -12,6 +12,5 @@ export async function initialize(appInstance) {
export default {
name: 'setup-extensions',
after: ['load-extensions', 'initialize-registries', 'initialize-widgets'],
initialize
initialize,
};

View File

@@ -96,10 +96,7 @@ export default class ApplicationRoute extends Route {
* @memberof ApplicationRoute
*/
afterModel() {
if (!this.session.isAuthenticated) {
console.log('boot loader removed');
removeBootLoader();
}
if (!this.session.isAuthenticated) removeBootLoader();
}
/**

View File

@@ -39,7 +39,6 @@ export default class ConsoleRoute extends Route {
async afterModel(model, transition) {
this.hookService.execute('console:after-model', this.session, this.router, model, transition);
removeBootLoader();
console.log('boot loader removed');
}
/**

View File

@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ConsoleAccountVirtualRoute extends Route {
@service('universe/menu-service') menuService;
@service universe;
queryParams = {
@@ -12,6 +13,6 @@ export default class ConsoleAccountVirtualRoute extends Route {
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console:account', slug, view);
return this.menuService.lookupMenuItem('console:account', slug, view);
}
}

View File

@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ConsoleAdminVirtualRoute extends Route {
@service('universe/menu-service') menuService;
@service universe;
queryParams = {
@@ -12,6 +13,6 @@ export default class ConsoleAdminVirtualRoute extends Route {
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console:admin', slug, view);
return this.menuService.lookupMenuItem('console:admin', slug, view);
}
}

View File

@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ConsoleSettingsVirtualRoute extends Route {
@service('universe/menu-service') menuService;
@service universe;
queryParams = {
@@ -12,6 +13,6 @@ export default class ConsoleSettingsVirtualRoute extends Route {
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console:settings', slug, view);
return this.menuService.lookupMenuItem('console:settings', slug, view);
}
}

View File

@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ConsoleVirtualRoute extends Route {
@service('universe/menu-service') menuService;
@service universe;
queryParams = {
@@ -12,6 +13,6 @@ export default class ConsoleVirtualRoute extends Route {
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console', slug, view);
return this.menuService.lookupMenuItem('console', slug, view);
}
}

View File

@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class VirtualRoute extends Route {
@service('universe/menu-service') menuService;
@service universe;
queryParams = {
@@ -12,6 +13,6 @@ export default class VirtualRoute extends Route {
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('auth:login', slug, view);
return this.menuService.lookupMenuItem('auth:login', slug, view);
}
}

View File

@@ -4,7 +4,7 @@
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-">
{{component @model.component params=@model.componentParams}}
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />
</div>
</div>
<Spacer @height="300px" />

View File

@@ -6,6 +6,7 @@
<Layout::Sidebar::Item @route="console.admin.branding" @icon="palette">{{t "console.admin.menu.branding"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.two-fa-settings" @icon="shield-halved">{{t "console.admin.menu.2fa-config"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.schedule-monitor" @icon="calendar-check">{{t "console.admin.schedule-monitor.schedule-monitor"}}</Layout::Sidebar::Item>
{{#each this.universe.adminMenuItems as |menuItem|}}
<Layout::Sidebar::Item
@onClick={{fn this.universe.transitionMenuItem "console.admin.virtual" menuItem}}
@@ -13,6 +14,7 @@
@icon={{menuItem.icon}}
>{{menuItem.title}}</Layout::Sidebar::Item>
{{/each}}
{{#each this.universe.adminMenuPanels as |menuPanel|}}
<Layout::Sidebar::Panel @open={{menuPanel.open}} @title={{menuPanel.title}}>
{{#each menuPanel.items as |menuItem|}}

View File

@@ -4,7 +4,7 @@
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-">
{{component @model.component params=@model.componentParams}}
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />
</div>
</div>
<Spacer @height="300px" />

View File

@@ -4,7 +4,7 @@
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-">
{{component @model.component params=@model.componentParams}}
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />
</div>
</div>
<Spacer @height="300px" />

View File

@@ -1,6 +1,6 @@
{{page-title @model.title}}
<Layout::Section::Body class="overflow-y-scroll h-full">
{{component @model.component params=@model.componentParams}}
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -1,2 +1,2 @@
{{page-title @model.title}}
{{component @model.component params=@model.componentParams}}
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />

View File

@@ -60,21 +60,21 @@ export function applyRuntimeConfig(rawConfig = {}) {
const coercedValue = coerceValue(key, value);
set(config, configPath, coercedValue);
} else {
debug(`[runtime-config] Ignored unknown key: ${key}`);
debug(`[Runtime Config] Ignored unknown key: ${key}`);
}
});
}
/**
* Get cached config from localStorage
*
*
* @returns {Object|null} Cached config or null
*/
function getCachedConfig() {
try {
const cached = localStorage.getItem(CACHE_KEY);
const cachedVersion = localStorage.getItem(CACHE_VERSION_KEY);
if (!cached || !cachedVersion) {
return null;
}
@@ -84,55 +84,55 @@ function getCachedConfig() {
// Check if cache is still valid (within TTL)
if (cacheAge > CACHE_TTL) {
debug('[runtime-config] Cache expired');
debug('[Runtime Config] Cache expired');
return null;
}
debug(`[runtime-config] Using cached config (age: ${Math.round(cacheAge / 1000)}s)`);
debug(`[Runtime Config] Using cached config (age: ${Math.round(cacheAge / 1000)}s)`);
return cacheData.config;
} catch (e) {
debug(`[runtime-config] Failed to read cache: ${e.message}`);
debug(`[Runtime Config] Failed to read cache: ${e.message}`);
return null;
}
}
/**
* Save config to localStorage cache
*
*
* @param {Object} config Config object
*/
function setCachedConfig(config) {
try {
const cacheData = {
config,
timestamp: Date.now()
timestamp: Date.now(),
};
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
localStorage.setItem(CACHE_VERSION_KEY, '1');
debug('[runtime-config] Config cached to localStorage');
debug('[Runtime Config] Config cached to localStorage');
} catch (e) {
debug(`[runtime-config] Failed to cache config: ${e.message}`);
debug(`[Runtime Config] Failed to cache config: ${e.message}`);
}
}
/**
* Clear cached config
*
*
* @export
*/
export function clearRuntimeConfigCache() {
try {
localStorage.removeItem(CACHE_KEY);
localStorage.removeItem(CACHE_VERSION_KEY);
debug('[runtime-config] Cache cleared');
debug('[Runtime Config] Cache cleared');
} catch (e) {
debug(`[runtime-config] Failed to clear cache: ${e.message}`);
debug(`[Runtime Config] Failed to clear cache: ${e.message}`);
}
}
/**
* Load and apply runtime config with localStorage caching.
*
*
* Strategy:
* 1. Check localStorage cache first (instant, no HTTP request)
* 2. If cache hit and valid, use it immediately
@@ -147,34 +147,34 @@ export default async function loadRuntimeConfig() {
return;
}
// Try cache first
const cachedConfig = getCachedConfig();
if (cachedConfig) {
applyRuntimeConfig(cachedConfig);
return;
}
// // Try cache first
// const cachedConfig = getCachedConfig();
// if (cachedConfig) {
// applyRuntimeConfig(cachedConfig);
// return;
// }
// Cache miss - fetch from server
try {
const startTime = performance.now();
const response = await fetch(`/fleetbase.config.json`, {
cache: 'default' // Use browser cache if available
const response = await fetch('/fleetbase.config.json', {
cache: 'default', // Use browser cache if available
});
if (!response.ok) {
debug('[runtime-config] No fleetbase.config.json found, using built-in config defaults');
debug('[Runtime Config] No fleetbase.config.json found, using built-in config defaults');
return;
}
const runtimeConfig = await response.json();
const endTime = performance.now();
debug(`[runtime-config] Fetched from server in ${(endTime - startTime).toFixed(2)}ms`);
debug(`[Runtime Config] Fetched from server in ${(endTime - startTime).toFixed(2)}ms`);
// Apply and cache
applyRuntimeConfig(runtimeConfig);
setCachedConfig(runtimeConfig);
} catch (e) {
debug(`[runtime-config] Failed to load runtime config: ${e.message}`);
debug(`[Runtime Config] Failed to load runtime config: ${e.message}`);
}
}

View File

@@ -21,13 +21,7 @@ module.exports = function (defaults) {
storeConfigInMeta: false,
fingerprint: {
exclude: [
'leaflet/',
'leaflet-images/',
'socketcluster-client.min.js',
'fleetbase.config.json',
'extensions.json'
],
exclude: ['leaflet/', 'leaflet-images/', 'socketcluster-client.min.js', 'fleetbase.config.json', 'extensions.json'],
},
liveReload: {
@@ -71,7 +65,7 @@ module.exports = function (defaults) {
plugins: [require.resolve('ember-auto-import/babel-plugin')],
},
});
let runtimeConfigTree;
if (toBoolean(process.env.DISABLE_RUNTIME_CONFIG)) {
runtimeConfigTree = writeFile('fleetbase.config.json', '{}');

View File

@@ -13,7 +13,7 @@ module.exports = {
name: require('./package').name,
getGeneratedFileHeader() {
const year = (new Date()).getFullYear();
const year = new Date().getFullYear();
return `/**
* ███████╗██╗ ███████╗███████╗████████╗██████╗ █████╗ ███████╗███████╗
* ██╔════╝██║ ██╔════╝██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝
@@ -37,41 +37,41 @@ module.exports = {
included(app) {
this._super.included.apply(this, arguments);
console.log('\n' + '/'.repeat(70));
console.log('[Fleetbase] Extension Build System');
console.log('/'.repeat(70));
// Generate files on startup
this.generateExtensionFiles();
// Watch for changes in development
this.watchExtensionFiles();
},
async generateExtensionFiles() {
const extensions = await this.getExtensions();
if (extensions.length === 0) {
console.log('[Fleetbase] No extensions found');
return;
}
console.log(`[Fleetbase] Discovered ${extensions.length} extension(s)`);
extensions.forEach(ext => {
extensions.forEach((ext) => {
console.log(`[Fleetbase] - ${ext.name} (v${ext.version})`);
});
console.log('');
// Generate extension shims
this.generateExtensionShims(extensions);
// Generate extension loaders
this.generateExtensionLoaders(extensions);
// Generate router
this.generateRouter(extensions);
// Generate manifest
this.generateExtensionsManifest(extensions);
},
@@ -184,12 +184,14 @@ module.exports = {
const mountPath = extension.fleetbase?.route || this.getExtensionMountPath(extension.name);
const camelCaseName = mountPath.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
imports.push(`import ${camelCaseName} from './${mountPath}';`);
loaders[extension.name] = `() => ${camelCaseName}`;
});
const loadersContent = this.getGeneratedFileHeader() + `${imports.join('\n')}
const loadersContent =
this.getGeneratedFileHeader() +
`${imports.join('\n')}
export const EXTENSION_LOADERS = {
${Object.entries(loaders)
@@ -227,7 +229,7 @@ export default getExtensionLoader;
functionExpression = arg;
}
});
if (functionExpression) {
// Check and add the new engine mounts
consoleExtensions.forEach((extension) => {
@@ -337,7 +339,7 @@ export default getExtensionLoader;
}
const self = this;
this.getExtensions().then((extensions) => {
const extensionFiles = [];

View File

@@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Controller | virtual', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup('controller:virtual');
assert.ok(controller);
});
});