mirror of
https://github.com/fleetbase/fleetbase.git
synced 2025-12-19 22:27:22 +00:00
preparing for major release w/ customer portal extension and order tracking page, real time ETAs and order progress tracker info
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,6 +30,7 @@ packages/flespi
|
||||
packages/loconav
|
||||
packages/internals
|
||||
packages/projectargus-engine
|
||||
packages/customer-portal
|
||||
# wip
|
||||
packages/solid
|
||||
solid
|
||||
|
||||
@@ -34,4 +34,9 @@ return [
|
||||
'api_key' => env('SENDGRID_API_KEY'),
|
||||
],
|
||||
|
||||
'stripe' => [
|
||||
'key' => env('STRIPE_KEY', env('STRIPE_API_KEY')),
|
||||
'secret' => env('STRIPE_SECRET', env('STRIPE_API_SECRET')),
|
||||
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<div class="fleetbase-dashboard-grid flex items-center justify-between mb-4 mt-6 px-14">
|
||||
<div class="left-section">
|
||||
<h1 class="text-lg font-bold">{{this.dashboard.currentDashboard.name}}</h1>
|
||||
</div>
|
||||
<div class="fleetbase-dashboard-actions right-section ml-4 flex items-center">
|
||||
<div class="fleetbase-model-select fleetbase-power-select ember-model-select h-10">
|
||||
|
||||
<DropdownButton
|
||||
class="h-10"
|
||||
@text={{if this.dashboard.currentDashboard.name this.dashboard.currentDashboard.name (t "component.dashboard.select-dashboard")}}
|
||||
@textClass="text-sm mr-2"
|
||||
@buttonClass="flex-row-reverse w-44 justify-between"
|
||||
@icon="caret-down"
|
||||
@iconClass="mr-0i"
|
||||
@size="sm"
|
||||
@iconPrefix="fas"
|
||||
@triggerClass="hidden md:flex"
|
||||
as |dd|
|
||||
>
|
||||
<div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
|
||||
<div class="p-1">
|
||||
{{#each this.dashboard.dashboards as |dashboard|}}
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.selectDashboard dashboard)}}>
|
||||
<div class="flex-1 flex flex-row items-center">
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="desktop" />
|
||||
</div>
|
||||
<span>{{dashboard.name}}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{#if (eq this.dashboard.currentDashboard.id dashboard.id)}}
|
||||
<FaIcon @icon="check" class="text-green-500" />
|
||||
{{/if}}
|
||||
</div>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
|
||||
<div class="ml-2 relative h-10">
|
||||
<DropdownButton class="h-10" @icon="ellipsis-h" @size="sm" @iconPrefix="fas" @triggerClass="hidden md:flex" as |dd|>
|
||||
<div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
|
||||
<div class="p-1">
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.createDashboard)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="add" />
|
||||
</div>
|
||||
<span>{{t "component.dashboard.create-new-dashboard"}}</span>
|
||||
</a>
|
||||
|
||||
{{#unless (eq this.dashboard.currentDashboard.user_uuid "system")}}
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.onChangeEdit true)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="edit" />
|
||||
</div>
|
||||
<span>{{t "component.dashboard.edit-layout"}}</span>
|
||||
</a>
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.onAddingWidget true)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="add" />
|
||||
</div>
|
||||
<span>{{t "component.dashboard.add-widgets"}}</span>
|
||||
</a>
|
||||
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.deleteDashboard this.dashboard.currentDashboard)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="trash" />
|
||||
</div>
|
||||
<span>{{t "component.dashboard.delete-dashboard"}}</span>
|
||||
</a>
|
||||
{{/unless}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
{{#if this.dashboard.isEditingDashboard}}
|
||||
<div class="ml-2 h-10">
|
||||
<Button @type="magic" @icon="save" @helpText={{t "component.dashboard.save-dashboard"}} @onClick={{fn this.onChangeEdit false}} class="h-10" />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-10">
|
||||
<Dashboard::Create @isEdit={{this.dashboard.isEditingDashboard}} @isAddingWidget={{this.dashboard.isAddingWidget}} @dashboard={{this.dashboard.currentDashboard}} />
|
||||
{{#if this.dashboard.isAddingWidget}}
|
||||
<EmberWormhole @to="console-home-wormhole">
|
||||
<Dashboard::WidgetPanel
|
||||
@isOpen={{this.dashboard.isAddingWidget}}
|
||||
@onLoad={{this.setWidgetSelectorPanelContext}}
|
||||
@dashboard={{this.dashboard.currentDashboard}}
|
||||
@onClose={{fn this.onAddingWidget false}}
|
||||
/>
|
||||
</EmberWormhole>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -1,140 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* DashboardComponent for managing dashboards in an Ember application.
|
||||
* This component handles actions such as selecting, creating, deleting dashboards,
|
||||
* and managing widget selectors and dashboard editing states.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class DashboardComponent extends Component {
|
||||
/**
|
||||
* Ember Data store service.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service store;
|
||||
|
||||
/**
|
||||
* Internationalization service for managing translations.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service intl;
|
||||
|
||||
/**
|
||||
* Notifications service for displaying alerts or confirmations.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Modals manager service for handling modal dialogs.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service modalsManager;
|
||||
|
||||
/**
|
||||
* Fetch service for handling HTTP requests.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* Dashboard service for business logic related to dashboards.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service dashboard;
|
||||
|
||||
/**
|
||||
* Creates an instance of DashboardComponent.
|
||||
* @memberof DashboardComponent
|
||||
*/
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.dashboard.loadDashboards.perform();
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to select a dashboard.
|
||||
* @param {Object} dashboard - The dashboard to be selected.
|
||||
*/
|
||||
@action selectDashboard(dashboard) {
|
||||
this.dashboard.selectDashboard.perform(dashboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the context for the widget selector panel.
|
||||
* @param {Object} widgetSelectorContext - The context object for the widget selector.
|
||||
*/
|
||||
@action setWidgetSelectorPanelContext(widgetSelectorContext) {
|
||||
this.widgetSelectorContext = widgetSelectorContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new dashboard.
|
||||
* @param {Object} dashboard - The dashboard to be created.
|
||||
* @param {Object} [options={}] - Optional parameters for dashboard creation.
|
||||
*/
|
||||
@action createDashboard(dashboard, options = {}) {
|
||||
this.modalsManager.show('modals/create-dashboard', {
|
||||
title: this.intl.t('component.dashboard.create-a-new-dashboard'),
|
||||
acceptButtonText: this.intl.t('component.dashboard.confirm-create-dashboard'),
|
||||
confirm: async (modal, done) => {
|
||||
modal.startLoading();
|
||||
|
||||
// Get the name from the modal options
|
||||
const { name } = modal.getOptions();
|
||||
|
||||
await this.dashboard.createDashboard.perform(name);
|
||||
done();
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a dashboard.
|
||||
* @param {Object} dashboard - The dashboard to be deleted.
|
||||
* @param {Object} [options={}] - Optional parameters for dashboard deletion.
|
||||
*/
|
||||
@action deleteDashboard(dashboard, options = {}) {
|
||||
if (this.dashboard.dashboards?.length === 1) {
|
||||
return this.notifications.error(this.intl.t('component.dashboard.you-cannot-delete-this-dashboard'));
|
||||
}
|
||||
|
||||
this.modalsManager.confirm({
|
||||
title: this.intl.t('component.dashboard.are-you-sure-you-want-delete-dashboard', { dashboardName: dashboard.name }),
|
||||
confirm: async (modal, done) => {
|
||||
modal.startLoading();
|
||||
await this.dashboard.deleteDashboard.perform(dashboard);
|
||||
done();
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to handle the addition of a widget.
|
||||
* @param {boolean} [state=true] - The state to set for adding a widget.
|
||||
*/
|
||||
@action onAddingWidget(state = true) {
|
||||
this.dashboard.onAddingWidget(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current dashboard.
|
||||
* @param {Object} dashboard - The dashboard to be set as current.
|
||||
*/
|
||||
@action setCurrentDashboard(dashboard) {
|
||||
this.dashboard.setCurrentDashboard.perform(dashboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the editing state of the dashboard.
|
||||
* @param {boolean} [state=true] - The state to set for editing the dashboard.
|
||||
*/
|
||||
@action onChangeEdit(state = true) {
|
||||
this.dashboard.onChangeEdit(state);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<div class="fleetbase-dashboard-grid" ...attributes>
|
||||
<GridStack @options={{this.gridOptions}} @onChange={{this.onChangeGrid}}>
|
||||
{{#each @dashboard.widgets as |widget|}}
|
||||
{{#if (component-resolvable widget.component)}}
|
||||
<GridStackItem id={{widget.id}} @options={{spread-widget-options (hash id=widget.id options=widget.grid_options)}} class="relative">
|
||||
{{component widget.component options=widget.options}}
|
||||
{{#if @isEdit}}
|
||||
<div class="absolute top-2 right-2">
|
||||
<Button @type="default" @icon="trash" @helpText={{"Remove widget from the dashboard"}} @onClick={{fn this.removeWidget widget}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</GridStackItem>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</GridStack>
|
||||
</div>
|
||||
@@ -1,99 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action, computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* Component responsible for creating and managing the dashboard layout.
|
||||
* Provides functionalities such as toggling widget float, changing grid layout, and removing widgets.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class DashboardCreateComponent extends Component {
|
||||
/**
|
||||
* Notifications service for displaying alerts or errors.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Tracked array to keep track of widgets that have been updated.
|
||||
* @type {Array}
|
||||
*/
|
||||
@tracked updatedWidgets = [];
|
||||
|
||||
/**
|
||||
* Action to toggle the floating state of widgets on the grid.
|
||||
*/
|
||||
@action toggleFloat() {
|
||||
this.shouldFloat = !this.shouldFloat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles changes to the grid layout, such as repositioning or resizing widgets.
|
||||
* Iterates over each widget event detail and updates the corresponding widget's properties if necessary.
|
||||
*
|
||||
* @param {Event} event - Event containing details about the grid change.
|
||||
* @action
|
||||
*/
|
||||
@action onChangeGrid(event) {
|
||||
const { dashboard } = this.args;
|
||||
|
||||
event.detail.forEach((currentWidgetEvent) => {
|
||||
const alreadyUpdated = this.updatedWidgets.find((item) => item.id === currentWidgetEvent.id);
|
||||
if (alreadyUpdated || !this.dashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
const changedWidget = dashboard.widgets.find((widget) => widget.id === currentWidgetEvent.id);
|
||||
if (!changedWidget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { x, y, w, h } = currentWidgetEvent;
|
||||
const response = changedWidget.updateProperties({
|
||||
grid_options: { x, y, w, h },
|
||||
});
|
||||
if (response) {
|
||||
this.updatedWidgets.push(changedWidget);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a specified widget from the dashboard.
|
||||
* Performs a removal operation on the dashboard and handles any errors that occur during the process.
|
||||
*
|
||||
* @param {Object} widget - The widget object to be removed.
|
||||
* @action
|
||||
*/
|
||||
@action removeWidget(widget) {
|
||||
const { dashboard } = this.args;
|
||||
|
||||
if (dashboard) {
|
||||
dashboard.removeWidget(widget.id).catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed property that returns grid options based on the current edit state.
|
||||
* Configures grid behavior such as floating, animation, and drag and resize capabilities.
|
||||
*
|
||||
* @computed
|
||||
* @returns {Object} An object containing grid configuration options.
|
||||
*/
|
||||
@computed('args.isEdit') get gridOptions() {
|
||||
return {
|
||||
float: true,
|
||||
animate: true,
|
||||
acceptWidgets: true,
|
||||
alwaysShowResizeHandle: this.args.isEdit,
|
||||
disableDrag: !this.args.isEdit,
|
||||
disableResize: !this.args.isEdit,
|
||||
resizable: { handles: 'all' },
|
||||
cellHeight: 30,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export default class AuthLoginController extends Controller {
|
||||
* @return {void}
|
||||
* @memberof AuthLoginController
|
||||
*/
|
||||
@action async login(event) {
|
||||
@action async login (event) {
|
||||
// firefox patch
|
||||
event.preventDefault();
|
||||
// get user credentials
|
||||
@@ -116,7 +116,7 @@ export default class AuthLoginController extends Controller {
|
||||
this.reset('success');
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
this.notifications.serverError(error);
|
||||
this.reset('error');
|
||||
|
||||
@@ -153,14 +153,14 @@ export default class AuthLoginController extends Controller {
|
||||
/**
|
||||
* Transition user to onboarding screen
|
||||
*/
|
||||
@action transitionToOnboard() {
|
||||
@action transitionToOnboard () {
|
||||
return this.router.transitionTo('onboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition to forgot password screen, if email is set - set it.
|
||||
*/
|
||||
@action forgotPassword() {
|
||||
@action forgotPassword () {
|
||||
return this.router.transitionTo('auth.forgot-password').then(() => {
|
||||
if (this.email) {
|
||||
this.forgotPasswordController.email = this.email;
|
||||
@@ -175,7 +175,7 @@ export default class AuthLoginController extends Controller {
|
||||
* @return {Promise<Transition>}
|
||||
* @memberof AuthLoginController
|
||||
*/
|
||||
@action sendUserForEmailVerification(email) {
|
||||
@action sendUserForEmailVerification (email) {
|
||||
return this.fetch.post('auth/create-verification-session', { email, send: true }).then(({ token, session }) => {
|
||||
return this.session.store.persist({ email }).then(() => {
|
||||
this.notifications.warning(this.intl.t('auth.login.unverified-notification'));
|
||||
@@ -193,7 +193,7 @@ export default class AuthLoginController extends Controller {
|
||||
* @return {Promise<Transition>}
|
||||
* @memberof AuthLoginController
|
||||
*/
|
||||
@action sendUserForPasswordReset(email) {
|
||||
@action sendUserForPasswordReset (email) {
|
||||
this.notifications.warning(this.intl.t('auth.login.password-reset-required'));
|
||||
return this.router.transitionTo('auth.forgot-password', { queryParams: { email } }).then(() => {
|
||||
this.reset('error');
|
||||
@@ -205,7 +205,7 @@ export default class AuthLoginController extends Controller {
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
setRedirect() {
|
||||
setRedirect () {
|
||||
const shift = this.urlSearchParams.get('shift');
|
||||
|
||||
if (shift) {
|
||||
@@ -218,7 +218,7 @@ export default class AuthLoginController extends Controller {
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
success() {
|
||||
success () {
|
||||
this.reset('success');
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ export default class AuthLoginController extends Controller {
|
||||
* @param {String} error An error message
|
||||
* @void
|
||||
*/
|
||||
failure(error) {
|
||||
failure (error) {
|
||||
this.notifications.serverError(error);
|
||||
this.reset('error');
|
||||
}
|
||||
@@ -238,7 +238,7 @@ export default class AuthLoginController extends Controller {
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
slowConnection() {
|
||||
slowConnection () {
|
||||
this.notifications.error(this.intl.t('auth.login.slow-connection-message'));
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ export default class AuthLoginController extends Controller {
|
||||
* @param {String} type
|
||||
* @void
|
||||
*/
|
||||
reset(type) {
|
||||
reset (type) {
|
||||
// reset login form state
|
||||
this.isLoading = false;
|
||||
this.isSlowConnection = false;
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
import Controller, { inject as controller } from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import pathToRoute from '@fleetbase/ember-core/utils/path-to-route';
|
||||
|
||||
export default class AuthPortalLoginController extends Controller {
|
||||
@controller('auth.forgot-password') forgotPasswordController;
|
||||
@service notifications;
|
||||
@service urlSearchParams;
|
||||
@service session;
|
||||
@service router;
|
||||
@service intl;
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* Whether or not to remember the users session
|
||||
*
|
||||
* @var {Boolean}
|
||||
*/
|
||||
@tracked rememberMe = false;
|
||||
|
||||
/**
|
||||
* The identity to authenticate with
|
||||
*
|
||||
* @var {String}
|
||||
*/
|
||||
@tracked identity = null;
|
||||
|
||||
/**
|
||||
* The password to authenticate with
|
||||
*
|
||||
* @var {String}
|
||||
*/
|
||||
@tracked password = null;
|
||||
|
||||
/**
|
||||
* Login is validating user input
|
||||
*
|
||||
* @var {Boolean}
|
||||
*/
|
||||
@tracked isValidating = false;
|
||||
|
||||
/**
|
||||
* Login is processing
|
||||
*
|
||||
* @var {Boolean}
|
||||
*/
|
||||
@tracked isLoading = false;
|
||||
|
||||
/**
|
||||
* If the connection or requesst it taking too long
|
||||
*
|
||||
* @var {Boolean}
|
||||
*/
|
||||
@tracked isSlowConnection = false;
|
||||
|
||||
/**
|
||||
* Interval to determine when to timeout the request
|
||||
*
|
||||
* @var {Integer}
|
||||
*/
|
||||
@tracked timeout = null;
|
||||
|
||||
/**
|
||||
* Number of failed login attempts
|
||||
*
|
||||
* @var {Integer}
|
||||
*/
|
||||
@tracked failedAttempts = 0;
|
||||
|
||||
/**
|
||||
* Authentication token.
|
||||
*
|
||||
* @memberof AuthPortalLoginController
|
||||
*/
|
||||
@tracked token;
|
||||
|
||||
/**
|
||||
* Action to login user.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @return {void}
|
||||
* @memberof AuthPortalLoginController
|
||||
*/
|
||||
@action async login(event) {
|
||||
// firefox patch
|
||||
event.preventDefault();
|
||||
// get user credentials
|
||||
const { identity, password, rememberMe } = this;
|
||||
|
||||
// If no password error
|
||||
if (!identity) {
|
||||
return this.notifications.warning(this.intl.t('auth.login.no-identity-notification'));
|
||||
}
|
||||
|
||||
// If no password error
|
||||
if (!password) {
|
||||
return this.notifications.warning(this.intl.t('auth.login.no-identity-notification'));
|
||||
}
|
||||
|
||||
// start loader
|
||||
this.set('isLoading', true);
|
||||
// set where to redirect on login
|
||||
this.setRedirect();
|
||||
|
||||
// send request to check for 2fa
|
||||
try {
|
||||
let { twoFaSession, isTwoFaEnabled } = await this.session.checkForTwoFactor(identity);
|
||||
|
||||
if (isTwoFaEnabled) {
|
||||
return this.session.store
|
||||
.persist({ identity })
|
||||
.then(() => {
|
||||
return this.router.transitionTo('auth.two-fa', { queryParams: { token: twoFaSession } }).then(() => {
|
||||
this.reset('success');
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
this.reset('error');
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return this.notifications.serverError(error);
|
||||
}
|
||||
|
||||
try {
|
||||
this.session.setRedirect('portal');
|
||||
await this.session.authenticate('authenticator:fleetbase', { identity, password }, rememberMe);
|
||||
} catch (error) {
|
||||
this.failedAttempts++;
|
||||
|
||||
// Handle unverified user
|
||||
if (error.toString().includes('not verified')) {
|
||||
return this.sendUserForEmailVerification(identity);
|
||||
}
|
||||
|
||||
// Handle password reset required
|
||||
if (error.toString().includes('reset required')) {
|
||||
return this.sendUserForPasswordReset(identity);
|
||||
}
|
||||
|
||||
return this.failure(error);
|
||||
}
|
||||
|
||||
if (this.session.isAuthenticated) {
|
||||
this.success();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition user to onboarding screen
|
||||
*/
|
||||
@action transitionToOnboard() {
|
||||
return this.router.transitionTo('onboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition to forgot password screen, if email is set - set it.
|
||||
*/
|
||||
@action forgotPassword() {
|
||||
return this.router.transitionTo('auth.forgot-password').then(() => {
|
||||
if (this.email) {
|
||||
this.forgotPasswordController.email = this.email;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an email verification session and transitions user to verification route.
|
||||
*
|
||||
* @param {String} email
|
||||
* @return {Promise<Transition>}
|
||||
* @memberof AuthPortalLoginController
|
||||
*/
|
||||
@action sendUserForEmailVerification(email) {
|
||||
return this.fetch.post('auth/create-verification-session', { email, send: true }).then(({ token, session }) => {
|
||||
return this.session.store.persist({ email }).then(() => {
|
||||
this.notifications.warning(this.intl.t('auth.login.unverified-notification'));
|
||||
return this.router.transitionTo('auth.verification', { queryParams: { token, hello: session } }).then(() => {
|
||||
this.reset('error');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends user to forgot password flow.
|
||||
*
|
||||
* @param {String} email
|
||||
* @return {Promise<Transition>}
|
||||
* @memberof AuthPortalLoginController
|
||||
*/
|
||||
@action sendUserForPasswordReset(email) {
|
||||
this.notifications.warning(this.intl.t('auth.login.password-reset-required'));
|
||||
return this.router.transitionTo('auth.forgot-password', { queryParams: { email } }).then(() => {
|
||||
this.reset('error');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets correct route to send user to after login.
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
setRedirect() {
|
||||
const shift = this.urlSearchParams.get('shift');
|
||||
|
||||
if (shift) {
|
||||
this.session.setRedirect(pathToRoute(shift));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the authentication success
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
success() {
|
||||
this.reset('success');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the authentication failure
|
||||
*
|
||||
* @param {String} error An error message
|
||||
* @void
|
||||
*/
|
||||
failure(error) {
|
||||
this.notifications.serverError(error);
|
||||
this.reset('error');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the request slow connection
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
slowConnection() {
|
||||
this.notifications.error(this.intl.t('auth.login.slow-connection-message'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the login form
|
||||
*
|
||||
* @param {String} type
|
||||
* @void
|
||||
*/
|
||||
reset(type) {
|
||||
// reset login form state
|
||||
this.isLoading = false;
|
||||
this.isSlowConnection = false;
|
||||
// reset login form state depending on type of reset
|
||||
switch (type) {
|
||||
case 'success':
|
||||
this.identity = null;
|
||||
this.password = null;
|
||||
this.isValidating = false;
|
||||
break;
|
||||
case 'error':
|
||||
case 'fail':
|
||||
this.password = null;
|
||||
break;
|
||||
}
|
||||
// clearTimeout(this.timeout);
|
||||
}
|
||||
}
|
||||
@@ -155,9 +155,9 @@ export default class ConsoleController extends Controller {
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
@action invalidateSession(noop, event) {
|
||||
@action async invalidateSession(noop, event) {
|
||||
event.preventDefault();
|
||||
this.session.invalidateWithLoader();
|
||||
await this.session.invalidateWithLoader();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,32 +12,9 @@ import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
|
||||
* @extends Controller
|
||||
*/
|
||||
export default class ConsoleAccountAuthController extends Controller {
|
||||
/**
|
||||
* Service for handling data fetching.
|
||||
*
|
||||
* @type {fetch}
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* Service for displaying notifications.
|
||||
*
|
||||
* @type {notifications}
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Service for managing application routing.
|
||||
*
|
||||
* @type {router}
|
||||
*/
|
||||
@service router;
|
||||
|
||||
/**
|
||||
* Service for managing modals.
|
||||
*
|
||||
* @type {router}
|
||||
*/
|
||||
@service modalsManager;
|
||||
|
||||
/**
|
||||
@@ -187,14 +164,12 @@ export default class ConsoleAccountAuthController extends Controller {
|
||||
* @param {Object} twoFaSettings - User-specific two-factor authentication settings.
|
||||
*/
|
||||
@task *saveUserTwoFaSettings(twoFaSettings = {}) {
|
||||
yield this.fetch
|
||||
.post('users/two-fa', { twoFaSettings })
|
||||
.then(() => {
|
||||
this.notifications.success('2FA Settings saved successfully.');
|
||||
})
|
||||
.catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
try {
|
||||
yield this.fetch.post('users/two-fa', { twoFaSettings });
|
||||
this.notifications.success('2FA Settings saved successfully.');
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,13 +178,17 @@ export default class ConsoleAccountAuthController extends Controller {
|
||||
* @method loadUserTwoFaSettings
|
||||
*/
|
||||
@task *loadUserTwoFaSettings() {
|
||||
const twoFaSettings = yield this.fetch.get('users/two-fa');
|
||||
try {
|
||||
const twoFaSettings = yield this.fetch.get('users/two-fa');
|
||||
if (twoFaSettings) {
|
||||
this.isUserTwoFaEnabled = twoFaSettings.enabled;
|
||||
this.twoFaSettings = twoFaSettings;
|
||||
}
|
||||
|
||||
if (twoFaSettings) {
|
||||
this.isUserTwoFaEnabled = twoFaSettings.enabled;
|
||||
this.twoFaSettings = twoFaSettings;
|
||||
return twoFaSettings;
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
return twoFaSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,12 +197,16 @@ export default class ConsoleAccountAuthController extends Controller {
|
||||
* @method loadSystemTwoFaConfig
|
||||
*/
|
||||
@task *loadSystemTwoFaConfig() {
|
||||
const twoFaConfig = yield this.fetch.get('two-fa/config');
|
||||
try {
|
||||
const twoFaConfig = yield this.fetch.get('two-fa/config');
|
||||
if (twoFaConfig) {
|
||||
this.isSystemTwoFaEnabled = twoFaConfig.enabled;
|
||||
this.twoFaConfig = twoFaConfig;
|
||||
}
|
||||
|
||||
if (twoFaConfig) {
|
||||
this.isSystemTwoFaEnabled = twoFaConfig.enabled;
|
||||
this.twoFaConfig = twoFaConfig;
|
||||
return twoFaConfig;
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
return twoFaConfig;
|
||||
}
|
||||
}
|
||||
|
||||
7
console/app/controllers/console/account/virtual.js
Normal file
7
console/app/controllers/console/account/virtual.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class ConsoleAccountVirtualController extends Controller {
|
||||
@tracked view;
|
||||
queryParams = ['view'];
|
||||
}
|
||||
7
console/app/controllers/console/admin/virtual.js
Normal file
7
console/app/controllers/console/admin/virtual.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class ConsoleAdminVirtualController extends Controller {
|
||||
@tracked view;
|
||||
queryParams = ['view'];
|
||||
}
|
||||
@@ -1,3 +1,26 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ConsoleHomeController extends Controller {}
|
||||
export default class ConsoleHomeController extends Controller {
|
||||
rows = [
|
||||
{
|
||||
name: 'Jason',
|
||||
age: 24,
|
||||
vehicle: 'Honda',
|
||||
},
|
||||
];
|
||||
|
||||
columns = [
|
||||
{
|
||||
label: 'Name',
|
||||
valuePath: 'name',
|
||||
},
|
||||
{
|
||||
label: 'Age',
|
||||
valuePath: 'age',
|
||||
},
|
||||
{
|
||||
label: 'Vehicle',
|
||||
valuePath: 'vehicle',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
7
console/app/controllers/console/settings/virtual.js
Normal file
7
console/app/controllers/console/settings/virtual.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class ConsoleSettingsVirtualController extends Controller {
|
||||
@tracked view;
|
||||
queryParams = ['view'];
|
||||
}
|
||||
7
console/app/controllers/console/virtual.js
Normal file
7
console/app/controllers/console/virtual.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class ConsoleVirtualController extends Controller {
|
||||
@tracked view;
|
||||
queryParams = ['view'];
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class PortalController extends Controller {
|
||||
@service session;
|
||||
|
||||
/**
|
||||
* Action handler.
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
@action onAction(action, ...params) {
|
||||
if (typeof this[action] === 'function') {
|
||||
this[action](...params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to invalidate and log user out
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
@action invalidateSession(event) {
|
||||
console.log('invalidateSession', ...arguments)
|
||||
// event.preventDefault();
|
||||
this.session.invalidateWithLoader();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export default helper(function spreadWidgetOptions([params]) {
|
||||
const { id, options } = params;
|
||||
const gridOptions = { id, ...options };
|
||||
return gridOptions;
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
export function initialize(owner) {
|
||||
const universe = owner.lookup('service:universe');
|
||||
if (universe) {
|
||||
universe.createRegistry('@fleetbase/console');
|
||||
universe.createRegistries(['@fleetbase/console', 'auth:login']);
|
||||
universe.bootEngines(owner);
|
||||
}
|
||||
}
|
||||
|
||||
18
console/app/instance-initializers/load-leaflet.js
Normal file
18
console/app/instance-initializers/load-leaflet.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export function initialize (owner) {
|
||||
const leafletService = owner.lookup('service:leaflet');
|
||||
if (leafletService) {
|
||||
leafletService.load({
|
||||
onReady: function (L) {
|
||||
// This will prevent the awkward scroll bug produced by Chrome browsers
|
||||
// https://github.com/Leaflet/Leaflet/issues/4125#issuecomment-356289643
|
||||
L.Control.include({
|
||||
_refocusOnMap: L.Util.falseFn,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize,
|
||||
};
|
||||
@@ -83,4 +83,14 @@ export default class DashboardModel extends Model {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getRegistry() {
|
||||
const owner = getOwner(this);
|
||||
const universe = owner.lookup('service:universe');
|
||||
if (universe) {
|
||||
return universe.getDashboardRegistry(this.id);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,11 @@ export default class UserModel extends Model {
|
||||
@attr('string') country;
|
||||
@attr('string') ip_address;
|
||||
@attr('string') slug;
|
||||
@attr('string') role_name;
|
||||
@attr('string') type;
|
||||
@attr('string') session_status;
|
||||
@attr('string') status;
|
||||
@attr('string') locale;
|
||||
@attr('boolean') is_online;
|
||||
@attr('boolean') is_admin;
|
||||
@attr('raw') meta;
|
||||
|
||||
@@ -11,7 +11,9 @@ export default class ApplicationRoute extends Route {
|
||||
@service urlSearchParams;
|
||||
@service modalsManager;
|
||||
@service intl;
|
||||
@service currentUser;
|
||||
@service router;
|
||||
@service universe;
|
||||
@tracked defaultTheme;
|
||||
|
||||
/**
|
||||
@@ -21,7 +23,7 @@ export default class ApplicationRoute extends Route {
|
||||
* @memberof ApplicationRoute
|
||||
*/
|
||||
// eslint-disable-next-line ember/classic-decorator-hooks
|
||||
async init() {
|
||||
async init () {
|
||||
super.init(...arguments);
|
||||
const { shouldInstall, shouldOnboard, defaultTheme } = await this.checkInstallationStatus();
|
||||
|
||||
@@ -43,13 +45,12 @@ export default class ApplicationRoute extends Route {
|
||||
* @return {Transition}
|
||||
* @memberof ApplicationRoute
|
||||
*/
|
||||
async beforeModel() {
|
||||
async beforeModel () {
|
||||
await this.session.setup();
|
||||
await this.universe.booting();
|
||||
|
||||
const { isAuthenticated } = this.session;
|
||||
const shift = this.urlSearchParams.get('shift');
|
||||
|
||||
if (isAuthenticated && shift) {
|
||||
if (this.session.isAuthenticated && shift) {
|
||||
return this.router.transitionTo(pathToRoute(shift));
|
||||
}
|
||||
}
|
||||
@@ -60,7 +61,20 @@ export default class ApplicationRoute extends Route {
|
||||
* @memberof ApplicationRoute
|
||||
* @void
|
||||
*/
|
||||
activate() {
|
||||
activate () {
|
||||
this.initializeTheme();
|
||||
this.initializeLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the application's theme settings, applying necessary class names and default theme configurations.
|
||||
*
|
||||
* This method prepares the theme by setting up an array of class names that should be applied to the
|
||||
* application's body element. If the application is running inside an Electron environment, it adds the
|
||||
* `'is-electron'` class to the array. It then calls the `initialize` method of the `theme` service,
|
||||
* passing in the `bodyClassNames` array and the `defaultTheme` configuration.
|
||||
*/
|
||||
initializeTheme () {
|
||||
const bodyClassNames = [];
|
||||
|
||||
if (isElectron()) {
|
||||
@@ -68,7 +82,18 @@ export default class ApplicationRoute extends Route {
|
||||
}
|
||||
|
||||
this.theme.initialize({ bodyClassNames, theme: this.defaultTheme });
|
||||
this.intl.setLocale(['en-us']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the application's locale settings based on the current user's preferences.
|
||||
*
|
||||
* This method retrieves the user's preferred locale using the `getOption` method from the `currentUser` service.
|
||||
* If no locale is set by the user, it defaults to `'en-us'`. It then sets the application's locale by calling
|
||||
* the `setLocale` method of the `intl` service with the retrieved locale.
|
||||
*/
|
||||
initializeLocale () {
|
||||
const locale = this.currentUser.getOption('locale', 'en-us');
|
||||
this.intl.setLocale([locale]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +102,7 @@ export default class ApplicationRoute extends Route {
|
||||
* @return {Promise}
|
||||
* @memberof ApplicationRoute
|
||||
*/
|
||||
checkInstallationStatus() {
|
||||
checkInstallationStatus () {
|
||||
return this.fetch.get('installer/initialize');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { inject as service } from '@ember/service';
|
||||
|
||||
export default class AuthLoginRoute extends Route {
|
||||
@service session;
|
||||
@service universe;
|
||||
|
||||
/**
|
||||
* If user is authentication redirect to console.
|
||||
@@ -10,7 +11,8 @@ export default class AuthLoginRoute extends Route {
|
||||
* @memberof AuthLoginRoute
|
||||
* @void
|
||||
*/
|
||||
beforeModel() {
|
||||
beforeModel (transition) {
|
||||
this.session.prohibitAuthentication('console');
|
||||
return this.universe.virtualRouteRedirect(transition, 'auth:login', 'virtual', { restoreQueryParams: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class AuthPortalLoginRoute extends Route {
|
||||
@service session;
|
||||
|
||||
/**
|
||||
* If user is authentication redirect to portal.
|
||||
*
|
||||
* @memberof AuthPortalLoginRoute
|
||||
* @void
|
||||
*/
|
||||
beforeModel() {
|
||||
this.session.prohibitAuthentication('portal');
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,10 @@ import '@fleetbase/leaflet-routing-machine';
|
||||
export default class ConsoleRoute extends Route {
|
||||
@service store;
|
||||
@service session;
|
||||
@service universe;
|
||||
@service router;
|
||||
@service currentUser;
|
||||
@service intl;
|
||||
|
||||
/**
|
||||
* Require authentication to access all `console` routes.
|
||||
@@ -15,13 +18,13 @@ export default class ConsoleRoute extends Route {
|
||||
* @memberof ConsoleRoute
|
||||
*/
|
||||
async beforeModel(transition) {
|
||||
this.session.requireAuthentication(transition, 'auth.login');
|
||||
await this.session.requireAuthentication(transition, 'auth.login');
|
||||
|
||||
if (this.session.data.authenticated.type === 'customer') {
|
||||
return this.router.transitionTo('portal');
|
||||
this.universe.callHooks('console:before-model', this.session, this.router, transition);
|
||||
|
||||
if (this.session.isAuthenticated) {
|
||||
return this.session.promiseCurrentUser(transition);
|
||||
}
|
||||
|
||||
return this.session.promiseCurrentUser(transition);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
|
||||
export default class ConsoleAccountVirtualRoute extends Route {
|
||||
@service universe;
|
||||
|
||||
model({ slug, view }) {
|
||||
return this.universe.lookupMenuItemFromRegistry('account', slug, view);
|
||||
queryParams = {
|
||||
view: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console:account', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
|
||||
export default class ConsoleAdminVirtualRoute extends Route {
|
||||
@service universe;
|
||||
|
||||
model({ slug, view }) {
|
||||
return this.universe.lookupMenuItemFromRegistry('admin', slug, view);
|
||||
queryParams = {
|
||||
view: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console:admin', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
|
||||
export default class ConsoleSettingsVirtualRoute extends Route {
|
||||
@service universe;
|
||||
|
||||
model({ slug, view }) {
|
||||
return this.universe.lookupMenuItemFromRegistry('settings', slug, view);
|
||||
queryParams = {
|
||||
view: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console:settings', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
|
||||
export default class ConsoleVirtualRoute extends Route {
|
||||
@service universe;
|
||||
|
||||
model({ slug, view }) {
|
||||
queryParams = {
|
||||
view: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class PortalRoute extends Route {
|
||||
@service store;
|
||||
@service session;
|
||||
@service theme;
|
||||
|
||||
/**
|
||||
* Require authentication to access all `portal` routes.
|
||||
*
|
||||
* @param {Transition} transition
|
||||
* @return {Promise}
|
||||
* @memberof PortalRoute
|
||||
*/
|
||||
async beforeModel(transition) {
|
||||
this.session.requireAuthentication(transition, 'auth.portal-login');
|
||||
|
||||
return this.session.promiseCurrentUser(transition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the branding settings.
|
||||
*
|
||||
* @return {BrandModel}
|
||||
* @memberof PortalRoute
|
||||
*/
|
||||
model() {
|
||||
return this.store.findRecord('brand', 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the fleetbase-portal body class.
|
||||
*
|
||||
* @memberof PortalRoute
|
||||
*/
|
||||
activate() {
|
||||
this.theme.setRoutebodyClassNames(['fleetbase-portal']);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class PortalAccountRoute extends Route {}
|
||||
@@ -1,3 +0,0 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class PortalHomeRoute extends Route {}
|
||||
17
console/app/routes/virtual.js
Normal file
17
console/app/routes/virtual.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class VirtualRoute extends Route {
|
||||
@service universe;
|
||||
|
||||
queryParams = {
|
||||
view: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
model ({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('auth:login', slug, view);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,14 @@ export default class UserSerializer extends ApplicationSerializer.extend(Embedde
|
||||
|
||||
// delete the password always
|
||||
delete json.password;
|
||||
// delete verification attributes
|
||||
delete json.email_verified_at;
|
||||
delete json.phone_verified_at;
|
||||
|
||||
// delete server managed dates
|
||||
delete json.deleted_at;
|
||||
delete json.created_at;
|
||||
delete json.updated_at;
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
import Service from '@ember/service';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
import { action } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
|
||||
/**
|
||||
* Service for managing dashboards, including loading, creating, and deleting dashboards, as well as managing the current dashboard and widget states.
|
||||
* Utilizes Ember services such as `store`, `fetch`, `notifications`, and `universe` for data management and user interaction.
|
||||
*
|
||||
* @extends Service
|
||||
*/
|
||||
export default class DashboardService extends Service {
|
||||
/**
|
||||
* Ember Data store service for managing model data.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service store;
|
||||
|
||||
/**
|
||||
* Fetch service for making network requests.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* Notifications service for displaying user notifications.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Universe service for accessing global application state or utility methods.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service universe;
|
||||
|
||||
/**
|
||||
* Internationalization service.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service intl;
|
||||
|
||||
/**
|
||||
* Tracked array of available dashboards.
|
||||
* @type {Array}
|
||||
*/
|
||||
@tracked dashboards = [];
|
||||
|
||||
/**
|
||||
* Tracked property representing the currently selected dashboard.
|
||||
* @type {Object}
|
||||
*/
|
||||
@tracked currentDashboard;
|
||||
|
||||
/**
|
||||
* Tracked boolean indicating if the dashboard is in editing mode.
|
||||
* @type {boolean}
|
||||
*/
|
||||
@tracked isEditingDashboard = false;
|
||||
|
||||
/**
|
||||
* Tracked boolean indicating if a widget is being added.
|
||||
* @type {boolean}
|
||||
*/
|
||||
@tracked isAddingWidget = false;
|
||||
|
||||
/**
|
||||
* Task for loading dashboards from the store. It sets the current dashboard and checks if adding widget is necessary.
|
||||
*/
|
||||
@task *loadDashboards() {
|
||||
const dashboards = yield this.store.findAll('dashboard');
|
||||
|
||||
if (isArray(dashboards)) {
|
||||
this.dashboards = dashboards.toArray();
|
||||
|
||||
// insert default dashboard if it's not loaded
|
||||
const defaultDashboard = this._createDefaultDashboard();
|
||||
if (this._isDefaultDashboardNotLoaded()) {
|
||||
this.dashboards.unshiftObject(defaultDashboard);
|
||||
}
|
||||
|
||||
// Set the current dashboard
|
||||
this.currentDashboard = this._getNextDashboard();
|
||||
if (this.currentDashboard && this.currentDashboard.widgets.length === 0) {
|
||||
this.onAddingWidget(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for selecting a dashboard. Handles dashboard switching and updates the current dashboard.
|
||||
* @param {Object} dashboard - The dashboard object to select.
|
||||
*/
|
||||
@task *selectDashboard(dashboard) {
|
||||
if (dashboard.user_uuid === 'system') {
|
||||
this.currentDashboard = dashboard;
|
||||
yield this.fetch.post('dashboards/reset-default');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDashboard = yield this.fetch.post('dashboards/switch', { dashboard_uuid: dashboard.id }, { normalizeToEmberData: true }).catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
|
||||
if (currentDashboard) {
|
||||
this.currentDashboard = currentDashboard;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for creating a new dashboard. It handles dashboard creation, success notification, and dashboard selection.
|
||||
* @param {string} name - Name of the new dashboard.
|
||||
*/
|
||||
@task *createDashboard(name) {
|
||||
const dashboardRecord = this.store.createRecord('dashboard', { name, is_default: true });
|
||||
const dashboard = yield dashboardRecord.save().catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
|
||||
if (dashboard) {
|
||||
this.notifications.success(this.intl.t('services.dashboard-service.create-dashboard-success-notification', { dashboardName: dashboard.name }));
|
||||
this.selectDashboard.perform(dashboard);
|
||||
this.dashboards.pushObject(dashboard);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for deleting a dashboard. Handles dashboard deletion and success notification.
|
||||
* @param {Object} dashboard - The dashboard object to delete.
|
||||
* @param {Object} [options={}] - Optional configuration options.
|
||||
*/
|
||||
@task *deleteDashboard(dashboard, options = {}) {
|
||||
yield dashboard.destroyRecord().catch((error) => {
|
||||
this.notification.serverError(error);
|
||||
|
||||
if (typeof options.onError === 'function') {
|
||||
options.onError(error, dashboard);
|
||||
}
|
||||
});
|
||||
|
||||
this.notifications.success(this.intl.t('services.dashboard-service.delete-dashboard-success-notification', { dashboardName: dashboard.name }));
|
||||
yield this.loadDashboards.perform();
|
||||
yield this.selectDashboard.perform(this._getNextDashboard());
|
||||
|
||||
if (typeof options.callback === 'function') {
|
||||
options.callback(this.currentDashboard);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for setting the current dashboard.
|
||||
* @param {Object} dashboard - The dashboard object to set as current.
|
||||
*/
|
||||
@task *setCurrentDashboard(dashboard) {
|
||||
const currentDashboard = yield this.fetch.post('dashboards/switch', { dashboard_uuid: dashboard.id }, { normalizeToEmberData: true }).catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
|
||||
if (currentDashboard) {
|
||||
this.currentDashboard = currentDashboard;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to toggle dashboard editing state.
|
||||
* @param {boolean} [state=true] - State to set for editing.
|
||||
*/
|
||||
@action onChangeEdit(state = true) {
|
||||
this.isEditingDashboard = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to toggle the state of adding a widget.
|
||||
* @param {boolean} [state=true] - State to set for adding a widget.
|
||||
*/
|
||||
@action onAddingWidget(state = true) {
|
||||
this.isAddingWidget = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default dashboard with predefined widgets.
|
||||
* @private
|
||||
* @returns {Object} The default dashboard object.
|
||||
*/
|
||||
_createDefaultDashboard() {
|
||||
let defaultDashboard;
|
||||
|
||||
// check store for default dashboard
|
||||
const loadedDashboars = this.store.peekAll('dashboard');
|
||||
|
||||
// check for default dashboard loaded in store
|
||||
defaultDashboard = loadedDashboars.find((dashboard) => dashboard.id === 'system');
|
||||
if (defaultDashboard) {
|
||||
return defaultDashboard;
|
||||
}
|
||||
|
||||
// create new default dashboard
|
||||
defaultDashboard = this.store.createRecord('dashboard', {
|
||||
id: 'system',
|
||||
uuid: 'system',
|
||||
name: 'Default Dashboard',
|
||||
is_default: false,
|
||||
user_uuid: 'system',
|
||||
widgets: this._createDefaultDashboardWidgets(),
|
||||
});
|
||||
|
||||
return defaultDashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates default widgets for the default dashboard.
|
||||
* @private
|
||||
* @returns {Array} An array of default dashboard widgets.
|
||||
*/
|
||||
_createDefaultDashboardWidgets() {
|
||||
const widgets = this.universe.getDefaultDashboardWidgets().map((defaultWidget) => {
|
||||
return this.store.createRecord('dashboard-widget', defaultWidget);
|
||||
});
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if default dashboard is already loaded.
|
||||
* @private
|
||||
* @return {Boolean}
|
||||
* @memberof DashboardService
|
||||
*/
|
||||
_isDefaultDashboardLoaded() {
|
||||
const defaultDashboard = this._createDefaultDashboard();
|
||||
return this.dashboards.some((dashboard) => dashboard.id === defaultDashboard.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if default dashboard is not already loaded.
|
||||
* @private
|
||||
* @return {Boolean}
|
||||
* @memberof DashboardService
|
||||
*/
|
||||
_isDefaultDashboardNotLoaded() {
|
||||
return !this._isDefaultDashboardLoaded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current dasbhoard or next available dashboard.
|
||||
*
|
||||
* @return {DashboardModel}
|
||||
* @memberof DashboardService
|
||||
*/
|
||||
_getNextDashboard() {
|
||||
return this.dashboards.find((dashboard) => dashboard.is_default) || this.dashboards[0];
|
||||
}
|
||||
}
|
||||
@@ -13,16 +13,6 @@ body[data-theme='dark'] .two-fa-enforcement-alert button#two-fa-setup-button.btn
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.app-version-in-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-top: 0.2rem;
|
||||
}
|
||||
|
||||
.fleetbase-pagination-meta-info-wrapper.within-layout-section-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{page-title (t "app.name")}}
|
||||
<ModalsContainer />
|
||||
<NotificationContainer @position="top" @zindex="99999" />
|
||||
<div id="application-root-wormhole"></div>
|
||||
{{outlet}}
|
||||
@@ -19,7 +19,13 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button @text={{t "auth.login.failed-attempt.button-text"}} @type="link" class="text-yellow-100" @wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50" @onClick={{this.forgotPassword}} />
|
||||
<Button
|
||||
@text={{t "auth.login.failed-attempt.button-text"}}
|
||||
@type="link"
|
||||
class="text-yellow-100"
|
||||
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
|
||||
@onClick={{this.forgotPassword}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@@ -67,7 +73,7 @@
|
||||
href="javascript:;"
|
||||
{{on "click" this.forgotPassword}}
|
||||
disabled={{this.isLoading}}
|
||||
class="font-medium transition duration-150 ease-in-out text-sky-600 hover:text-sky-500 focus:outline-none focus:underline"
|
||||
class="font-medium transition duration-150 ease-in-out text-sky-500 hover:text-sky-400 focus:outline-none focus:underline"
|
||||
>
|
||||
{{t "auth.login.form.forgot-password-label"}}
|
||||
</a>
|
||||
@@ -75,8 +81,26 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-6 space-y-4">
|
||||
<Button @buttonType="submit" @type="primary" @text={{t "auth.login.form.sign-in-button"}} @icon="lock" @wrapperClass="btn-block" @isLoading={{this.isLoading}} @onClick={{this.login}} />
|
||||
<Button @text={{t "auth.login.form.create-account-button"}} @wrapperClass="btn-block" @disabled={{this.isLoading}} @onClick={{fn (transition-to "onboard")}} />
|
||||
<Button @text="Customer Login" @type="link" @wrapperClass="btn-block py-1 border dark:border-gray-700 border-gray-200 hover:opacity-50" @disabled={{this.isLoading}} @onClick={{fn (transition-to "auth.portal-login")}} />
|
||||
<Button
|
||||
@buttonType="submit"
|
||||
@type="primary"
|
||||
@text={{t "auth.login.form.sign-in-button"}}
|
||||
@icon="lock"
|
||||
@wrapperClass="btn-block"
|
||||
@isLoading={{this.isLoading}}
|
||||
@onClick={{this.login}}
|
||||
/>
|
||||
<Button @text={{t "auth.login.form.create-account-button"}} @icon="briefcase" @wrapperClass="btn-block" @disabled={{this.isLoading}} @onClick={{fn (transition-to "onboard")}} />
|
||||
<RegistryYield @type="menu" @registry="auth:login" as |menuItem|>
|
||||
<Button
|
||||
@text={{menuItem.title}}
|
||||
@icon={{menuItem.icon}}
|
||||
@type={{menuItem.type}}
|
||||
@wrapperClass={{menuItem.wrapperClass}}
|
||||
@disabled={{this.isLoading}}
|
||||
@onClick={{menuItem.onClick}}
|
||||
@permission={{menuItem.permission}}
|
||||
/>
|
||||
</RegistryYield>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,88 +0,0 @@
|
||||
<div>
|
||||
<div class="mx-auto w-12 h-12">
|
||||
<LogoIcon @url={{@brand.icon_url}} @size="12" class="mx-auto" />
|
||||
</div>
|
||||
<h2 class="mt-6 mb-3 text-3xl font-semibold leading-9 text-center text-gray-900 dark:text-gray-100">
|
||||
Customer Login
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{{#if (gte this.failedAttempts 3)}}
|
||||
<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">
|
||||
<p class="flex-1 text-sm text-yellow-100">
|
||||
{{t "auth.login.failed-attempt.message" htmlSafe=true}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button @text={{t "auth.login.failed-attempt.button-text"}} @type="link" class="text-yellow-100" @wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50" @onClick={{this.forgotPassword}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<form class="mt-8" {{on "submit" this.login}}>
|
||||
<input type="hidden" name="remember" value="true" />
|
||||
<div class="rounded-md shadow-sm">
|
||||
<div>
|
||||
<Input
|
||||
@value={{this.identity}}
|
||||
aria-label={{t "auth.login.form.email-label"}}
|
||||
name="email"
|
||||
@type="email"
|
||||
autocomplete="username"
|
||||
required
|
||||
class="relative block w-full px-3 py-2 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-none appearance-none rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5 dark:text-white dark:bg-gray-700 dark:border-gray-900"
|
||||
placeholder={{t "auth.login.form.email-label"}}
|
||||
disabled={{this.isLoading}}
|
||||
/>
|
||||
</div>
|
||||
<div class="-mt-px">
|
||||
<Input
|
||||
@value={{this.password}}
|
||||
aria-label={{t "auth.login.form.password-label"}}
|
||||
name="password"
|
||||
@type="password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
class="relative block w-full px-3 py-2 text-gray-900 placeholder-gray-500 border border-gray-300 rounded-none appearance-none rounded-b-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5 dark:text-white dark:bg-gray-700 dark:border-gray-900"
|
||||
placeholder={{t "auth.login.form.password-label"}}
|
||||
disabled={{this.isLoading}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-6">
|
||||
<div class="flex items-center">
|
||||
<Input id="rememberMe" @type="checkbox" @checked={{this.rememberMe}} disabled={{this.isLoading}} class="w-4 h-4 transition duration-150 ease-in-out form-checkbox text-sky-500" />
|
||||
<label for="rememberMe" class="block ml-2 text-sm leading-5 text-gray-900 dark:text-gray-100">
|
||||
{{t "auth.login.form.remember-me-label"}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="text-sm leading-5">
|
||||
<a
|
||||
href="javascript:;"
|
||||
{{on "click" this.forgotPassword}}
|
||||
disabled={{this.isLoading}}
|
||||
class="font-medium transition duration-150 ease-in-out text-sky-600 hover:text-sky-500 focus:outline-none focus:underline"
|
||||
>
|
||||
{{t "auth.login.form.forgot-password-label"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<Button
|
||||
@buttonType="submit"
|
||||
@type="primary"
|
||||
@text={{t "auth.login.form.sign-in-button"}}
|
||||
@icon="lock"
|
||||
@wrapperClass="btn-block"
|
||||
@isLoading={{this.isLoading}}
|
||||
@onClick={{this.login}}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
@@ -2,7 +2,7 @@
|
||||
<Layout::Section::Header @title="Account Auth" />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<ContentPanel @title="Change Password" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<form id="change-password-form" aria-label="change-password" {{on "submit" (perform this.changePassword)}}>
|
||||
@@ -31,4 +31,5 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="500px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{@model.title}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-">
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{t "console.admin.config.database.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<Configure::Database />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{t "console.admin.config.filesystem.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<Configure::Filesystem />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{t "console.admin.config.mail.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<Configure::Mail />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{t "console.admin.config.notification-channels.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<Configure::NotificationChannels />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{t "console.admin.config.queue.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<Configure::Queue />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{t "console.admin.config.services.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 900}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<Configure::Services />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{t "console.admin.config.socket.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 900}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<Configure::Socket />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,7 +2,7 @@
|
||||
<Layout::Section::Header @title={{t "common.overview"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<div class="grid grid-cols-3 xs:grid-cols-1 gap-4">
|
||||
<StatWidget @title={{t "console.admin.index.total-users"}} @value={{@model.total_users}} />
|
||||
@@ -11,4 +11,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{@model.title}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-">
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -1,7 +1,7 @@
|
||||
{{page-title "Dashboard"}}
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<TwoFaEnforcementAlert />
|
||||
<Dashboard @sidebar={{this.sidebarContext}} />
|
||||
<Dashboard @sidebar={{this.sidebarContext}} class="flex items-center justify-between mb-4 mt-6 px-14" />
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
<div id="console-home-wormhole" />
|
||||
@@ -1,7 +1,7 @@
|
||||
<Layout::Section::Header @title={{t "console.settings.index.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<ContentPanel @title={{t "console.settings.index.title"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<form {{on "submit" this.saveSettings}}>
|
||||
@@ -65,4 +65,5 @@
|
||||
</ContentPanel>
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="500px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,7 +2,7 @@
|
||||
<Layout::Section::Header @title="2FA" />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<ContentPanel @title="2FA Settings" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<div class="mb-3">
|
||||
@@ -33,4 +33,5 @@
|
||||
</ContentPanel>
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{@model.title}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-">
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{@model.title}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-">
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -1,5 +1,6 @@
|
||||
<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="w-full max-w-md h-screen flex items-center justify-center py-4" {{increase-height-by 300}}>
|
||||
<div class="w-full max-w-md h-screen flex items-center justify-center py-4">
|
||||
{{outlet}}
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</div>
|
||||
@@ -1,14 +0,0 @@
|
||||
{{page-title "Portal"}}
|
||||
<Portal::Container>
|
||||
<Portal::Header @onAction={{this.onAction}} />
|
||||
<Portal::Main>
|
||||
<Portal::Sidebar />
|
||||
<Portal::Section>{{outlet}}</Portal::Section>
|
||||
</Portal::Main>
|
||||
</Portal::Container>
|
||||
{{!-- <div class="container">
|
||||
<div class="flex flex-row">
|
||||
<a href="javascript:;" {{on "click" this.invalidateSession}}>Sign out</a>
|
||||
</div>
|
||||
</div>
|
||||
</div> --}}
|
||||
@@ -1,2 +0,0 @@
|
||||
{{page-title "Account"}}
|
||||
{{outlet}}
|
||||
@@ -1,2 +0,0 @@
|
||||
{{page-title "Home"}}
|
||||
{{outlet}}
|
||||
2
console/app/templates/virtual.hbs
Normal file
2
console/app/templates/virtual.hbs
Normal file
@@ -0,0 +1,2 @@
|
||||
{{page-title @model.title}}
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
@@ -7,5 +7,14 @@ module.exports = function asArray(value) {
|
||||
return value.split(',');
|
||||
}
|
||||
|
||||
return Array.from(value);
|
||||
try {
|
||||
let iterable = Array.from(value);
|
||||
if (Array.isArray(iterable)) {
|
||||
return iterable;
|
||||
}
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
diff --git a/node_modules/ember-gridstack/addon/components/grid-stack.js b/node_modules/ember-gridstack/addon/components/grid-stack.js
|
||||
index fa51392..fdabb2a 100644
|
||||
--- a/node_modules/ember-gridstack/addon/components/grid-stack.js
|
||||
+++ b/node_modules/ember-gridstack/addon/components/grid-stack.js
|
||||
@@ -133,5 +133,6 @@ export default class GridStackComponent extends Component {
|
||||
removeWidget(element, removeDOM = false, triggerEvent = true) {
|
||||
triggerEvent = triggerEvent && !this.isDestroying && !this.isDestroyed;
|
||||
this.gridStack?.removeWidget(element, removeDOM, triggerEvent);
|
||||
+ this.gridStack?.compact();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ const recast = require('recast');
|
||||
const babelParser = require('recast/parsers/babel');
|
||||
const builders = recast.types.builders;
|
||||
|
||||
function getExtensionMountPath(extensionName) {
|
||||
function getExtensionMountPath (extensionName) {
|
||||
let extensionNameSegments = extensionName.split('/');
|
||||
let mountName = extensionNameSegments[1];
|
||||
|
||||
@@ -16,7 +16,7 @@ function getExtensionMountPath(extensionName) {
|
||||
return mountName.replace('-engine', '');
|
||||
}
|
||||
|
||||
function only(subject, props = []) {
|
||||
function only (subject, props = []) {
|
||||
const keys = Object.keys(subject);
|
||||
const result = {};
|
||||
|
||||
@@ -31,13 +31,13 @@ function only(subject, props = []) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function getExtensions() {
|
||||
function getExtensions () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const extensions = [];
|
||||
const seenPackages = new Set();
|
||||
|
||||
return fg(['node_modules/*/package.json', 'node_modules/*/*/package.json'])
|
||||
.then((results) => {
|
||||
.then(results => {
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const packagePath = results[i];
|
||||
const packageJson = fs.readFileSync(packagePath);
|
||||
@@ -69,7 +69,7 @@ function getExtensions() {
|
||||
});
|
||||
}
|
||||
|
||||
function getRouterFileContents() {
|
||||
function getRouterFileContents () {
|
||||
const routerFilePath = path.join(__dirname, 'router.map.js');
|
||||
const routerFileContents = fs.readFileSync(routerFilePath, 'utf-8');
|
||||
|
||||
@@ -78,24 +78,25 @@ function getRouterFileContents() {
|
||||
|
||||
(async () => {
|
||||
const extensions = await getExtensions();
|
||||
const consoleExtensions = extensions.filter(extension => !extension.fleetbase || extension.fleetbase.mount !== 'root');
|
||||
const rootExtensions = extensions.filter(extension => extension.fleetbase && extension.fleetbase.mount === 'root');
|
||||
const routerFileContents = getRouterFileContents();
|
||||
const ast = recast.parse(routerFileContents, { parser: babelParser });
|
||||
|
||||
recast.visit(ast, {
|
||||
visitCallExpression(path) {
|
||||
visitCallExpression (path) {
|
||||
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'route' && path.value.arguments[0].value === 'console') {
|
||||
let functionExpression;
|
||||
|
||||
// Find the function expression
|
||||
path.value.arguments.forEach((arg) => {
|
||||
path.value.arguments.forEach(arg => {
|
||||
if (arg.type === 'FunctionExpression') {
|
||||
functionExpression = arg;
|
||||
}
|
||||
});
|
||||
|
||||
if (functionExpression) {
|
||||
// Check and add the new engine mounts
|
||||
extensions.forEach((extension) => {
|
||||
consoleExtensions.forEach(extension => {
|
||||
const mountPath = getExtensionMountPath(extension.name);
|
||||
let route = mountPath;
|
||||
|
||||
@@ -104,7 +105,7 @@ function getRouterFileContents() {
|
||||
}
|
||||
|
||||
// Check if engine is already mounted
|
||||
const isMounted = functionExpression.body.body.some((expressionStatement) => {
|
||||
const isMounted = functionExpression.body.body.some(expressionStatement => {
|
||||
return expressionStatement.expression.arguments[0].value === extension.name;
|
||||
});
|
||||
|
||||
@@ -123,8 +124,46 @@ function getRouterFileContents() {
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
// console.log(path.value.callee.property.name);
|
||||
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'map') {
|
||||
let functionExpression;
|
||||
|
||||
path.value.arguments.forEach(arg => {
|
||||
if (arg.type === 'FunctionExpression') {
|
||||
functionExpression = arg;
|
||||
}
|
||||
});
|
||||
|
||||
if (functionExpression) {
|
||||
rootExtensions.forEach(extension => {
|
||||
const mountPath = getExtensionMountPath(extension.name);
|
||||
let route = mountPath;
|
||||
|
||||
if (extension.fleetbase && extension.fleetbase.route) {
|
||||
route = extension.fleetbase.route;
|
||||
}
|
||||
|
||||
const isMounted = functionExpression.body.body.some(expressionStatement => {
|
||||
return expressionStatement.expression.arguments[0].value === extension.name;
|
||||
});
|
||||
|
||||
if (!isMounted) {
|
||||
functionExpression.body.body.push(
|
||||
builders.expressionStatement(
|
||||
builders.callExpression(builders.memberExpression(builders.thisExpression(), builders.identifier('mount')), [
|
||||
builders.literal(extension.name),
|
||||
builders.objectExpression([
|
||||
builders.property('init', builders.identifier('as'), builders.literal(route)),
|
||||
builders.property('init', builders.identifier('path'), builders.literal(route)),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,11 @@ export default class Router extends EmberRouter {
|
||||
}
|
||||
|
||||
Router.map(function () {
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('install');
|
||||
this.route('onboard', function () {
|
||||
this.route('verify-email');
|
||||
});
|
||||
this.route('auth', function () {
|
||||
this.route('login', { path: '/' });
|
||||
this.route('forgot-password');
|
||||
@@ -16,28 +20,22 @@ Router.map(function () {
|
||||
this.route('verification');
|
||||
this.route('portal-login', { path: '/portal' });
|
||||
});
|
||||
this.route('onboard', function () {
|
||||
this.route('verify-email');
|
||||
});
|
||||
this.route('invite', { path: 'join' }, function () {
|
||||
this.route('for-driver', { path: '/fleet/:public_id' });
|
||||
this.route('for-user', { path: '/org/:public_id' });
|
||||
});
|
||||
this.route('portal', function () {
|
||||
this.route('account');
|
||||
});
|
||||
this.route('console', { path: '/' }, function () {
|
||||
this.route('home', { path: '/' });
|
||||
this.route('notifications');
|
||||
this.route('account', function () {
|
||||
this.route('virtual', { path: '/:slug/:view' });
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('auth');
|
||||
});
|
||||
this.route('settings', function () {
|
||||
this.route('virtual', { path: '/:slug/:view' });
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('two-fa');
|
||||
});
|
||||
this.route('virtual', { path: '/:slug/:view' });
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('admin', function () {
|
||||
this.route('config', function () {
|
||||
this.route('database');
|
||||
@@ -52,7 +50,7 @@ Router.map(function () {
|
||||
this.route('branding');
|
||||
this.route('notifications');
|
||||
this.route('two-fa-settings');
|
||||
this.route('virtual', { path: '/:slug/:view' });
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('organizations', function () {
|
||||
this.route('index', { path: '/' }, function () {
|
||||
this.route('users', { path: '/:public_id/users' });
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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 | dashboard', 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`<Dashboard />`);
|
||||
|
||||
assert.dom(this.element).hasText('');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
<Dashboard>
|
||||
template block text
|
||||
</Dashboard>
|
||||
`);
|
||||
|
||||
assert.dom(this.element).hasText('template block text');
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
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 | dashboard/create', 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`<Dashboard::Create />`);
|
||||
|
||||
assert.dom(this.element).hasText('');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
<Dashboard::Create>
|
||||
template block text
|
||||
</Dashboard::Create>
|
||||
`);
|
||||
|
||||
assert.dom(this.element).hasText('template block text');
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
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 | Helper | spread-widget-options', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it renders', async function (assert) {
|
||||
this.set('inputValue', '1234');
|
||||
|
||||
await render(hbs`{{spread-widget-options this.inputValue}}`);
|
||||
|
||||
assert.dom().hasText('1234');
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Service | dashboard', function (hooks) {
|
||||
module('Unit | Controller | console/account/virtual', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let service = this.owner.lookup('service:dashboard');
|
||||
assert.ok(service);
|
||||
let controller = this.owner.lookup('controller:console/account/virtual');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Controller | auth/portal-login', function (hooks) {
|
||||
module('Unit | Controller | console/admin/virtual', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:auth/portal-login');
|
||||
let controller = this.owner.lookup('controller:console/admin/virtual');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Controller | console/settings/virtual', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:console/settings/virtual');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Controller | portal', function (hooks) {
|
||||
module('Unit | Controller | console/virtual', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:portal');
|
||||
let controller = this.owner.lookup('controller:console/virtual');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import Application from '@ember/application';
|
||||
|
||||
import config from '@fleetbase/console/config/environment';
|
||||
import { initialize } from '@fleetbase/console/instance-initializers/load-leaflet';
|
||||
import { module, test } from 'qunit';
|
||||
import Resolver from 'ember-resolver';
|
||||
import { run } from '@ember/runloop';
|
||||
|
||||
module('Unit | Instance Initializer | load-leaflet', 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);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Route | auth/portal-login', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:auth/portal-login');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Route | portal/account', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:portal/account');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Route | portal/home', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:portal/home');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Route | portal', function (hooks) {
|
||||
module('Unit | Route | virtual', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:portal');
|
||||
let route = this.owner.lookup('route:virtual');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
364
console/translations/ar-ae.yml
Normal file
364
console/translations/ar-ae.yml
Normal file
@@ -0,0 +1,364 @@
|
||||
app:
|
||||
name: Fleetbase
|
||||
terms:
|
||||
new: جديد
|
||||
sort: ترتيب
|
||||
filter: تصفية
|
||||
columns: أعمدة
|
||||
settings: إعدادات
|
||||
home: الصفحة الرئيسية
|
||||
admin: مشرف
|
||||
logout: تسجيل الخروج
|
||||
dashboard: لوحة القيادة
|
||||
search: بحث
|
||||
search-input: إدخال البحث
|
||||
common:
|
||||
confirm: تأكيد
|
||||
edit: تحرير
|
||||
save: حفظ
|
||||
save-changes: حفظ التغييرات
|
||||
cancel: إلغاء
|
||||
2fa-config: تكوين المصادقة الثنائية
|
||||
account: حساب
|
||||
admin: مشرف
|
||||
branding: العلامة التجارية
|
||||
columns: أعمدة
|
||||
dashboard: لوحة القيادة
|
||||
date-of-birth: تاريخ الميلاد
|
||||
delete: حذف
|
||||
email: البريد الإلكتروني
|
||||
filesystem: نظام الملفات
|
||||
filter: تصفية
|
||||
home: الصفحة الرئيسية
|
||||
logout: تسجيل الخروج
|
||||
mail: بريد
|
||||
name: اسم
|
||||
new: جديد
|
||||
notification-channels: قنوات الإشعارات
|
||||
notifications: إشعارات
|
||||
organization: منظمة
|
||||
organizations: منظمات
|
||||
overview: نظرة عامة
|
||||
phone: رقم هاتفك
|
||||
profile: الملف الشخصي
|
||||
queue: قائمة الانتظار
|
||||
save-button-text: حفظ التغييرات
|
||||
search-input: إدخال البحث
|
||||
search: بحث
|
||||
services: خدمات
|
||||
settings: إعدادات
|
||||
socket: مقبس
|
||||
sort: ترتيب
|
||||
two-factor: المصادقة الثنائية
|
||||
uploading: جارٍ التحميل...
|
||||
your-profile: ملفك الشخصي
|
||||
created-at: تم الإنشاء في
|
||||
country: البلد
|
||||
phone-number: الهاتف
|
||||
status: الحالة
|
||||
close-and-save: إغلاق وحفظ
|
||||
users: المستخدمون
|
||||
changelog: سجل التغييرات
|
||||
ok: موافق
|
||||
select-file: اختر ملف
|
||||
back: رجوع
|
||||
next: التالي
|
||||
continue: متابعة
|
||||
done: تم
|
||||
export: تصدير
|
||||
reload: إعادة تحميل
|
||||
reload-data: إعادة تحميل البيانات
|
||||
unauthorized: غير مصرح
|
||||
unauthorized-to: غير مصرح لـ
|
||||
unauthorized-access: وصول غير مصرح
|
||||
unauthorized-access-message: وصول غير مصرح، يجب عليك طلب الأذونات للوصول.
|
||||
permissions-required-for-changes: ليس لديك الأذونات المطلوبة لإجراء التغييرات.
|
||||
push-notifications: إشعارات الدفع
|
||||
component:
|
||||
file:
|
||||
dropdown-label: إجراءات الملف
|
||||
import-modal:
|
||||
loading-message: جارٍ معالجة الاستيراد...
|
||||
drop-upload: إسقاط للتحميل
|
||||
invalid: غير صالح
|
||||
ready-upload: جاهز للتحميل.
|
||||
upload-spreadsheets: تحميل جداول البيانات
|
||||
drag-drop: اسحب وأفلت ملفات جداول البيانات في منطقة الإسقاط هذه
|
||||
button-text: أو اختر جداول البيانات للتحميل
|
||||
spreadsheets: جداول البيانات
|
||||
upload-queue: قائمة انتظار التحميل
|
||||
dropzone:
|
||||
file: ملف
|
||||
drop-to-upload: إسقاط للتحميل
|
||||
invalid: غير صالح
|
||||
files-ready-for-upload: >-
|
||||
{numOfFiles} جاهز للتحميل.
|
||||
upload-images-videos: تحميل الصور ومقاطع الفيديو
|
||||
upload-documents: تحميل المستندات
|
||||
upload-documents-files: تحميل المستندات والملفات
|
||||
upload-avatar-files: تحميل الصور الرمزية المخصصة
|
||||
dropzone-supported-images-videos: اسحب وأفلت ملفات الصور والفيديو في منطقة الإسقاط هذه
|
||||
dropzone-supported-avatars: اسحب وأفلت ملفات SVG أو PNG
|
||||
dropzone-supported-files: اسحب وأفلت الملفات في منطقة الإسقاط هذه
|
||||
or-select-button-text: أو اختر الملفات للتحميل.
|
||||
upload-queue: قائمة انتظار التحميل
|
||||
uploading: جارٍ التحميل...
|
||||
two-fa-enforcement-alert:
|
||||
message: لتعزيز أمان حسابك، تتطلب مؤسستك المصادقة الثنائية (2FA). قم بتمكين المصادقة الثنائية في إعدادات حسابك للحصول على طبقة إضافية من الحماية.
|
||||
button-text: إعداد المصادقة الثنائية
|
||||
comment-thread:
|
||||
publish-comment-button-text: نشر التعليق
|
||||
publish-reply-button-text: نشر الرد
|
||||
reply-comment-button-text: رد
|
||||
edit-comment-button-text: تحرير
|
||||
delete-comment-button-text: حذف
|
||||
comment-published-ago: >-
|
||||
{createdAgo} منذ
|
||||
comment-input-placeholder: أدخل تعليقًا جديدًا...
|
||||
comment-reply-placeholder: أدخل ردك...
|
||||
comment-input-empty-notification: لا يمكنك نشر تعليقات فارغة...
|
||||
comment-min-length-notification: يجب أن يكون التعليق على الأقل 2 حرف
|
||||
dashboard:
|
||||
select-dashboard: اختر لوحة القيادة
|
||||
create-new-dashboard: إنشاء لوحة قيادة جديدة
|
||||
create-a-new-dashboard: إنشاء لوحة قيادة جديدة
|
||||
confirm-create-dashboard: إنشاء لوحة القيادة!
|
||||
edit-layout: تحرير التخطيط
|
||||
add-widgets: إضافة عناصر واجهة
|
||||
delete-dashboard: حذف لوحة القيادة
|
||||
save-dashboard: حفظ لوحة القيادة
|
||||
you-cannot-delete-this-dashboard: لا يمكنك حذف هذه اللوحة.
|
||||
are-you-sure-you-want-delete-dashboard: هل أنت متأكد من حذف {dashboardName}؟
|
||||
dashboard-widget-panel:
|
||||
widget-name: >-
|
||||
عنصر واجهة {widgetName}
|
||||
select-widgets: اختر عناصر الواجهة
|
||||
close-and-save: إغلاق وحفظ
|
||||
services:
|
||||
dashboard-service:
|
||||
create-dashboard-success-notification: تم إنشاء لوحة القيادة الجديدة `{dashboardName}` بنجاح.
|
||||
delete-dashboard-success-notification: تم حذف لوحة القيادة `{dashboardName}`.
|
||||
auth:
|
||||
verification:
|
||||
header-title: التحقق من الحساب
|
||||
title: تحقق من عنوان بريدك الإلكتروني
|
||||
message-text: <strong>اقتربت من الانتهاء!</strong><br> تحقق من بريدك الإلكتروني للحصول على رمز التحقق.
|
||||
verification-code-text: أدخل رمز التحقق الذي تلقيته عبر البريد الإلكتروني.
|
||||
verification-input-label: رمز التحقق
|
||||
verify-button-text: تحقق واستمر
|
||||
didnt-receive-a-code: لم تتلق رمزًا بعد؟
|
||||
not-sent:
|
||||
message: لم تتلق رمزًا بعد؟
|
||||
alternative-choice: استخدم الخيارات البديلة أدناه للتحقق من حسابك.
|
||||
resend-email: إعادة إرسال البريد الإلكتروني
|
||||
send-by-sms: إرسال عبر الرسائل القصيرة
|
||||
two-fa:
|
||||
verify-code:
|
||||
verification-code: رمز التحقق
|
||||
check-title: تحقق من بريدك الإلكتروني أو هاتفك
|
||||
check-subtitle: لقد أرسلنا لك رمز تحقق. أدخل الرمز أدناه لإكمال عملية تسجيل الدخول.
|
||||
expired-help-text: انتهت صلاحية رمز المصادقة الثنائية الخاص بك. يمكنك طلب رمز آخر إذا كنت بحاجة إلى مزيد من الوقت.
|
||||
resend-code: إعادة إرسال الرمز
|
||||
verify-code: تحقق من الرمز
|
||||
cancel-two-factor: إلغاء المصادقة الثنائية
|
||||
invalid-session-error-notification: جلسة غير صالحة. حاول مرة أخرى.
|
||||
verification-successful-notification: تم التحقق بنجاح!
|
||||
verification-code-expired-notification: انتهت صلاحية رمز التحقق. يرجى طلب رمز جديد.
|
||||
verification-code-failed-notification: فشل التحقق. حاول مرة أخرى.
|
||||
resend-code:
|
||||
verification-code-resent-notification: تم إرسال رمز التحقق الجديد.
|
||||
verification-code-resent-error-notification: خطأ في إعادة إرسال رمز التحقق. حاول مرة أخرى.
|
||||
forgot-password:
|
||||
success-message: تحقق من بريدك الإلكتروني للمتابعة!
|
||||
is-sent:
|
||||
title: اقتربت من الانتهاء!
|
||||
message: <strong>تحقق من بريدك الإلكتروني!</strong><br> لقد أرسلنا لك رابطًا سحريًا إلى بريدك الإلكتروني سيسمح لك بإعادة تعيين كلمة المرور الخاصة بك. ينتهي صلاحية الرابط في 15 دقيقة.
|
||||
not-sent:
|
||||
title: هل نسيت كلمة المرور؟
|
||||
message: <strong>لا تقلق، نحن هنا لمساعدتك.</strong><br> أدخل البريد الإلكتروني الذي تستخدمه لتسجيل الدخول إلى {appName} وسنرسل لك رابطًا آمنًا لإعادة تعيين كلمة المرور الخاصة بك.
|
||||
form:
|
||||
email-label: عنوان بريدك الإلكتروني
|
||||
submit-button: حسنًا، أرسل لي رابطًا سحريًا!
|
||||
nevermind-button: لا تهتم
|
||||
login:
|
||||
title: تسجيل الدخول إلى حسابك
|
||||
no-identity-notification: هل نسيت إدخال بريدك الإلكتروني؟
|
||||
no-password-notification: هل نسيت إدخال كلمة المرور الخاصة بك؟
|
||||
unverified-notification: يجب التحقق من حسابك للمتابعة.
|
||||
password-reset-required: مطلوب إعادة تعيين كلمة المرور للمتابعة.
|
||||
failed-attempt:
|
||||
message: <strong>هل نسيت كلمة المرور الخاصة بك؟</strong><br> انقر على الزر أدناه لإعادة تعيين كلمة المرور الخاصة بك.
|
||||
button-text: حسنًا، ساعدني في إعادة التعيين!
|
||||
form:
|
||||
email-label: عنوان البريد الإلكتروني
|
||||
password-label: كلمة المرور
|
||||
remember-me-label: تذكرني
|
||||
forgot-password-label: هل نسيت كلمة المرور الخاصة بك؟
|
||||
sign-in-button: تسجيل الدخول
|
||||
create-account-button: إنشاء حساب جديد
|
||||
slow-connection-message: تواجه مشكلات في الاتصال.
|
||||
reset-password:
|
||||
success-message: تم إعادة تعيين كلمة المرور الخاصة بك! تسجيل الدخول للمتابعة.
|
||||
invalid-verification-code: هذا الرابط لإعادة تعيين كلمة المرور غير صالح أو منتهي الصلاحية.
|
||||
title: إعادة تعيين كلمة المرور الخاصة بك
|
||||
form:
|
||||
code:
|
||||
label: رمز إعادة التعيين الخاص بك
|
||||
help-text: رمز التحقق الذي تلقيته في بريدك الإلكتروني.
|
||||
password:
|
||||
label: كلمة مرور جديدة
|
||||
help-text: أدخل كلمة مرور لا تقل عن 6 أحرف للمتابعة.
|
||||
confirm-password:
|
||||
label: تأكيد كلمة المرور الجديدة
|
||||
help-text: أدخل كلمة مرور لا تقل عن 6 أحرف للمتابعة.
|
||||
submit-button: إعادة تعيين كلمة المرور
|
||||
back-button: رجوع
|
||||
console:
|
||||
create-or-join-organization:
|
||||
modal-title: إنشاء أو الانضمام إلى منظمة
|
||||
join-success-notification: لقد انضممت إلى منظمة جديدة!
|
||||
create-success-notification: لقد أنشأت منظمة جديدة!
|
||||
switch-organization:
|
||||
modal-title: هل أنت متأكد أنك تريد التبديل إلى المنظمة {organizationName}؟
|
||||
modal-body: من خلال التأكيد، سيظل حسابك مسجلاً الدخول، ولكن سيتم تبديل المنظمة الأساسية الخاصة بك.
|
||||
modal-accept-button-text: نعم، أريد التبديل إلى المنظمة
|
||||
success-notification: لقد قمت بتبديل المنظمات
|
||||
account:
|
||||
index:
|
||||
upload-new: تحميل جديد
|
||||
phone: رقم هاتفك.
|
||||
photos: الصور
|
||||
admin:
|
||||
schedule-monitor:
|
||||
schedule-monitor: مراقب الجدول
|
||||
task-logs-for: >-
|
||||
سجلات المهام لـ:
|
||||
showing-last-count: عرض آخر {count} سجلات
|
||||
name: الاسم
|
||||
type: النوع
|
||||
timezone: المنطقة الزمنية
|
||||
last-started: آخر بدء
|
||||
last-finished: آخر انتهاء
|
||||
last-failure: آخر فشل
|
||||
date: التاريخ
|
||||
memory: الذاكرة
|
||||
runtime: وقت التشغيل
|
||||
output: المخرجات
|
||||
no-output: لا توجد مخرجات
|
||||
config:
|
||||
database:
|
||||
title: تكوين قاعدة البيانات
|
||||
filesystem:
|
||||
title: تكوين نظام الملفات
|
||||
mail:
|
||||
title: تكوين البريد
|
||||
notification-channels:
|
||||
title: تكوين إشعارات الدفع
|
||||
queue:
|
||||
title: تكوين قائمة الانتظار
|
||||
services:
|
||||
title: تكوين الخدمات
|
||||
socket:
|
||||
title: تكوين المقبس
|
||||
branding:
|
||||
title: العلامة التجارية
|
||||
icon-text: أيقونة
|
||||
upload-new: تحميل جديد
|
||||
reset-default: إعادة التعيين إلى الافتراضي
|
||||
logo-text: شعار
|
||||
theme: السمة الافتراضية
|
||||
index:
|
||||
total-users: إجمالي المستخدمين
|
||||
total-organizations: إجمالي المنظمات
|
||||
total-transactions: إجمالي المعاملات
|
||||
notifications:
|
||||
title: الإشعارات
|
||||
notification-settings: إعدادات الإشعارات
|
||||
organizations:
|
||||
index:
|
||||
title: المنظمات
|
||||
owner-name-column: المالك
|
||||
owner-phone-column: هاتف المالك
|
||||
owner-email-column: بريد المالك
|
||||
users-count-column: المستخدمون
|
||||
phone-column: الهاتف
|
||||
email-column: البريد الإلكتروني
|
||||
users:
|
||||
title: المستخدمون
|
||||
settings:
|
||||
index:
|
||||
title: إعدادات المنظمة
|
||||
organization-name: اسم المنظمة
|
||||
organization-description: وصف المنظمة
|
||||
organization-phone: رقم هاتف المنظمة
|
||||
organization-currency: عملة المنظمة
|
||||
organization-id: معرف المنظمة
|
||||
organization-branding: العلامة التجارية للمنظمة
|
||||
logo: الشعار
|
||||
logo-help-text: شعار منظمتك.
|
||||
upload-new-logo: تحميل شعار جديد
|
||||
backdrop: الخلفية
|
||||
backdrop-help-text: لافتة اختيارية أو صورة خلفية لمنظمتك.
|
||||
upload-new-backdrop: تحميل خلفية جديدة
|
||||
extensions:
|
||||
title: الإضافات قادمة قريبًا!
|
||||
message: يرجى التحقق مرة أخرى في الإصدارات القادمة حيث نستعد لإطلاق مستودع الإضافات والسوق.
|
||||
notifications:
|
||||
select-all: تحديد الكل
|
||||
mark-as-read: تعليم كمقروء
|
||||
received: >-
|
||||
تم الاستلام:
|
||||
message: لا توجد إشعارات لعرضها.
|
||||
invite:
|
||||
for-users:
|
||||
invitation-message: لقد تمت دعوتك للانضمام إلى {companyName}
|
||||
invitation-sent-message: لقد تمت دعوتك للانضمام إلى منظمة {companyName} على {appName}. لقبول هذه الدعوة، أدخل رمز الدعوة الذي تلقيته عبر البريد الإلكتروني وانقر على متابعة.
|
||||
invitation-code-sent-text: رمز الدعوة الخاص بك
|
||||
accept-invitation-text: قبول الدعوة
|
||||
onboard:
|
||||
index:
|
||||
title: أنشئ حسابك
|
||||
welcome-title: <strong>مرحبًا بك في {companyName}!</strong><br />
|
||||
welcome-text: أكمل التفاصيل المطلوبة أدناه للبدء.
|
||||
full-name: الاسم الكامل
|
||||
full-name-help-text: اسمك الكامل
|
||||
your-email: عنوان البريد الإلكتروني
|
||||
your-email-help-text: عنوان بريدك الإلكتروني
|
||||
phone: رقم الهاتف
|
||||
phone-help-text: رقم هاتفك
|
||||
organization-name: اسم المنظمة
|
||||
organization-help-text: اسم منظمتك، سيتم إدارة جميع خدماتك ومواردك تحت هذه المنظمة، لاحقًا يمكنك إنشاء العديد من المنظمات كما تريد أو تحتاج.
|
||||
password: أدخل كلمة مرور
|
||||
password-help-text: كلمة مرورك، تأكد من أنها جيدة.
|
||||
confirm-password: تأكيد كلمة المرور
|
||||
confirm-password-help-text: فقط لتأكيد كلمة المرور التي أدخلتها أعلاه.
|
||||
continue-button-text: متابعة
|
||||
verify-email:
|
||||
header-title: التحقق من الحساب
|
||||
title: تحقق من عنوان بريدك الإلكتروني
|
||||
message-text: <strong>اقتربت من الانتهاء!</strong><br> تحقق من بريدك الإلكتروني للحصول على رمز التحقق.
|
||||
verification-code-text: أدخل رمز التحقق الذي تلقيته عبر البريد الإلكتروني.
|
||||
verification-input-label: رمز التحقق
|
||||
verify-button-text: تحقق واستمر
|
||||
didnt-receive-a-code: لم تتلق رمزًا بعد؟
|
||||
not-sent:
|
||||
message: لم تتلق رمزًا بعد؟
|
||||
alternative-choice: استخدم الخيارات البديلة أدناه للتحقق من حسابك.
|
||||
resend-email: إعادة إرسال البريد الإلكتروني
|
||||
send-by-sms: إرسال عبر الرسائل القصيرة
|
||||
install:
|
||||
installer-header: المثبت
|
||||
failed-message-sent: فشل التثبيت! انقر على الزر أدناه لإعادة محاولة التثبيت.
|
||||
retry-install: إعادة محاولة التثبيت
|
||||
start-install: بدء التثبيت
|
||||
layout:
|
||||
header:
|
||||
menus:
|
||||
organization:
|
||||
settings: إعدادات المنظمة
|
||||
create-or-join: إنشاء أو الانضمام إلى المنظمات
|
||||
explore-extensions: استكشاف الإضافات
|
||||
user:
|
||||
view-profile: عرض الملف الشخصي
|
||||
keyboard-shortcuts: عرض اختصارات لوحة المفاتيح
|
||||
changelog: سجل التغييرات
|
||||
363
console/translations/vi-vn.yaml
Normal file
363
console/translations/vi-vn.yaml
Normal file
@@ -0,0 +1,363 @@
|
||||
app:
|
||||
name: Fleetbase
|
||||
terms:
|
||||
new: Mới
|
||||
sort: Sắp xếp
|
||||
filter: Lọc
|
||||
columns: Cột
|
||||
settings: Cài đặt
|
||||
home: Trang chủ
|
||||
admin: Admin
|
||||
logout: Đăng xuất
|
||||
dashboard: Bảng điều khiển
|
||||
search: Tìm kiếm
|
||||
search-input: Đầu vào tìm kiếm
|
||||
common:
|
||||
confirm: Xác nhận
|
||||
edit: Chỉnh sửa
|
||||
save: Lưu
|
||||
save-changes: Lưu thay đổi
|
||||
cancel: Hủy
|
||||
2fa-config: Cấu hình 2FA
|
||||
account: Tài khoản
|
||||
admin: Admin
|
||||
branding: Thương hiệu
|
||||
columns: Cột
|
||||
dashboard: Bảng điều khiển
|
||||
date-of-birth: Ngày sinh
|
||||
delete: Xóa
|
||||
email: Email
|
||||
filesystem: Hệ thống tệp
|
||||
filter: Lọc
|
||||
home: Trang chủ
|
||||
logout: Đăng xuất
|
||||
mail: Mail
|
||||
name: Tên
|
||||
new: Mới
|
||||
notification-channels: Kênh thông báo
|
||||
notifications: Thông báo
|
||||
organization: Tổ chức
|
||||
organizations: Các tổ chức
|
||||
overview: Tổng quan
|
||||
phone: Số điện thoại của bạn
|
||||
profile: Hồ sơ
|
||||
queue: Hàng đợi
|
||||
save-button-text: Lưu thay đổi
|
||||
search-input: Đầu vào tìm kiếm
|
||||
search: Tìm kiếm
|
||||
services: Dịch vụ
|
||||
settings: Cài đặt
|
||||
socket: Socket
|
||||
sort: Sắp xếp
|
||||
two-factor: Hai yếu tố
|
||||
uploading: Đang tải lên...
|
||||
your-profile: Hồ sơ của bạn
|
||||
created-at: Được tạo vào
|
||||
country: Quốc qua
|
||||
phone-number: Số điện thoại
|
||||
status: Trạng thái
|
||||
close-and-save: Đóng và Lưu
|
||||
users: Người dùng
|
||||
changelog: Nhật ký thay đổi
|
||||
ok: OK
|
||||
select-file: Chọn tệp
|
||||
back: Quay lại
|
||||
next: Tiếp theo
|
||||
continue: Tiếp tục
|
||||
done: Đã xong
|
||||
export: Xuất
|
||||
reload: Tải lại
|
||||
reload-data: Tải lại dữ liệu
|
||||
unauthorized: Không có quyền
|
||||
unauthorized-to: Không có quyền để
|
||||
unauthorized-access: Không có quyền truy cập
|
||||
unauthorized-access-message: Không có quyền truy cập, bạn phải yêu cầu quyền để truy cập.
|
||||
permissions-required-for-changes: Bạn không có quyền cần thiết để thực hiện thay đổi.
|
||||
push-notifications: Đẩy thông báo
|
||||
component:
|
||||
file:
|
||||
dropdown-label: Hành động với tệp
|
||||
import-modal:
|
||||
loading-message: Đang tiến hành nhập...
|
||||
drop-upload: Thả để tải lên
|
||||
invalid: Không hợp lệ
|
||||
ready-upload: sẵn sàng tải lên.
|
||||
upload-spreadsheets: Tải lên Bảng tính
|
||||
drag-drop: Kéo và thả các tệp bảng tính vào vùng thả này
|
||||
button-text: hoặc chọn bảng tính để tải lên
|
||||
spreadsheets: bảng tính
|
||||
upload-queue: Tải lên hàng đợi
|
||||
dropzone:
|
||||
file: file
|
||||
drop-to-upload: Thả để tải lên
|
||||
invalid: Không hợp lệ
|
||||
files-ready-for-upload: >-
|
||||
{numOfFiles} sẵn sàng tải lên.
|
||||
upload-images-videos: Tải lên hình ảnh và video
|
||||
upload-documents: Tải lên tài liệu
|
||||
upload-documents-files: Tải lên tài liệu và tập tin
|
||||
upload-avatar-files: Tải lên ảnh đại diện tùy chỉnh
|
||||
dropzone-supported-images-videos: Kéo và thả ảnh hoặc video vào vùng thả này
|
||||
dropzone-supported-avatars: Kéo và thả tệp SVG hoặc PNG
|
||||
dropzone-supported-files: Kéo và thả tệp vào vùng thả này
|
||||
or-select-button-text: hoặc chọn file để tải lên.
|
||||
upload-queue: Tải lên hàng đợi
|
||||
uploading: Đang tải lên...
|
||||
two-fa-enforcement-alert:
|
||||
message: Để tăng cường bảo mật cho tài khoản của bạn, tổ chức của bạn yêu cầu Xác thực hai yếu tố (2FA). Bật 2FA trong cài đặt tài khoản của bạn để có thêm một lớp bảo vệ.
|
||||
button-text: Cấu hình 2FA
|
||||
comment-thread:
|
||||
publish-comment-button-text: Đăng bình luận
|
||||
publish-reply-button-text: Đăng phản hồi
|
||||
reply-comment-button-text: Phản hồi
|
||||
edit-comment-button-text: Chỉnh sửa
|
||||
delete-comment-button-text: Xóa
|
||||
comment-published-ago: >-
|
||||
{createdAgo} trước
|
||||
comment-input-placeholder: Nhập một bình luận mới...
|
||||
comment-reply-placeholder: Nhập phản hồi của bạn...
|
||||
comment-input-empty-notification: Bạn không thể đăng bình luận trống...
|
||||
comment-min-length-notification: Bình luận phải chứa ít nhất 2 ký tự
|
||||
dashboard:
|
||||
select-dashboard: Chọn Bảng điều khiển
|
||||
create-new-dashboard: Tạo mới Bảng điều khiển
|
||||
create-a-new-dashboard: Tạo mới một Bảng điều khiển
|
||||
confirm-create-dashboard: Tạo Bảng điều khiển!
|
||||
edit-layout: Chỉnh sửa bố cục
|
||||
add-widgets: Thêm tiện ích
|
||||
delete-dashboard: Xóa bảng điều khiển
|
||||
save-dashboard: Lưu bảng điều khiển
|
||||
you-cannot-delete-this-dashboard: Bạn không thể xóa bảng điều khiển này.
|
||||
are-you-sure-you-want-delete-dashboard: Bạn có chắc chắn xóa {dashboardName}?
|
||||
dashboard-widget-panel:
|
||||
widget-name: >-
|
||||
{widgetName} Tiện ích
|
||||
select-widgets: Chọn tiện ích
|
||||
close-and-save: Đóng và Lưu
|
||||
services:
|
||||
dashboard-service:
|
||||
create-dashboard-success-notification: Bảng điều khiển `{dashboardName}` đã được tạo thành công.
|
||||
delete-dashboard-success-notification: Bảng điều khiển `{dashboardName}` đã được xóa.
|
||||
auth:
|
||||
verification:
|
||||
header-title: Xác thực tài khoản
|
||||
title: Xác thực email của bạn
|
||||
message-text: <strong>Gần xong rồi!</strong><br> Kiểm tả email của bạn để lấy mã xác thực.
|
||||
verification-code-text: Nhập mã xác thực bạn vừa nhận qua email.
|
||||
verification-input-label: Mã xác thực
|
||||
verify-button-text: Xác thực & Tiếp tục
|
||||
didnt-receive-a-code: Chưa nhận được mã?
|
||||
not-sent:
|
||||
message: Chưa nhận được mã?
|
||||
alternative-choice: Sử dụng các tùy chọn thay thế bên dưới để xác thực tài khoản của bạn.
|
||||
resend-email: Gửi lại email
|
||||
send-by-sms: Gửi qua SMS
|
||||
two-fa:
|
||||
verify-code:
|
||||
verification-code: Mã xác thực
|
||||
check-title: Kiểm tra email hoặc điện thoại của bạn.
|
||||
check-subtitle: Chúng tôi đã gửi cho bạn mã xác thực. Nhập mã bên dưới để hoàn tất quá trình đăng nhập.
|
||||
expired-help-text: Mã xác thực 2FA của bạn đã hết hạn. Bạn có thể yêu cầu mã khác nếu cần thêm thời gian.
|
||||
resend-code: Gửi lại mã
|
||||
verify-code: Xác thực mã
|
||||
cancel-two-factor: Hủy 2 yếu tố
|
||||
invalid-session-error-notification: Phiên không hợp lệ. Vui lòng thử lại.
|
||||
verification-successful-notification: Xác thực thành công!
|
||||
verification-code-expired-notification: Mã xác thực đã hết hạn. Vui lòng yêu cầu một mã mới.
|
||||
verification-code-failed-notification: Xác thực thất bại. Vui lòng thử lại.
|
||||
resend-code:
|
||||
verification-code-resent-notification: Mã xác thực mới đã được gửi.
|
||||
verification-code-resent-error-notification: Xảy ra lỗi khi gửi lại mã xác thực. Vui lòng thử lại.
|
||||
forgot-password:
|
||||
success-message: Kiểm tra email của bạn để tiếp tục!
|
||||
is-sent:
|
||||
title: Gần xong rồi!
|
||||
message: <strong>Kiểm tra email của bạn!</strong><br> Chúng tôi đã gửi cho bạn một liên kết đến email của bạn, cho phép bạn đặt lại mật khẩu. Liên kết sẽ hết hạn sau 15 phút.
|
||||
not-sent:
|
||||
title: Quên mật khẩu?
|
||||
message: <strong>Đừng lo lắng, chúng tôi luôn hỗ trợ bạn.</strong><br> Nhập email bạn sử dụng để đăng nhập {appName} và chúng tôi sẽ gửi cho bạn một liên kết an toàn để đặt lại mật khẩu.
|
||||
form:
|
||||
email-label: Địa chỉ email của bạn
|
||||
submit-button: OK, Gửi cho tôi một liên kết!
|
||||
nevermind-button: Ok
|
||||
login:
|
||||
title: Đăng nhập vào tài khoản của bạn
|
||||
no-identity-notification: Bạn quên nhập email của bạn?
|
||||
no-password-notification: Bạn quên nhập mật khẩu của bạn?
|
||||
unverified-notification: Tài khoản của bạn cần được xác minh để tiếp tục.
|
||||
failed-attempt:
|
||||
message: <strong>Quên mật khẩu?</strong><br> Nhấp vào nút bên dưới để đặt lại mật khẩu của bạn.
|
||||
button-text: Được, giúp tôi thiết lập lại nhé!
|
||||
form:
|
||||
email-label: Địa chỉ email
|
||||
password-label: Mật khẩu
|
||||
remember-me-label: Ghi nhớ đăng nhập
|
||||
forgot-password-label: Quên mật khẩu?
|
||||
sign-in-button: Đăng nhập
|
||||
create-account-button: Tạo mới tài khoản
|
||||
slow-connection-message: Đang gặp sự cố kết nối.
|
||||
reset-password:
|
||||
success-message: Mật khẩu của bạn đã được đặt lại! Đăng nhập để tiếp tục.
|
||||
invalid-verification-code: Liên kết đặt lại mật khẩu này không hợp lệ hoặc đã hết hạn.
|
||||
title: Đặt lại mật khẩu của bạn
|
||||
form:
|
||||
code:
|
||||
label: Mã đặt lại của bạn
|
||||
help-text: Mã xác thực bạn đã nhận qua email.
|
||||
password:
|
||||
label: Mật khẩu mới
|
||||
help-text: Nhập mật khẩu ít nhất 6 ký tự để tiếp tục.
|
||||
confirm-password:
|
||||
label: Xác nhận mật khẩu mới
|
||||
help-text: Nhập mật khẩu ít nhất 6 ký tự để tiếp tục.
|
||||
submit-button: Đặt lại mật khẩu
|
||||
back-button: Quay lại
|
||||
console:
|
||||
create-or-join-organization:
|
||||
modal-title: Tạo mới hoặc tham gia một tổ chức
|
||||
join-success-notification: Bạn đã tham gia một tổ chức mới!
|
||||
create-success-notification: Bạn vừa tạo ra một tổ chức mới!
|
||||
switch-organization:
|
||||
modal-title: Bạn có chắc chắn muốn chuyển đổi sang tổ chức {organizationName}?
|
||||
modal-body: Bằng cách xác nhận, tài khoản của bạn sẽ vẫn được đăng nhập, nhưng tổ chức chính của bạn sẽ được chuyển đổi.
|
||||
modal-accept-button-text: Vâng, tôi muốn chuyển đổi tổ chức
|
||||
success-notification: Bạn đã chuyển đổi tổ chức
|
||||
account:
|
||||
index:
|
||||
upload-new: Tải lên mới
|
||||
phone: Số điện thoại của bạn.
|
||||
photos: ảnh
|
||||
admin:
|
||||
schedule-monitor:
|
||||
schedule-monitor: Lịch trình giám sát
|
||||
task-logs-for: >-
|
||||
Nhật ký tác vụ cho:
|
||||
showing-last-count: Đang hiển thị {count} nhật ký cuối
|
||||
name: Tên
|
||||
type: Loại
|
||||
timezone: Múi giờ
|
||||
last-started: Lần bắt đầu cuối
|
||||
last-finished: Lần kết thúc cuối
|
||||
last-failure: Lần thất bại cuối
|
||||
date: Ngày
|
||||
memory: Memory
|
||||
runtime: Runtime
|
||||
output: Đầu ra
|
||||
no-output: Không có đầu ra
|
||||
config:
|
||||
database:
|
||||
title: Cấu hình cơ sở dữ liệu
|
||||
filesystem:
|
||||
title: Cấu hình hệ thống tập tin
|
||||
mail:
|
||||
title: Cấu hình Mail
|
||||
notification-channels:
|
||||
title: Cấu hình kênh thông báo
|
||||
queue:
|
||||
title: Cấu hình hàng đợi
|
||||
services:
|
||||
title: Cấu hình dịch vụ
|
||||
socket:
|
||||
title: Cấu hình Socket
|
||||
branding:
|
||||
title: Thương hiệu
|
||||
icon-text: Icon
|
||||
upload-new: Tải lên
|
||||
reset-default: Đặt về mặc định
|
||||
logo-text: Logo
|
||||
theme: Chủ đề mặc định
|
||||
index:
|
||||
total-users: Tổng số người dùng
|
||||
total-organizations: Tổng số tổ chức
|
||||
total-transactions: Tổng số giao dịch
|
||||
notifications:
|
||||
title: Thông báo
|
||||
notification-settings: Cài đặt thông báo
|
||||
organizations:
|
||||
index:
|
||||
title: Tổ chức
|
||||
owner-name-column: Người sở hữu
|
||||
owner-phone-column: Điện thoại chủ sở hữu
|
||||
owner-email-column: Email chủ sở hữu
|
||||
users-count-column: Người dùng
|
||||
phone-column: Số điện thoại
|
||||
email-column: Email
|
||||
users:
|
||||
title: Người dùng
|
||||
settings:
|
||||
index:
|
||||
title: Cài đặt tổ chức
|
||||
organization-name: Tên tổ chức
|
||||
organization-description: Mô tả
|
||||
organization-phone: Số điện thoại
|
||||
organization-currency: Tiền tệ
|
||||
organization-id: ID
|
||||
organization-branding: Thương hiệu
|
||||
logo: Logo
|
||||
logo-help-text: Logo cho tổ chức của bạn..
|
||||
upload-new-logo: Tải lên logo mới
|
||||
backdrop: Phông nền
|
||||
backdrop-help-text: Biểu ngữ hoặc hình ảnh nền tùy chọn cho tổ chức của bạn.
|
||||
upload-new-backdrop: Tải lên phông nền mới
|
||||
extensions:
|
||||
title: Các tiện ích mở rộng sẽ sớm ra mắt!
|
||||
message: Vui lòng kiểm tra lại trong các phiên bản sắp tới khi chúng tôi chuẩn bị ra mắt kho lưu trữ và thị trường Tiện ích mở rộng.
|
||||
notifications:
|
||||
select-all: Chọn tất cả
|
||||
mark-as-read: Đánh dấu là đã đọc
|
||||
received: >-
|
||||
Đã nhận:
|
||||
message: Không có thông báo nào để hiển thị.
|
||||
invite:
|
||||
for-users:
|
||||
invitation-message: Bạn đã được mời tham gia {companyName}
|
||||
invitation-sent-message: Bạn đã được mời tham gia tổ chức {companyName} trên {appName}. Để chấp nhận lời mời này, hãy nhập mã mời nhận được qua email và nhấp vào tiếp tục.
|
||||
invitation-code-sent-text: Mã mời của bạn
|
||||
accept-invitation-text: Chấp nhận lời mời
|
||||
onboard:
|
||||
index:
|
||||
title: Tạo tài khoản mới
|
||||
welcome-title: <strong>Chào mừng tới {companyName}!</strong><br />
|
||||
welcome-text: Hoàn tất các thông tin yêu cầu bên dưới để bắt đầu.
|
||||
full-name: Tên đầy đủ
|
||||
full-name-help-text: Tên đầy đủ của bạn
|
||||
your-email: Địa chỉ email
|
||||
your-email-help-text: Địa chỉ email của bạn
|
||||
phone: Số điện thoại
|
||||
phone-help-text: Số điện thoại của bạn
|
||||
organization-name: Tên tổ chức
|
||||
organization-help-text: Tên tổ chức của bạn, tất cả các dịch vụ và tài nguyên của bạn sẽ được quản lý theo tổ chức này, sau đó bạn có thể tạo nhiều tổ chức tùy ý hoặc theo nhu cầu.
|
||||
password: Nhập mật khẩu
|
||||
password-help-text: Mật khẩu của bạn phải an toàn.
|
||||
confirm-password: Xác nhận mật khẩu của bạn
|
||||
confirm-password-help-text: Chỉ để xác nhận mật khẩu bạn đã nhập ở trên.
|
||||
continue-button-text: Tiếp tục
|
||||
verify-email:
|
||||
header-title: Xác thực tài khoản
|
||||
title: Xác thực địa chỉ email của bạn
|
||||
message-text: <strong>Gần xong rồi!</strong><br> Kiêm tra mã xác thực trong email của bạn.
|
||||
verification-code-text: Nhập mã xác thực bạn vừa nhận qua email.
|
||||
verification-input-label: Mã xác thực
|
||||
verify-button-text: Xác thực & Tiếp tục
|
||||
didnt-receive-a-code: Bạn vẫn chưa nhận được mã?
|
||||
not-sent:
|
||||
message: Bạn vẫn chưa nhận được mã?
|
||||
alternative-choice: Sử dụng các tùy chọn thay thế bên dưới để xác thực tài khoản của bạn.
|
||||
resend-email: Gửi lại
|
||||
send-by-sms: Gửi qua SMS
|
||||
install:
|
||||
installer-header: Trình cài đặt
|
||||
failed-message-sent: Cài đặt thất bại! Nhấp vào nút bên dưới để thử cài đặt lại.
|
||||
retry-install: Thử lại cài đặt
|
||||
start-install: Bắt đầu cài đặt
|
||||
layout:
|
||||
header:
|
||||
menus:
|
||||
organization:
|
||||
settings: Cài đặt tổ chức
|
||||
create-or-join: Tạo mới hoặc tham gia một tổ chức
|
||||
explore-extensions: Khám phá phần mở rộng
|
||||
user:
|
||||
view-profile: Xem hồ sơ
|
||||
keyboard-shortcuts: Hiển thị phím tắt
|
||||
changelog: Nhật ký thay đổi
|
||||
Submodule packages/ember-core updated: 612626582d...a0781b5e13
Submodule packages/ember-ui updated: 280301ec27...bf518999cb
Reference in New Issue
Block a user