Compare commits

...

5 Commits

Author SHA1 Message Date
Ron
e41cd62ea5 Merge pull request #481 from fleetbase/feature/onboarding-wrapper-architecture
feat: Add wrapper component support to onboarding orchestrator
2025-12-19 15:56:49 +08:00
Ronald A. Richardson
1ca1342052 feat: fixed optimization changes for octane, added deprecation workflow 2025-12-19 15:56:03 +08:00
roncodes
a9b172081a feat: Add lifecycle hooks support to onboarding orchestrator
- Add onFlowWillStart, onFlowDidStart, onStepWillChange, onStepDidChange, onFlowWillEnd, onFlowDidEnd hooks
- Hooks are optional and backward compatible with existing flows
- Add getCurrentPath() and isStepInPath() helper methods for multi-path flows
- Support dynamic next() functions (already existed, now documented)
- Maintain full backward compatibility with default@v1 flow
2025-12-11 23:10:23 -05:00
Ronald A. Richardson
a29ca0ecb9 feat: update onboarding-registry service to allow set default onboard flow on registration 2025-12-09 09:48:23 +08:00
roncodes
6442644438 feat: Add wrapper component support to onboarding orchestrator
- Add wrapper property to OnboardingOrchestratorService
- Update onboarding/yield component to render wrapper using lazy-engine-component
- Clean up onboard.hbs template to remove styling constraints
- Enable extensions to provide custom wrapper components for onboarding flows
2025-12-08 20:38:59 -05:00
10 changed files with 100 additions and 28 deletions

View File

@@ -1,9 +1,6 @@
{ {
frankenphp { frankenphp {
# Reduced from 24 to 20 for better resource management num_threads 24
# With 4 containers: 20 × 4 = 80 total workers
# Requires db.t3.large (591 max connections) or better
num_threads 20
} }
order php_server before file_server order php_server before file_server
} }
@@ -12,14 +9,6 @@ http://:8000 {
root * /fleetbase/api/public root * /fleetbase/api/public
encode zstd br gzip encode zstd br gzip
# Request timeouts to prevent hanging
timeouts {
read_body 10s
read_header 5s
write 60s
idle 120s
}
php_server { php_server {
resolve_root_symlink resolve_root_symlink
} }

View File

@@ -105,8 +105,8 @@ return [
OperationTerminated::class => [ OperationTerminated::class => [
FlushOnce::class, FlushOnce::class,
FlushTemporaryContainerInstances::class, FlushTemporaryContainerInstances::class,
DisconnectFromDatabases::class, // ✅ Release DB connections after each request DisconnectFromDatabases::class,
CollectGarbage::class, // ✅ Prevent memory leaks CollectGarbage::class,
], ],
WorkerErrorOccurred::class => [ WorkerErrorOccurred::class => [

View File

@@ -2,6 +2,7 @@ import Application from '@ember/application';
import Resolver from 'ember-resolver'; import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers'; import loadInitializers from 'ember-load-initializers';
import config from '@fleetbase/console/config/environment'; import config from '@fleetbase/console/config/environment';
import './deprecation-workflow';
export default class App extends Application { export default class App extends Application {
modulePrefix = config.modulePrefix; modulePrefix = config.modulePrefix;

View File

@@ -1,7 +1,9 @@
<section class="onboarding step-host"> <section class="onboarding step-host">
{{#if this.initialized}} {{#if this.initialized}}
{{#if this.currentComponent}} {{#if this.orchestrator.wrapper}}
{{component this.currentComponent context=this.context orchestrator=this.orchestrator brand=@brand}} {{component (lazy-engine-component this.orchestrator.wrapper) currentStepComponent=this.currentComponent context=this.context orchestrator=this.orchestrator brand=@brand}}
{{else if this.currentComponent}}
{{component (lazy-engine-component this.currentComponent) context=this.context orchestrator=this.orchestrator brand=@brand}}
{{/if}} {{/if}}
{{else}} {{else}}
<div class="flex items-center justify-center min-h-24"> <div class="flex items-center justify-center min-h-24">

View File

@@ -0,0 +1,9 @@
import setupDeprecationWorkflow from 'ember-cli-deprecation-workflow';
setupDeprecationWorkflow({
workflow: [
{ handler: 'silence', matchId: 'ember-concurrency.deprecate-decorator-task' },
{ handler: 'silence', matchId: 'new-helper-names' },
{ handler: 'silence', matchId: 'ember-data:deprecate-non-strict-relationships' },
],
});

View File

@@ -7,17 +7,31 @@ export default class OnboardingOrchestratorService extends Service {
@service onboardingContext; @service onboardingContext;
@tracked flow = null; @tracked flow = null;
@tracked wrapper = null;
@tracked current = null; @tracked current = null;
@tracked history = []; @tracked history = [];
@tracked sessionId = null; @tracked sessionId = null;
start(flowId = null, opts = {}) { async start(flowId = null, opts = {}) {
const flow = this.onboardingRegistry.getFlow(flowId ?? this.onboardingRegistry.defaultFlow); const flow = this.onboardingRegistry.getFlow(flowId ?? this.onboardingRegistry.defaultFlow);
if (!flow) throw new Error(`Onboarding flow '${flowId}' not found`); if (!flow) throw new Error(`Onboarding flow '${flowId}' not found`);
this.flow = flow; this.flow = flow;
this.wrapper = flow.wrapper || null;
this.sessionId = opts.sessionId || null; this.sessionId = opts.sessionId || null;
this.history = []; this.history = [];
this.goto(flow.entry);
// Execute onFlowWillStart hook if defined
if (typeof this.flow.onFlowWillStart === 'function') {
await this.flow.onFlowWillStart(this.flow, this);
}
await this.goto(flow.entry);
// Execute onFlowDidStart hook if defined
if (typeof this.flow.onFlowDidStart === 'function') {
await this.flow.onFlowDidStart(this.flow, this);
}
} }
async goto(stepId) { async goto(stepId) {
@@ -25,27 +39,43 @@ export default class OnboardingOrchestratorService extends Service {
const step = this.flow.steps.find((s) => s.id === stepId); const step = this.flow.steps.find((s) => s.id === stepId);
if (!step) throw new Error(`Step '${stepId}' not found`); if (!step) throw new Error(`Step '${stepId}' not found`);
// Execute onStepWillChange hook if defined
const previousStep = this.current;
if (typeof this.flow.onStepWillChange === 'function') {
await this.flow.onStepWillChange(step, previousStep, this);
}
// Guard function - skip step if guard returns false
if (typeof step.guard === 'function' && !step.guard(this.onboardingContext)) { if (typeof step.guard === 'function' && !step.guard(this.onboardingContext)) {
return this.next(); return this.next();
} }
// beforeEnter lifecycle hook
if (typeof step.beforeEnter === 'function') { if (typeof step.beforeEnter === 'function') {
await step.beforeEnter(this.onboardingContext); await step.beforeEnter(this.onboardingContext);
} }
this.current = step; this.current = step;
// Execute onStepDidChange hook if defined
if (typeof this.flow.onStepDidChange === 'function') {
await this.flow.onStepDidChange(this.current, previousStep, this);
}
} }
async next() { async next() {
if (!this.flow || !this.current) return; if (!this.flow || !this.current) return;
const leaving = this.current; const leaving = this.current;
// afterLeave lifecycle hook
if (typeof leaving.afterLeave === 'function') { if (typeof leaving.afterLeave === 'function') {
await leaving.afterLeave(this.onboardingContext); await leaving.afterLeave(this.onboardingContext);
} }
if (!this.history.includes(leaving)) this.history.push(leaving); if (!this.history.includes(leaving)) this.history.push(leaving);
// Support both string and function for next property
let nextId; let nextId;
if (typeof leaving.next === 'function') { if (typeof leaving.next === 'function') {
nextId = leaving.next(this.onboardingContext); nextId = leaving.next(this.onboardingContext);
@@ -53,8 +83,20 @@ export default class OnboardingOrchestratorService extends Service {
nextId = leaving.next; nextId = leaving.next;
} }
// If no next step, flow is complete
if (!nextId) { if (!nextId) {
// Execute onFlowWillEnd hook if defined
if (typeof this.flow.onFlowWillEnd === 'function') {
await this.flow.onFlowWillEnd(leaving, this);
}
this.current = null; // finished this.current = null; // finished
// Execute onFlowDidEnd hook if defined
if (typeof this.flow.onFlowDidEnd === 'function') {
await this.flow.onFlowDidEnd(leaving, this);
}
return; return;
} }
@@ -68,4 +110,31 @@ export default class OnboardingOrchestratorService extends Service {
this.history = this.history.slice(0, -1); this.history = this.history.slice(0, -1);
await this.goto(prev.id); await this.goto(prev.id);
} }
/**
* Get the current path (for flows with multiple paths)
* This is a helper method that can be used by flows to determine the current path
*/
getCurrentPath() {
if (!this.flow || !this.flow.paths) return null;
// Determine path based on context or current step
for (const [pathId, pathDef] of Object.entries(this.flow.paths)) {
if (pathDef.steps && pathDef.steps.some(s => s.id === this.current?.id)) {
return pathDef;
}
}
return null;
}
/**
* Check if a step is in the current path
*/
isStepInPath(stepId) {
const currentPath = this.getCurrentPath();
if (!currentPath) return true; // If no paths defined, all steps are valid
return currentPath.steps?.some(s => s.id === stepId) ?? false;
}
} }

View File

@@ -9,7 +9,7 @@ export default class OnboardingRegistryService extends Service {
this.defaultFlow = flowId; this.defaultFlow = flowId;
} }
registerFlow(flow) { registerFlow(flow, options = {}) {
if (!flow || !flow.id || !flow.entry || !Array.isArray(flow.steps)) { if (!flow || !flow.id || !flow.entry || !Array.isArray(flow.steps)) {
throw new Error('Invalid FlowDef: id, entry, steps are required'); throw new Error('Invalid FlowDef: id, entry, steps are required');
} }
@@ -23,6 +23,11 @@ export default class OnboardingRegistryService extends Service {
} }
} }
this.flows.set(flow.id, flow); this.flows.set(flow.id, flow);
// If specified, set as default flow
if (options.default) {
this.defaultFlow = flow.id;
}
} }
getFlow(id) { getFlow(id) {

View File

@@ -1,6 +1,3 @@
<div class="flex items-center justify-center h-screen min-h-screen px-4 py-12 bg-gray-50 dark:bg-gray-900 sm:px-6 lg:px-8 overflow-y-scroll"> <div class="onboard-route-wrapper">
<div class="w-full max-w-md h-screen flex items-center justify-center py-4"> {{outlet}}
{{outlet}}
</div>
<Spacer @height="300px" />
</div> </div>

View File

@@ -23,7 +23,7 @@ module.exports = function (environment) {
APP: { APP: {
autoboot: true, autoboot: true,
extensions: asArray(getenv('EXTENSIONS')), extensions: asArray(getenv('EXTENSIONS')),
disableRuntimeConfig: toBoolean(getenv('DISABLE_RUNTIME_CONFIG')), disableRuntimeConfig: toBoolean(getenv('DISABLE_RUNTIME_CONFIG', environment === 'production')),
}, },
API: { API: {

View File

@@ -158,14 +158,14 @@ CMD ["php", "artisan", "queue:work"]
# Application dev stage # Application dev stage
FROM base AS app-dev FROM base AS app-dev
ENTRYPOINT ["docker-php-entrypoint"] ENTRYPOINT ["docker-php-entrypoint"]
CMD ["sh", "-c", "php artisan octane:frankenphp --workers=20 --max-requests=1000 --port=8000 --host=0.0.0.0 --watch"] CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=1000 --port=8000 --host=0.0.0.0 --watch"]
# Application release stage # Application release stage
FROM base AS app-release FROM base AS app-release
ENTRYPOINT ["docker-php-entrypoint"] ENTRYPOINT ["docker-php-entrypoint"]
CMD ["sh", "-c", "php artisan octane:frankenphp --workers=20 --max-requests=1000 --port=8000 --host=0.0.0.0"] CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=1000 --port=8000 --host=0.0.0.0"]
# Application stage # Application stage
FROM base AS app FROM base AS app
ENTRYPOINT ["/sbin/ssm-parent", "-c", ".ssm-parent.yaml", "run", "--", "docker-php-entrypoint"] ENTRYPOINT ["/sbin/ssm-parent", "-c", ".ssm-parent.yaml", "run", "--", "docker-php-entrypoint"]
CMD ["sh", "-c", "php artisan octane:frankenphp --workers=20 --max-requests=1000 --port=8000 --host=0.0.0.0"] CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=1000 --port=8000 --host=0.0.0.0"]