Compare commits

...

33 Commits

Author SHA1 Message Date
Ron
23d5ecfdb8 Merge pull request #300 from fleetbase/dev-v0.5.10
patches and improvements
2024-10-08 21:42:07 +08:00
Ronald A. Richardson
2795a2f1be update app router.js 2024-10-08 21:41:07 +08:00
Ron
69afdee975 Merge pull request #299 from fleetbase/dev-v0.5.10
v0.5.10 - patches and improvements
2024-10-08 21:39:34 +08:00
Ronald A. Richardson
60845b9953 patches and improvements 2024-10-08 21:33:55 +08:00
Ron
f3997a1bb7 Merge pull request #298 from fleetbase/dev-v0.5.9
v0.5.9
2024-10-03 17:48:38 +08:00
Ronald A. Richardson
23a691e7e7 removed ember-leaflet patches, using perm fix and improved engine boot timeout and callbacks 2024-10-03 17:43:25 +08:00
Ronald A. Richardson
3dc562987a Hotfix release 2024-10-03 14:58:39 +08:00
Ron
e8ac2a3796 Merge pull request #295 from fleetbase/dev-v0.5.8
v0.5.8 - Feature: Customer Portal, Order Tracking Page, New Translations, Improved UX
2024-10-02 18:43:09 +08:00
Ronald A. Richardson
b78c59ad89 updated README roadmap 2024-10-02 18:38:17 +08:00
Ronald A. Richardson
bd71b1921b v0.5.8 ready 2024-10-02 18:34:40 +08:00
Ron
2596ccbded Merge pull request #290 from CassioDalla/console-pt-br-translation
Create pt-br.yaml File
2024-10-02 13:57:54 +08:00
Ronald A. Richardson
81159b7411 preparing for major release w/ customer portal extension and order tracking page, real time ETAs and order progress tracker info 2024-10-02 00:15:13 +08:00
Ronald A. Richardson
acc4cfba35 fix as-array utility 2024-09-06 12:18:11 +08:00
Ronald A. Richardson
ea47bdc09d little progress and bugfixes 2024-09-06 12:17:30 +08:00
Ronald A. Richardson
c60c460257 Feature: Customer Portal, improved Customer, Contact, and Vendor Management 2024-09-04 12:31:10 +08:00
Ron
dc00ac3892 Merge pull request #294 from fleetbase/dev-v0.5.7
upgrade core-api
2024-08-31 15:22:56 +07:00
Ronald A. Richardson
f18ec886a7 upgrade core-api 2024-08-31 16:21:45 +08:00
Ron
f039b61d79 Merge pull request #293 from fleetbase/dev-v0.5.7
hotfix for resource loading and relation loading for organizations
2024-08-31 15:01:36 +07:00
Ronald A. Richardson
248f70e31c hotfix for resource loading and relation loading for organizations 2024-08-31 15:51:30 +08:00
Ron
6bc76a1b33 Merge pull request #292 from fleetbase/dev-v0.5.6
v0.5.6 - hotfix only load organization with valid owners - no stale org
2024-08-30 17:18:07 +07:00
Ronald A. Richardson
30695b3ebe hotfix only load organization with valid owners - no stale org 2024-08-30 18:06:42 +08:00
Ron
7ff9c24ad5 Merge pull request #288 from fleetbase/dev-v0.5.5
v0.5.5 - Documentation & README Updates
2024-08-30 14:20:57 +07:00
Ronald A. Richardson
1a9b9c06e5 fully implemented iam permission based restrictions and controls, few bugfixes and improvements 2024-08-30 15:14:56 +08:00
Ronald A. Richardson
5f949c3b7f implementing IAM permission controls, policies and roles 2024-08-13 20:26:32 +08:00
CassioDalla
211a3a9808 Create pt-br.yaml File
Added Brazilian Portuguese translation file to the project. It would be nice if another translator reviewed it to make sure everything is correct.
2024-08-10 16:04:45 -03:00
Ronald A. Richardson
0e5e4e07dd updating docs WIP, and updated README 2024-08-08 17:01:18 +08:00
Ronald A. Richardson
2eabfc4698 version 0.5.4 2024-08-06 13:51:29 +08:00
Ron
77c2c01e58 Merge pull request #286 from fleetbase/dev-v0.5.5
v0.5.5
2024-08-06 12:48:41 +07:00
Ronald A. Richardson
7c5b5b5858 again, fix depn order 2024-08-06 13:33:29 +08:00
Ronald A. Richardson
aad072cf4c reverted dependency order in package.json 2024-08-06 13:32:39 +08:00
Ronald A. Richardson
c1c6dcafd8 ordered packages 2024-08-06 13:30:34 +08:00
Ronald A. Richardson
61992ee924 fix lockfile 2024-08-06 13:28:39 +08:00
Ronald A. Richardson
b18d6197bc upgraded registry bridge and updated erd 2024-08-06 13:25:32 +08:00
118 changed files with 16007 additions and 15159 deletions

View File

@@ -132,11 +132,15 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Check for _GITHUB_AUTH_TOKEN and create .npmrc
- name: Create and Setup .npmrc
run: |
if [[ -n "${{ secrets._GITHUB_AUTH_TOKEN }}" ]]; then
echo "//npm.pkg.github.com/:_authToken=${{ secrets._GITHUB_AUTH_TOKEN }}" > .npmrc
fi
if [[ -n "${{ secrets.FLEETBASE_REGISTRY_TOKEN }}" ]]; then
echo "//registry.fleetbase.io/:_authToken=${{ secrets.FLEETBASE_REGISTRY_TOKEN }}" >> .npmrc
echo "@fleetbase:registry=https://registry.fleetbase.io" >> .npmrc
fi
working-directory: ./console
- name: Set Env Variables for QA

1
.gitignore vendored
View File

@@ -30,6 +30,7 @@ packages/flespi
packages/loconav
packages/internals
packages/projectargus-engine
packages/customer-portal
# wip
packages/solid
solid

View File

@@ -42,6 +42,7 @@ sh deploy.sh
- [Features](#-features)
- [Install](#-install)
- [Extensions](#-extensions)
- [Apps](#-apps)
- [Roadmap](#-roadmap)
- [Bugs and Feature Requests](#-bugs-and--feature-requests)
@@ -90,7 +91,7 @@ Fleetbase API: http://localhost:8000
**CORS:** If youre installing directly on a server you may need to add your IP address or domain to the `api/config/cors.php` file in the `allowed_hosts` array.
**Routing:** Fleetbase ships with its own OSRM server hosted at `[bundle.routing.fleetbase.io](https://bundle.routing.fleetbase.io)` but youre able to use your own or any other OSRM compatible server. You can modify this in the `console/environments` directory by modifying the env file of the environment youre deploying and setting the `OSRM_HOST` to the OSRM server for Fleetbase to use.
**Routing:** Fleetbase ships with a default OSRM server hosted by `[router.project-osrm.org](https://router.project-osrm.org)` but youre able to use your own or any other OSRM compatible server. You can modify this in the `console/environments` directory by modifying the .env file of the environment youre deploying and setting the `OSRM_HOST` to the OSRM server for Fleetbase to use.
**Services:** There are a few environment variables which need to be set for Fleetbase to function with full features. If youre deploying with docker then its easiest to just create a `docker-compose.override.yml` and supply the environment variables in this file.
@@ -100,7 +101,7 @@ services:
application:
environment:
MAIL_MAILER: (ses, smtp, mailgun, postmark, sendgrid)
OSRM_HOST: https://bundle.routing.fleetbase.io
OSRM_HOST: https://router.project-osrm.org
IPINFO_API_KEY:
GOOGLE_MAPS_API_KEY:
GOOGLE_MAPS_LOCALE: us
@@ -112,6 +113,26 @@ services:
You can learn more about full installation, and configuration in the [official documentation](https://docs.fleetbase.io/getting-started/install).
# 🧩 Extensions
Extensions are modular components that enhance the functionality of your Fleetbase instance. They allow you to add new features, customize existing behavior, or integrate with external systems.
You can find extensions available from the official [Fleetbase Console](https://console.fleetbase.io), here you will also be able get your registry token to install extensions to a self-hosted Fleetbase instance.
Additionally you're able to develop and publish your own extensions as well which you can read more about developing extensions via the [extension building guide](https://docs.fleetbase.io/developers/building-an-extension).
## ⌨️ Fleetbase CLI
The Fleetbase CLI is a powerful tool designed to simplify the management of extensions for your Fleetbase instance. With the CLI, you can effortlessly handle authentication, install and uninstall extensions, and scaffold new extensions if you are developing your own.
Get started with the CLI with npm:
```bash
npm i -g @fleetbase/cli
```
Once installed, you can access a variety of commands to manage your Fleetbase extensions.
# 📱 Apps
Fleetbase offers a few open sourced apps which are built on Fleetbase which can be cloned and customized. Every app is built so that the Fleetbase instance can be switched out whether on-premise install or cloud hosted.
@@ -122,13 +143,11 @@ Fleetbase offers a few open sourced apps which are built on Fleetbase which can
</ul>
## 🛣️ Roadmap
1. **Extensions Registry and Marketplace** ~ Allows users to publish and sell installable extensions on Fleetbase instances.
2. **Inventory and Warehouse Management** ~ Pallet will be Fleetbases first official extension for WMS & Inventory.
3. **Customer Facing Views** ~ Extensions will be able to create public/customer facing views tracking and management from outside of the console UI.
4. **Binary Builds** ~ Run Fleetbase from a single binary.
5. **Fleetbase CLI** ~ Official CLI for publishing and managing extensions, as well as scaffolding extensions.
6. **Fleetbase for Desktop** ~ Desktop builds for OSX and Windows.
7. **Custom Maps and Routing Engines** ~ Feature to enable easy integrations with custom maps and routing engines like Google Maps or Mapbox etc…
1. **Inventory and Warehouse Management** ~ Pallet will be Fleetbases first official extension for WMS & Inventory.
2. **Accounting and Invoicing** ~ Ledger will be Fleetbases first official extension accounting and invoicing.
3. **Binary Builds** ~ Run Fleetbase from a single binary.
4. **Fleetbase for Desktop** ~ Desktop builds for OSX and Windows.
5. **Custom Maps and Routing Engines** ~ Feature to enable easy integrations with custom maps and routing engines like Google Maps or Mapbox etc…
## 🪲 Bugs and 💡 Feature Requests

View File

@@ -9,10 +9,10 @@
"license": "AGPL-3.0-or-later",
"require": {
"php": "^8.0",
"fleetbase/core-api": "^1.5.1",
"fleetbase/fleetops-api": "^0.5.5",
"fleetbase/registry-bridge": "^0.0.11",
"fleetbase/storefront-api": "^0.3.13",
"fleetbase/core-api": "^1.5.10",
"fleetbase/fleetops-api": "^0.5.8",
"fleetbase/registry-bridge": "^0.0.15",
"fleetbase/storefront-api": "^0.3.15",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^10.0",
"laravel/octane": "^2.3",

1450
api/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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'),
],
];

View File

@@ -15,6 +15,9 @@ php artisan sandbox:migrate --force
# Seed database
php artisan fleetbase:seed
# Create permissions, policies, and roles
php artisan fleetbase:create-permissions
# Restart queue
php artisan queue:restart

View File

@@ -106,10 +106,8 @@ export default class ConfigureFilesystemComponent extends Component {
type: 'gcs_credentials',
},
(uploadedFile) => {
console.log('uploadedFile', uploadedFile);
this.gcsCredentialsFileId = uploadedFile.id;
this.gcsCredentialsFile = uploadedFile;
console.log('this.gcsCredentialsFile', this.gcsCredentialsFile);
}
);
} catch (error) {

View File

@@ -21,7 +21,7 @@
{{/if}}
<div class="flex flex-row items-center mt-3">
<Input @value={{this.twilioTestPhone}} @type="tel" placeholder="Send Test SMS Here" class="form-input form-input-sm" />
<Button @wrapperClass="ml-2" @icon="plug" @text="Test Twilio Config" @onClick={{this.testTwilio}} @isLoading={{this.isLoading}} @disabled={{not this.twilioTestPhone}} />
<Button @wrapperClass="ml-2" @icon="plug" @text="Test Twilio Config" @onClick={{perform this.testTwilio}} @isLoading={{this.testTwilio.isRunning}} @disabled={{not this.twilioTestPhone}} />
</div>
</ContentPanel>
@@ -33,13 +33,15 @@
<span class="text-xs">{{this.this.sentryTestResponse.message}}</span>
</div>
{{/if}}
<Button @wrapperClass="mt-3" @icon="plug" @text="Test Sentry Config" @onClick={{this.testSentry}} @isLoading={{this.isLoading}} />
<Button @wrapperClass="mt-3" @icon="plug" @text="Test Sentry Config" @onClick={{perform this.testSentry}} @isLoading={{this.testSentry.isRunning}} @disabled={{not this.sentryDsn}} />
</ContentPanel>
<ContentPanel @title="IP Info" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name="IP Info API Key" @value={{this.ipinfoApiKey}} disabled={{this.isLoading}} />
</ContentPanel>
<Spacer @height="200px" />
<EmberWormhole @to="next-view-section-subheader-actions">
<Button @type="primary" @size="sm" @icon="save" @text="Save Changes" @onClick={{this.save}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
<Button @type="primary" @size="sm" @icon="save" @text="Save Changes" @onClick={{perform this.save}} @disabled={{or this.save.isRunning this.loadConfigValues.isRunning}} @isLoading={{or this.save.isRunning this.loadConfigValues.isRunning}} />
</EmberWormhole>

View File

@@ -2,6 +2,7 @@ import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { task } from 'ember-concurrency';
export default class ConfigureServicesComponent extends Component {
@service fetch;
@@ -37,7 +38,7 @@ export default class ConfigureServicesComponent extends Component {
*/
constructor() {
super(...arguments);
this.loadConfigValues();
this.loadConfigValues.perform();
}
@action setConfigValues(config) {
@@ -48,24 +49,19 @@ export default class ConfigureServicesComponent extends Component {
}
}
@action loadConfigValues() {
this.isLoading = true;
this.fetch
.get('settings/services-config')
.then((response) => {
this.setConfigValues(response);
})
.finally(() => {
this.isLoading = false;
});
@task *loadConfigValues() {
try {
const config = yield this.fetch.get('settings/services-config');
this.setConfigValues(config);
return config;
} catch (error) {
this.notifications.serverError(error);
}
}
@action save() {
this.isLoading = true;
this.fetch
.post('settings/services-config', {
@task *save() {
try {
yield this.fetch.post('settings/services-config', {
aws: {
key: this.awsKey,
secret: this.awsSecret,
@@ -86,45 +82,36 @@ export default class ConfigureServicesComponent extends Component {
sentry: {
dsn: this.sentryDsn,
},
})
.then(() => {
this.notifications.success('Services configuration saved.');
})
.finally(() => {
this.isLoading = false;
});
} catch (error) {
this.notifications.serverError(error);
}
}
@action testTwilio() {
this.isLoading = true;
this.fetch
.post('settings/test-twilio-config', {
@task *testTwilio() {
try {
const twilioTestResponse = yield this.fetch.post('settings/test-twilio-config', {
sid: this.twilioSid,
token: this.twilioToken,
from: this.twilioFrom,
phone: this.twilioTestPhone,
})
.then((response) => {
this.twilioTestResponse = response;
})
.finally(() => {
this.isLoading = false;
});
this.twilioTestResponse = twilioTestResponse;
return twilioTestResponse;
} catch (error) {
this.notifications.serverError(error);
}
}
@action testSentry() {
this.isLoading = true;
this.fetch
.post('settings/test-sentry-config', {
@task *testSentry() {
try {
const sentryTestResponse = yield this.fetch.post('settings/test-sentry-config', {
dsn: this.sentryDsn,
})
.then((response) => {
this.sentryTestResponse = response;
})
.finally(() => {
this.isLoading = false;
});
this.sentryTestResponse = sentryTestResponse;
return sentryTestResponse;
} catch (error) {
this.notifications.serverError(error);
}
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -27,9 +27,10 @@ export default class DashboardCountComponent extends Component {
* @param {Object} { options }
* @memberof WidgetKeyMetricsCountComponent
*/
constructor(owner, { options, title }) {
constructor(owner, { options, title, value = null }) {
super(...arguments);
this.title = title;
this.value = value;
this.createRenderValueFromOptions(options);
}
@@ -40,6 +41,10 @@ export default class DashboardCountComponent extends Component {
* @memberof WidgetKeyMetricsCountComponent
*/
createRenderValueFromOptions(options = {}) {
if (value !== null) {
return;
}
let { format, currency, dateFormat, value } = options;
switch (format) {

View File

@@ -1,14 +0,0 @@
<div class="fleetbase-dashboard-grid" ...attributes>
<GridStack @options={{this.gridOptions}} @onChange={{this.onChangeGrid}}>
{{#each @dashboard.widgets as |widget|}}
<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>
{{/each}}
</GridStack>
</div>

View File

@@ -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,
};
}
}

View File

@@ -1,6 +1,6 @@
<div class="fleetbase-github-card relative flex-1 w-full" ...attributes>
<div class="border dark:border-gray-700 border-gray-200 dark:bg-gray-800 bg-gray-50 rounded-lg shadow-sm flex flex-col">
{{#if this.isLoading}}
{{#if this.getRepositoryData.isRunning}}
<div class="p-4">
<Spinner />
</div>

View File

@@ -10,9 +10,12 @@ import fetch from 'fetch';
export default class GithubCardComponent extends Component {
@storageFor('local-cache') localCache;
@tracked data;
@tracked tags;
@tracked isLoading = false;
@tracked data = {
owner: {
avatar_url: 'https://avatars.githubusercontent.com/u/38091894?v=4',
},
};
@tracked tags = [];
@computed('tags.length') get latestRelease() {
if (isArray(this.tags) && this.tags.length) {

View File

@@ -1,28 +1,11 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { task } from 'ember-concurrency';
export default class AuthForgotPasswordController extends Controller {
/**
* Inject the `fetch` service
*
* @memberof AuthForgotPasswordController
*/
@service fetch;
/**
* Inject the `notifications` service
*
* @memberof AuthForgotPasswordController
*/
@service notifications;
/**
* Inject the `intl` service
*
* @memberof AuthForgotPasswordController
*/
@service intl;
/**
@@ -32,13 +15,6 @@ export default class AuthForgotPasswordController extends Controller {
*/
@tracked email;
/**
* The loading state
*
* @memberof AuthForgotPasswordController
*/
@tracked isLoading;
/**
* Indicator if request has been sent.
*
@@ -46,30 +22,27 @@ export default class AuthForgotPasswordController extends Controller {
*/
@tracked isSent = false;
/**
* Query parameters.
*
* @memberof AuthForgotPasswordController
*/
queryParams = ['email'];
/**
* Sends a secure magic reset link to the user provided email.
*
* @memberof AuthForgotPasswordController
*/
@action sendSecureLink(event) {
// firefox patch
@task *sendSecureLink(event) {
event.preventDefault();
const { email } = this;
this.isLoading = true;
this.fetch
.post('auth/get-magic-reset-link', { email })
.then(() => {
this.notifications.success(this.intl.t('auth.forgot-password.success-message'));
this.isSent = true;
})
.catch((error) => {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
try {
yield this.fetch.post('auth/get-magic-reset-link', { email: this.email });
this.notifications.success(this.intl.t('auth.forgot-password.success-message'));
this.isSent = true;
} catch (error) {
this.notifications.serverError(error);
}
}
}

View File

@@ -5,53 +5,12 @@ import { action } from '@ember/object';
import pathToRoute from '@fleetbase/ember-core/utils/path-to-route';
export default class AuthLoginController extends Controller {
/**
* Inject the `forgotPassword` controller
*
* @var {Controller}
*/
@controller('auth.forgot-password') forgotPasswordController;
/**
* Inject the `notifications` service
*
* @var {Service}
*/
@service notifications;
/**
* Inject the `urlSearchParams` service
*
* @var {Service}
*/
@service urlSearchParams;
/**
* Inject the `session` service
*
* @var {Service}
*/
@service session;
/**
* Inject the `router` service
*
* @var {Service}
*/
@service router;
/**
* Inject the `intl` service
*
* @var {Service}
*/
@service intl;
/**
* Inject the `fetch` service
*
* @var {Service}
*/
@service fetch;
/**
@@ -110,8 +69,20 @@ export default class AuthLoginController extends Controller {
*/
@tracked failedAttempts = 0;
/**
* Authentication token.
*
* @memberof AuthLoginController
*/
@tracked token;
/**
* Action to login user.
*
* @param {Event} event
* @return {void}
* @memberof AuthLoginController
*/
@action async login(event) {
// firefox patch
event.preventDefault();
@@ -166,6 +137,11 @@ export default class AuthLoginController extends Controller {
return this.sendUserForEmailVerification(identity);
}
// Handle password reset required
if (error.toString().includes('reset required')) {
return this.sendUserForPasswordReset(identity);
}
return this.failure(error);
}
@@ -210,6 +186,20 @@ export default class AuthLoginController extends Controller {
});
}
/**
* Sends user to forgot password flow.
*
* @param {String} email
* @return {Promise<Transition>}
* @memberof AuthLoginController
*/
@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.
*

View File

@@ -53,6 +53,13 @@ export default class AuthResetPasswordController extends Controller {
*/
@tracked password_confirmation;
/**
* Query parameters.
*
* @memberof AuthResetPasswordController
*/
queryParams = ['code'];
/**
* The reset password task.
*

View File

@@ -1,69 +1,21 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { getOwner } from '@ember/application';
import { later } from '@ember/runloop';
import { action, computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { action } from '@ember/object';
import { isArray } from '@ember/array';
import first from '@fleetbase/ember-core/utils/first';
export default class ConsoleController extends Controller {
/**
* Inject the `currentUser` service.
*
* @var {Service}
*/
@service currentUser;
/**
* Inject the `modalsManager` service.
*
* @var {Service}
*/
@service modalsManager;
/**
* Inject the `session` service.
*
* @var {Service}
*/
@service session;
/**
* Inject the `fetch` service.
*
* @var {Service}
*/
@service fetch;
/**
* Inject the `notifications` service.
*
* @var {Service}
*/
@service notifications;
/**
* Inject the `router` service.
*
* @var {Service}
*/
@service router;
/**
* Inject the `intl` service.
*
* @var {Service}
*/
@service intl;
/**
* Inject the `universe` service.
*
* @var {Service}
*/
@service universe;
@service abilities;
/**
* Authenticated user organizations.
@@ -98,23 +50,28 @@ export default class ConsoleController extends Controller {
*
* @var {Array}
*/
@tracked hiddenSidebarRoutes = ['console.home', 'console.extensions', 'console.notifications'];
@tracked hiddenSidebarRoutes = ['console.home', 'console.notifications'];
/**
* Installed extensions.
* Menu items to be added to the main header navigation bar.
*
* @var {Array}
* @memberof ConsoleController
*/
@computed() get extensions() {
return getOwner(this).application.extensions;
}
@tracked menuItems = [];
/**
* Get the currently authenticated user
* Menu items to be added to the user dropdown menu located in the header.
*
* @var {Model}
* @memberof ConsoleController
*/
@alias('currentUser.user') user;
@tracked userMenuItems = [];
/**
* Menu items to be added to the organization dropdown menu located in the header.
*
* @memberof ConsoleController
*/
@tracked organizationMenuItems = [];
/**
* Creates an instance of ConsoleController.
@@ -198,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();
}
/**
@@ -228,53 +185,51 @@ export default class ConsoleController extends Controller {
changeAction: (action) => {
this.modalsManager.setOption('action', action);
},
confirm: (modal) => {
confirm: async (modal) => {
modal.startLoading();
const { action, next, name, description, phone, currency, country, timezone } = modal.getOptions();
if (action === 'join') {
return this.fetch
.post('auth/join-organization', { next })
.then(() => {
this.fetch.flushRequestCache('auth/organizations');
this.notifications.success(this.intl.t('console.create-or-join-organization.join-success-notification'));
later(
this,
() => {
window.location.reload();
},
900
);
})
.catch((error) => {
this.notifications.serverError(error);
});
}
return this.fetch
.post('auth/create-organization', {
name,
description,
phone,
currency,
country,
timezone,
})
.then(() => {
try {
await this.fetch.post('auth/join-organization', { next });
this.fetch.flushRequestCache('auth/organizations');
this.notifications.success(this.intl.t('console.create-or-join-organization.create-success-notification'));
later(
this.notifications.success(this.intl.t('console.create-or-join-organization.join-success-notification'));
return later(
this,
() => {
window.location.reload();
},
900
);
})
.catch((error) => {
this.notifications.serverError(error);
} catch (error) {
modal.stopLoading();
return this.notifications.serverError(error);
}
}
try {
await this.fetch.post('auth/create-organization', {
name,
description,
phone,
currency,
country,
timezone,
});
this.fetch.flushRequestCache('auth/organizations');
this.notifications.success(this.intl.t('console.create-or-join-organization.create-success-notification'));
return later(
this,
() => {
window.location.reload();
},
900
);
} catch (error) {
modal.stopLoading();
return this.notifications.serverError(error);
}
},
});
}
@@ -294,25 +249,24 @@ export default class ConsoleController extends Controller {
body: this.intl.t('console.switch-organization.modal-body'),
acceptButtonText: this.intl.t('console.switch-organization.modal-accept-button-text'),
acceptButtonScheme: 'primary',
confirm: (modal) => {
confirm: async (modal) => {
modal.startLoading();
return this.fetch
.post('auth/switch-organization', { next: organization.uuid })
.then(() => {
this.fetch.flushRequestCache('auth/organizations');
this.notifications.success(this.intl.t('console.switch-organization.success-notification'));
later(
this,
() => {
window.location.reload();
},
900
);
})
.catch((error) => {
this.notifications.serverError(error);
});
try {
await this.fetch.post('auth/switch-organization', { next: organization.uuid });
this.fetch.flushRequestCache('auth/organizations');
this.notifications.success(this.intl.t('console.switch-organization.success-notification'));
return later(
this,
() => {
window.location.reload();
},
900
);
} catch (error) {
modal.stopLoading();
return this.notifications.serverError(error);
}
},
});
}

View File

@@ -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;
}
}

View 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'];
}

View 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'];
}

View File

@@ -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',
},
];
}

View 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'];
}

View 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'];
}

View File

@@ -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;
});

View File

@@ -1,7 +1,8 @@
export function initialize (owner) {
const universe = owner.lookup('service:universe');
export function initialize (application) {
const universe = application.lookup('service:universe');
if (universe) {
universe.bootEngines(owner);
universe.createRegistries(['@fleetbase/console', 'auth:login']);
universe.bootEngines(application);
}
}

View File

@@ -0,0 +1,18 @@
export function initialize(application) {
const leafletService = application.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,
};

View File

@@ -1,21 +0,0 @@
import config from 'ember-get-config';
export function initialize(owner) {
const universe = owner.lookup('service:universe');
if (universe) {
universe.registerOrganizationMenuItem(`v${config.version}`, {
index: 4,
route: null,
icon: 'code-branch',
iconSize: 'xs',
iconClass: 'mr-1.5',
wrapperClass: 'app-version-in-nav',
overwriteWrapperClass: true,
});
}
}
export default {
initialize,
};

View File

@@ -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;
}
}

View File

@@ -1,5 +1,6 @@
import Model, { attr } from '@ember-data/model';
import { computed } from '@ember/object';
import { capitalize } from '@ember/string';
import { pluralize } from 'ember-inflector';
import { format, formatDistanceToNow } from 'date-fns';
import humanize from '@fleetbase/ember-core/utils/humanize';
@@ -26,14 +27,27 @@ export const getPermissionResource = function (permissionName) {
return parserPermissionName(permissionName, 2);
};
const lowercase = function (string) {
let words = string.split(' ');
words[0] = words[0].toLowerCase();
return words.join(' ');
const titleize = function (string = '') {
if (typeof string !== 'string') {
return '';
}
return humanize(string)
.split(' ')
.map((w) => capitalize(w))
.join(' ');
};
const titleize = function (string) {
return lowercase(humanize(string));
const smartTitleize = function (string = '') {
if (typeof string !== 'string') {
return '';
}
let titleized = titleize(string);
if (titleized === 'Iam') {
titleized = titleized.toUpperCase();
}
return titleized;
};
/**
@@ -49,6 +63,7 @@ export default class PermissionModel extends Model {
/** @attributes */
@attr('string') name;
@attr('string') guard_name;
@attr('string') service;
/** @dates */
@attr('date') created_at;
@@ -59,6 +74,7 @@ export default class PermissionModel extends Model {
return {
name: this.name,
guard_name: this.guard_name,
service: this.service,
created_at: this.created_at,
updated_at: this.updated_at,
};
@@ -80,6 +96,10 @@ export default class PermissionModel extends Model {
return 'do anything';
}
if (action === 'see') {
return 'Visibly See';
}
return titleize(action);
}
@@ -90,9 +110,9 @@ export default class PermissionModel extends Model {
@computed('actionName', 'name', 'resourceName', 'extensionName') get description() {
let actionName = this.actionName;
let actionPreposition = 'to';
let resourceName = pluralize(humanize(this.resourceName));
let resourceName = pluralize(smartTitleize(this.resourceName));
let resourcePreposition = getPermissionAction(this.name) === '*' && resourceName ? 'with' : '';
let extensionName = humanize(this.extensionName);
let extensionName = smartTitleize(this.extensionName);
let extensionPreposition = 'on';
let descriptionParts = ['Permission', actionPreposition, actionName, resourcePreposition, resourceName, extensionPreposition, extensionName];

View File

@@ -12,6 +12,7 @@ export default class PolicyModel extends Model {
/** @attributes */
@attr('string') name;
@attr('string') type;
@attr('string') service;
@attr('string') guard_name;
@attr('string') description;
@attr('boolean') is_mutable;

View File

@@ -14,6 +14,10 @@ export default class RoleModel extends Model {
@attr('string') name;
@attr('string') guard_name;
@attr('string') description;
@attr('string') service;
@attr('string') type;
@attr('boolean') is_mutable;
@attr('boolean') is_deletable;
/** @dates */
@attr('date') created_at;

View File

@@ -1,4 +1,4 @@
import Model, { attr } from '@ember-data/model';
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
import { computed, get } from '@ember/object';
import { not } from '@ember/object/computed';
import { getOwner } from '@ember/application';
@@ -23,14 +23,20 @@ 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') types;
@attr('raw') meta;
/** @relationships */
@belongsTo('role') role;
@hasMany('policy') policies;
@hasMany('permission') permissions;
/** @dates */
@attr('date') last_seen_at;
@attr('date') phone_verified_at;
@@ -43,7 +49,7 @@ export default class UserModel extends Model {
/** @methods */
deactivate() {
const owner = getOwner(this);
const fetch = owner.lookup(`service:fetch`);
const fetch = owner.lookup('service:fetch');
return fetch.patch(`users/deactivate/${this.id}`).then((response) => {
this.session_status = 'inactive';
@@ -54,7 +60,7 @@ export default class UserModel extends Model {
activate() {
const owner = getOwner(this);
const fetch = owner.lookup(`service:fetch`);
const fetch = owner.lookup('service:fetch');
return fetch.patch(`users/activate/${this.id}`).then((response) => {
this.session_status = 'active';
@@ -65,27 +71,72 @@ export default class UserModel extends Model {
removeFromCurrentCompany() {
const owner = getOwner(this);
const fetch = owner.lookup(`service:fetch`);
const fetch = owner.lookup('service:fetch');
return fetch.delete(`users/remove-from-company/${this.id}`);
}
resendInvite() {
const owner = getOwner(this);
const fetch = owner.lookup(`service:fetch`);
const fetch = owner.lookup('service:fetch');
return fetch.post(`users/resend-invite`, { user: this.id });
}
getPermissions() {
const permissions = [];
// get direct applied permissions
if (this.get('permissions')) {
permissions.pushObjects(this.get('permissions').toArray());
}
// get role permissions and role policies permissions
if (this.get('role')) {
if (this.get('role.permissions')) {
permissions.pushObjects(this.get('role.permissions').toArray());
}
if (this.get('role.policies')) {
for (let i = 0; i < this.get('role.policies').length; i++) {
const policy = this.get('role.policies').objectAt(i);
if (policy.get('permissions')) {
permissions.pushObjects(policy.get('permissions').toArray());
}
}
}
}
// get direct applied policy permissions
if (this.get('policies')) {
for (let i = 0; i < this.get('policies').length; i++) {
const policy = this.get('policies').objectAt(i);
if (policy.get('permissions')) {
permissions.pushObjects(policy.get('permissions').toArray());
}
}
}
return permissions;
}
/** @computed */
@not('isEmailVerified') emailIsNotVerified;
@not('isPhoneVerified') phoneIsNotVerified;
/** @computed */
get allPermissions() {
return this.getPermissions();
}
@computed('meta.two_factor_enabled') get isTwoFactorEnabled() {
return this.meta && this.meta.two_factor_enabled;
}
@computed('is_admin') get isAdmin() {
return this.is_admin === true;
}
@computed('types') get typesList() {
const types = Array.from(this.types);
return types.join(', ');

View File

@@ -7,15 +7,18 @@ 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');
this.route('reset-password', { path: '/reset-password/:id' });
this.route('two-fa');
this.route('verification');
});
this.route('onboard', function () {
this.route('verify-email');
this.route('portal-login', { path: '/portal' });
});
this.route('invite', { path: 'join' }, function () {
this.route('for-driver', { path: '/fleet/:public_id' });
@@ -23,24 +26,23 @@ Router.map(function () {
});
this.route('console', { path: '/' }, function () {
this.route('home', { path: '/' });
this.route('extensions');
this.route('notifications');
this.route('account', function () {
this.route('virtual', { path: '/:slug/:view' });
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');
this.route('cache');
this.route('filesystem');
this.route('mail');
this.route('notification-channels');
this.route('notification-channels', { path: '/push-notifications' });
this.route('queue');
this.route('services');
this.route('socket');
@@ -48,12 +50,16 @@ 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: '/' });
this.route('users', { path: '/:company_id' });
this.route('index', { path: '/' }, function () {
this.route('users', { path: '/:public_id/users' });
});
});
this.route('schedule-monitor', function () {
this.route('logs', { path: '/:id/logs' });
});
});
});
this.route('install');
this.route('catch', { path: '/*' });
});

View File

@@ -1,6 +1,7 @@
import Route from '@ember/routing/route';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import isElectron from '@fleetbase/ember-core/utils/is-electron';
import pathToRoute from '@fleetbase/ember-core/utils/path-to-route';
@@ -11,9 +12,32 @@ export default class ApplicationRoute extends Route {
@service urlSearchParams;
@service modalsManager;
@service intl;
@service currentUser;
@service router;
@service universe;
@tracked defaultTheme;
/**
* The application loading event.
* Here will just run extension hooks.
*
* @memberof ApplicationRoute
*/
@action loading (transition) {
this.universe.callHooks('application:loading', this.session, this.router, transition);
}
/**
* On application route activation
*
* @memberof ApplicationRoute
* @void
*/
@action activate() {
this.initializeTheme();
this.initializeLocale();
}
/**
* Check the installation status of Fleetbase and transition user accordingly.
*
@@ -43,24 +67,27 @@ export default class ApplicationRoute extends Route {
* @return {Transition}
* @memberof ApplicationRoute
*/
async beforeModel() {
async beforeModel(transition) {
await this.session.setup();
await this.universe.booting();
this.universe.callHooks('application:before-model', this.session, this.router, transition);
const { isAuthenticated } = this.session;
const shift = this.urlSearchParams.get('shift');
if (isAuthenticated && shift) {
if (this.session.isAuthenticated && shift) {
return this.router.transitionTo(pathToRoute(shift));
}
}
/**
* On application route activation
* Initializes the application's theme settings, applying necessary class names and default theme configurations.
*
* @memberof ApplicationRoute
* @void
* 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.
*/
activate() {
initializeTheme() {
const bodyClassNames = [];
if (isElectron()) {
@@ -68,7 +95,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]);
}
/**

View File

@@ -4,6 +4,12 @@ import { inject as service } from '@ember/service';
export default class AuthForgotPasswordRoute extends Route {
@service store;
queryParams = {
email: {
refreshModel: false,
},
};
model() {
return this.store.findRecord('brand', 1);
}

View File

@@ -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 });
}
}

View File

@@ -0,0 +1,10 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class CatchRoute extends Route {
@service router;
beforeModel() {
return this.router.transitionTo('auth.login');
}
}

View File

@@ -1,43 +1,14 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import '@fleetbase/leaflet-routing-machine';
export default class ConsoleRoute extends Route {
/**
* Inject the `store` service
*
* @var {Service}
*/
@service store;
/**
* Inject the `fetch` service
*
* @var {Service}
*/
@service fetch;
/**
* Inject the `session` service
*
* @var {Service}
*/
@service session;
/**
* Inject the `intl` service
*
* @var {Service}
*/
@service intl;
/**
* Inject the `currentUser` service
*
* @var {Service}
*/
@service universe;
@service router;
@service currentUser;
@service intl;
/**
* Require authentication to access all `console` routes.
@@ -46,10 +17,14 @@ export default class ConsoleRoute extends Route {
* @return {Promise}
* @memberof ConsoleRoute
*/
@action async beforeModel(transition) {
this.session.requireAuthentication(transition, 'auth.login');
async beforeModel(transition) {
await this.session.requireAuthentication(transition, 'auth.login');
return this.session.promiseCurrentUser(transition);
this.universe.callHooks('console:before-model', this.session, this.router, transition);
if (this.session.isAuthenticated) {
return this.session.promiseCurrentUser(transition);
}
}
/**
@@ -61,54 +36,4 @@ export default class ConsoleRoute extends Route {
model() {
return this.store.findRecord('brand', 1);
}
/**
* We will use this hook to preload engines
*
* @void
*/
@action afterModel() {
this.fetchSessionInfo();
}
/**
* We will use this hook to setup controller and more
*
* @void
*/
@action setupController(controller, model) {
super.setupController(controller, model);
// Get and set user locale
this.fetch.get('users/locale').then(({ locale }) => {
this.intl.setLocale(locale);
});
// Get user organizations
this.fetch.get('auth/organizations').then((organizations) => {
this.currentUser.setOption('organizations', organizations);
controller.organizations = organizations;
});
}
/**
* Use this hook to fetch user related queries
*
* @void
*/
@action fetchSessionInfo() {
this.fetch.shouldResetCache();
this.fetch
.cachedGet(
'lookup/whois',
{},
{
expirationInterval: 60,
expirationIntervalUnit: 'minutes',
}
)
.then((whois) => {
this.currentUser.setOption('whois', whois);
});
}
}

View File

@@ -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);
}
}

View File

@@ -7,8 +7,7 @@ export default class ConsoleAdminRoute extends Route {
@service router;
beforeModel() {
// USER MUST BE ADMIN
if (!this.currentUser.user.is_admin) {
if (!this.currentUser.isAdmin) {
return this.router.transitionTo('console').then(() => {
this.notifications.error('You do not have authorization to access admin!');
});

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -1,6 +1,20 @@
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
export default class UserSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
/**
* Embedded relationship attributes
*
* @var {Object}
*/
get attrs() {
return {
role: { serialize: 'ids', deserialize: 'records' },
policies: { serialize: 'ids', deserialize: 'records' },
permissions: { serialize: 'ids', deserialize: 'records' },
};
}
export default class UserSerializer extends ApplicationSerializer {
/**
* Customize serializer so that the password is never sent to the server via Ember Data
*
@@ -13,6 +27,14 @@ export default class UserSerializer extends ApplicationSerializer {
// 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;
}

View File

@@ -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];
}
}

View File

@@ -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;

View File

@@ -1,4 +1,5 @@
{{page-title (t "app.name")}}
<ModalsContainer />
<NotificationContainer @position="top" @zindex="99999" />
<div id="application-root-wormhole"></div>
{{outlet}}

View File

@@ -15,6 +15,9 @@
{{t "auth.forgot-password.is-sent.message" htmlSafe=true}}
</p>
</div>
<div class="flex flex-row mt-4">
<Button @icon="check" @type="primary" @text={{t "common.continue"}} @onClick={{transition-to "auth.login"}} />
</div>
{{else}}
<div class="flex px-3 py-2 mb-6 rounded-md shadow-sm bg-blue-200">
<div>
@@ -25,18 +28,33 @@
</p>
</div>
<form class="space-y-6" {{on "submit" this.sendSecureLink}}>
<form class="space-y-6" {{on "submit" (perform this.sendSecureLink)}}>
<div>
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-50">
{{t "auth.forgot-password.form.email-label"}}
</label>
<div class="mt-2">
<Input @value={{this.email}} @type="email" id="email" name="email" required class="form-input form-input-lg w-full" placeholder={{t "auth.forgot-password.form.email-label"}} />
<Input
@value={{this.email}}
@type="email"
id="email"
name="email"
required
class="form-input form-input-lg w-full"
placeholder={{t "auth.forgot-password.form.email-label"}}
/>
</div>
</div>
<div class="flex flex-row space-x-2">
<Button @icon="magic" @type="primary" @buttonType="submit" @text={{t "auth.forgot-password.form.submit-button"}} @onClick={{this.sendSecureLink}} @isLoading={{this.isLoading}} />
<Button
@icon="magic"
@type="primary"
@buttonType="submit"
@text={{t "auth.forgot-password.form.submit-button"}}
@onClick={{perform this.sendSecureLink}}
@isLoading={{this.sendSecureLink.isRunning}}
/>
<Button @buttonType="button" @text={{t "auth.forgot-password.form.nevermind-button"}} @onClick={{fn (transition-to "auth.login")}} @disabled={{this.isLoading}} />
</div>
</form>

View File

@@ -1,6 +1,6 @@
<div>
<div class="mx-auto w-12 h-12">
<LogoIcon @url={{@brand.icon_url}} @size="12" class="mx-auto" />
<LogoIcon @size="12" class="mx-auto rounded-sm" />
</div>
<h2 class="mt-6 mb-3 text-3xl font-extrabold leading-9 text-center text-gray-900 dark:text-gray-100">
{{t "auth.login.title"}}
@@ -8,16 +8,24 @@
</div>
{{#if (gte this.failedAttempts 3)}}
<div class="px-3 py-2 my-6 rounded-md shadow-sm bg-yellow-200">
<div class="flex mb-5">
<div>
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-900 mr-4" />
<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>
<p class="flex-1 text-sm text-yellow-900 dark:yellow-red-900">
{{t "auth.login.failed-attempt.message" htmlSafe=true}}
</p>
</div>
<Button @text={{t "auth.login.failed-attempt.button-text"}} @type="warning" @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}}
@@ -65,18 +73,34 @@
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>
</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>
<div class="mt-3">
<Button @text={{t "auth.login.form.create-account-button"}} @wrapperClass="btn-block" @disabled={{this.isLoading}} @onClick={{fn (transition-to "onboard")}} />
<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"}} @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>

View File

@@ -2,7 +2,7 @@
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
<div class="mb-8">
<img class="mx-auto h-12 w-auto " src="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}}>
<img class="mx-auto h-12 w-auto" src="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} />
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "auth.verification.title"}}
</h2>
@@ -13,34 +13,66 @@
<FaIcon @icon="shield-check" @size="lg" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
{{t "auth.verification.message-text" htmlSafe=true}}
{{t "auth.verification.message-text" htmlSafe=true}}
</p>
</div>
<form class="mt-8 space-y-6" {{on "submit" this.verifyCode}}>
<InputGroup @type="tel" @name={{t "auth.verification.verification-input-label"}} @value={{this.code}} @helpText={{t "auth.verification.verification-code-text"}} @inputClass="input-lg" {{on "input" this.validateInput}} {{did-insert this.validateInitInput}} />
<InputGroup
@type="tel"
@name={{t "auth.verification.verification-input-label"}}
@value={{this.code}}
@helpText={{t "auth.verification.verification-code-text"}}
@inputClass="input-lg"
{{on "input" this.validateInput}}
{{did-insert this.validateInitInput}}
/>
<div class="flex flex-row items-center space-x-4">
<Button @icon="check" @iconPrefix="fas" @buttonType="submit" @type="primary" @size="lg" @text="Verify & Continue" @isLoading={{this.isLoading}} @disabled={{this.isNotReadyToSubmit}} @onClick={{this.verifyCode}} />
<a href="#" {{on "click" this.onDidntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "auth.verification.didnt-receive-a-code"}}</a>
<Button
@icon="check"
@iconPrefix="fas"
@buttonType="submit"
@type="primary"
@size="lg"
@text="Verify & Continue"
@isLoading={{this.isLoading}}
@disabled={{this.isNotReadyToSubmit}}
@onClick={{this.verifyCode}}
/>
<a href="javascript:;" {{on "click" this.onDidntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "auth.verification.didnt-receive-a-code"}}</a>
</div>
{{#if this.stillWaiting}}
<div class="bg-yellow-50 rounded shadow-sm border-l-4 border-yellow-400 px-4 py-2">
<div class="flex">
<div class="flex-shrink-0">
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-400" />
<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="ml-3 flex items-center">
<span class="text-lg font-extrabold text-yellow-800">{{t "auth.verification.didnt-receive-a-code"}}</span>
<div class="flex-1">
<div class="flex-1 text-sm text-yellow-100">
<div>{{t "auth.verification.didnt-receive-a-code" htmlSafe=true}}</div>
<div>{{t "auth.verification.not-sent.alternative-choice" htmlSafe=true}}</div>
</div>
</div>
</div>
<div class="py-1">
<p class="text-yellow-700 text-sm">{{t "auth.verification.not-sent.alternative-choice"}}</p>
<div class="flex items-center mt-3">
<Button @buttonType="button" @type="warning" @wrapperClass="mr-2" @onClick={{this.resendEmail}} class="btn-warning-alert">{{t "auth.verification.not-sent.resend-email"}}</Button>
<Button @buttonType="button" @type="warning" @onClick={{this.resendBySms}} class="btn-warning-alert">{{t "auth.verification.not-sent.send-by-sms"}}</Button>
</div>
<div class="flex items-center space-x-2">
<Button
@text={{t "auth.verification.not-sent.resend-email"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.resendEmail}}
/>
<Button
@text={{t "auth.verification.not-sent.send-by-sms"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.resendBySms}}
/>
</div>
</div>
{{/if}}

View File

@@ -0,0 +1,2 @@
{{page-title "Catch"}}
{{outlet}}

View File

@@ -1,6 +1,6 @@
{{page-title (t "app.name")}}
<Layout::Container>
<Layout::Header @brand={{@model}} @user={{this.user}} @organizations={{this.organizations}} @menuItems={{this.universe.headerMenuItems}} @extensions={{this.extensions}} @onAction={{this.onAction}} @showSidebarToggle={{true}} @sidebarToggleEnabled={{this.sidebarToggleEnabled}} @onSidebarToggle={{this.onSidebarToggle}} />
<Layout::Header @brand={{@model}} @menuItems={{this.menuItems}} @organizationMenuItems={{this.organizationMenuItems}} @userMenuItems={{this.userMenuItems}} @onAction={{this.onAction}} @showSidebarToggle={{true}} @sidebarToggleEnabled={{this.sidebarToggleEnabled}} @onSidebarToggle={{this.onSidebarToggle}} />
<Layout::Main>
<Layout::Sidebar @onSetup={{this.setSidebarContext}}>
<div class="next-sidebar-content-inner">
@@ -12,7 +12,11 @@
{{outlet}}
</Layout::Section>
</Layout::Main>
<Layout::MobileNavbar @brand={{@model}} @user={{this.user}} @organizations={{this.organizations}} @menuItems={{this.universe.headerMenuItems}} @extensions={{this.extensions}} @onAction={{this.onAction}} />
<Layout::MobileNavbar @brand={{@model}} @user={{this.user}} @organizations={{this.organizations}} @menuItems={{this.menuItems}} @extensions={{this.extensions}} @onAction={{this.onAction}} />
</Layout::Container>
<ChatContainer />
<ConsoleWormhole />
{{!-- template-lint-disable no-potential-path-strings --}}
<RegistryYield @registry="@fleetbase/console" as |RegistryComponent|>
<RegistryComponent @controller={{this}} />
</RegistryYield>

View File

@@ -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>

View File

@@ -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>

View File

@@ -31,8 +31,10 @@
<Layout::Sidebar::Item @route="console.admin.config.filesystem" @icon="hard-drive">{{t "common.filesystem"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.queue" @icon="layer-group">{{t "common.queue"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.socket" @icon="plug">{{t "common.socket"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.notification-channels" @icon="tower-broadcast">{{t "common.notification-channels"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.config.notification-channels" @icon="tower-broadcast">{{t "common.push-notifications"}}</Layout::Sidebar::Item>
</Layout::Sidebar::Panel>
</EmberWormhole>
{{outlet}}
<Layout::Section::Container>
{{outlet}}
</Layout::Section::Container>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
{{page-title "Notifications"}}
{{page-title "Push Notifications"}}
<Layout::Section::Header @title={{t "console.admin.notifications.title"}}>
<Button @type="primary" @size="sm" @icon="save" @text={{t "common.save-button-text"}} @onClick={{this.saveSettings}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
</Layout::Section::Header>
@@ -7,9 +7,9 @@
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-4">
{{#each-in this.groupedNotifications as |groupName notifications|}}
<ContentPanel @title={{concat (smart-humanize groupName) (t "console.admin.notifications.notification-settings") }} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title={{concat (smart-humanize groupName) " " (t "console.admin.notifications.notification-settings") }} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
{{#each notifications as |notification|}}
<InputGroup @name={{notification.name}} @helpText={{notification.description}}>
<InputGroup @name={{titleize notification.name}} @helpText={{notification.description}}>
<div class="fleetbase-model-select fleetbase-power-select ember-model-select">
<PowerSelectMultiple
@searchEnabled={{true}}

View File

@@ -15,9 +15,8 @@
@paginationMeta={{@model.meta}}
@page={{this.page}}
@onPageChange={{fn (mut this.page)}}
@tfootVerticalOffset="53"
@tfootVerticalOffsetElements=".next-view-section-subheader"
@onRowClick={{this.goToCompany}}
/>
</Layout::Section::Body>
{{outlet}}

View File

@@ -1,5 +1,15 @@
{{page-title (t "common.users")}}
<Overlay @isOpen={{@isOpen}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width="600px" @isResizable={{true}}>
<Overlay
@onLoad={{this.setOverlayContext}}
@onOpen={{this.onOpen}}
@onClose={{this.onClose}}
@onToggle={{this.onToggle}}
@position="right"
@noBackdrop={{true}}
@fullHeight={{true}}
@isResizeble={{true}}
@width="800px"
>
<Overlay::Header @title={{concat this.company.name " - " (t "common.users")}} @hideStatusDot={{true}} @titleWrapperClass="leading-5">
<div class="flex flex-1 justify-end">
<Button @type="default" @icon="times" @helpText={{t "common.close-and-save"}} @onClick={{this.onPressClose}} />
@@ -7,9 +17,17 @@
</Overlay::Header>
<Overlay::Body class="without-padding">
{{!-- template-lint-disable no-unbound --}}
{{! template-lint-disable no-unbound }}
<Layout::Section::Header @title={{t "console.admin.organizations.users.title"}} @searchQuery={{unbound this.nestedQuery}} @onSearch={{this.search}}>
<Pagination @meta={{@model.meta}} @page={{this.nestedPage}} @onPageChange={{fn (mut this.nestedPage)}} @metaInfoClass="hidden" @metaInfoWrapperClass="within-layout-section-header" />
{{#if (gt @model.meta.total this.nestedLimit)}}
<Pagination
@meta={{@model.meta}}
@page={{this.nestedPage}}
@onPageChange={{fn (mut this.nestedPage)}}
@metaInfoClass="hidden"
@metaInfoWrapperClass="within-layout-section-header"
/>
{{/if}}
</Layout::Section::Header>
<Layout::Section::Body>

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -13,7 +13,7 @@
<FaIcon @icon="info-circle" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
{{t "invite.invitation-sent-message" htnmlSafe=true companyName=@model.name appName=(t "app.name")}}
{{t "invite.for-users.invitation-sent-message" htnmlSafe=true companyName=@model.name appName=(t "app.name")}}
</p>
</div>

View File

@@ -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>

View File

@@ -26,21 +26,35 @@
</div>
{{#if this.stillWaiting}}
<div class="bg-yellow-50 rounded shadow-sm border-l-4 border-yellow-400 px-4 py-2">
<div class="flex">
<div class="flex-shrink-0">
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-400" />
<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="ml-3 flex items-center">
<span class="text-lg font-extrabold text-yellow-800">{{t "onboard.verify-email.didnt-receive-a-code"}}</span>
<div class="flex-1">
<div class="flex-1 text-sm text-yellow-100">
<div>{{t "onboard.verify-email.didnt-receive-a-code" htmlSafe=true}}</div>
<div>{{t "onboard.verify-email.not-sent.alternative-choice" htmlSafe=true}}</div>
</div>
</div>
</div>
<div class="py-1">
<p class="text-yellow-700 text-sm">{{t "onboard.verify-email.not-sent.alternative-choice"}}</p>
<div class="flex items-center mt-3">
<Button @buttonType="button" @type="warning" @wrapperClass="mr-2" @onClick={{this.resendEmail}} class="btn-warning-alert">{{t "onboard.verify-email.not-sent.resend-email"}}</Button>
<Button @buttonType="button" @type="warning" @onClick={{this.resendBySms}} class="btn-warning-alert">{{t "onboard.verify-email.not-sent.send-by-sms"}}</Button>
</div>
<div class="flex items-center space-x-2">
<Button
@text={{t "onboard.verify-email.not-sent.resend-email"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.resendEmail}}
/>
<Button
@text={{t "onboard.verify-email.not-sent.send-by-sms"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.resendBySms}}
/>
</div>
</div>
{{/if}}

View File

@@ -0,0 +1,2 @@
{{page-title @model.title}}
{{component @model.component params=@model.componentParams}}

View File

@@ -42,7 +42,7 @@ module.exports = function (environment) {
},
stripe: {
publishableKey: getenv('STRIPE_KEY')
publishableKey: getenv('STRIPE_KEY'),
},
defaultValues: {

View File

@@ -3,9 +3,18 @@ module.exports = function asArray(value) {
return value;
}
if (typeof value === 'string' && value.includes(',')) {
if (typeof value === 'string') {
return value.split(',');
}
try {
let iterable = Array.from(value);
if (Array.isArray(iterable)) {
return iterable;
}
} catch (error) {
return [];
}
return [];
};

View File

@@ -1,6 +1,6 @@
{
"name": "@fleetbase/console",
"version": "0.5.3",
"version": "0.5.10",
"private": true,
"description": "Modular logistics and supply chain operating system (LSOS)",
"repository": "https://github.com/fleetbase/fleetbase",
@@ -21,7 +21,6 @@
"lint:hbs:fix": "ember-template-lint . --fix",
"lint:js": "eslint . --cache",
"lint:js:fix": "eslint . --fix",
"postinstall": "patch-package",
"lint:intl": "fleetbase-intl-lint",
"start": "pnpm run prebuild && ember serve",
"start:dev": "pnpm run prebuild && ember serve --environment development",
@@ -29,23 +28,22 @@
"test:ember": "ember test"
},
"dependencies": {
"@fleetbase/ember-core": "^0.2.14",
"@fleetbase/ember-ui": "^0.2.20",
"@fleetbase/fleetops-engine": "^0.5.5",
"@fleetbase/storefront-engine": "^0.3.13",
"@fleetbase/dev-engine": "^0.2.5",
"@fleetbase/iam-engine": "^0.0.14",
"@fleetbase/registry-bridge-engine": "^0.0.11",
"@fleetbase/fleetops-data": "^0.1.17",
"@ember/legacy-built-in-components": "^0.4.2",
"@fleetbase/dev-engine": "^0.2.7",
"@fleetbase/ember-core": "^0.2.21",
"@fleetbase/ember-ui": "^0.2.34",
"@fleetbase/fleetops-data": "^0.1.18",
"@fleetbase/fleetops-engine": "^0.5.8",
"@fleetbase/iam-engine": "^0.1.1",
"@fleetbase/leaflet-routing-machine": "^3.2.16",
"@ember/legacy-built-in-components": "^0.4.1",
"@fleetbase/registry-bridge-engine": "^0.0.15",
"@fleetbase/storefront-engine": "^0.3.15",
"@fortawesome/ember-fontawesome": "^2.0.0",
"ember-changeset": "^4.1.2",
"ember-changeset-validations": "^4.1.1",
"ember-composable-helpers": "^5.0.0",
"ember-concurrency": "^3.0.0",
"ember-concurrency": "^3.1.1",
"ember-concurrency-decorators": "^2.0.3",
"ember-gridstack": "^4.0.0",
"ember-intl": "6.3.2",
"ember-math-helpers": "^2.18.2",
"ember-power-select": "^7.2.0",
@@ -53,34 +51,32 @@
"ember-radio-button": "3.0.0-beta.1",
"ember-tag-input": "^3.1.0",
"fleetbase-extensions-indexer": "^0.0.5",
"gridstack": "^7.2.2",
"patch-package": "^8.0.0",
"postcss-at-rules-variables": "^0.3.0",
"postcss-custom-properties": "^12.1.9",
"postcss-custom-properties": "^12.1.11",
"postcss-nth-list": "^1.0.2"
},
"devDependencies": {
"@fleetbase/intl-lint": "^0.0.1",
"@babel/core": "^7.23.2",
"@babel/eslint-parser": "^7.22.15",
"@babel/plugin-proposal-decorators": "^7.23.2",
"@ember/optional-features": "^2.0.0",
"@babel/core": "^7.25.2",
"@babel/eslint-parser": "^7.25.1",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@ember/optional-features": "^2.1.0",
"@ember/string": "^3.1.1",
"@ember/test-helpers": "^3.2.0",
"@ember/test-helpers": "^3.3.1",
"@fleetbase/intl-lint": "^0.0.1",
"@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-brands-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2",
"@tailwindcss/forms": "^0.5.3",
"autoprefixer": "^10.4.8",
"@tailwindcss/forms": "^0.5.7",
"autoprefixer": "^10.4.20",
"broccoli-asset-rev": "^3.0.0",
"broccoli-funnel": "^3.0.8",
"concurrently": "^8.2.2",
"date-fns": "^2.30.0",
"dragula": "^3.7.3",
"ember-auto-import": "^2.6.3",
"ember-cli": "~5.4.1",
"ember-auto-import": "^2.7.4",
"ember-cli": "~5.4.2",
"ember-cli-app-version": "^6.0.1",
"ember-cli-babel": "^8.2.0",
"ember-cli-clean-css": "^3.0.0",
@@ -92,48 +88,46 @@
"ember-cli-sri": "^2.1.1",
"ember-cli-string-helpers": "^6.1.0",
"ember-cli-terser": "^4.0.2",
"ember-data": "^4.12.5",
"ember-engines": "^0.8.23",
"ember-data": "^4.12.8",
"ember-engines": "^0.9.0",
"ember-fetch": "^8.1.2",
"ember-leaflet": "^5.1.1",
"ember-load-initializers": "^2.1.2",
"ember-modifier": "^4.1.0",
"ember-page-title": "^8.0.0",
"ember-qunit": "^8.0.1",
"ember-modifier": "^4.2.0",
"ember-page-title": "^8.2.3",
"ember-qunit": "^8.1.0",
"ember-resolver": "^11.0.1",
"ember-responsive": "^5.0.0",
"ember-source": "~5.4.0",
"ember-template-lint": "^5.11.2",
"ember-source": "~5.4.1",
"ember-template-lint": "^5.13.0",
"ember-wormhole": "^0.6.0",
"eslint": "^8.52.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-ember": "^11.11.1",
"eslint-plugin-n": "^16.2.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-qunit": "^8.0.1",
"fast-glob": "^3.3.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-ember": "^11.12.0",
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-qunit": "^8.1.2",
"fast-glob": "^3.3.2",
"fs": "0.0.1-security",
"inter-ui": "^3.19.3",
"leaflet": "^1.9.4",
"loader.js": "^4.7.0",
"normalize.css": "^8.0.1",
"postcss": "^8.4.21",
"postcss": "^8.4.41",
"postcss-conditionals-renewed": "^1.0.0",
"postcss-each": "^1.1.0",
"postcss-import": "14.1.0",
"postcss-mixins": "^9.0.4",
"postcss-preset-env": "^7.8.2",
"postcss-simple-vars": "^7.0.0",
"prettier": "^3.0.3",
"qunit": "^2.20.0",
"postcss-preset-env": "^7.8.3",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.3.3",
"qunit": "^2.22.0",
"qunit-dom": "^2.0.0",
"recast": "^0.23.3",
"recast": "^0.23.9",
"stylelint": "^15.11.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-prettier": "^4.0.2",
"tailwindcss": "^3.1.8",
"stylelint-prettier": "^4.1.0",
"tailwindcss": "^3.4.10",
"tracked-built-ins": "^3.3.0",
"webpack": "^5.89.0"
"webpack": "^5.94.0"
},
"engines": {
"node": ">= 18"
@@ -143,9 +137,9 @@
},
"pnpm": {
"overrides": {
"@fleetbase/ember-core": "^0.2.14",
"@fleetbase/ember-ui": "^0.2.20",
"@fleetbase/fleetops-data": "^0.1.17"
"@fleetbase/ember-core": "^0.2.21",
"@fleetbase/ember-ui": "^0.2.34",
"@fleetbase/fleetops-data": "^0.1.18"
}
},
"prettier": {

View File

@@ -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();
}
}

5369
console/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -78,6 +78,8 @@ 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 });
@@ -92,10 +94,9 @@ function getRouterFileContents() {
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;
@@ -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)),
]),
])
)
);
}
});
}
}

View File

@@ -7,15 +7,18 @@ 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');
this.route('reset-password', { path: '/reset-password/:id' });
this.route('two-fa');
this.route('verification');
});
this.route('onboard', function () {
this.route('verify-email');
this.route('portal-login', { path: '/portal' });
});
this.route('invite', { path: 'join' }, function () {
this.route('for-driver', { path: '/fleet/:public_id' });
@@ -25,21 +28,21 @@ Router.map(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');
this.route('cache');
this.route('filesystem');
this.route('mail');
this.route('notification-channels');
this.route('notification-channels', { path: '/push-notifications' });
this.route('queue');
this.route('services');
this.route('socket');
@@ -47,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' });
@@ -58,5 +61,6 @@ Router.map(function () {
});
});
});
this.route('install');
this.route('catch', { path: '/*' });
});

View File

@@ -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');
});
});

View File

@@ -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');
});
});

View File

@@ -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');
});
});

View File

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

View File

@@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
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:console/admin/virtual');
assert.ok(controller);
});
});

View File

@@ -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);
});
});

View File

@@ -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/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/virtual');
assert.ok(controller);
});
});

View File

@@ -1,12 +1,12 @@
import Application from '@ember/application';
import config from '@fleetbase/console/config/environment';
import { initialize } from '@fleetbase/console/instance-initializers/register-app-version';
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 | register-app-version', function (hooks) {
module('Unit | Instance Initializer | load-leaflet', function (hooks) {
hooks.beforeEach(function () {
this.TestApplication = class TestApplication extends Application {
modulePrefix = config.modulePrefix;

View File

@@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Route | catch', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup('route:catch');
assert.ok(route);
});
});

View File

@@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Route | virtual', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup('route:virtual');
assert.ok(route);
});
});

View 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: سجل التغييرات

View File

@@ -68,6 +68,12 @@ common:
export: Export
reload: Reload
reload-data: Reload data
unauthorized: Unauthorized
unauthorized-to: Unauthorized to
unauthorized-access: Unauthorized Access
unauthorized-access-message: Unauthorized Access, you must request permissions to access.
permissions-required-for-changes: You do not have the required permissions to make changes.
push-notifications: Push Notifications
component:
file:
dropdown-label: File actions
@@ -179,6 +185,7 @@ auth:
no-identity-notification: Did you forget to enter your email?
no-password-notification: Did you forget to enter your password?
unverified-notification: Your account needs to be verified to proceed.
password-reset-required: A password reset is required to continue.
failed-attempt:
message: <strong>Forgot your password?</strong><br> Click the button below to reset your password.
button-text: Ok, help me reset!
@@ -246,7 +253,7 @@ console:
mail:
title: Mail Configuration
notification-channels:
title: Notification Channels Configuration
title: Push Notifications Configuration
queue:
title: Queue Configuration
services:

Some files were not shown because too many files have changed in this diff Show More