Compare commits

...

30 Commits

Author SHA1 Message Date
Ronald A. Richardson
658568e4ec bump console/package.json version
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
2025-11-17 19:24:00 +08:00
Ron
8a487b2352 Merge pull request #469 from fleetbase/dev-v0.7.19
v0.7.19
2025-11-17 19:22:41 +08:00
Ronald A. Richardson
bc89218a26 add fleetops-data 2025-11-17 19:17:01 +08:00
Ronald A. Richardson
5a4f7e2ae3 upgraded dependencies 2025-11-17 18:43:28 +08:00
Ronald A. Richardson
9fa1bf54d2 v0.7.19 ~ A major leap forward in scheduling, reporting, and user interface capabilities. 2025-11-17 18:17:51 +08:00
Ronald A. Richardson
b98eb3adf5 Merge branch 'main' of github.com:fleetbase/fleetbase into ron/dev-v0.7.19 2025-11-10 11:30:12 +08:00
Ronald A. Richardson
5473b50c40 development in progress 2025-11-10 11:27:50 +08:00
Ronald A. Richardson
d9f415528e hotfix: update api/composer.json
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
2025-11-10 11:02:06 +08:00
Ron
76b0bfbfcd Merge pull request #463 from fleetbase/dev-v0.7.18
v0.7.18
2025-11-10 10:52:51 +08:00
Ronald A. Richardson
0432003163 latest storefront 2025-11-10 10:52:09 +08:00
Ronald A. Richardson
da420f0b4a ready for release 2025-11-10 10:49:29 +08:00
Ronald A. Richardson
e923a89719 v0.7.18 2025-11-10 10:35:47 +08:00
Ronald A. Richardson
0742603b43 added storefront release 2025-11-10 10:31:02 +08:00
Ronald A. Richardson
e1788a4ad6 v0.7.17 2025-11-10 10:29:26 +08:00
Ron
7cb4654c86 Merge pull request #460 from fleetbase/dev-v0.7.17
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.17 ~ hotfix router map
2025-11-06 21:59:37 +08:00
Ronald A. Richardson
a17aa3f5cc hotfix router map 2025-11-06 21:58:40 +08:00
Ron
0bf1a7fadd Merge pull request #459 from fleetbase/dev-v0.7.16
v0.7.16
2025-11-06 21:17:49 +08:00
Ronald A. Richardson
aa1ea2de89 Merge branch 'main' of github.com:fleetbase/fleetbase into dev-v0.7.16 2025-11-06 21:10:23 +08:00
Ronald A. Richardson
235f1ce80c upgraded dependencies 2025-11-06 20:45:42 +08:00
Ronald A. Richardson
5aa50504a4 updated RELEASE.md 2025-11-06 20:34:56 +08:00
Ronald A. Richardson
5d1b2e1939 - Made the LogApiRequests middleware more robust
- Fixed controller validation handling
- Added microsoft365/graph mail driver
- Improved password requirements (including breached password check)
- Patched creating duplicate users by email in IAM
- Patch env mapper
- Vehicle/driver tracking API doesnt fire resource lifecycle events or log requests - only tracking events
- Patched `<ModelCoordinatesInput />` component
- Security patch on Storefront customers API
- Styling updates on Storefront
2025-11-06 20:33:23 +08:00
Ron
fc5d90189c Merge pull request #456 from fleetbase/dev-v0.7.15
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.15
2025-11-01 14:08:41 +08:00
Ronald A. Richardson
2fee78e534 performed upgrades 2025-11-01 14:07:55 +08:00
Ronald A. Richardson
83fc794702 v0.7.15 2025-11-01 14:00:59 +08:00
Ron
66f669ad80 Merge pull request #454 from fleetbase/dev-v0.7.14
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.14
2025-10-30 18:53:33 +08:00
Ronald A. Richardson
a11b77592c fix console/package.json 2025-10-30 18:23:44 +08:00
Ronald A. Richardson
e5156829dc update release.md 2025-10-30 18:22:01 +08:00
Ronald A. Richardson
6cd7ddffcb v0.7.14 2025-10-30 18:20:04 +08:00
Ron
cbdf1d489b Merge pull request #447 from fleetbase/dev-v0.7.13
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.13 ~ Connectivity Module + Positions Playback + Positions & Device Events Drawer
2025-10-28 05:48:35 +08:00
Ronald A. Richardson
785bc55bb7 v0.7.13 ~ Connectivity Module + Positions Playback + Positions & Device Events Drawer 2025-10-28 05:39:54 +08:00
48 changed files with 2616 additions and 1878 deletions

View File

@@ -1,13 +1,17 @@
# 🚀 Fleetbase v0.7.12 — 2025-10-22
# 🚀 Fleetbase v0.7.19 — 2025-11-17
> “Patches”
> "A major leap forward in scheduling, reporting, and user interface capabilities."
---
## ✨ Highlights
- Improvements to Fleet-Ops orders Kanban view, filter by order type and search/filters working.
- Patches to food truck API.
- Patches to multiple waypoints API handling.
- **Core Scheduling Module**: A comprehensive, polymorphic, and reusable scheduling system has been integrated into the core API, providing the foundation for a wide range of scheduling applications.
- **Driver Scheduling with HOS Compliance**: FleetOps now includes built-in compliance for FMCSA Hours of Service regulations.
- **Computed Columns in Query Builder**: The query builder now supports computed columns, allowing for more complex and powerful data queries with secure expression validation.
- **Advanced Table Functionality**: The Ember UI table component now supports multi-column sorting, horizontal scrolling, and sticky columns.
- **New Filter Components**: New filter components for multi-input and range selection have been added to the Ember UI.
- **Dispatched Flag Control**: The order creation process in FleetOps now allows for explicit control over the dispatch behavior.
- **Vehicle Attributes Enhancement**: The vehicle model and API resources in FleetOps have been enhanced with additional attributes.
---
@@ -30,4 +34,4 @@ docker compose exec application bash -c "./deploy.sh"
```
## Need help?
Join the discussion on [GitHub Discussions](https://github.com/fleetbase/fleetbase/discussions) or drop by [#fleetbase on Discord](https://discord.com/invite/HnTqQ6zAVn)
Join the discussion on [GitHub Discussions](https://github.com/fleetbase/fleetbase/discussions) or drop by [#fleetbase on Discord](https://discord.com/invite/HnTqQ6zAVn)

View File

@@ -20,10 +20,10 @@
"require": {
"php": "^8.0",
"appstract/laravel-opcache": "^4.0",
"fleetbase/core-api": "^1.6.19",
"fleetbase/fleetops-api": "^0.6.20",
"fleetbase/core-api": "^1.6.25",
"fleetbase/fleetops-api": "^0.6.26",
"fleetbase/registry-bridge": "^0.1.0",
"fleetbase/storefront-api": "^0.4.3",
"fleetbase/storefront-api": "^0.4.7",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^10.0",
"laravel/octane": "^2.3",
@@ -36,6 +36,7 @@
"psr/http-factory-implementation": "*",
"resend/resend-php": "^0.14.0",
"s-ichikawa/laravel-sendgrid-driver": "^4.0",
"stripe/stripe-php": "13.13.0",
"symfony/mailgun-mailer": "^7.1",
"symfony/postmark-mailer": "^7.1"
},

658
api/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -66,6 +66,18 @@ return [
'resend' => [],
'microsoft-graph' => [
'transport' => 'microsoft-graph',
'client_id' => env('MICROSOFT_GRAPH_CLIENT_ID'),
'client_secret' => env('MICROSOFT_GRAPH_CLIENT_SECRET'),
'tenant_id' => env('MICROSOFT_GRAPH_TENANT_ID'),
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@fleetbase.io'),
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Fleetbase')),
],
'save_to_sent_items' => env('MAIL_SAVE_TO_SENT_ITEMS', false),
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -t -i'),

View File

@@ -13,6 +13,14 @@
<InputGroup @name="SMTP Timeout" @value={{this.smtpTimeout}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="SMTP Auth Mode" @value={{this.smtpAuth_mode}} disabled={{this.loadConfigValues.isRunning}} />
{{/if}}
{{#if (eq this.mailer "microsoft-graph")}}
<InputGroup @name="Client ID" @value={{this.microsoftGraphClient_id}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="Client Secret" @value={{this.microsoftGraphClient_secret}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="Tenant ID" @value={{this.microsoftGraphTenant_id}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup>
<Toggle @isToggled={{this.microsoftGraphSave_to_sent_items}} @onToggle={{fn (mut this.microsoftGraphSave_to_sent_items)}} @label="Save to sent items" />
</InputGroup>
{{/if}}
{{#if (eq this.mailer "mailgun")}}
<InputGroup @name="Mailgun Domain" @value={{this.mailgunDomain}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="Mailgun Endpoint" @value={{this.mailgunEndpoint}} disabled={{this.loadConfigValues.isRunning}} />

View File

@@ -26,6 +26,10 @@ export default class ConfigureMailComponent extends Component {
@tracked postmarkToken = null;
@tracked sendgridApi_key = null;
@tracked resendKey = null;
@tracked microsoftGraphClient_id = null;
@tracked microsoftGraphClient_secret = null;
@tracked microsoftGraphTenant_id = null;
@tracked microsoftGraphSave_to_sent_items = false;
/**
* Creates an instance of ConfigureFilesystemComponent.
@@ -64,6 +68,19 @@ export default class ConfigureMailComponent extends Component {
};
}
@action serializeMicrosoftGraphConfig() {
return {
client_id: this.microsoftGraphClient_id,
client_secret: this.microsoftGraphClient_secret,
tenant_id: this.microsoftGraphTenant_id,
save_to_sent_items: this.microsoftGraphSave_to_sent_items,
from: {
address: this.fromAddress,
name: this.fromName,
},
};
}
@action serializeMailgunConfig() {
return {
domain: this.mailgunDomain,
@@ -112,6 +129,7 @@ export default class ConfigureMailComponent extends Component {
postmark: this.serializePostmarkConfig(),
sendgrid: this.serializeSendgridConfig(),
resend: this.serializeResendConfig(),
microsoftGraph: this.serializeMicrosoftGraphConfig(),
});
} catch (error) {
this.notifications.serverError(error);
@@ -131,6 +149,7 @@ export default class ConfigureMailComponent extends Component {
postmark: this.serializePostmarkConfig(),
sendgrid: this.serializeSendgridConfig(),
resend: this.serializeResendConfig(),
microsoftGraph: this.serializeMicrosoftGraphConfig(),
});
this.notifications.success('Mail configuration saved.');
} catch (error) {

View File

@@ -0,0 +1,42 @@
<div class="bg-white dark:bg-gray-800 py-5 px-4 shadow rounded-lg w-full">
<div class="mb-4">
<Image src={{@brand.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} height="56" class="h-10 object-contain mx-auto" />
<div class="mt-2">
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.index.title"}}
</h2>
</div>
</div>
<div class="flex px-3 py-2 mb-4 rounded-md shadow-sm bg-blue-200">
<div>
<FaIcon @icon="hand-spock" @size="lg" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
{{t "onboard.index.welcome-title" htmlSafe=true companyName=(t "app.name")}}
{{t "onboard.index.welcome-text"}}
</p>
</div>
<form {{on "submit" (perform this.onboard)}}>
{{#if this.error}}
<InfoBlock @icon="exclamation-triangle" @text={{this.error}} class="mb-6 px-3 py-2 bg-red-300 text-red-900" @textClass="text-red-900" />
{{/if}}
<InputGroup @name={{t "onboard.index.full-name"}} @value={{this.name}} @helpText={{t "onboard.index.full-name-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.your-email"}} @type="email" @value={{this.email}} @helpText={{t "onboard.index.your-email-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.phone"}} @helpText={{t "onboard.index.phone-help-text"}}>
<PhoneInput @onInput={{fn (mut this.phone)}} class="form-input input-lg w-full" />
</InputGroup>
<InputGroup @name={{t "onboard.index.organization-name"}} @value={{this.organization_name}} @helpText={{t "onboard.index.organization-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.password"}} @value={{this.password}} @type="password" @helpText={{t "onboard.index.password-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.confirm-password"}} @value={{this.password_confirmation}} @type="password" @helpText={{t "onboard.index.confirm-password-help-text"}} @inputClass="input-lg" />
<div class="flex items-center justify-end mt-5">
<Button @buttonType="submit" @icon="check" @iconPrefix="fas" @type="primary" @size="lg" @text={{t "onboard.index.continue-button-text"}} @isLoading={{this.onboard.isRunning}} @disabled={{not this.filled}} />
</div>
</form>
<RegistryYield @registry="onboard" as |YieldedComponent ctx|>
<YieldedComponent @context={{ctx}} />
</RegistryYield>
</div>

View File

@@ -0,0 +1,77 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action, getProperties } from '@ember/object';
import { isBlank } from '@ember/utils';
import { task } from 'ember-concurrency';
import OnboardValidations from '../../validations/onboard';
import lookupValidator from 'ember-changeset-validations';
import Changeset from 'ember-changeset';
export default class OnboardingFormComponent extends Component {
@service fetch;
@service session;
@service router;
@service notifications;
@service urlSearchParams;
@tracked name;
@tracked email;
@tracked phone;
@tracked organization_name;
@tracked password;
@tracked password_confirmation;
@tracked error;
get filled() {
// eslint-disable-next-line ember/no-get
const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation');
return Object.values(input).every((val) => !isBlank(val));
}
@task *onboard(event) {
event?.preventDefault?.();
// eslint-disable-next-line ember/no-get
const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation');
const changeset = new Changeset(input, lookupValidator(OnboardValidations), OnboardValidations);
yield changeset.validate();
if (changeset.get('isInvalid')) {
const errorMessage = changeset.errors.firstObject.validation.firstObject;
this.notifications.error(errorMessage);
return;
}
// Set user timezone
input.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
try {
const { status, skipVerification, token, session } = yield this.fetch.post('onboard/create-account', input);
if (status !== 'success') {
this.notifications.error('Onboard failed');
return;
}
// save session
this.args.context.persist('session', session);
if (skipVerification === true && token) {
// only manually authenticate if skip verification
this.session.isOnboarding().manuallyAuthenticate(token);
yield this.router.transitionTo('console');
return this.notifications.success('Welcome to Fleetbase!');
} else {
this.args.orchestrator.next();
this.urlSearchParams.setParamsToCurrentUrl({
step: this.args.orchestrator?.current?.id,
session,
});
}
} catch (err) {
this.notifications.serverError(err);
}
}
}

View File

@@ -0,0 +1,78 @@
{{page-title (t "onboard.verify-email.header-title")}}
{{#if this.initialized}}
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
<div class="mb-6">
<LinkTo @route="console" class="flex items-center justify-center">
<LogoIcon @size="12" class="rounded-md" />
</LinkTo>
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.verify-email.title"}}
</h2>
</div>
<InfoBlock @type="info" @icon="shield-halved" @iconSize="lg">
{{t "onboard.verify-email.message-text" htmlSafe=true}}
</InfoBlock>
<form class="mt-8 space-y-6" {{on "submit" (perform this.verify)}}>
<InputGroup
@type="tel"
@name={{t "onboard.verify-email.verification-input-label"}}
@value={{this.code}}
@helpText={{t "onboard.verify-email.verification-code-text"}}
@inputClass="input-lg"
{{on "input" this.verification.validateInput}}
{{did-insert this.verification.validateInput}}
/>
<div class="flex flex-row items-center space-x-4">
<Button
@icon="check"
@iconPrefix="fas"
@buttonType="submit"
@type="primary"
@size="lg"
@text="Verify & Continue"
@isLoading={{this.verify.isRunning}}
@disabled={{not this.verification.ready}}
/>
<a href="#" {{on "click" this.verification.didntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "onboard.verify-email.didnt-receive-a-code"}}</a>
</div>
{{#if this.verification.waiting}}
<div class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
<div class="flex flex-row items-start mb-2">
<div class="w-8 flex-grow-0 flex-shrink-0">
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
</div>
<div class="flex-1">
<div class="flex-1 text-sm text-yellow-100">
<div>{{t "auth.verification.didnt-receive-a-code" htmlSafe=true}}</div>
<div>{{t "auth.verification.not-sent.alternative-choice" htmlSafe=true}}</div>
</div>
</div>
</div>
<div class="flex items-center space-x-2">
<Button
@text={{t "auth.verification.not-sent.resend-email"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.verification.resendEmail}}
/>
<Button
@text={{t "auth.verification.not-sent.send-by-sms"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.verification.resendBySms}}
/>
</div>
</div>
{{/if}}
</form>
</div>
{{/if}}

View File

@@ -0,0 +1,53 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { later, next } from '@ember/runloop';
import { not } from '@ember/object/computed';
import { task } from 'ember-concurrency';
export default class OnboardingVerifyEmailComponent extends Component {
@service('session') authSession;
@service('user-verification') verification;
@service fetch;
@service notifications;
@service router;
@service urlSearchParams;
@tracked code;
@tracked session;
@tracked initialized = false;
constructor() {
super(...arguments);
next(() => this.#initialize());
}
#initialize() {
this.code = this.urlSearchParams.get('code');
this.session = this.args.context.get('session') ?? this.urlSearchParams.get('session');
this.initialized = true;
this.verification.start();
}
@task *verify(event) {
event?.preventDefault?.();
try {
const { status, token } = yield this.fetch.post('onboard/verify-email', { session: this.session, code: this.code });
if (status === 'ok') {
this.notifications.success('Email successfully verified!');
if (token) {
this.notifications.info('Welcome to Fleetbase!');
this.authSession.manuallyAuthenticate(token);
return this.router.transitionTo('console');
}
return this.router.transitionTo('auth.login');
}
} catch (error) {
this.notifications.serverError(error);
}
}
}

View File

@@ -0,0 +1,11 @@
<section class="onboarding step-host">
{{#if this.initialized}}
{{#if this.currentComponent}}
{{component this.currentComponent context=this.context orchestrator=this.orchestrator brand=@brand}}
{{/if}}
{{else}}
<div class="flex items-center justify-center min-h-24">
<Spinner />
</div>
{{/if}}
</section>

View File

@@ -0,0 +1,27 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { next } from '@ember/runloop';
export default class OnboardingYieldComponent extends Component {
@service('onboarding-orchestrator') orchestrator;
@service('onboarding-context') context;
@tracked initialized = false;
get currentComponent() {
return this.orchestrator.current && this.orchestrator.current.component;
}
constructor(owner, { step, session, code }) {
super(...arguments);
next(() => this.#initialize(step, session, code));
}
#initialize(step, session, code) {
if (step) this.orchestrator.goto(step);
if (session) this.context.persist('session', session);
if (code) this.context.set('code', code);
this.initialized = true;
}
}

View File

@@ -17,9 +17,9 @@ export default class ConsoleController extends Controller {
@service intl;
@service universe;
@service abilities;
@service sidebar;
@tracked organizations = [];
@tracked sidebarContext;
@tracked sidebarToggleEnabled = true;
@tracked sidebarToggleState = {};
@tracked hiddenSidebarRoutes = ['console.home', 'console.notifications', 'console.virtual'];
@tracked menuItems = [];
@@ -42,17 +42,17 @@ export default class ConsoleController extends Controller {
// Hide the sidebar if the current route is in hiddenSidebarRoutes
if (shouldHideSidebar) {
this.sidebarContext.hideNow();
this.sidebar.hideNow();
this.sidebarToggleEnabled = false;
return; // Exit early as no further action is required
}
// If the sidebar was manually closed and not on a hidden route, keep it closed
if (isSidebarManuallyClosed) {
this.sidebarContext.hideNow();
this.sidebar.hideNow();
} else {
// Otherwise, show the sidebar
this.sidebarContext.show();
this.sidebar.show();
}
// Ensure toggle is enabled unless on a hidden route
@@ -87,7 +87,7 @@ export default class ConsoleController extends Controller {
this.universe.trigger('sidebarContext.available', sidebarContext);
if (this.hiddenSidebarRoutes.includes(this.router.currentRouteName)) {
this.sidebarContext.hideNow();
this.sidebar.hideNow();
this.sidebarToggleEnabled = false;
}
}

View File

@@ -1,151 +1,8 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action, getProperties } from '@ember/object';
import OnboardValidations from '../../validations/onboard';
import lookupValidator from 'ember-changeset-validations';
import Changeset from 'ember-changeset';
export default class OnboardIndexController extends Controller {
/**
* Inject the `fetch` service
*
* @memberof OnboardIndexController
*/
@service fetch;
/**
* Inject the `session` service
*
* @memberof OnboardIndexController
*/
@service session;
/**
* Inject the `router` service
*
* @memberof OnboardIndexController
*/
@service router;
/**
* Inject the `notifications` service
*
* @memberof OnboardIndexController
*/
@service notifications;
/**
* The name input field.
*
* @memberof OnboardIndexController
*/
@tracked name;
/**
* The email input field.
*
* @memberof OnboardIndexController
*/
@tracked email;
/**
* The phone input field.
*
* @memberof OnboardIndexController
*/
@tracked phone;
/**
* The organization_name input field.
*
* @memberof OnboardIndexController
*/
@tracked organization_name;
/**
* The password input field.
*
* @memberof OnboardIndexController
*/
@tracked password;
/**
* The name password confirmation field.
*
* @memberof OnboardIndexController
*/
@tracked password_confirmation;
/**
* The property for error message.
*
* @memberof OnboardIndexController
*/
@tracked error;
/**
* The loading state of the onboard request.
*
* @memberof OnboardIndexController
*/
@tracked isLoading = false;
/**
* The ready state for the form.
*
* @memberof OnboardIndexController
*/
@tracked readyToSubmit = false;
/**
* Start the onboard process.
*
* @return {Promise}
* @memberof OnboardIndexController
*/
@action async startOnboard(event) {
event.preventDefault();
// eslint-disable-next-line ember/no-get
const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation');
const changeset = new Changeset(input, lookupValidator(OnboardValidations), OnboardValidations);
await changeset.validate();
if (changeset.get('isInvalid')) {
const errorMessage = changeset.errors.firstObject.validation.firstObject;
this.notifications.error(errorMessage);
return;
}
// Set user timezone
input.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
this.isLoading = true;
return this.fetch
.post('onboard/create-account', input)
.then(({ status, skipVerification, token, session }) => {
if (status === 'success') {
if (skipVerification === true && token) {
// only manually authenticate if skip verification
this.session.isOnboarding().manuallyAuthenticate(token);
return this.router.transitionTo('console').then(() => {
this.notifications.success('Welcome to Fleetbase!');
});
}
return this.router.transitionTo('onboard.verify-email', { queryParams: { hello: session } });
}
})
.catch((error) => {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
}
@tracked step;
@tracked session;
@tracked code;
}

View File

@@ -0,0 +1,19 @@
export function initialize(owner) {
const registry = owner.lookup('service:onboarding-registry');
if (registry) {
const defaultFlow = {
id: 'default@v1',
entry: 'signup',
steps: [
{ id: 'signup', component: 'onboarding/form', next: 'verify-email' },
{ id: 'verify-email', component: 'onboarding/verify-email' },
],
};
registry.registerFlow(defaultFlow);
}
}
export default {
initialize,
};

View File

@@ -13,7 +13,11 @@ export default class DashboardModel extends Model {
/** @attributes */
@attr('string') name;
@attr('string') extension;
@attr('boolean') is_default;
@attr('array') tags;
@attr('object') options;
@attr('object') meta;
/** @dates */
@attr('date') created_at;

View File

@@ -3,6 +3,7 @@ import { computed } from '@ember/object';
import { isArray } from '@ember/array';
import { getOwner } from '@ember/application';
import { isPresent, isEmpty } from '@ember/utils';
import { format, formatDistanceToNow } from 'date-fns';
export default class ReportModel extends Model {
/** @ids */
@@ -35,6 +36,10 @@ export default class ReportModel extends Model {
@attr('raw') meta;
@attr('string') status;
/** @dates */
@attr('date') created_at;
@attr('date') updated_at;
/** @relationships */
// @belongsTo('company') company;
// @belongsTo('user') createdBy;
@@ -54,8 +59,23 @@ export default class ReportModel extends Model {
}
/** @computed */
@computed('query_config.columns.length', 'query_config.table.name')
get hasValidConfig() {
@computed('updated_at') get updatedAgo() {
return formatDistanceToNow(this.updated_at);
}
@computed('updated_at') get updatedAt() {
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
}
@computed('created_at') get createdAgo() {
return formatDistanceToNow(this.created_at);
}
@computed('created_at') get createdAt() {
return format(this.created_at, 'yyyy-MM-dd HH:mm');
}
@computed('query_config.columns.length', 'query_config.table.name') get hasValidConfig() {
return (
isPresent(this.query_config) &&
isPresent(this.query_config.table) &&
@@ -65,23 +85,19 @@ export default class ReportModel extends Model {
);
}
@computed('query_config.table.name')
get tableName() {
@computed('query_config.table.name') get tableName() {
return this.query_config?.table?.name || '';
}
@computed('query_config.table.label', 'tableName')
get tableLabel() {
@computed('query_config.table.label', 'tableName') get tableLabel() {
return this.query_config?.table?.label || this.tableName;
}
@computed('query_config.columns.[]')
get selectedColumns() {
@computed('query_config.columns.[]') get selectedColumns() {
return this.query_config?.columns || [];
}
@computed('selectedColumns.[]', 'query_config.joins.[]')
get totalSelectedColumns() {
@computed('selectedColumns.[]', 'query_config.joins.[]') get totalSelectedColumns() {
let count = this.selectedColumns.length;
// Add columns from joins
@@ -96,13 +112,11 @@ export default class ReportModel extends Model {
return count;
}
@computed('query_config.joins.[]')
get hasJoins() {
@computed('query_config.joins.[]') get hasJoins() {
return isArray(this.query_config?.joins) && this.query_config.joins.length > 0;
}
@computed('hasJoins', 'query_config.joins.[]')
get joinedTables() {
@computed('hasJoins', 'query_config.joins.[]') get joinedTables() {
if (!this.hasJoins) {
return [];
}
@@ -115,13 +129,11 @@ export default class ReportModel extends Model {
}));
}
@computed('query_config.conditions.[]')
get hasConditions() {
@computed('query_config.conditions.[]') get hasConditions() {
return isArray(this.query_config?.conditions) && this.query_config.conditions.length > 0;
}
@computed('hasConditions', 'query_config.conditions.[]')
get conditionsCount() {
@computed('hasConditions', 'query_config.conditions.[]') get conditionsCount() {
if (!this.hasConditions) {
return 0;
}
@@ -129,23 +141,19 @@ export default class ReportModel extends Model {
return this.countConditionsRecursively(this.query_config.conditions);
}
@computed('query_config.groupBy.[]')
get hasGrouping() {
@computed('query_config.groupBy.[]') get hasGrouping() {
return isArray(this.query_config?.groupBy) && this.query_config.groupBy.length > 0;
}
@computed('query_config.sortBy.[]')
get hasSorting() {
@computed('query_config.sortBy.[]') get hasSorting() {
return isArray(this.query_config?.sortBy) && this.query_config.sortBy.length > 0;
}
@computed('query_config.limit')
get hasLimit() {
@computed('query_config.limit') get hasLimit() {
return isPresent(this.query_config?.limit) && this.query_config.limit > 0;
}
@computed('conditionsCount', 'hasGrouping', 'hasJoins', 'joinedTables.length', 'totalSelectedColumns')
get complexity() {
@computed('conditionsCount', 'hasGrouping', 'hasJoins', 'joinedTables.length', 'totalSelectedColumns') get complexity() {
let score = 0;
score += this.totalSelectedColumns;
@@ -162,8 +170,7 @@ export default class ReportModel extends Model {
}
}
@computed('complexity', 'totalSelectedColumns', 'joinedTables.length')
get estimatedPerformance() {
@computed('complexity', 'totalSelectedColumns', 'joinedTables.length') get estimatedPerformance() {
if (this.complexity === 'simple' && this.totalSelectedColumns <= 10) {
return 'fast';
} else if (this.complexity === 'moderate' && this.joinedTables.length <= 2) {
@@ -173,8 +180,7 @@ export default class ReportModel extends Model {
}
}
@computed('last_executed_at')
get lastExecutedDisplay() {
@computed('last_executed_at') get lastExecutedDisplay() {
if (!this.last_executed_at) {
return 'Never executed';
}
@@ -182,8 +188,7 @@ export default class ReportModel extends Model {
return this.last_executed_at.toLocaleDateString() + ' ' + this.last_executed_at.toLocaleTimeString();
}
@computed('average_execution_time')
get averageExecutionTimeDisplay() {
@computed('average_execution_time') get averageExecutionTimeDisplay() {
if (!this.average_execution_time) {
return 'N/A';
}
@@ -195,13 +200,11 @@ export default class ReportModel extends Model {
}
}
@computed('execution_count')
get executionCountDisplay() {
@computed('execution_count') get executionCountDisplay() {
return this.execution_count || 0;
}
@computed('last_result_count')
get lastResultCountDisplay() {
@computed('last_result_count') get lastResultCountDisplay() {
if (this.last_result_count === null || this.last_result_count === undefined) {
return 'N/A';
}
@@ -209,23 +212,19 @@ export default class ReportModel extends Model {
return this.last_result_count.toLocaleString();
}
@computed('export_formats.[]')
get availableExportFormats() {
@computed('export_formats.[]') get availableExportFormats() {
return this.export_formats || ['csv', 'excel', 'json'];
}
@computed('tags.[]')
get tagsList() {
@computed('tags.[]') get tagsList() {
return this.tags || [];
}
@computed('shared_with.[]')
get sharedWithList() {
@computed('shared_with.[]') get sharedWithList() {
return this.shared_with || [];
}
@computed('is_scheduled', 'next_scheduled_run', 'schedule_frequency', 'schedule_timezone')
get scheduleInfo() {
@computed('is_scheduled', 'next_scheduled_run', 'schedule_frequency', 'schedule_timezone') get scheduleInfo() {
if (!this.is_scheduled) {
return null;
}
@@ -237,8 +236,7 @@ export default class ReportModel extends Model {
};
}
@computed('hasConditions', 'query_config.conditions.[]')
get conditionsSummary() {
@computed('hasConditions', 'query_config.conditions.[]') get conditionsSummary() {
if (!this.hasConditions) {
return [];
}
@@ -246,8 +244,7 @@ export default class ReportModel extends Model {
return this.extractConditionsSummary(this.query_config.conditions);
}
@computed('status')
get statusDisplay() {
@computed('status') get statusDisplay() {
const statusMap = {
draft: 'Draft',
active: 'Active',
@@ -258,8 +255,7 @@ export default class ReportModel extends Model {
return statusMap[this.status] || this.status;
}
@computed('status')
get statusClass() {
@computed('status') get statusClass() {
const statusClasses = {
draft: 'status-draft',
active: 'status-active',

View File

@@ -0,0 +1,18 @@
import Model, { attr } from '@ember-data/model';
export default class ScheduleAvailabilityModel extends Model {
@attr('string') subject_uuid;
@attr('string') subject_type;
@attr('date') start_at;
@attr('date') end_at;
@attr('boolean', { defaultValue: true }) is_available;
@attr('number') preference_level;
@attr('string') rrule;
@attr('string') reason;
@attr('string') notes;
@attr('object') meta;
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;
}

View File

@@ -0,0 +1,23 @@
import Model, { attr, belongsTo } from '@ember-data/model';
export default class ScheduleConstraintModel extends Model {
@attr('string') company_uuid;
@attr('string') subject_uuid;
@attr('string') subject_type;
@attr('string') name;
@attr('string') description;
@attr('string') type;
@attr('string') category;
@attr('string') constraint_key;
@attr('string') constraint_value;
@attr('string') jurisdiction;
@attr('number', { defaultValue: 0 }) priority;
@attr('boolean', { defaultValue: true }) is_active;
@attr('object') meta;
@belongsTo('company') company;
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;
}

View File

@@ -0,0 +1,23 @@
import Model, { attr, belongsTo } from '@ember-data/model';
export default class ScheduleItemModel extends Model {
@attr('string') public_id;
@attr('string') schedule_uuid;
@attr('string') assignee_uuid;
@attr('string') assignee_type;
@attr('string') resource_uuid;
@attr('string') resource_type;
@attr('date') start_at;
@attr('date') end_at;
@attr('number') duration;
@attr('date') break_start_at;
@attr('date') break_end_at;
@attr('string', { defaultValue: 'pending' }) status;
@attr('object') meta;
@belongsTo('schedule') schedule;
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;
}

View File

@@ -0,0 +1,22 @@
import Model, { attr, belongsTo } from '@ember-data/model';
export default class ScheduleTemplateModel extends Model {
@attr('string') public_id;
@attr('string') company_uuid;
@attr('string') subject_uuid;
@attr('string') subject_type;
@attr('string') name;
@attr('string') description;
@attr('string') start_time;
@attr('string') end_time;
@attr('number') duration;
@attr('number') break_duration;
@attr('string') rrule;
@attr('object') meta;
@belongsTo('company') company;
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;
}

View File

@@ -0,0 +1,27 @@
import Model, { attr, hasMany, belongsTo } from '@ember-data/model';
export default class ScheduleModel extends Model {
/** @ids */
@attr('string') public_id;
@attr('string') company_uuid;
@attr('string') subject_uuid;
@attr('string') subject_type;
/** @attributes */
@attr('string') name;
@attr('string') description;
@attr('date') start_date;
@attr('date') end_date;
@attr('string') timezone;
@attr('string', { defaultValue: 'draft' }) status;
@attr('object') meta;
/** @relationships */
@hasMany('schedule-item') items;
@belongsTo('company') company;
/** @dates */
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;
}

View File

@@ -3,6 +3,17 @@ import { inject as service } from '@ember/service';
export default class OnboardIndexRoute extends Route {
@service store;
@service('onboarding-orchestrator') orchestrator;
queryParams = {
step: { refreshModel: false },
session: { refreshModel: false },
code: { refreshModel: false },
};
beforeModel() {
this.orchestrator.start();
}
model() {
return this.store.findRecord('brand', 1);

View File

@@ -0,0 +1,39 @@
import Service, { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default class OnboardingContextService extends Service {
@service appCache;
@tracked data = {};
get(key) {
return this.data[key] ?? this.appCache.get(`onboarding:context:${key}`);
}
getFromCache(key) {
return this.appCache.get(`onboarding:context:${key}`);
}
set(key, value, options = {}) {
this.data = { ...this.data, [key]: value };
if (options?.persist === true) {
this.appCache.set(`onboarding:context:${key}`, value);
}
}
persist(key, value) {
this.set(key, value, { persist: true });
}
del(key) {
const { [key]: _drop, ...rest } = this.data; // eslint-disable-line no-unused-vars
this.data = rest;
this.appCache.set(`onboarding:context:${key}`, undefined);
}
reset() {
for (let key in this.data) {
this.appCache.set(`onboarding:context:${key}`, undefined);
}
this.data = {};
}
}

View File

@@ -0,0 +1,71 @@
import Service from '@ember/service';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default class OnboardingOrchestratorService extends Service {
@service onboardingRegistry;
@service onboardingContext;
@tracked flow = null;
@tracked current = null;
@tracked history = [];
@tracked sessionId = null;
start(flowId = null, opts = {}) {
const flow = this.onboardingRegistry.getFlow(flowId ?? this.onboardingRegistry.defaultFlow);
if (!flow) throw new Error(`Onboarding flow '${flowId}' not found`);
this.flow = flow;
this.sessionId = opts.sessionId || null;
this.history = [];
this.goto(flow.entry);
}
async goto(stepId) {
if (!this.flow) throw new Error('No active onboarding flow');
const step = this.flow.steps.find((s) => s.id === stepId);
if (!step) throw new Error(`Step '${stepId}' not found`);
if (typeof step.guard === 'function' && !step.guard(this.onboardingContext)) {
return this.next();
}
if (typeof step.beforeEnter === 'function') {
await step.beforeEnter(this.onboardingContext);
}
this.current = step;
}
async next() {
if (!this.flow || !this.current) return;
const leaving = this.current;
if (typeof leaving.afterLeave === 'function') {
await leaving.afterLeave(this.onboardingContext);
}
if (!this.history.includes(leaving)) this.history.push(leaving);
let nextId;
if (typeof leaving.next === 'function') {
nextId = leaving.next(this.onboardingContext);
} else {
nextId = leaving.next;
}
if (!nextId) {
this.current = null; // finished
return;
}
return this.goto(nextId);
}
async back() {
if (!this.flow || this.history.length === 0) return;
const prev = this.history[this.history.length - 1];
if (prev && prev.allowBack === false) return;
this.history = this.history.slice(0, -1);
await this.goto(prev.id);
}
}

View File

@@ -0,0 +1,31 @@
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default class OnboardingRegistryService extends Service {
flows = new Map();
@tracked defaultFlow = 'default@v1';
useFlow(flowId) {
this.defaultFlow = flowId;
}
registerFlow(flow) {
if (!flow || !flow.id || !flow.entry || !Array.isArray(flow.steps)) {
throw new Error('Invalid FlowDef: id, entry, steps are required');
}
const ids = new Set(flow.steps.map((s) => s.id));
if (!ids.has(flow.entry)) {
throw new Error(`Flow '${flow.id}' entry '${flow.entry}' not found in steps`);
}
for (const s of flow.steps) {
if (typeof s.next === 'string' && s.next && !ids.has(s.next)) {
throw new Error(`Flow '${flow.id}' step '${s.id}' has unknown next '${s.next}'`);
}
}
this.flows.set(flow.id, flow);
}
getFlow(id) {
return this.flows.get(id);
}
}

View File

@@ -0,0 +1,114 @@
import Service, { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { later } from '@ember/runloop';
import { task } from 'ember-concurrency';
export default class UserVerificationService extends Service {
@service fetch;
@service notifications;
@service modalsManager;
@service currentUser;
@service router;
@service session;
@service intl;
@tracked token;
@tracked code;
@tracked ready;
@tracked waiting = false;
@action start(options = {}) {
this.#wait(options?.timeout ?? 75000);
}
@action didntReceiveCode() {
this.waiting = true;
}
@action validateInput(event) {
const value = event instanceof HTMLElement ? event.value : (event?.target?.value ?? '');
this.ready = value?.length > 5;
}
@action resendBySms() {
this.modalsManager.show('modals/verify-by-sms', {
title: 'Verify Account by Phone',
acceptButtonText: 'Send',
phone: this.currentUser.phone,
confirm: async (modal) => {
modal.startLoading();
const phone = modal.getOption('phone');
if (!phone) {
this.notifications.error('No phone number provided.');
}
try {
await this.fetch.post('onboard/send-verification-sms', { phone, session: this.hello });
this.notifications.success('Verification code SMS sent!');
modal.done();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
});
}
@action resendEmail() {
this.modalsManager.show('modals/resend-verification-email', {
title: 'Resend Verification Code',
acceptButtonText: 'Send',
email: this.currentUser.email,
confirm: async (modal) => {
modal.startLoading();
const email = modal.getOption('email');
if (!email) {
this.notifications.error('No email number provided.');
}
try {
await this.fetch.post('onboard/send-verification-email', { email, session: this.hello });
this.notifications.success('Verification code email sent!');
modal.done();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
});
}
@task *verifyCode() {
try {
const { status, token } = yield this.fetch.post('auth/verify-email', { token: this.token, code: this.code, email: this.email, authenticate: true });
if (status === 'ok') {
this.notifications.success('Email successfully verified!');
if (token) {
this.notifications.info(`Welcome to ${this.intl.t('app.name')}`);
this.session.manuallyAuthenticate(token);
return this.router.transitionTo('console');
}
return this.router.transitionTo('auth.login');
}
} catch (error) {
this.notifications.serverError(error);
}
}
setToken(token) {
this.token = token;
}
setCode(code) {
this.code = code;
}
#wait(timeout = 75000) {
return later(this, () => {
this.waiting = true;
}, timeout);
}
}

View File

@@ -7,7 +7,7 @@
@userMenuItems={{this.userMenuItems}}
@onAction={{this.onAction}}
@showSidebarToggle={{true}}
@sidebarToggleEnabled={{this.sidebarToggleEnabled}}
@sidebarToggleEnabled={{true}}
@onSidebarToggle={{this.onSidebarToggle}}
/>
<Layout::Main class={{this.currentRouteClass}}>

View File

@@ -1,7 +1,7 @@
{{page-title "Dashboard"}}
<Layout::Section::Body class="overflow-y-scroll h-full">
<TwoFaEnforcementAlert />
<Dashboard @sidebar={{this.sidebarContext}} @createWrapperClass="px-10" class="flex items-center justify-between mb-4 mt-6 px-14" />
<Dashboard @extension="core" @createWrapperClass="px-10" class="flex items-center justify-between mb-4 mt-6 px-14" />
<Spacer @height="300px" />
</Layout::Section::Body>
<div id="console-home-wormhole" />

View File

@@ -1,42 +1 @@
<div class="bg-white dark:bg-gray-800 py-5 px-4 shadow rounded-lg w-full">
<div class="mb-4">
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} height="56" class="h-10 object-contain mx-auto" />
<div class="mt-2">
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.index.title"}}
</h2>
</div>
</div>
<div class="flex px-3 py-2 mb-4 rounded-md shadow-sm bg-blue-200">
<div>
<FaIcon @icon="hand-spock" @size="lg" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
{{t "onboard.index.welcome-title" htmlSafe=true companyName=(t "app.name")}}
{{t "onboard.index.welcome-text"}}
</p>
</div>
<form {{on "submit" this.startOnboard}}>
{{#if this.error}}
<InfoBlock @icon="exclamation-triangle" @text={{this.error}} class="mb-6 px-3 py-2 bg-red-300 text-red-900" @textClass="text-red-900" />
{{/if}}
<InputGroup @name={{t "onboard.index.full-name"}} @value={{this.name}} @helpText={{t "onboard.index.full-name-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.your-email"}} @type="email" @value={{this.email}} @helpText={{t "onboard.index.your-email-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.phone"}} @helpText={{t "onboard.index.phone-help-text"}}>
<PhoneInput @onInput={{fn (mut this.phone)}} class="form-input input-lg w-full" />
</InputGroup>
<InputGroup @name={{t "onboard.index.organization-name"}} @value={{this.organization_name}} @helpText={{t "onboard.index.organization-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.password"}} @value={{this.password}} @type="password" @helpText={{t "onboard.index.password-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.confirm-password"}} @value={{this.password_confirmation}} @type="password" @helpText={{t "onboard.index.confirm-password-help-text"}} @inputClass="input-lg" />
<div class="flex items-center justify-end mt-5">
<Button @icon="check" @iconPrefix="fas" @type="primary" @size="lg" @text={{t "onboard.index.continue-button-text"}} @isLoading={{this.isLoading}} @disabled={{this.readyToSubmit}} @onClick={{this.startOnboard}} />
</div>
</form>
<RegistryYield @registry="onboard" as |YieldedComponent ctx|>
<YieldedComponent @context={{ctx}} />
</RegistryYield>
</div>
<Onboarding::Yield @step={{this.step}} @session={{this.session}} @code={{this.code}} @brand={{@model}} />

View File

@@ -49,7 +49,8 @@ module.exports = function (environment) {
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'),
placeholderImage: getenv('DEFAULT_PLACEHOLDER_IMAGE', 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/image-file-icon.png'),
placeholderImageOld: getenv('DEFAULT_PLACEHOLDER_IMAGE_OLD', 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/images/fallback-placeholder-2.png'),
driverImage: getenv('DEFAULT_DRIVER_IMAGE', 'https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/no-avatar.png'),
userImage: getenv('DEFAULT_USER_IMAGE', 'https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/no-avatar.png'),
contactImage: getenv('DEFAULT_CONTACT_IMAGE', 'https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/no-avatar.png'),

View File

@@ -1,6 +1,6 @@
{
"name": "@fleetbase/console",
"version": "0.7.12",
"version": "0.7.19",
"private": true,
"description": "Modular logistics and supply chain operating system (LSOS)",
"repository": "https://github.com/fleetbase/fleetbase",
@@ -31,13 +31,13 @@
"@ember/legacy-built-in-components": "^0.4.2",
"@fleetbase/dev-engine": "^0.2.10",
"@fleetbase/ember-core": "latest",
"@fleetbase/ember-ui": "latest",
"@fleetbase/fleetops-data": "latest",
"@fleetbase/fleetops-engine": "^0.6.20",
"@fleetbase/ember-ui": "^0.3.11",
"@fleetbase/fleetops-data": "^0.1.23",
"@fleetbase/fleetops-engine": "^0.6.26",
"@fleetbase/iam-engine": "^0.1.4",
"@fleetbase/leaflet-routing-machine": "^3.2.17",
"@fleetbase/registry-bridge-engine": "^0.1.0",
"@fleetbase/storefront-engine": "^0.4.3",
"@fleetbase/storefront-engine": "^0.4.7",
"@formatjs/intl-datetimeformat": "^6.18.2",
"@formatjs/intl-numberformat": "^8.15.6",
"@formatjs/intl-pluralrules": "^5.4.6",

2590
console/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ Router.map(function () {
this.route('virtual', { path: '/:slug' });
this.route('install');
this.route('onboard', function () {
this.route('verify-email');
this.route('index', { path: '/' });
});
this.route('auth', function () {
this.route('login', { path: '/' });

View File

@@ -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 | onboarding/form', 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`<Onboarding::Form />`);
assert.dom().hasText('');
// Template block usage:
await render(hbs`
<Onboarding::Form>
template block text
</Onboarding::Form>
`);
assert.dom().hasText('template block text');
});
});

View File

@@ -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 | onboarding/verify-email', 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`<Onboarding::VerifyEmail />`);
assert.dom().hasText('');
// Template block usage:
await render(hbs`
<Onboarding::VerifyEmail>
template block text
</Onboarding::VerifyEmail>
`);
assert.dom().hasText('template block text');
});
});

View File

@@ -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 | onboarding/yield', 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`<Onboarding::Yield />`);
assert.dom().hasText('');
// Template block usage:
await render(hbs`
<Onboarding::Yield>
template block text
</Onboarding::Yield>
`);
assert.dom().hasText('template block text');
});
});

View File

@@ -0,0 +1,39 @@
import Application from '@ember/application';
import config from '@fleetbase/console/config/environment';
import { initialize } from '@fleetbase/console/instance-initializers/register-default-onboarding-flow';
import { module, test } from 'qunit';
import Resolver from 'ember-resolver';
import { run } from '@ember/runloop';
module('Unit | Instance Initializer | register-default-onboarding-flow', function (hooks) {
hooks.beforeEach(function () {
this.TestApplication = class TestApplication extends Application {
modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix;
Resolver = Resolver;
};
this.TestApplication.instanceInitializer({
name: 'initializer under test',
initialize,
});
this.application = this.TestApplication.create({
autoboot: false,
});
this.instance = this.application.buildInstance();
});
hooks.afterEach(function () {
run(this.instance, 'destroy');
run(this.application, 'destroy');
});
// TODO: Replace this with your real tests.
test('it works', async function (assert) {
await this.instance.boot();
assert.ok(true);
});
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -75,7 +75,7 @@ ENV QUEUE_CONNECTION=redis
ENV CADDYFILE_PATH=/fleetbase/Caddyfile
ENV CONSOLE_PATH=/fleetbase/console
ENV OCTANE_SERVER=frankenphp
ENV FLEETBASE_VERSION=0.7.12
ENV FLEETBASE_VERSION=0.7.19
# Set environment
ARG ENVIRONMENT=production