mirror of
https://github.com/fleetbase/fleetbase.git
synced 2026-01-10 16:29:07 +00:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8ac2a3796 | ||
|
|
b78c59ad89 | ||
|
|
bd71b1921b | ||
|
|
2596ccbded | ||
|
|
81159b7411 | ||
|
|
acc4cfba35 | ||
|
|
ea47bdc09d | ||
|
|
c60c460257 | ||
|
|
dc00ac3892 | ||
|
|
f18ec886a7 | ||
|
|
f039b61d79 | ||
|
|
248f70e31c | ||
|
|
6bc76a1b33 | ||
|
|
30695b3ebe | ||
|
|
7ff9c24ad5 | ||
|
|
1a9b9c06e5 | ||
|
|
5f949c3b7f | ||
|
|
211a3a9808 | ||
|
|
0e5e4e07dd | ||
|
|
2eabfc4698 | ||
|
|
77c2c01e58 | ||
|
|
7c5b5b5858 | ||
|
|
aad072cf4c | ||
|
|
c1c6dcafd8 | ||
|
|
61992ee924 | ||
|
|
b18d6197bc | ||
|
|
04bdb52c08 | ||
|
|
8e5a45dd09 | ||
|
|
196af155ae | ||
|
|
056a717d08 | ||
|
|
fd008d7f73 |
10
.github/workflows/cd.yml
vendored
10
.github/workflows/cd.yml
vendored
@@ -132,25 +132,29 @@ 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
|
||||
if: startsWith(github.ref, 'refs/heads/deploy/qa')
|
||||
run: |
|
||||
echo "STRIPE_KEY=${{ secrets.STRIPE_TEST_KEY }}" >> ./environments/.env.production
|
||||
echo "EXTENSIONS=@fleetbase/billing-engine,@fleetbase/internals-engine"
|
||||
echo "EXTENSIONS=@fleetbase/billing-engine,@fleetbase/internals-engine" >> ./environments/.env.production
|
||||
working-directory: ./console
|
||||
|
||||
- name: Set Env Variables for Production
|
||||
if: startsWith(github.ref, 'refs/heads/deploy/production')
|
||||
run: |
|
||||
echo "STRIPE_KEY=${{ secrets.STRIPE_KEY }}" >> ./environments/.env.production
|
||||
echo "EXTENSIONS=@fleetbase/billing-engine,@fleetbase/internals-engine"
|
||||
echo "EXTENSIONS=@fleetbase/billing-engine,@fleetbase/internals-engine" >> ./environments/.env.production
|
||||
working-directory: ./console
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,6 +30,7 @@ packages/flespi
|
||||
packages/loconav
|
||||
packages/internals
|
||||
packages/projectargus-engine
|
||||
packages/customer-portal
|
||||
# wip
|
||||
packages/solid
|
||||
solid
|
||||
|
||||
37
README.md
37
README.md
@@ -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 you’re 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 you’re 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 you’re 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 you’re 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 you’re 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 you’re deploying with docker then it’s 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 Fleetbase’s 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 Fleetbase’s first official extension for WMS & Inventory.
|
||||
2. **Accounting and Invoicing** ~ Ledger will be Fleetbase’s 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
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"fleetbase/core-api": "^1.5.0",
|
||||
"fleetbase/fleetops-api": "^0.5.5",
|
||||
"fleetbase/registry-bridge": "^0.0.9",
|
||||
"fleetbase/storefront-api": "^0.3.13",
|
||||
"fleetbase/core-api": "^1.5.9",
|
||||
"fleetbase/fleetops-api": "^0.5.8",
|
||||
"fleetbase/registry-bridge": "^0.0.14",
|
||||
"fleetbase/storefront-api": "^0.3.15",
|
||||
"guzzlehttp/guzzle": "^7.0.1",
|
||||
"laravel/framework": "^10.0",
|
||||
"laravel/octane": "^2.3",
|
||||
|
||||
1409
api/composer.lock
generated
1409
api/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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'),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<div class="fleetbase-dashboard-grid flex items-center justify-between mb-4 mt-6 px-14">
|
||||
<div class="left-section">
|
||||
<h1 class="text-lg font-bold">{{this.dashboard.currentDashboard.name}}</h1>
|
||||
</div>
|
||||
<div class="fleetbase-dashboard-actions right-section ml-4 flex items-center">
|
||||
<div class="fleetbase-model-select fleetbase-power-select ember-model-select h-10">
|
||||
|
||||
<DropdownButton
|
||||
class="h-10"
|
||||
@text={{if this.dashboard.currentDashboard.name this.dashboard.currentDashboard.name (t "component.dashboard.select-dashboard")}}
|
||||
@textClass="text-sm mr-2"
|
||||
@buttonClass="flex-row-reverse w-44 justify-between"
|
||||
@icon="caret-down"
|
||||
@iconClass="mr-0i"
|
||||
@size="sm"
|
||||
@iconPrefix="fas"
|
||||
@triggerClass="hidden md:flex"
|
||||
as |dd|
|
||||
>
|
||||
<div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
|
||||
<div class="p-1">
|
||||
{{#each this.dashboard.dashboards as |dashboard|}}
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.selectDashboard dashboard)}}>
|
||||
<div class="flex-1 flex flex-row items-center">
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="desktop" />
|
||||
</div>
|
||||
<span>{{dashboard.name}}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{#if (eq this.dashboard.currentDashboard.id dashboard.id)}}
|
||||
<FaIcon @icon="check" class="text-green-500" />
|
||||
{{/if}}
|
||||
</div>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
|
||||
<div class="ml-2 relative h-10">
|
||||
<DropdownButton class="h-10" @icon="ellipsis-h" @size="sm" @iconPrefix="fas" @triggerClass="hidden md:flex" as |dd|>
|
||||
<div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
|
||||
<div class="p-1">
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.createDashboard)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="add" />
|
||||
</div>
|
||||
<span>{{t "component.dashboard.create-new-dashboard"}}</span>
|
||||
</a>
|
||||
|
||||
{{#unless (eq this.dashboard.currentDashboard.user_uuid "system")}}
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.onChangeEdit true)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="edit" />
|
||||
</div>
|
||||
<span>{{t "component.dashboard.edit-layout"}}</span>
|
||||
</a>
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.onAddingWidget true)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="add" />
|
||||
</div>
|
||||
<span>{{t "component.dashboard.add-widgets"}}</span>
|
||||
</a>
|
||||
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.deleteDashboard this.dashboard.currentDashboard)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="trash" />
|
||||
</div>
|
||||
<span>{{t "component.dashboard.delete-dashboard"}}</span>
|
||||
</a>
|
||||
{{/unless}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
{{#if this.dashboard.isEditingDashboard}}
|
||||
<div class="ml-2 h-10">
|
||||
<Button @type="magic" @icon="save" @helpText={{t "component.dashboard.save-dashboard"}} @onClick={{fn this.onChangeEdit false}} class="h-10" />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-10">
|
||||
<Dashboard::Create @isEdit={{this.dashboard.isEditingDashboard}} @isAddingWidget={{this.dashboard.isAddingWidget}} @dashboard={{this.dashboard.currentDashboard}} />
|
||||
{{#if this.dashboard.isAddingWidget}}
|
||||
<EmberWormhole @to="console-home-wormhole">
|
||||
<Dashboard::WidgetPanel
|
||||
@isOpen={{this.dashboard.isAddingWidget}}
|
||||
@onLoad={{this.setWidgetSelectorPanelContext}}
|
||||
@dashboard={{this.dashboard.currentDashboard}}
|
||||
@onClose={{fn this.onAddingWidget false}}
|
||||
/>
|
||||
</EmberWormhole>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -1,140 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* DashboardComponent for managing dashboards in an Ember application.
|
||||
* This component handles actions such as selecting, creating, deleting dashboards,
|
||||
* and managing widget selectors and dashboard editing states.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class DashboardComponent extends Component {
|
||||
/**
|
||||
* Ember Data store service.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service store;
|
||||
|
||||
/**
|
||||
* Internationalization service for managing translations.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service intl;
|
||||
|
||||
/**
|
||||
* Notifications service for displaying alerts or confirmations.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Modals manager service for handling modal dialogs.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service modalsManager;
|
||||
|
||||
/**
|
||||
* Fetch service for handling HTTP requests.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* Dashboard service for business logic related to dashboards.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service dashboard;
|
||||
|
||||
/**
|
||||
* Creates an instance of DashboardComponent.
|
||||
* @memberof DashboardComponent
|
||||
*/
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.dashboard.loadDashboards.perform();
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to select a dashboard.
|
||||
* @param {Object} dashboard - The dashboard to be selected.
|
||||
*/
|
||||
@action selectDashboard(dashboard) {
|
||||
this.dashboard.selectDashboard.perform(dashboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the context for the widget selector panel.
|
||||
* @param {Object} widgetSelectorContext - The context object for the widget selector.
|
||||
*/
|
||||
@action setWidgetSelectorPanelContext(widgetSelectorContext) {
|
||||
this.widgetSelectorContext = widgetSelectorContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new dashboard.
|
||||
* @param {Object} dashboard - The dashboard to be created.
|
||||
* @param {Object} [options={}] - Optional parameters for dashboard creation.
|
||||
*/
|
||||
@action createDashboard(dashboard, options = {}) {
|
||||
this.modalsManager.show('modals/create-dashboard', {
|
||||
title: this.intl.t('component.dashboard.create-a-new-dashboard'),
|
||||
acceptButtonText: this.intl.t('component.dashboard.confirm-create-dashboard'),
|
||||
confirm: async (modal, done) => {
|
||||
modal.startLoading();
|
||||
|
||||
// Get the name from the modal options
|
||||
const { name } = modal.getOptions();
|
||||
|
||||
await this.dashboard.createDashboard.perform(name);
|
||||
done();
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a dashboard.
|
||||
* @param {Object} dashboard - The dashboard to be deleted.
|
||||
* @param {Object} [options={}] - Optional parameters for dashboard deletion.
|
||||
*/
|
||||
@action deleteDashboard(dashboard, options = {}) {
|
||||
if (this.dashboard.dashboards?.length === 1) {
|
||||
return this.notifications.error(this.intl.t('component.dashboard.you-cannot-delete-this-dashboard'));
|
||||
}
|
||||
|
||||
this.modalsManager.confirm({
|
||||
title: this.intl.t('component.dashboard.are-you-sure-you-want-delete-dashboard', { dashboardName: dashboard.name }),
|
||||
confirm: async (modal, done) => {
|
||||
modal.startLoading();
|
||||
await this.dashboard.deleteDashboard.perform(dashboard);
|
||||
done();
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to handle the addition of a widget.
|
||||
* @param {boolean} [state=true] - The state to set for adding a widget.
|
||||
*/
|
||||
@action onAddingWidget(state = true) {
|
||||
this.dashboard.onAddingWidget(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current dashboard.
|
||||
* @param {Object} dashboard - The dashboard to be set as current.
|
||||
*/
|
||||
@action setCurrentDashboard(dashboard) {
|
||||
this.dashboard.setCurrentDashboard.perform(dashboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the editing state of the dashboard.
|
||||
* @param {boolean} [state=true] - The state to set for editing the dashboard.
|
||||
*/
|
||||
@action onChangeEdit(state = true) {
|
||||
this.dashboard.onChangeEdit(state);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -53,6 +53,13 @@ export default class AuthResetPasswordController extends Controller {
|
||||
*/
|
||||
@tracked password_confirmation;
|
||||
|
||||
/**
|
||||
* Query parameters.
|
||||
*
|
||||
* @memberof AuthResetPasswordController
|
||||
*/
|
||||
queryParams = ['code'];
|
||||
|
||||
/**
|
||||
* The reset password task.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,32 +12,9 @@ import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
|
||||
* @extends Controller
|
||||
*/
|
||||
export default class ConsoleAccountAuthController extends Controller {
|
||||
/**
|
||||
* Service for handling data fetching.
|
||||
*
|
||||
* @type {fetch}
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* Service for displaying notifications.
|
||||
*
|
||||
* @type {notifications}
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Service for managing application routing.
|
||||
*
|
||||
* @type {router}
|
||||
*/
|
||||
@service router;
|
||||
|
||||
/**
|
||||
* Service for managing modals.
|
||||
*
|
||||
* @type {router}
|
||||
*/
|
||||
@service modalsManager;
|
||||
|
||||
/**
|
||||
@@ -187,14 +164,12 @@ export default class ConsoleAccountAuthController extends Controller {
|
||||
* @param {Object} twoFaSettings - User-specific two-factor authentication settings.
|
||||
*/
|
||||
@task *saveUserTwoFaSettings(twoFaSettings = {}) {
|
||||
yield this.fetch
|
||||
.post('users/two-fa', { twoFaSettings })
|
||||
.then(() => {
|
||||
this.notifications.success('2FA Settings saved successfully.');
|
||||
})
|
||||
.catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
try {
|
||||
yield this.fetch.post('users/two-fa', { twoFaSettings });
|
||||
this.notifications.success('2FA Settings saved successfully.');
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,13 +178,17 @@ export default class ConsoleAccountAuthController extends Controller {
|
||||
* @method loadUserTwoFaSettings
|
||||
*/
|
||||
@task *loadUserTwoFaSettings() {
|
||||
const twoFaSettings = yield this.fetch.get('users/two-fa');
|
||||
try {
|
||||
const twoFaSettings = yield this.fetch.get('users/two-fa');
|
||||
if (twoFaSettings) {
|
||||
this.isUserTwoFaEnabled = twoFaSettings.enabled;
|
||||
this.twoFaSettings = twoFaSettings;
|
||||
}
|
||||
|
||||
if (twoFaSettings) {
|
||||
this.isUserTwoFaEnabled = twoFaSettings.enabled;
|
||||
this.twoFaSettings = twoFaSettings;
|
||||
return twoFaSettings;
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
return twoFaSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,12 +197,16 @@ export default class ConsoleAccountAuthController extends Controller {
|
||||
* @method loadSystemTwoFaConfig
|
||||
*/
|
||||
@task *loadSystemTwoFaConfig() {
|
||||
const twoFaConfig = yield this.fetch.get('two-fa/config');
|
||||
try {
|
||||
const twoFaConfig = yield this.fetch.get('two-fa/config');
|
||||
if (twoFaConfig) {
|
||||
this.isSystemTwoFaEnabled = twoFaConfig.enabled;
|
||||
this.twoFaConfig = twoFaConfig;
|
||||
}
|
||||
|
||||
if (twoFaConfig) {
|
||||
this.isSystemTwoFaEnabled = twoFaConfig.enabled;
|
||||
this.twoFaConfig = twoFaConfig;
|
||||
return twoFaConfig;
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
return twoFaConfig;
|
||||
}
|
||||
}
|
||||
|
||||
7
console/app/controllers/console/account/virtual.js
Normal file
7
console/app/controllers/console/account/virtual.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class ConsoleAccountVirtualController extends Controller {
|
||||
@tracked view;
|
||||
queryParams = ['view'];
|
||||
}
|
||||
7
console/app/controllers/console/admin/virtual.js
Normal file
7
console/app/controllers/console/admin/virtual.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class ConsoleAdminVirtualController extends Controller {
|
||||
@tracked view;
|
||||
queryParams = ['view'];
|
||||
}
|
||||
@@ -1,3 +1,26 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ConsoleHomeController extends Controller {}
|
||||
export default class ConsoleHomeController extends Controller {
|
||||
rows = [
|
||||
{
|
||||
name: 'Jason',
|
||||
age: 24,
|
||||
vehicle: 'Honda',
|
||||
},
|
||||
];
|
||||
|
||||
columns = [
|
||||
{
|
||||
label: 'Name',
|
||||
valuePath: 'name',
|
||||
},
|
||||
{
|
||||
label: 'Age',
|
||||
valuePath: 'age',
|
||||
},
|
||||
{
|
||||
label: 'Vehicle',
|
||||
valuePath: 'vehicle',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
7
console/app/controllers/console/settings/virtual.js
Normal file
7
console/app/controllers/console/settings/virtual.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class ConsoleSettingsVirtualController extends Controller {
|
||||
@tracked view;
|
||||
queryParams = ['view'];
|
||||
}
|
||||
7
console/app/controllers/console/virtual.js
Normal file
7
console/app/controllers/console/virtual.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class ConsoleVirtualController extends Controller {
|
||||
@tracked view;
|
||||
queryParams = ['view'];
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export default helper(function spreadWidgetOptions([params]) {
|
||||
const { id, options } = params;
|
||||
const gridOptions = { id, ...options };
|
||||
return gridOptions;
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
export function initialize (owner) {
|
||||
export function initialize(owner) {
|
||||
const universe = owner.lookup('service:universe');
|
||||
if (universe) {
|
||||
universe.createRegistries(['@fleetbase/console', 'auth:login']);
|
||||
universe.bootEngines(owner);
|
||||
}
|
||||
}
|
||||
|
||||
18
console/app/instance-initializers/load-leaflet.js
Normal file
18
console/app/instance-initializers/load-leaflet.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export function initialize(owner) {
|
||||
const leafletService = owner.lookup('service:leaflet');
|
||||
if (leafletService) {
|
||||
leafletService.load({
|
||||
onReady: function (L) {
|
||||
// This will prevent the awkward scroll bug produced by Chrome browsers
|
||||
// https://github.com/Leaflet/Leaflet/issues/4125#issuecomment-356289643
|
||||
L.Control.include({
|
||||
_refocusOnMap: L.Util.falseFn,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(', ');
|
||||
|
||||
@@ -11,7 +11,9 @@ export default class ApplicationRoute extends Route {
|
||||
@service urlSearchParams;
|
||||
@service modalsManager;
|
||||
@service intl;
|
||||
@service currentUser;
|
||||
@service router;
|
||||
@service universe;
|
||||
@tracked defaultTheme;
|
||||
|
||||
/**
|
||||
@@ -45,11 +47,10 @@ export default class ApplicationRoute extends Route {
|
||||
*/
|
||||
async beforeModel() {
|
||||
await this.session.setup();
|
||||
await this.universe.booting();
|
||||
|
||||
const { isAuthenticated } = this.session;
|
||||
const shift = this.urlSearchParams.get('shift');
|
||||
|
||||
if (isAuthenticated && shift) {
|
||||
if (this.session.isAuthenticated && shift) {
|
||||
return this.router.transitionTo(pathToRoute(shift));
|
||||
}
|
||||
}
|
||||
@@ -61,6 +62,19 @@ export default class ApplicationRoute extends Route {
|
||||
* @void
|
||||
*/
|
||||
activate() {
|
||||
this.initializeTheme();
|
||||
this.initializeLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the application's theme settings, applying necessary class names and default theme configurations.
|
||||
*
|
||||
* This method prepares the theme by setting up an array of class names that should be applied to the
|
||||
* application's body element. If the application is running inside an Electron environment, it adds the
|
||||
* `'is-electron'` class to the array. It then calls the `initialize` method of the `theme` service,
|
||||
* passing in the `bodyClassNames` array and the `defaultTheme` configuration.
|
||||
*/
|
||||
initializeTheme() {
|
||||
const bodyClassNames = [];
|
||||
|
||||
if (isElectron()) {
|
||||
@@ -68,7 +82,18 @@ export default class ApplicationRoute extends Route {
|
||||
}
|
||||
|
||||
this.theme.initialize({ bodyClassNames, theme: this.defaultTheme });
|
||||
this.intl.setLocale(['en-us']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the application's locale settings based on the current user's preferences.
|
||||
*
|
||||
* This method retrieves the user's preferred locale using the `getOption` method from the `currentUser` service.
|
||||
* If no locale is set by the user, it defaults to `'en-us'`. It then sets the application's locale by calling
|
||||
* the `setLocale` method of the `intl` service with the retrieved locale.
|
||||
*/
|
||||
initializeLocale() {
|
||||
const locale = this.currentUser.getOption('locale', 'en-us');
|
||||
this.intl.setLocale([locale]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { inject as service } from '@ember/service';
|
||||
|
||||
export default class AuthLoginRoute extends Route {
|
||||
@service session;
|
||||
@service universe;
|
||||
|
||||
/**
|
||||
* If user is authentication redirect to console.
|
||||
@@ -10,7 +11,8 @@ export default class AuthLoginRoute extends Route {
|
||||
* @memberof AuthLoginRoute
|
||||
* @void
|
||||
*/
|
||||
beforeModel() {
|
||||
beforeModel(transition) {
|
||||
this.session.prohibitAuthentication('console');
|
||||
return this.universe.virtualRouteRedirect(transition, 'auth:login', 'virtual', { restoreQueryParams: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!');
|
||||
});
|
||||
|
||||
@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
|
||||
export default class ConsoleAdminVirtualRoute extends Route {
|
||||
@service universe;
|
||||
|
||||
model({ slug, view }) {
|
||||
return this.universe.lookupMenuItemFromRegistry('admin', slug, view);
|
||||
queryParams = {
|
||||
view: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console:admin', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
|
||||
export default class ConsoleSettingsVirtualRoute extends Route {
|
||||
@service universe;
|
||||
|
||||
model({ slug, view }) {
|
||||
return this.universe.lookupMenuItemFromRegistry('settings', slug, view);
|
||||
queryParams = {
|
||||
view: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console:settings', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
|
||||
export default class ConsoleVirtualRoute extends Route {
|
||||
@service universe;
|
||||
|
||||
model({ slug, view }) {
|
||||
queryParams = {
|
||||
view: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
17
console/app/routes/virtual.js
Normal file
17
console/app/routes/virtual.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class VirtualRoute extends Route {
|
||||
@service universe;
|
||||
|
||||
queryParams = {
|
||||
view: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('auth:login', slug, view);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
import Service from '@ember/service';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
import { action } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
|
||||
/**
|
||||
* Service for managing dashboards, including loading, creating, and deleting dashboards, as well as managing the current dashboard and widget states.
|
||||
* Utilizes Ember services such as `store`, `fetch`, `notifications`, and `universe` for data management and user interaction.
|
||||
*
|
||||
* @extends Service
|
||||
*/
|
||||
export default class DashboardService extends Service {
|
||||
/**
|
||||
* Ember Data store service for managing model data.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service store;
|
||||
|
||||
/**
|
||||
* Fetch service for making network requests.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* Notifications service for displaying user notifications.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Universe service for accessing global application state or utility methods.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service universe;
|
||||
|
||||
/**
|
||||
* Internationalization service.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service intl;
|
||||
|
||||
/**
|
||||
* Tracked array of available dashboards.
|
||||
* @type {Array}
|
||||
*/
|
||||
@tracked dashboards = [];
|
||||
|
||||
/**
|
||||
* Tracked property representing the currently selected dashboard.
|
||||
* @type {Object}
|
||||
*/
|
||||
@tracked currentDashboard;
|
||||
|
||||
/**
|
||||
* Tracked boolean indicating if the dashboard is in editing mode.
|
||||
* @type {boolean}
|
||||
*/
|
||||
@tracked isEditingDashboard = false;
|
||||
|
||||
/**
|
||||
* Tracked boolean indicating if a widget is being added.
|
||||
* @type {boolean}
|
||||
*/
|
||||
@tracked isAddingWidget = false;
|
||||
|
||||
/**
|
||||
* Task for loading dashboards from the store. It sets the current dashboard and checks if adding widget is necessary.
|
||||
*/
|
||||
@task *loadDashboards() {
|
||||
const dashboards = yield this.store.findAll('dashboard');
|
||||
|
||||
if (isArray(dashboards)) {
|
||||
this.dashboards = dashboards.toArray();
|
||||
|
||||
// insert default dashboard if it's not loaded
|
||||
const defaultDashboard = this._createDefaultDashboard();
|
||||
if (this._isDefaultDashboardNotLoaded()) {
|
||||
this.dashboards.unshiftObject(defaultDashboard);
|
||||
}
|
||||
|
||||
// Set the current dashboard
|
||||
this.currentDashboard = this._getNextDashboard();
|
||||
if (this.currentDashboard && this.currentDashboard.widgets.length === 0) {
|
||||
this.onAddingWidget(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for selecting a dashboard. Handles dashboard switching and updates the current dashboard.
|
||||
* @param {Object} dashboard - The dashboard object to select.
|
||||
*/
|
||||
@task *selectDashboard(dashboard) {
|
||||
if (dashboard.user_uuid === 'system') {
|
||||
this.currentDashboard = dashboard;
|
||||
yield this.fetch.post('dashboards/reset-default');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDashboard = yield this.fetch.post('dashboards/switch', { dashboard_uuid: dashboard.id }, { normalizeToEmberData: true }).catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
|
||||
if (currentDashboard) {
|
||||
this.currentDashboard = currentDashboard;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for creating a new dashboard. It handles dashboard creation, success notification, and dashboard selection.
|
||||
* @param {string} name - Name of the new dashboard.
|
||||
*/
|
||||
@task *createDashboard(name) {
|
||||
const dashboardRecord = this.store.createRecord('dashboard', { name, is_default: true });
|
||||
const dashboard = yield dashboardRecord.save().catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
|
||||
if (dashboard) {
|
||||
this.notifications.success(this.intl.t('services.dashboard-service.create-dashboard-success-notification', { dashboardName: dashboard.name }));
|
||||
this.selectDashboard.perform(dashboard);
|
||||
this.dashboards.pushObject(dashboard);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for deleting a dashboard. Handles dashboard deletion and success notification.
|
||||
* @param {Object} dashboard - The dashboard object to delete.
|
||||
* @param {Object} [options={}] - Optional configuration options.
|
||||
*/
|
||||
@task *deleteDashboard(dashboard, options = {}) {
|
||||
yield dashboard.destroyRecord().catch((error) => {
|
||||
this.notification.serverError(error);
|
||||
|
||||
if (typeof options.onError === 'function') {
|
||||
options.onError(error, dashboard);
|
||||
}
|
||||
});
|
||||
|
||||
this.notifications.success(this.intl.t('services.dashboard-service.delete-dashboard-success-notification', { dashboardName: dashboard.name }));
|
||||
yield this.loadDashboards.perform();
|
||||
yield this.selectDashboard.perform(this._getNextDashboard());
|
||||
|
||||
if (typeof options.callback === 'function') {
|
||||
options.callback(this.currentDashboard);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for setting the current dashboard.
|
||||
* @param {Object} dashboard - The dashboard object to set as current.
|
||||
*/
|
||||
@task *setCurrentDashboard(dashboard) {
|
||||
const currentDashboard = yield this.fetch.post('dashboards/switch', { dashboard_uuid: dashboard.id }, { normalizeToEmberData: true }).catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
|
||||
if (currentDashboard) {
|
||||
this.currentDashboard = currentDashboard;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to toggle dashboard editing state.
|
||||
* @param {boolean} [state=true] - State to set for editing.
|
||||
*/
|
||||
@action onChangeEdit(state = true) {
|
||||
this.isEditingDashboard = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to toggle the state of adding a widget.
|
||||
* @param {boolean} [state=true] - State to set for adding a widget.
|
||||
*/
|
||||
@action onAddingWidget(state = true) {
|
||||
this.isAddingWidget = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default dashboard with predefined widgets.
|
||||
* @private
|
||||
* @returns {Object} The default dashboard object.
|
||||
*/
|
||||
_createDefaultDashboard() {
|
||||
let defaultDashboard;
|
||||
|
||||
// check store for default dashboard
|
||||
const loadedDashboars = this.store.peekAll('dashboard');
|
||||
|
||||
// check for default dashboard loaded in store
|
||||
defaultDashboard = loadedDashboars.find((dashboard) => dashboard.id === 'system');
|
||||
if (defaultDashboard) {
|
||||
return defaultDashboard;
|
||||
}
|
||||
|
||||
// create new default dashboard
|
||||
defaultDashboard = this.store.createRecord('dashboard', {
|
||||
id: 'system',
|
||||
uuid: 'system',
|
||||
name: 'Default Dashboard',
|
||||
is_default: false,
|
||||
user_uuid: 'system',
|
||||
widgets: this._createDefaultDashboardWidgets(),
|
||||
});
|
||||
|
||||
return defaultDashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates default widgets for the default dashboard.
|
||||
* @private
|
||||
* @returns {Array} An array of default dashboard widgets.
|
||||
*/
|
||||
_createDefaultDashboardWidgets() {
|
||||
const widgets = this.universe.getDefaultDashboardWidgets().map((defaultWidget) => {
|
||||
return this.store.createRecord('dashboard-widget', defaultWidget);
|
||||
});
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if default dashboard is already loaded.
|
||||
* @private
|
||||
* @return {Boolean}
|
||||
* @memberof DashboardService
|
||||
*/
|
||||
_isDefaultDashboardLoaded() {
|
||||
const defaultDashboard = this._createDefaultDashboard();
|
||||
return this.dashboards.some((dashboard) => dashboard.id === defaultDashboard.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if default dashboard is not already loaded.
|
||||
* @private
|
||||
* @return {Boolean}
|
||||
* @memberof DashboardService
|
||||
*/
|
||||
_isDefaultDashboardNotLoaded() {
|
||||
return !this._isDefaultDashboardLoaded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current dasbhoard or next available dashboard.
|
||||
*
|
||||
* @return {DashboardModel}
|
||||
* @memberof DashboardService
|
||||
*/
|
||||
_getNextDashboard() {
|
||||
return this.dashboards.find((dashboard) => dashboard.is_default) || this.dashboards[0];
|
||||
}
|
||||
}
|
||||
@@ -13,16 +13,6 @@ body[data-theme='dark'] .two-fa-enforcement-alert button#two-fa-setup-button.btn
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.app-version-in-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-top: 0.2rem;
|
||||
}
|
||||
|
||||
.fleetbase-pagination-meta-info-wrapper.within-layout-section-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{page-title (t "app.name")}}
|
||||
<ModalsContainer />
|
||||
<NotificationContainer @position="top" @zindex="99999" />
|
||||
<div id="application-root-wormhole"></div>
|
||||
{{outlet}}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Layout::Section::Header @title="Account Auth" />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<ContentPanel @title="Change Password" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<form id="change-password-form" aria-label="change-password" {{on "submit" (perform this.changePassword)}}>
|
||||
@@ -31,4 +31,5 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="500px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{@model.title}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-">
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -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>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{t "console.admin.config.database.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<Configure::Database />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{t "console.admin.config.filesystem.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<Configure::Filesystem />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{t "console.admin.config.mail.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<Configure::Mail />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{t "console.admin.config.notification-channels.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<Configure::NotificationChannels />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{t "console.admin.config.queue.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<Configure::Queue />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{t "console.admin.config.services.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 900}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<Configure::Services />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{t "console.admin.config.socket.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 900}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<Configure::Socket />
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,7 +2,7 @@
|
||||
<Layout::Section::Header @title={{t "common.overview"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<div class="grid grid-cols-3 xs:grid-cols-1 gap-4">
|
||||
<StatWidget @title={{t "console.admin.index.total-users"}} @value={{@model.total_users}} />
|
||||
@@ -11,4 +11,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -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}}
|
||||
|
||||
@@ -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}}
|
||||
@@ -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>
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{@model.title}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-">
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -1,7 +1,7 @@
|
||||
{{page-title "Dashboard"}}
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<TwoFaEnforcementAlert />
|
||||
<Dashboard @sidebar={{this.sidebarContext}} />
|
||||
<Dashboard @sidebar={{this.sidebarContext}} class="flex items-center justify-between mb-4 mt-6 px-14" />
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
<div id="console-home-wormhole" />
|
||||
@@ -1,7 +1,7 @@
|
||||
<Layout::Section::Header @title={{t "console.settings.index.title"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<ContentPanel @title={{t "console.settings.index.title"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<form {{on "submit" this.saveSettings}}>
|
||||
@@ -65,4 +65,5 @@
|
||||
</ContentPanel>
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="500px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,7 +2,7 @@
|
||||
<Layout::Section::Header @title="2FA" />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<ContentPanel @title="2FA Settings" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<div class="mb-3">
|
||||
@@ -33,4 +33,5 @@
|
||||
</ContentPanel>
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{@model.title}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-">
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,9 +2,10 @@
|
||||
<Layout::Section::Header @title={{@model.title}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-">
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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}}
|
||||
|
||||
2
console/app/templates/virtual.hbs
Normal file
2
console/app/templates/virtual.hbs
Normal file
@@ -0,0 +1,2 @@
|
||||
{{page-title @model.title}}
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
@@ -30,8 +30,8 @@ module.exports = function (environment) {
|
||||
},
|
||||
|
||||
osrm: {
|
||||
host: getenv('OSRM_HOST', 'https://bundle.routing.fleetbase.io'),
|
||||
servers: getenv('OSRM_SERVERS', '').split(',').filter(Boolean),
|
||||
host: getenv('OSRM_HOST', 'https://router.project-osrm.org'),
|
||||
servers: {},
|
||||
},
|
||||
|
||||
socket: {
|
||||
@@ -42,7 +42,7 @@ module.exports = function (environment) {
|
||||
},
|
||||
|
||||
stripe: {
|
||||
publishableKey: getenv('STRIPE_KEY')
|
||||
publishableKey: getenv('STRIPE_KEY'),
|
||||
},
|
||||
|
||||
defaultValues: {
|
||||
@@ -74,6 +74,12 @@ module.exports = function (environment) {
|
||||
autoClear: true,
|
||||
clearDuration: 1000 * 3.5,
|
||||
},
|
||||
|
||||
'ember-leaflet': {
|
||||
excludeCSS: true,
|
||||
excludeJS: true,
|
||||
excludeImages: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (environment === 'development') {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module.exports = function () {
|
||||
return {
|
||||
'free-solid-svg-icons': 'all',
|
||||
'free-brands-svg-icons': 'all',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 [];
|
||||
};
|
||||
|
||||
@@ -4,5 +4,4 @@ SOCKETCLUSTER_PATH=/socketcluster/
|
||||
SOCKETCLUSTER_HOST=localhost
|
||||
SOCKETCLUSTER_SECURE=false
|
||||
SOCKETCLUSTER_PORT=38000
|
||||
OSRM_HOST=https://bundle.routing.fleetbase.io
|
||||
OSRM_SERVERS=https://canada.routing.fleetbase.io,https://us.routing.fleetbase.io
|
||||
OSRM_HOST=https://router.project-osrm.org
|
||||
@@ -5,5 +5,4 @@ SOCKETCLUSTER_PATH=/socketcluster/
|
||||
SOCKETCLUSTER_HOST=
|
||||
SOCKETCLUSTER_SECURE=true
|
||||
SOCKETCLUSTER_PORT=38000
|
||||
OSRM_HOST=https://bundle.routing.fleetbase.io
|
||||
OSRM_SERVERS=https://canada.routing.fleetbase.io,https://us.routing.fleetbase.io
|
||||
OSRM_HOST=https://router.project-osrm.org
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fleetbase/console",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.8",
|
||||
"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.19",
|
||||
"@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.9",
|
||||
"@fleetbase/fleetops-data": "^0.1.17",
|
||||
"@fleetbase/ember-core": "^0.2.19",
|
||||
"@fleetbase/ember-ui": "^0.2.32",
|
||||
"@fleetbase/fleetops-engine": "^0.5.8",
|
||||
"@fleetbase/storefront-engine": "^0.3.15",
|
||||
"@fleetbase/dev-engine": "^0.2.7",
|
||||
"@fleetbase/iam-engine": "^0.1.1",
|
||||
"@fleetbase/registry-bridge-engine": "^0.0.14",
|
||||
"@fleetbase/fleetops-data": "^0.1.18",
|
||||
"@fleetbase/leaflet-routing-machine": "^3.2.16",
|
||||
"@ember/legacy-built-in-components": "^0.4.1",
|
||||
"@ember/legacy-built-in-components": "^0.4.2",
|
||||
"@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,12 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@fleetbase/ember-core": "^0.2.14",
|
||||
"@fleetbase/ember-ui": "^0.2.19",
|
||||
"@fleetbase/fleetops-data": "^0.1.17"
|
||||
"@fleetbase/ember-core": "^0.2.19",
|
||||
"@fleetbase/ember-ui": "^0.2.32",
|
||||
"@fleetbase/fleetops-data": "^0.1.18"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"ember-leaflet@5.1.3": "patches/ember-leaflet@5.1.3.patch"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
95
console/patches/ember-leaflet@5.1.3.patch
Normal file
95
console/patches/ember-leaflet@5.1.3.patch
Normal file
@@ -0,0 +1,95 @@
|
||||
diff --git a/index.js b/index.js
|
||||
index 7361871adfe0b79c70a3699e38cc435f3741cb33..e02f7e6d61605d8b770ed5c0454b6c905d538ff9 100644
|
||||
--- a/index.js
|
||||
+++ b/index.js
|
||||
@@ -1,87 +1,5 @@
|
||||
-'use strict';
|
||||
-const resolve = require('resolve');
|
||||
-const path = require('path');
|
||||
-const mergeTrees = require('broccoli-merge-trees');
|
||||
-const Funnel = require('broccoli-funnel');
|
||||
-const fastbootTransform = require('fastboot-transform');
|
||||
+'use strict'
|
||||
|
||||
module.exports = {
|
||||
- name: require('./package').name,
|
||||
-
|
||||
- treeForVendor() {
|
||||
- let dist = path.join(this.pathBase('leaflet'), 'dist');
|
||||
-
|
||||
- let leafletJs = fastbootTransform(
|
||||
- new Funnel(dist, {
|
||||
- files: ['leaflet-src.js'],
|
||||
- destDir: 'leaflet'
|
||||
- })
|
||||
- );
|
||||
-
|
||||
- let leafletFiles = new Funnel(dist, {
|
||||
- exclude: ['leaflet.js', 'leaflet-src.js', '*.html'],
|
||||
- destDir: 'leaflet'
|
||||
- });
|
||||
-
|
||||
- return mergeTrees([leafletJs, leafletFiles]);
|
||||
- },
|
||||
-
|
||||
- included(app) {
|
||||
- this._super.included.apply(this, arguments);
|
||||
-
|
||||
- // Addon options from the apps ember-cli-build.js
|
||||
- let options = app.options[this.name] || {};
|
||||
-
|
||||
- // If the addon has the _findHost() method (in ember-cli >= 2.7.0), we'll just
|
||||
- // use that.
|
||||
- // if (typeof this._findHost === 'function') {
|
||||
- // app = this._findHost();
|
||||
- // }
|
||||
-
|
||||
- // Otherwise, we'll use this implementation borrowed from the _findHost()
|
||||
- // method in ember-cli.
|
||||
- // Keep iterating upward until we don't have a grandparent.
|
||||
- // Has to do this grandparent check because at some point we hit the project.
|
||||
- let current = this;
|
||||
- do {
|
||||
- if (current.lazyLoading === true || (current.lazyLoading && current.lazyLoading.enabled === true)) {
|
||||
- app = current;
|
||||
- break;
|
||||
- }
|
||||
- app = current.app || app;
|
||||
- } while (current.parent.parent && (current = current.parent));
|
||||
-
|
||||
- if (!options.excludeJS) {
|
||||
- app.import('vendor/leaflet/leaflet-src.js');
|
||||
- }
|
||||
-
|
||||
- // Import leaflet css
|
||||
- if (!options.excludeCSS) {
|
||||
- app.import('vendor/leaflet/leaflet.css');
|
||||
- }
|
||||
-
|
||||
- // Import leaflet images
|
||||
- if (!options.excludeImages) {
|
||||
- let imagesDestDir = '/assets/images';
|
||||
- app.import('vendor/leaflet/images/layers-2x.png', {
|
||||
- destDir: imagesDestDir
|
||||
- });
|
||||
- app.import('vendor/leaflet/images/layers.png', {
|
||||
- destDir: imagesDestDir
|
||||
- });
|
||||
- app.import('vendor/leaflet/images/marker-icon-2x.png', {
|
||||
- destDir: imagesDestDir
|
||||
- });
|
||||
- app.import('vendor/leaflet/images/marker-icon.png', {
|
||||
- destDir: imagesDestDir
|
||||
- });
|
||||
- app.import('vendor/leaflet/images/marker-shadow.png', {
|
||||
- destDir: imagesDestDir
|
||||
- });
|
||||
- }
|
||||
- },
|
||||
-
|
||||
- pathBase(packageName) {
|
||||
- return path.dirname(resolve.sync(packageName + '/package.json', { basedir: __dirname }));
|
||||
- }
|
||||
-};
|
||||
+ name: require('./package').name
|
||||
+}
|
||||
4954
console/pnpm-lock.yaml
generated
4954
console/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ const recast = require('recast');
|
||||
const babelParser = require('recast/parsers/babel');
|
||||
const builders = recast.types.builders;
|
||||
|
||||
function getExtensionMountPath(extensionName) {
|
||||
function getExtensionMountPath (extensionName) {
|
||||
let extensionNameSegments = extensionName.split('/');
|
||||
let mountName = extensionNameSegments[1];
|
||||
|
||||
@@ -16,7 +16,7 @@ function getExtensionMountPath(extensionName) {
|
||||
return mountName.replace('-engine', '');
|
||||
}
|
||||
|
||||
function only(subject, props = []) {
|
||||
function only (subject, props = []) {
|
||||
const keys = Object.keys(subject);
|
||||
const result = {};
|
||||
|
||||
@@ -31,13 +31,13 @@ function only(subject, props = []) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function getExtensions() {
|
||||
function getExtensions () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const extensions = [];
|
||||
const seenPackages = new Set();
|
||||
|
||||
return fg(['node_modules/*/package.json', 'node_modules/*/*/package.json'])
|
||||
.then((results) => {
|
||||
.then(results => {
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const packagePath = results[i];
|
||||
const packageJson = fs.readFileSync(packagePath);
|
||||
@@ -69,7 +69,7 @@ function getExtensions() {
|
||||
});
|
||||
}
|
||||
|
||||
function getRouterFileContents() {
|
||||
function getRouterFileContents () {
|
||||
const routerFilePath = path.join(__dirname, 'router.map.js');
|
||||
const routerFileContents = fs.readFileSync(routerFilePath, 'utf-8');
|
||||
|
||||
@@ -78,24 +78,25 @@ function getRouterFileContents() {
|
||||
|
||||
(async () => {
|
||||
const extensions = await getExtensions();
|
||||
const consoleExtensions = extensions.filter(extension => !extension.fleetbase || extension.fleetbase.mount !== 'root');
|
||||
const rootExtensions = extensions.filter(extension => extension.fleetbase && extension.fleetbase.mount === 'root');
|
||||
const routerFileContents = getRouterFileContents();
|
||||
const ast = recast.parse(routerFileContents, { parser: babelParser });
|
||||
|
||||
recast.visit(ast, {
|
||||
visitCallExpression(path) {
|
||||
visitCallExpression (path) {
|
||||
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'route' && path.value.arguments[0].value === 'console') {
|
||||
let functionExpression;
|
||||
|
||||
// Find the function expression
|
||||
path.value.arguments.forEach((arg) => {
|
||||
path.value.arguments.forEach(arg => {
|
||||
if (arg.type === 'FunctionExpression') {
|
||||
functionExpression = arg;
|
||||
}
|
||||
});
|
||||
|
||||
if (functionExpression) {
|
||||
// Check and add the new engine mounts
|
||||
extensions.forEach((extension) => {
|
||||
consoleExtensions.forEach(extension => {
|
||||
const mountPath = getExtensionMountPath(extension.name);
|
||||
let route = mountPath;
|
||||
|
||||
@@ -104,7 +105,7 @@ function getRouterFileContents() {
|
||||
}
|
||||
|
||||
// Check if engine is already mounted
|
||||
const isMounted = functionExpression.body.body.some((expressionStatement) => {
|
||||
const isMounted = functionExpression.body.body.some(expressionStatement => {
|
||||
return expressionStatement.expression.arguments[0].value === extension.name;
|
||||
});
|
||||
|
||||
@@ -123,8 +124,46 @@ function getRouterFileContents() {
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
// console.log(path.value.callee.property.name);
|
||||
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'map') {
|
||||
let functionExpression;
|
||||
|
||||
path.value.arguments.forEach(arg => {
|
||||
if (arg.type === 'FunctionExpression') {
|
||||
functionExpression = arg;
|
||||
}
|
||||
});
|
||||
|
||||
if (functionExpression) {
|
||||
rootExtensions.forEach(extension => {
|
||||
const mountPath = getExtensionMountPath(extension.name);
|
||||
let route = mountPath;
|
||||
|
||||
if (extension.fleetbase && extension.fleetbase.route) {
|
||||
route = extension.fleetbase.route;
|
||||
}
|
||||
|
||||
const isMounted = functionExpression.body.body.some(expressionStatement => {
|
||||
return expressionStatement.expression.arguments[0].value === extension.name;
|
||||
});
|
||||
|
||||
if (!isMounted) {
|
||||
functionExpression.body.body.push(
|
||||
builders.expressionStatement(
|
||||
builders.callExpression(builders.memberExpression(builders.thisExpression(), builders.identifier('mount')), [
|
||||
builders.literal(extension.name),
|
||||
builders.objectExpression([
|
||||
builders.property('init', builders.identifier('as'), builders.literal(route)),
|
||||
builders.property('init', builders.identifier('path'), builders.literal(route)),
|
||||
]),
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,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,4 @@ Router.map(function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
this.route('install');
|
||||
});
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | dashboard', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.set('myAction', function(val) { ... });
|
||||
|
||||
await render(hbs`<Dashboard />`);
|
||||
|
||||
assert.dom(this.element).hasText('');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
<Dashboard>
|
||||
template block text
|
||||
</Dashboard>
|
||||
`);
|
||||
|
||||
assert.dom(this.element).hasText('template block text');
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | dashboard/create', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.set('myAction', function(val) { ... });
|
||||
|
||||
await render(hbs`<Dashboard::Create />`);
|
||||
|
||||
assert.dom(this.element).hasText('');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
<Dashboard::Create>
|
||||
template block text
|
||||
</Dashboard::Create>
|
||||
`);
|
||||
|
||||
assert.dom(this.element).hasText('template block text');
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Helper | spread-widget-options', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it renders', async function (assert) {
|
||||
this.set('inputValue', '1234');
|
||||
|
||||
await render(hbs`{{spread-widget-options this.inputValue}}`);
|
||||
|
||||
assert.dom().hasText('1234');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
12
console/tests/unit/controllers/console/admin/virtual-test.js
Normal file
12
console/tests/unit/controllers/console/admin/virtual-test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Controller | console/settings/virtual', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:console/settings/virtual');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | 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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
11
console/tests/unit/routes/virtual-test.js
Normal file
11
console/tests/unit/routes/virtual-test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
364
console/translations/ar-ae.yml
Normal file
364
console/translations/ar-ae.yml
Normal file
@@ -0,0 +1,364 @@
|
||||
app:
|
||||
name: Fleetbase
|
||||
terms:
|
||||
new: جديد
|
||||
sort: ترتيب
|
||||
filter: تصفية
|
||||
columns: أعمدة
|
||||
settings: إعدادات
|
||||
home: الصفحة الرئيسية
|
||||
admin: مشرف
|
||||
logout: تسجيل الخروج
|
||||
dashboard: لوحة القيادة
|
||||
search: بحث
|
||||
search-input: إدخال البحث
|
||||
common:
|
||||
confirm: تأكيد
|
||||
edit: تحرير
|
||||
save: حفظ
|
||||
save-changes: حفظ التغييرات
|
||||
cancel: إلغاء
|
||||
2fa-config: تكوين المصادقة الثنائية
|
||||
account: حساب
|
||||
admin: مشرف
|
||||
branding: العلامة التجارية
|
||||
columns: أعمدة
|
||||
dashboard: لوحة القيادة
|
||||
date-of-birth: تاريخ الميلاد
|
||||
delete: حذف
|
||||
email: البريد الإلكتروني
|
||||
filesystem: نظام الملفات
|
||||
filter: تصفية
|
||||
home: الصفحة الرئيسية
|
||||
logout: تسجيل الخروج
|
||||
mail: بريد
|
||||
name: اسم
|
||||
new: جديد
|
||||
notification-channels: قنوات الإشعارات
|
||||
notifications: إشعارات
|
||||
organization: منظمة
|
||||
organizations: منظمات
|
||||
overview: نظرة عامة
|
||||
phone: رقم هاتفك
|
||||
profile: الملف الشخصي
|
||||
queue: قائمة الانتظار
|
||||
save-button-text: حفظ التغييرات
|
||||
search-input: إدخال البحث
|
||||
search: بحث
|
||||
services: خدمات
|
||||
settings: إعدادات
|
||||
socket: مقبس
|
||||
sort: ترتيب
|
||||
two-factor: المصادقة الثنائية
|
||||
uploading: جارٍ التحميل...
|
||||
your-profile: ملفك الشخصي
|
||||
created-at: تم الإنشاء في
|
||||
country: البلد
|
||||
phone-number: الهاتف
|
||||
status: الحالة
|
||||
close-and-save: إغلاق وحفظ
|
||||
users: المستخدمون
|
||||
changelog: سجل التغييرات
|
||||
ok: موافق
|
||||
select-file: اختر ملف
|
||||
back: رجوع
|
||||
next: التالي
|
||||
continue: متابعة
|
||||
done: تم
|
||||
export: تصدير
|
||||
reload: إعادة تحميل
|
||||
reload-data: إعادة تحميل البيانات
|
||||
unauthorized: غير مصرح
|
||||
unauthorized-to: غير مصرح لـ
|
||||
unauthorized-access: وصول غير مصرح
|
||||
unauthorized-access-message: وصول غير مصرح، يجب عليك طلب الأذونات للوصول.
|
||||
permissions-required-for-changes: ليس لديك الأذونات المطلوبة لإجراء التغييرات.
|
||||
push-notifications: إشعارات الدفع
|
||||
component:
|
||||
file:
|
||||
dropdown-label: إجراءات الملف
|
||||
import-modal:
|
||||
loading-message: جارٍ معالجة الاستيراد...
|
||||
drop-upload: إسقاط للتحميل
|
||||
invalid: غير صالح
|
||||
ready-upload: جاهز للتحميل.
|
||||
upload-spreadsheets: تحميل جداول البيانات
|
||||
drag-drop: اسحب وأفلت ملفات جداول البيانات في منطقة الإسقاط هذه
|
||||
button-text: أو اختر جداول البيانات للتحميل
|
||||
spreadsheets: جداول البيانات
|
||||
upload-queue: قائمة انتظار التحميل
|
||||
dropzone:
|
||||
file: ملف
|
||||
drop-to-upload: إسقاط للتحميل
|
||||
invalid: غير صالح
|
||||
files-ready-for-upload: >-
|
||||
{numOfFiles} جاهز للتحميل.
|
||||
upload-images-videos: تحميل الصور ومقاطع الفيديو
|
||||
upload-documents: تحميل المستندات
|
||||
upload-documents-files: تحميل المستندات والملفات
|
||||
upload-avatar-files: تحميل الصور الرمزية المخصصة
|
||||
dropzone-supported-images-videos: اسحب وأفلت ملفات الصور والفيديو في منطقة الإسقاط هذه
|
||||
dropzone-supported-avatars: اسحب وأفلت ملفات SVG أو PNG
|
||||
dropzone-supported-files: اسحب وأفلت الملفات في منطقة الإسقاط هذه
|
||||
or-select-button-text: أو اختر الملفات للتحميل.
|
||||
upload-queue: قائمة انتظار التحميل
|
||||
uploading: جارٍ التحميل...
|
||||
two-fa-enforcement-alert:
|
||||
message: لتعزيز أمان حسابك، تتطلب مؤسستك المصادقة الثنائية (2FA). قم بتمكين المصادقة الثنائية في إعدادات حسابك للحصول على طبقة إضافية من الحماية.
|
||||
button-text: إعداد المصادقة الثنائية
|
||||
comment-thread:
|
||||
publish-comment-button-text: نشر التعليق
|
||||
publish-reply-button-text: نشر الرد
|
||||
reply-comment-button-text: رد
|
||||
edit-comment-button-text: تحرير
|
||||
delete-comment-button-text: حذف
|
||||
comment-published-ago: >-
|
||||
{createdAgo} منذ
|
||||
comment-input-placeholder: أدخل تعليقًا جديدًا...
|
||||
comment-reply-placeholder: أدخل ردك...
|
||||
comment-input-empty-notification: لا يمكنك نشر تعليقات فارغة...
|
||||
comment-min-length-notification: يجب أن يكون التعليق على الأقل 2 حرف
|
||||
dashboard:
|
||||
select-dashboard: اختر لوحة القيادة
|
||||
create-new-dashboard: إنشاء لوحة قيادة جديدة
|
||||
create-a-new-dashboard: إنشاء لوحة قيادة جديدة
|
||||
confirm-create-dashboard: إنشاء لوحة القيادة!
|
||||
edit-layout: تحرير التخطيط
|
||||
add-widgets: إضافة عناصر واجهة
|
||||
delete-dashboard: حذف لوحة القيادة
|
||||
save-dashboard: حفظ لوحة القيادة
|
||||
you-cannot-delete-this-dashboard: لا يمكنك حذف هذه اللوحة.
|
||||
are-you-sure-you-want-delete-dashboard: هل أنت متأكد من حذف {dashboardName}؟
|
||||
dashboard-widget-panel:
|
||||
widget-name: >-
|
||||
عنصر واجهة {widgetName}
|
||||
select-widgets: اختر عناصر الواجهة
|
||||
close-and-save: إغلاق وحفظ
|
||||
services:
|
||||
dashboard-service:
|
||||
create-dashboard-success-notification: تم إنشاء لوحة القيادة الجديدة `{dashboardName}` بنجاح.
|
||||
delete-dashboard-success-notification: تم حذف لوحة القيادة `{dashboardName}`.
|
||||
auth:
|
||||
verification:
|
||||
header-title: التحقق من الحساب
|
||||
title: تحقق من عنوان بريدك الإلكتروني
|
||||
message-text: <strong>اقتربت من الانتهاء!</strong><br> تحقق من بريدك الإلكتروني للحصول على رمز التحقق.
|
||||
verification-code-text: أدخل رمز التحقق الذي تلقيته عبر البريد الإلكتروني.
|
||||
verification-input-label: رمز التحقق
|
||||
verify-button-text: تحقق واستمر
|
||||
didnt-receive-a-code: لم تتلق رمزًا بعد؟
|
||||
not-sent:
|
||||
message: لم تتلق رمزًا بعد؟
|
||||
alternative-choice: استخدم الخيارات البديلة أدناه للتحقق من حسابك.
|
||||
resend-email: إعادة إرسال البريد الإلكتروني
|
||||
send-by-sms: إرسال عبر الرسائل القصيرة
|
||||
two-fa:
|
||||
verify-code:
|
||||
verification-code: رمز التحقق
|
||||
check-title: تحقق من بريدك الإلكتروني أو هاتفك
|
||||
check-subtitle: لقد أرسلنا لك رمز تحقق. أدخل الرمز أدناه لإكمال عملية تسجيل الدخول.
|
||||
expired-help-text: انتهت صلاحية رمز المصادقة الثنائية الخاص بك. يمكنك طلب رمز آخر إذا كنت بحاجة إلى مزيد من الوقت.
|
||||
resend-code: إعادة إرسال الرمز
|
||||
verify-code: تحقق من الرمز
|
||||
cancel-two-factor: إلغاء المصادقة الثنائية
|
||||
invalid-session-error-notification: جلسة غير صالحة. حاول مرة أخرى.
|
||||
verification-successful-notification: تم التحقق بنجاح!
|
||||
verification-code-expired-notification: انتهت صلاحية رمز التحقق. يرجى طلب رمز جديد.
|
||||
verification-code-failed-notification: فشل التحقق. حاول مرة أخرى.
|
||||
resend-code:
|
||||
verification-code-resent-notification: تم إرسال رمز التحقق الجديد.
|
||||
verification-code-resent-error-notification: خطأ في إعادة إرسال رمز التحقق. حاول مرة أخرى.
|
||||
forgot-password:
|
||||
success-message: تحقق من بريدك الإلكتروني للمتابعة!
|
||||
is-sent:
|
||||
title: اقتربت من الانتهاء!
|
||||
message: <strong>تحقق من بريدك الإلكتروني!</strong><br> لقد أرسلنا لك رابطًا سحريًا إلى بريدك الإلكتروني سيسمح لك بإعادة تعيين كلمة المرور الخاصة بك. ينتهي صلاحية الرابط في 15 دقيقة.
|
||||
not-sent:
|
||||
title: هل نسيت كلمة المرور؟
|
||||
message: <strong>لا تقلق، نحن هنا لمساعدتك.</strong><br> أدخل البريد الإلكتروني الذي تستخدمه لتسجيل الدخول إلى {appName} وسنرسل لك رابطًا آمنًا لإعادة تعيين كلمة المرور الخاصة بك.
|
||||
form:
|
||||
email-label: عنوان بريدك الإلكتروني
|
||||
submit-button: حسنًا، أرسل لي رابطًا سحريًا!
|
||||
nevermind-button: لا تهتم
|
||||
login:
|
||||
title: تسجيل الدخول إلى حسابك
|
||||
no-identity-notification: هل نسيت إدخال بريدك الإلكتروني؟
|
||||
no-password-notification: هل نسيت إدخال كلمة المرور الخاصة بك؟
|
||||
unverified-notification: يجب التحقق من حسابك للمتابعة.
|
||||
password-reset-required: مطلوب إعادة تعيين كلمة المرور للمتابعة.
|
||||
failed-attempt:
|
||||
message: <strong>هل نسيت كلمة المرور الخاصة بك؟</strong><br> انقر على الزر أدناه لإعادة تعيين كلمة المرور الخاصة بك.
|
||||
button-text: حسنًا، ساعدني في إعادة التعيين!
|
||||
form:
|
||||
email-label: عنوان البريد الإلكتروني
|
||||
password-label: كلمة المرور
|
||||
remember-me-label: تذكرني
|
||||
forgot-password-label: هل نسيت كلمة المرور الخاصة بك؟
|
||||
sign-in-button: تسجيل الدخول
|
||||
create-account-button: إنشاء حساب جديد
|
||||
slow-connection-message: تواجه مشكلات في الاتصال.
|
||||
reset-password:
|
||||
success-message: تم إعادة تعيين كلمة المرور الخاصة بك! تسجيل الدخول للمتابعة.
|
||||
invalid-verification-code: هذا الرابط لإعادة تعيين كلمة المرور غير صالح أو منتهي الصلاحية.
|
||||
title: إعادة تعيين كلمة المرور الخاصة بك
|
||||
form:
|
||||
code:
|
||||
label: رمز إعادة التعيين الخاص بك
|
||||
help-text: رمز التحقق الذي تلقيته في بريدك الإلكتروني.
|
||||
password:
|
||||
label: كلمة مرور جديدة
|
||||
help-text: أدخل كلمة مرور لا تقل عن 6 أحرف للمتابعة.
|
||||
confirm-password:
|
||||
label: تأكيد كلمة المرور الجديدة
|
||||
help-text: أدخل كلمة مرور لا تقل عن 6 أحرف للمتابعة.
|
||||
submit-button: إعادة تعيين كلمة المرور
|
||||
back-button: رجوع
|
||||
console:
|
||||
create-or-join-organization:
|
||||
modal-title: إنشاء أو الانضمام إلى منظمة
|
||||
join-success-notification: لقد انضممت إلى منظمة جديدة!
|
||||
create-success-notification: لقد أنشأت منظمة جديدة!
|
||||
switch-organization:
|
||||
modal-title: هل أنت متأكد أنك تريد التبديل إلى المنظمة {organizationName}؟
|
||||
modal-body: من خلال التأكيد، سيظل حسابك مسجلاً الدخول، ولكن سيتم تبديل المنظمة الأساسية الخاصة بك.
|
||||
modal-accept-button-text: نعم، أريد التبديل إلى المنظمة
|
||||
success-notification: لقد قمت بتبديل المنظمات
|
||||
account:
|
||||
index:
|
||||
upload-new: تحميل جديد
|
||||
phone: رقم هاتفك.
|
||||
photos: الصور
|
||||
admin:
|
||||
schedule-monitor:
|
||||
schedule-monitor: مراقب الجدول
|
||||
task-logs-for: >-
|
||||
سجلات المهام لـ:
|
||||
showing-last-count: عرض آخر {count} سجلات
|
||||
name: الاسم
|
||||
type: النوع
|
||||
timezone: المنطقة الزمنية
|
||||
last-started: آخر بدء
|
||||
last-finished: آخر انتهاء
|
||||
last-failure: آخر فشل
|
||||
date: التاريخ
|
||||
memory: الذاكرة
|
||||
runtime: وقت التشغيل
|
||||
output: المخرجات
|
||||
no-output: لا توجد مخرجات
|
||||
config:
|
||||
database:
|
||||
title: تكوين قاعدة البيانات
|
||||
filesystem:
|
||||
title: تكوين نظام الملفات
|
||||
mail:
|
||||
title: تكوين البريد
|
||||
notification-channels:
|
||||
title: تكوين إشعارات الدفع
|
||||
queue:
|
||||
title: تكوين قائمة الانتظار
|
||||
services:
|
||||
title: تكوين الخدمات
|
||||
socket:
|
||||
title: تكوين المقبس
|
||||
branding:
|
||||
title: العلامة التجارية
|
||||
icon-text: أيقونة
|
||||
upload-new: تحميل جديد
|
||||
reset-default: إعادة التعيين إلى الافتراضي
|
||||
logo-text: شعار
|
||||
theme: السمة الافتراضية
|
||||
index:
|
||||
total-users: إجمالي المستخدمين
|
||||
total-organizations: إجمالي المنظمات
|
||||
total-transactions: إجمالي المعاملات
|
||||
notifications:
|
||||
title: الإشعارات
|
||||
notification-settings: إعدادات الإشعارات
|
||||
organizations:
|
||||
index:
|
||||
title: المنظمات
|
||||
owner-name-column: المالك
|
||||
owner-phone-column: هاتف المالك
|
||||
owner-email-column: بريد المالك
|
||||
users-count-column: المستخدمون
|
||||
phone-column: الهاتف
|
||||
email-column: البريد الإلكتروني
|
||||
users:
|
||||
title: المستخدمون
|
||||
settings:
|
||||
index:
|
||||
title: إعدادات المنظمة
|
||||
organization-name: اسم المنظمة
|
||||
organization-description: وصف المنظمة
|
||||
organization-phone: رقم هاتف المنظمة
|
||||
organization-currency: عملة المنظمة
|
||||
organization-id: معرف المنظمة
|
||||
organization-branding: العلامة التجارية للمنظمة
|
||||
logo: الشعار
|
||||
logo-help-text: شعار منظمتك.
|
||||
upload-new-logo: تحميل شعار جديد
|
||||
backdrop: الخلفية
|
||||
backdrop-help-text: لافتة اختيارية أو صورة خلفية لمنظمتك.
|
||||
upload-new-backdrop: تحميل خلفية جديدة
|
||||
extensions:
|
||||
title: الإضافات قادمة قريبًا!
|
||||
message: يرجى التحقق مرة أخرى في الإصدارات القادمة حيث نستعد لإطلاق مستودع الإضافات والسوق.
|
||||
notifications:
|
||||
select-all: تحديد الكل
|
||||
mark-as-read: تعليم كمقروء
|
||||
received: >-
|
||||
تم الاستلام:
|
||||
message: لا توجد إشعارات لعرضها.
|
||||
invite:
|
||||
for-users:
|
||||
invitation-message: لقد تمت دعوتك للانضمام إلى {companyName}
|
||||
invitation-sent-message: لقد تمت دعوتك للانضمام إلى منظمة {companyName} على {appName}. لقبول هذه الدعوة، أدخل رمز الدعوة الذي تلقيته عبر البريد الإلكتروني وانقر على متابعة.
|
||||
invitation-code-sent-text: رمز الدعوة الخاص بك
|
||||
accept-invitation-text: قبول الدعوة
|
||||
onboard:
|
||||
index:
|
||||
title: أنشئ حسابك
|
||||
welcome-title: <strong>مرحبًا بك في {companyName}!</strong><br />
|
||||
welcome-text: أكمل التفاصيل المطلوبة أدناه للبدء.
|
||||
full-name: الاسم الكامل
|
||||
full-name-help-text: اسمك الكامل
|
||||
your-email: عنوان البريد الإلكتروني
|
||||
your-email-help-text: عنوان بريدك الإلكتروني
|
||||
phone: رقم الهاتف
|
||||
phone-help-text: رقم هاتفك
|
||||
organization-name: اسم المنظمة
|
||||
organization-help-text: اسم منظمتك، سيتم إدارة جميع خدماتك ومواردك تحت هذه المنظمة، لاحقًا يمكنك إنشاء العديد من المنظمات كما تريد أو تحتاج.
|
||||
password: أدخل كلمة مرور
|
||||
password-help-text: كلمة مرورك، تأكد من أنها جيدة.
|
||||
confirm-password: تأكيد كلمة المرور
|
||||
confirm-password-help-text: فقط لتأكيد كلمة المرور التي أدخلتها أعلاه.
|
||||
continue-button-text: متابعة
|
||||
verify-email:
|
||||
header-title: التحقق من الحساب
|
||||
title: تحقق من عنوان بريدك الإلكتروني
|
||||
message-text: <strong>اقتربت من الانتهاء!</strong><br> تحقق من بريدك الإلكتروني للحصول على رمز التحقق.
|
||||
verification-code-text: أدخل رمز التحقق الذي تلقيته عبر البريد الإلكتروني.
|
||||
verification-input-label: رمز التحقق
|
||||
verify-button-text: تحقق واستمر
|
||||
didnt-receive-a-code: لم تتلق رمزًا بعد؟
|
||||
not-sent:
|
||||
message: لم تتلق رمزًا بعد؟
|
||||
alternative-choice: استخدم الخيارات البديلة أدناه للتحقق من حسابك.
|
||||
resend-email: إعادة إرسال البريد الإلكتروني
|
||||
send-by-sms: إرسال عبر الرسائل القصيرة
|
||||
install:
|
||||
installer-header: المثبت
|
||||
failed-message-sent: فشل التثبيت! انقر على الزر أدناه لإعادة محاولة التثبيت.
|
||||
retry-install: إعادة محاولة التثبيت
|
||||
start-install: بدء التثبيت
|
||||
layout:
|
||||
header:
|
||||
menus:
|
||||
organization:
|
||||
settings: إعدادات المنظمة
|
||||
create-or-join: إنشاء أو الانضمام إلى المنظمات
|
||||
explore-extensions: استكشاف الإضافات
|
||||
user:
|
||||
view-profile: عرض الملف الشخصي
|
||||
keyboard-shortcuts: عرض اختصارات لوحة المفاتيح
|
||||
changelog: سجل التغييرات
|
||||
@@ -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
Reference in New Issue
Block a user