Compare commits

...

70 Commits

Author SHA1 Message Date
Ron
4a5422e357 Merge pull request #364 from fleetbase/dev-v0.6.0
Some checks are pending
Fleetbase CI / Build and Start Docker Services (push) Waiting to run
v0.6.0 - Navigator App Refactor Support Release & Patches + Improvements
2025-03-26 22:14:02 +08:00
Ronald A. Richardson
21a0808b99 v0.6.0 - Navigator App Refactor Support Release & Patches + Improvements 2025-03-26 21:54:48 +08:00
Ron
f6cb850219 Merge pull request #358 from fleetbase/dev-v0.5.30
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
🏁 Consumable API Patches, Perf Improvements
2025-02-26 20:22:51 +08:00
Ronald A. Richardson
80707774ac 🏁 Consumable API Patches, Perf Improvements 2025-02-26 20:03:09 +08:00
Ron
96318bb909 Merge pull request #356 from fleetbase/dev-v0.5.29
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.5.29 ~ ️ Performance Upgrades 10x Faster, Critical Patches
2025-02-25 20:27:01 +08:00
Ronald A. Richardson
2c10f3551e dont run octane reload on deploy 2025-02-25 20:06:51 +08:00
Ronald A. Richardson
41a469c983 fix static issues with octane 2025-02-25 20:05:21 +08:00
Ronald A. Richardson
edf7efe167 v0.5.29 ~ ️ Performance Upgrades 10x Faster, Critical Patches 2025-02-25 16:44:14 +08:00
Ron
d7a2dd474a Merge pull request #355 from fleetbase/dev-v0.5.28
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.5.28 ~ patches for creating contact, and driver api hotfix
2025-02-21 16:29:47 +08:00
Ronald A. Richardson
2b0c6f793d patches for creating contact, and driver api hotfix 2025-02-21 16:24:23 +08:00
Ron
3e60479130 Merge pull request #352 from fleetbase/dev-v0.5.27
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.5.27 ~ Update for improved storefront callbacks and patches
2025-02-14 19:15:52 +08:00
Ronald A. Richardson
215d5dc42e v0.5.27 ~ Update for improved storefront callbacks and patches 2025-02-14 19:10:35 +08:00
Ron
a57467539b Merge pull request #351 from fleetbase/dev-v0.5.26
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.5.26 ~ Hotfix dark tile source
2025-02-13 14:48:35 +08:00
Ronald A. Richardson
c612c97e43 v0.5.26 ~ Hotfix dark tile source 2025-02-13 14:47:22 +08:00
Ron
683e93abe0 Merge pull request #350 from fleetbase/dev-v0.5.25
Some checks are pending
Fleetbase CI / Build and Start Docker Services (push) Waiting to run
v0.5.25 ~ Improved API, Performance and Design
2025-02-12 23:34:54 +08:00
Ronald A. Richardson
ba63441e7c v0.5.25 ~ Improved API, Performance and Design 2025-02-12 23:29:43 +08:00
Ron
879409d530 Merge pull request #348 from fleetbase/dev-v0.5.24
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
📖 Catalogs & 🚚 Food Trucks for Storefront + Patches
2025-02-04 23:44:40 +08:00
Ronald A. Richardson
6105b575c6 📖 Catalogs & 🚚 Food Trucks for Storefront + Patches 2025-02-04 23:34:44 +08:00
Ron
57c22eccb7 Merge pull request #344 from fleetbase/dev-v0.5.23
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.5.23 ~ More Features, More Patches
2025-01-30 01:21:02 +09:00
Ronald A. Richardson
989ca4d35e More Features, More Patches 2025-01-30 00:03:04 +08:00
Ron
afb1c1dbdc Merge pull request #341 from fleetbase/dev-v0.5.22
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.5.22 ~ Implemented Social OAuth, Critical Patches
2025-01-27 15:37:48 +09:00
Ronald A. Richardson
0ac52bc772 v0.5.22 ~ Implemented Social OAuth, Critical Patches 2025-01-27 14:32:03 +08:00
Ron
50d8ffee33 Merge pull request #337 from fleetbase/dev-v0.5.21
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.5.21 - maintenance release
2025-01-13 12:11:48 +09:00
Ronald A. Richardson
015b87ba82 v0.5.21 - maintenance release 2025-01-13 11:04:55 +08:00
Ron
983d4e5bae Merge pull request #335 from fleetbase/dev-v0.5.20
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.5.20
2025-01-11 18:31:43 +09:00
Ronald A. Richardson
9bc78c0bcc Several major patches and refactors for storefront and new data management capabilities 2025-01-11 17:26:27 +08:00
Ron
8f13603f4b Merge pull request #331 from fleetbase/dev-v0.5.19
🎄 Christmas Release - v0.5.19
2024-12-24 16:01:49 +09:00
Ronald A. Richardson
c6bef55839 fix api lockfile 2024-12-24 14:55:31 +08:00
Ronald A. Richardson
dcecbf2953 Upgraded all dependencies 2024-12-24 14:46:36 +08:00
Ronald A. Richardson
08b8566b90 hotfix issue fleetbase/fleetbase#326 2024-12-24 12:53:52 +08:00
Ronald A. Richardson
7f3aa5005d Christmas release stuff 2024-12-24 12:46:06 +08:00
Ron
5e36ac0aa2 Merge pull request #321 from fleetbase/dev-v0.5.18
v0.5.18 - Stability and Optimization Patches + New Console Commands to fix Lega…
2024-11-09 15:29:49 +09:00
Ronald A. Richardson
da6e8e79ba Stability and Optimization Patches + New Console Commands to fix Legacy data 2024-11-09 14:19:27 +08:00
Ronald A. Richardson
dd1271b1ce add LOGROCKET_APP_ID for QA deployment 2024-11-07 18:58:49 +08:00
Ronald A. Richardson
39f00789bf hotfix deploy.sh remove config and route cache 2024-11-07 18:57:11 +08:00
Ronald A. Richardson
ffc0d0fd1a update production deploy workflow to add LOGROCKET_APP_ID 2024-11-07 18:54:42 +08:00
Ron
f8196ccc03 Merge pull request #320 from fleetbase/dev-v0.5.17
v0.5.17
2024-11-07 19:39:55 +09:00
Ronald A. Richardson
6f1664e123 ran linter 2024-11-07 18:33:47 +08:00
Ronald A. Richardson
687af92752 * Fixed: service areas/zones creation
* Tweaked: few styling improvements
* Added: view label functions to customer portal components
* Added: bulk dispatch
* Fixed: route caching by generating and providing unique name via Fleetbase `RESTRegistrar`
* Implemented: impersonation feature for system admin
* Fixed: mail settings for smtp and added support for mailgun, sendgrid, postmark and resend
* Removed: model cache
* Improved test email HTML
* Added: self hosted instance extension install instructions
* Improved: Webhooks UI
2024-11-07 18:30:30 +08:00
Shiv Thakker
eb3f706791 Update README.md 2024-10-21 12:26:31 +08:00
Ron
5ceb3cbc84 Merge pull request #310 from fleetbase/dev-v05.16
fix logging of api request and webhook sending
2024-10-17 19:38:02 +08:00
Ronald A. Richardson
7792cf31e2 fix logging of api request and webhook sending 2024-10-17 19:37:12 +08:00
Ron
50f30742a8 Merge pull request #309 from fleetbase/dev-v0.5.15
v0.5.15
2024-10-17 17:09:03 +08:00
Ronald A. Richardson
c7b1a876f5 v0.5.15 - Additional recovery actions added, added ability for admins to reset user password, patches to webhook handler and logging 2024-10-17 17:02:18 +08:00
Ron
892eaeeca0 Merge pull request #306 from fleetbase/dev-v0.5.14
v0.5.14
2024-10-15 17:53:05 +08:00
Ronald A. Richardson
32f4b69697 order list overlay revamp, ability to create customer with order in consumable api, few patches and fixes 2024-10-15 17:46:36 +08:00
Ron
6317c4b2e4 Merge pull request #304 from fleetbase/dev-v0.5.13
v0.1.3 - critical patches for driver creation flow on fleetops, added ability …
2024-10-10 19:46:55 +08:00
Ronald A. Richardson
e7c229ece5 critical patches for driver creation flow on fleetops, added ability for registry to handle tar/gz bundle uploads, added registry endpoint to upload bundles using auth token, few minor patches 2024-10-10 19:35:45 +08:00
Ron
983a3d22b5 Merge pull request #303 from fleetbase/dev-v0.5.12
v0.5.12 - bump version
2024-10-10 10:56:59 +08:00
Ronald A. Richardson
42105380ca bump version 2024-10-10 10:56:08 +08:00
Ron
8fd4a40016 Merge pull request #302 from fleetbase/dev-v0.5.12
v1.5.12 - critical hotfix to make sure session store is set for json resources
2024-10-10 10:55:42 +08:00
Ronald A. Richardson
331e98af20 critical hotfix to make sure session store is set for json resources 2024-10-10 10:52:37 +08:00
Ron
b1d226256a Merge pull request #301 from fleetbase/dev-v0.5.11
v0.5.11 - Added Manual Verify for Users [IAM], Added ability to Transfer Org Ow…
2024-10-10 00:50:23 +08:00
Ronald A. Richardson
f1ee8b0c99 Added Manual Verify for Users [IAM], Added ability to Transfer Org Ownership and Leave Orgs, Added recovery command, fixes and patches 2024-10-10 00:49:03 +08:00
Ron
23d5ecfdb8 Merge pull request #300 from fleetbase/dev-v0.5.10
patches and improvements
2024-10-08 21:42:07 +08:00
Ronald A. Richardson
2795a2f1be update app router.js 2024-10-08 21:41:07 +08:00
Ron
69afdee975 Merge pull request #299 from fleetbase/dev-v0.5.10
v0.5.10 - patches and improvements
2024-10-08 21:39:34 +08:00
Ronald A. Richardson
60845b9953 patches and improvements 2024-10-08 21:33:55 +08:00
Ron
f3997a1bb7 Merge pull request #298 from fleetbase/dev-v0.5.9
v0.5.9
2024-10-03 17:48:38 +08:00
Ronald A. Richardson
23a691e7e7 removed ember-leaflet patches, using perm fix and improved engine boot timeout and callbacks 2024-10-03 17:43:25 +08:00
Ronald A. Richardson
3dc562987a Hotfix release 2024-10-03 14:58:39 +08:00
Ron
e8ac2a3796 Merge pull request #295 from fleetbase/dev-v0.5.8
v0.5.8 - Feature: Customer Portal, Order Tracking Page, New Translations, Improved UX
2024-10-02 18:43:09 +08:00
Ronald A. Richardson
b78c59ad89 updated README roadmap 2024-10-02 18:38:17 +08:00
Ronald A. Richardson
bd71b1921b v0.5.8 ready 2024-10-02 18:34:40 +08:00
Ron
2596ccbded Merge pull request #290 from CassioDalla/console-pt-br-translation
Create pt-br.yaml File
2024-10-02 13:57:54 +08:00
Ronald A. Richardson
81159b7411 preparing for major release w/ customer portal extension and order tracking page, real time ETAs and order progress tracker info 2024-10-02 00:15:13 +08:00
Ronald A. Richardson
acc4cfba35 fix as-array utility 2024-09-06 12:18:11 +08:00
Ronald A. Richardson
ea47bdc09d little progress and bugfixes 2024-09-06 12:17:30 +08:00
Ronald A. Richardson
c60c460257 Feature: Customer Portal, improved Customer, Contact, and Vendor Management 2024-09-04 12:31:10 +08:00
CassioDalla
211a3a9808 Create pt-br.yaml File
Added Brazilian Portuguese translation file to the project. It would be nice if another translator reviewed it to make sure everything is correct.
2024-08-10 16:04:45 -03:00
131 changed files with 8425 additions and 6545 deletions

View File

@@ -132,17 +132,22 @@ 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 "LOGROCKET_APP_ID=${{ secrets.LOGROCKET_APP_ID }}" >> ./environments/.env.production
echo "EXTENSIONS=@fleetbase/billing-engine,@fleetbase/internals-engine" >> ./environments/.env.production
working-directory: ./console
@@ -150,6 +155,7 @@ jobs:
if: startsWith(github.ref, 'refs/heads/deploy/production')
run: |
echo "STRIPE_KEY=${{ secrets.STRIPE_KEY }}" >> ./environments/.env.production
echo "LOGROCKET_APP_ID=${{ secrets.LOGROCKET_APP_ID }}" >> ./environments/.env.production
echo "EXTENSIONS=@fleetbase/billing-engine,@fleetbase/internals-engine" >> ./environments/.env.production
working-directory: ./console

1
.gitignore vendored
View File

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

View File

@@ -13,7 +13,7 @@
·
<a href="https://fleetbase.apichecker.com" target="_api_status" rel="nofollow">API Status</a>
·
<a href="https://meetings.hubspot.com/shiv-thakker" rel="nofollow">Book a Demo</a>
<a href="https://tally.so/r/3NBpAW" rel="nofollow">Book a Demo</a>
·
<a href="https://discord.gg/V7RVWRQ2Wm" target="discord" rel="nofollow">Discord</a>
</p>
@@ -143,12 +143,11 @@ Fleetbase offers a few open sourced apps which are built on Fleetbase which can
</ul>
## 🛣️ Roadmap
1. **Customer Facing Views** ~ Extensions will be able to create public/customer facing views tracking and management from outside of the console UI.
2. **Inventory and Warehouse Management** ~ Pallet will be Fleetbases first official extension for WMS & Inventory.
3. **Accounting and Invoicing** ~ Pallet will be Fleetbases first official extension for WMS & Inventory.
4. **Binary Builds** ~ Run Fleetbase from a single binary.
6. **Fleetbase for Desktop** ~ Desktop builds for OSX and Windows.
7. **Custom Maps and Routing Engines** ~ Feature to enable easy integrations with custom maps and routing engines like Google Maps or Mapbox etc…
1. **Inventory and Warehouse Management** ~ Pallet will be Fleetbases first official extension for WMS & Inventory.
2. **Accounting and Invoicing** ~ Ledger will be Fleetbases first official extension accounting and invoicing.
3. **Binary Builds** ~ Run Fleetbase from a single binary.
4. **Fleetbase for Desktop** ~ Desktop builds for OSX and Windows.
5. **Custom Maps and Routing Engines** ~ Feature to enable easy integrations with custom maps and routing engines like Google Maps or Mapbox etc…
## 🪲 Bugs and 💡 Feature Requests

View File

@@ -9,10 +9,11 @@
"license": "AGPL-3.0-or-later",
"require": {
"php": "^8.0",
"fleetbase/core-api": "^1.5.5",
"fleetbase/fleetops-api": "^0.5.7",
"fleetbase/registry-bridge": "^0.0.13",
"fleetbase/storefront-api": "^0.3.14",
"appstract/laravel-opcache": "^4.0",
"fleetbase/core-api": "^1.6.0",
"fleetbase/fleetops-api": "^0.6.0",
"fleetbase/registry-bridge": "^0.0.18",
"fleetbase/storefront-api": "^0.3.29",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^10.0",
"laravel/octane": "^2.3",
@@ -22,7 +23,10 @@
"phpoffice/phpspreadsheet": "^1.28",
"predis/predis": "^2.1",
"psr/http-factory-implementation": "*",
"s-ichikawa/laravel-sendgrid-driver": "^4.0"
"resend/resend-php": "^0.14.0",
"s-ichikawa/laravel-sendgrid-driver": "^4.0",
"symfony/mailgun-mailer": "^7.1",
"symfony/postmark-mailer": "^7.1"
},
"require-dev": {
"spatie/laravel-ignition": "^2.0",
@@ -34,10 +38,6 @@
"phpunit/phpunit": "^10.0"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/fleetbase/laravel-model-caching"
},
{
"type": "composer",
"url": "https://registry.fleetbase.io"

3493
api/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,8 @@ return [
'transport' => 'sendgrid',
],
'resend' => [],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -t -i'),

30
api/config/opcache.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
return [
'url' => env('OPCACHE_URL', config('app.url')),
'prefix' => 'opcache-api',
'verify' => true,
'headers' => [],
'directories' => [
base_path('app'),
base_path('bootstrap'),
base_path('public'),
base_path('resources'),
base_path('routes'),
base_path('storage'),
base_path('vendor'),
],
'exclude' => [
'test',
'Test',
'tests',
'Tests',
'stub',
'Stub',
'stubs',
'Stubs',
'dumper',
'Dumper',
'Autoload',
],
];

View File

@@ -34,4 +34,13 @@ return [
'api_key' => env('SENDGRID_API_KEY'),
],
'resend' => [
'key' => env('RESEND_KEY'),
],
'stripe' => [
'key' => env('STRIPE_KEY', env('STRIPE_API_KEY')),
'secret' => env('STRIPE_SECRET', env('STRIPE_API_SECRET')),
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],
];

View File

@@ -26,6 +26,14 @@ php artisan schedule-monitor:sync
# Clear cache
php artisan cache:clear
php artisan route:clear
# Optimize
php artisan config:cache
php artisan route:cache
# Initialize registry
php artisan registry:init
# Restart octane
# php artisan octane:reload

View File

@@ -1,54 +1,54 @@
# ---- Build Stage ----
FROM node:18.15.0-alpine as builder
FROM node:18.15.0-alpine as builder
# Set the working directory in the container to /console
WORKDIR /console
# Create the pnpm directory and set the PNPM_HOME environment variable
RUN mkdir -p ~/.pnpm
ENV PNPM_HOME /root/.pnpm
# Set environment
ARG ENVIRONMENT=production
# Add the pnpm global bin to the PATH
ENV PATH /root/.pnpm/bin:$PATH
# Copy pnpm-lock.yaml (or package.json) into the directory /console in the container
COPY console/package.json console/pnpm-lock.yaml ./
# Copy over .npmrc if applicable
COPY console/.npmr[c] ./
# Install global dependencies
RUN npm install -g ember-cli pnpm
# Install git
RUN apk update && apk add git openssh-client
# Trust GitHub's RSA host key
RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
# Install app dependencies
RUN pnpm install
# Copy the console directory contents into the container at /console
COPY console .
# Build the application
RUN pnpm build --environment $ENVIRONMENT
# ---- Serve Stage ----
FROM nginx:alpine
# Copy the built app to our served directory
COPY --from=builder /console/dist /usr/share/nginx/html
# Expose the port nginx is bound to
EXPOSE 4200
# Use custom nginx.conf
COPY console/nginx.conf /etc/nginx/conf.d/default.conf
# Start Nginx server
CMD ["nginx", "-g", "daemon off;"]
# Set the working directory in the container to /console
WORKDIR /console
# Create the pnpm directory and set the PNPM_HOME environment variable
RUN mkdir -p ~/.pnpm
ENV PNPM_HOME /root/.pnpm
# Set environment
ARG ENVIRONMENT=production
# Add the pnpm global bin to the PATH
ENV PATH /root/.pnpm/bin:$PATH
# Copy pnpm-lock.yaml (or package.json) into the directory /console in the container
COPY console/package.json console/pnpm-lock.yaml ./
# Copy over .npmrc if applicable
COPY console/.npmr[c] ./
# Install global dependencies
RUN npm install -g ember-cli pnpm
# Install git
RUN apk update && apk add git openssh-client
# Trust GitHub's RSA host key
RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
# Install app dependencies
RUN pnpm install
# Copy the console directory contents into the container at /console
COPY console .
# Build the application
RUN pnpm build --environment $ENVIRONMENT
# ---- Serve Stage ----
FROM nginx:alpine
# Copy the built app to our served directory
COPY --from=builder /console/dist /usr/share/nginx/html
# Expose the port nginx is bound to
EXPOSE 4200
# Use custom nginx.conf
COPY console/nginx.conf /etc/nginx/conf.d/default.conf
# Start Nginx server
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -33,7 +33,7 @@
</InputGroup>
{{/if}}
{{#if this.testResponse}}
<div class="animate-pulse flex flex-row items-center rounded-lg border {{if (eq this.testResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<div class="flex flex-row items-center rounded-lg border {{if (eq this.testResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<FaIcon @icon={{if (eq this.testResponse.status 'error') 'triangle-exclamation' 'circle-check'}} class="mr-1.5 {{if (eq this.testResponse.status 'error') 'text-red-200' 'text-green-200'}}" />
<span class="text-xs">{{this.this.testResponse.message}}</span>
</div>

View File

@@ -3,25 +3,54 @@
<Select @options={{this.mailers}} @value={{this.mailer}} @onSelect={{this.setMailer}} @placeholder="Select mailer" class="w-full" />
</InputGroup>
{{#if (eq this.mailer "smtp")}}
<InputGroup @name="SMTP Host" @value={{this.smtpHost}} disabled={{this.isLoading}} />
<InputGroup @name="SMTP Port" @type="number" @value={{this.smtpPort}} disabled={{this.isLoading}} />
<InputGroup @name="SMTP Encryption" @value={{this.smtpEncryption}} disabled={{this.isLoading}} />
<InputGroup @name="SMTP Username" @value={{this.smtpUsername}} disabled={{this.isLoading}} />
<InputGroup @name="SMTP Password" @type="password" @value={{this.smtpPassword}} disabled={{this.isLoading}} />
<InputGroup @name="SMTP Timeout" @value={{this.smtpTimeout}} disabled={{this.isLoading}} />
<InputGroup @name="SMTP Auth Mode" @value={{this.smtpAuth_mode}} disabled={{this.isLoading}} />
<InputGroup @name="SMTP Host" @value={{this.smtpHost}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="SMTP Port" @type="number" @value={{this.smtpPort}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup>
<Toggle @isToggled={{eq this.smtpEncryption "tls"}} @onToggle={{this.enableSmtpEncryption}} @label="SMTP Encryption" @helpText="Enabled TLS Encryption" />
</InputGroup>
<InputGroup @name="SMTP Username" @value={{this.smtpUsername}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="SMTP Password" @value={{this.smtpPassword}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="SMTP Timeout" @value={{this.smtpTimeout}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="SMTP Auth Mode" @value={{this.smtpAuth_mode}} disabled={{this.loadConfigValues.isRunning}} />
{{/if}}
<InputGroup @name="From Address" @helpText="Input the email address for Fleetbase to send emails from." @value={{this.fromAddress}} @placeholder="From Address" disabled={{this.isLoading}} />
{{#if (eq this.mailer "mailgun")}}
<InputGroup @name="Mailgun Domain" @value={{this.mailgunDomain}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="Mailgun Endpoint" @value={{this.mailgunEndpoint}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="Mailgun Secret" @value={{this.mailgunSecret}} disabled={{this.loadConfigValues.isRunning}} />
{{/if}}
{{#if (eq this.mailer "postmark")}}
<InputGroup @name="Postmark Token" @value={{this.postmarkToken}} disabled={{this.loadConfigValues.isRunning}} />
{{/if}}
{{#if (eq this.mailer "sendgrid")}}
<InputGroup @name="Sendgrid API Key" @value={{this.sendgridApi_key}} disabled={{this.loadConfigValues.isRunning}} />
{{/if}}
{{#if (eq this.mailer "resend")}}
<InputGroup @name="Resend API Key" @value={{this.resendKey}} disabled={{this.loadConfigValues.isRunning}} />
{{/if}}
<InputGroup
@name="From Address"
@helpText="Input the email address for Fleetbase to send emails from."
@value={{this.fromAddress}}
@placeholder="From Address"
disabled={{this.isLoading}}
/>
<InputGroup @name="From Name" @helpText="Input the name for Fleetbase to send emails from." @value={{this.fromName}} @placeholder="From Name" disabled={{this.isLoading}} />
{{#if this.testResponse}}
<div class="animate-pulse flex flex-row items-center rounded-lg border {{if (eq this.testResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<FaIcon @icon={{if (eq this.testResponse.status 'error') 'triangle-exclamation' 'circle-check'}} class="mr-1.5 {{if (eq this.testResponse.status 'error') 'text-red-200' 'text-green-200'}}" />
<span class="text-xs">{{this.this.testResponse.message}}</span>
<div
class="flex flex-row items-center rounded-lg border
{{if (eq this.testResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}}
shadow-sm my-2 px-4 py-2"
>
<FaIcon
@icon={{if (eq this.testResponse.status "error") "triangle-exclamation" "circle-check"}}
class="mr-1.5 {{if (eq this.testResponse.status 'error') 'text-red-200' 'text-green-200'}}"
/>
<span class="text-xs">{{this.testResponse.message}}</span>
</div>
{{/if}}
<Button @wrapperClass="mt-3" @icon="plug" @text="Test Config" @onClick={{this.test}} @isLoading={{this.isLoading}} />
<Button @wrapperClass="mt-3" @icon="plug" @text="Test Config" @onClick={{perform this.test}} @isLoading={{this.test.isRunning}} />
</ContentPanel>
<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={{this.save.isRunning}} @isLoading={{this.save.isRunning}} />
</EmberWormhole>

View File

@@ -2,6 +2,7 @@ import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { task } from 'ember-concurrency';
export default class ConfigureMailComponent extends Component {
@service fetch;
@@ -14,11 +15,17 @@ export default class ConfigureMailComponent extends Component {
@tracked fromName = null;
@tracked smtpHost = 'smtp.mailgun.org';
@tracked smtpPort = 587;
@tracked smtpEncryption = 'tls';
@tracked smtpEncryption = null;
@tracked smtpUsername = null;
@tracked smtpPassword = null;
@tracked smtpTimeout = null;
@tracked smtpAuth_mode = null;
@tracked mailgunDomain = null;
@tracked mailgunEndpoint = 'api.mailgun.net';
@tracked mailgunSecret = null;
@tracked postmarkToken = null;
@tracked sendgridApi_key = null;
@tracked resendKey = null;
/**
* Creates an instance of ConfigureFilesystemComponent.
@@ -26,7 +33,7 @@ export default class ConfigureMailComponent extends Component {
*/
constructor() {
super(...arguments);
this.loadConfigValues();
this.loadConfigValues.perform();
}
@action setConfigValues(config) {
@@ -37,6 +44,10 @@ export default class ConfigureMailComponent extends Component {
}
}
@action enableSmtpEncryption(enabled) {
this.smtpEncryption = enabled ? 'tls' : null;
}
@action setMailer(mailer) {
this.mailer = mailer;
}
@@ -53,56 +64,77 @@ export default class ConfigureMailComponent extends Component {
};
}
@action loadConfigValues() {
this.isLoading = true;
this.fetch
.get('settings/mail-config')
.then((response) => {
this.setConfigValues(response);
})
.finally(() => {
this.isLoading = false;
});
@action serializeMailgunConfig() {
return {
domain: this.mailgunDomain,
secret: this.mailgunSecret,
endpoint: this.mailgunEndpoint,
};
}
@action test() {
this.isLoading = true;
@action serializePostmarkConfig() {
return {
token: this.postmarkToken,
};
}
this.fetch
.post('settings/test-mail-config', {
@action serializeSendgridConfig() {
return {
api_key: this.sendgridApi_key,
};
}
@action serializeResendConfig() {
return {
key: this.resendKey,
};
}
@task *loadConfigValues() {
try {
const config = yield this.fetch.get('settings/mail-config');
this.setConfigValues(config);
} catch (error) {
this.notifications.serverError(error);
}
}
@task *test() {
try {
this.testResponse = yield this.fetch.post('settings/test-mail-config', {
mailer: this.mailer,
from: {
address: this.fromAddress,
name: this.fromName,
},
smtp: this.serializeSmtpConfig(),
})
.then((response) => {
this.testResponse = response;
})
.finally(() => {
this.isLoading = false;
mailgun: this.serializeMailgunConfig(),
postmark: this.serializePostmarkConfig(),
sendgrid: this.serializeSendgridConfig(),
resend: this.serializeResendConfig(),
});
} catch (error) {
this.notifications.serverError(error);
}
}
@action save() {
this.isLoading = true;
this.fetch
.post('settings/mail-config', {
@task *save() {
try {
yield this.fetch.post('settings/mail-config', {
mailer: this.mailer,
from: {
address: this.fromAddress,
name: this.fromName,
},
smtp: this.serializeSmtpConfig(),
})
.then(() => {
this.notifications.success('Mail configuration saved.');
})
.finally(() => {
this.isLoading = false;
mailgun: this.serializeMailgunConfig(),
postmark: this.serializePostmarkConfig(),
sendgrid: this.serializeSendgridConfig(),
resend: this.serializeResendConfig(),
});
this.notifications.success('Mail configuration saved.');
} catch (error) {
this.notifications.serverError(error);
}
}
}

View File

@@ -35,7 +35,7 @@
<ContentPanel @title="Test Push Notification" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-900">
{{#if this.testResponse}}
<div class="animate-pulse flex flex-row items-center rounded-lg border {{if (eq this.testResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<div class="flex flex-row items-center rounded-lg border {{if (eq this.testResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<FaIcon @icon={{if (eq this.testResponse.status 'error') 'triangle-exclamation' 'circle-check'}} class="mr-1.5 {{if (eq this.testResponse.status 'error') 'text-red-200' 'text-green-200'}}" />
<span class="text-xs">{{this.this.testResponse.message}}</span>
</div>

View File

@@ -12,7 +12,7 @@
<InputGroup @name="SQS Suffix" @value={{this.sqsSuffix}} disabled={{this.isLoading}} />
{{/if}}
{{#if this.testResponse}}
<div class="animate-pulse flex flex-row items-center rounded-lg border {{if (eq this.testResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<div class="flex flex-row items-center rounded-lg border {{if (eq this.testResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<FaIcon @icon={{if (eq this.testResponse.status 'error') 'triangle-exclamation' 'circle-check'}} class="mr-1.5 {{if (eq this.testResponse.status 'error') 'text-red-200' 'text-green-200'}}" />
<span class="text-xs">{{this.this.testResponse.message}}</span>
</div>

View File

@@ -14,7 +14,7 @@
<InputGroup @name="Twilio Token" @value={{this.twilioToken}} disabled={{this.isLoading}} />
<InputGroup @name="Twilio From" @value={{this.twilioFrom}} disabled={{this.isLoading}} />
{{#if this.twilioTestResponse}}
<div class="animate-pulse flex flex-row items-center rounded-lg border {{if (eq this.twilioTestResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<div class="flex flex-row items-center rounded-lg border {{if (eq this.twilioTestResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<FaIcon @icon={{if (eq this.twilioTestResponse.status 'error') 'triangle-exclamation' 'circle-check'}} class="mr-1.5 {{if (eq this.twilioTestResponse.status 'error') 'text-red-200' 'text-green-200'}}" />
<span class="text-xs">{{this.this.twilioTestResponse.message}}</span>
</div>
@@ -28,7 +28,7 @@
<ContentPanel @title="Sentry" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<InputGroup @name="Sentry DSN" @value={{this.sentryDsn}} disabled={{this.isLoading}} />
{{#if this.sentryTestResponse}}
<div class="animate-pulse flex flex-row items-center rounded-lg border {{if (eq this.sentryTestResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<div class="flex flex-row items-center rounded-lg border {{if (eq this.sentryTestResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<FaIcon @icon={{if (eq this.sentryTestResponse.status 'error') 'triangle-exclamation' 'circle-check'}} class="mr-1.5 {{if (eq this.sentryTestResponse.status 'error') 'text-red-200' 'text-green-200'}}" />
<span class="text-xs">{{this.this.sentryTestResponse.message}}</span>
</div>

View File

@@ -37,7 +37,7 @@
</div>
</div>
{{#if this.testResponse}}
<div class="animate-pulse flex flex-row items-center rounded-lg border {{if (eq this.testResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<div class="flex flex-row items-center rounded-lg border {{if (eq this.testResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<FaIcon @icon={{if (eq this.testResponse.status 'error') 'triangle-exclamation' 'circle-check'}} class="mr-1.5 {{if (eq this.testResponse.status 'error') 'text-red-200' 'text-green-200'}}" />
<span class="text-xs">{{this.this.testResponse.message}}</span>
</div>

View File

@@ -1,99 +0,0 @@
<div class="fleetbase-dashboard-grid flex items-center justify-between mb-4 mt-6 px-14">
<div class="left-section">
<h1 class="text-lg font-bold">{{this.dashboard.currentDashboard.name}}</h1>
</div>
<div class="fleetbase-dashboard-actions right-section ml-4 flex items-center">
<div class="fleetbase-model-select fleetbase-power-select ember-model-select h-10">
<DropdownButton
class="h-10"
@text={{if this.dashboard.currentDashboard.name this.dashboard.currentDashboard.name (t "component.dashboard.select-dashboard")}}
@textClass="text-sm mr-2"
@buttonClass="flex-row-reverse w-44 justify-between"
@icon="caret-down"
@iconClass="mr-0i"
@size="sm"
@iconPrefix="fas"
@triggerClass="hidden md:flex"
as |dd|
>
<div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
<div class="p-1">
{{#each this.dashboard.dashboards as |dashboard|}}
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.selectDashboard dashboard)}}>
<div class="flex-1 flex flex-row items-center">
<div class="w-6">
<FaIcon @icon="desktop" />
</div>
<span>{{dashboard.name}}</span>
</div>
<div>
{{#if (eq this.dashboard.currentDashboard.id dashboard.id)}}
<FaIcon @icon="check" class="text-green-500" />
{{/if}}
</div>
</a>
{{/each}}
</div>
</div>
</DropdownButton>
</div>
<div class="ml-2 relative h-10">
<DropdownButton class="h-10" @icon="ellipsis-h" @size="sm" @iconPrefix="fas" @triggerClass="hidden md:flex" as |dd|>
<div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
<div class="p-1">
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.createDashboard)}}>
<div class="w-6">
<FaIcon @icon="add" />
</div>
<span>{{t "component.dashboard.create-new-dashboard"}}</span>
</a>
{{#unless (eq this.dashboard.currentDashboard.user_uuid "system")}}
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.onChangeEdit true)}}>
<div class="w-6">
<FaIcon @icon="edit" />
</div>
<span>{{t "component.dashboard.edit-layout"}}</span>
</a>
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.onAddingWidget true)}}>
<div class="w-6">
<FaIcon @icon="add" />
</div>
<span>{{t "component.dashboard.add-widgets"}}</span>
</a>
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.deleteDashboard this.dashboard.currentDashboard)}}>
<div class="w-6">
<FaIcon @icon="trash" />
</div>
<span>{{t "component.dashboard.delete-dashboard"}}</span>
</a>
{{/unless}}
</div>
</div>
</DropdownButton>
</div>
{{#if this.dashboard.isEditingDashboard}}
<div class="ml-2 h-10">
<Button @type="magic" @icon="save" @helpText={{t "component.dashboard.save-dashboard"}} @onClick={{fn this.onChangeEdit false}} class="h-10" />
</div>
{{/if}}
</div>
</div>
<div class="px-10">
<Dashboard::Create @isEdit={{this.dashboard.isEditingDashboard}} @isAddingWidget={{this.dashboard.isAddingWidget}} @dashboard={{this.dashboard.currentDashboard}} />
{{#if this.dashboard.isAddingWidget}}
<EmberWormhole @to="console-home-wormhole">
<Dashboard::WidgetPanel
@isOpen={{this.dashboard.isAddingWidget}}
@onLoad={{this.setWidgetSelectorPanelContext}}
@dashboard={{this.dashboard.currentDashboard}}
@onClose={{fn this.onAddingWidget false}}
/>
</EmberWormhole>
{{/if}}
</div>

View File

@@ -1,140 +0,0 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
/**
* DashboardComponent for managing dashboards in an Ember application.
* This component handles actions such as selecting, creating, deleting dashboards,
* and managing widget selectors and dashboard editing states.
*
* @extends Component
*/
export default class DashboardComponent extends Component {
/**
* Ember Data store service.
* @type {Service}
*/
@service store;
/**
* Internationalization service for managing translations.
* @type {Service}
*/
@service intl;
/**
* Notifications service for displaying alerts or confirmations.
* @type {Service}
*/
@service notifications;
/**
* Modals manager service for handling modal dialogs.
* @type {Service}
*/
@service modalsManager;
/**
* Fetch service for handling HTTP requests.
* @type {Service}
*/
@service fetch;
/**
* Dashboard service for business logic related to dashboards.
* @type {Service}
*/
@service dashboard;
/**
* Creates an instance of DashboardComponent.
* @memberof DashboardComponent
*/
constructor() {
super(...arguments);
this.dashboard.loadDashboards.perform();
}
/**
* Action to select a dashboard.
* @param {Object} dashboard - The dashboard to be selected.
*/
@action selectDashboard(dashboard) {
this.dashboard.selectDashboard.perform(dashboard);
}
/**
* Sets the context for the widget selector panel.
* @param {Object} widgetSelectorContext - The context object for the widget selector.
*/
@action setWidgetSelectorPanelContext(widgetSelectorContext) {
this.widgetSelectorContext = widgetSelectorContext;
}
/**
* Creates a new dashboard.
* @param {Object} dashboard - The dashboard to be created.
* @param {Object} [options={}] - Optional parameters for dashboard creation.
*/
@action createDashboard(dashboard, options = {}) {
this.modalsManager.show('modals/create-dashboard', {
title: this.intl.t('component.dashboard.create-a-new-dashboard'),
acceptButtonText: this.intl.t('component.dashboard.confirm-create-dashboard'),
confirm: async (modal, done) => {
modal.startLoading();
// Get the name from the modal options
const { name } = modal.getOptions();
await this.dashboard.createDashboard.perform(name);
done();
},
...options,
});
}
/**
* Deletes a dashboard.
* @param {Object} dashboard - The dashboard to be deleted.
* @param {Object} [options={}] - Optional parameters for dashboard deletion.
*/
@action deleteDashboard(dashboard, options = {}) {
if (this.dashboard.dashboards?.length === 1) {
return this.notifications.error(this.intl.t('component.dashboard.you-cannot-delete-this-dashboard'));
}
this.modalsManager.confirm({
title: this.intl.t('component.dashboard.are-you-sure-you-want-delete-dashboard', { dashboardName: dashboard.name }),
confirm: async (modal, done) => {
modal.startLoading();
await this.dashboard.deleteDashboard.perform(dashboard);
done();
},
...options,
});
}
/**
* Action to handle the addition of a widget.
* @param {boolean} [state=true] - The state to set for adding a widget.
*/
@action onAddingWidget(state = true) {
this.dashboard.onAddingWidget(state);
}
/**
* Sets the current dashboard.
* @param {Object} dashboard - The dashboard to be set as current.
*/
@action setCurrentDashboard(dashboard) {
this.dashboard.setCurrentDashboard.perform(dashboard);
}
/**
* Changes the editing state of the dashboard.
* @param {boolean} [state=true] - The state to set for editing the dashboard.
*/
@action onChangeEdit(state = true) {
this.dashboard.onChangeEdit(state);
}
}

View File

@@ -1,16 +0,0 @@
<div class="fleetbase-dashboard-grid" ...attributes>
<GridStack @options={{this.gridOptions}} @onChange={{this.onChangeGrid}}>
{{#each @dashboard.widgets as |widget|}}
{{#if (component-resolvable widget.component)}}
<GridStackItem id={{widget.id}} @options={{spread-widget-options (hash id=widget.id options=widget.grid_options)}} class="relative">
{{component widget.component options=widget.options}}
{{#if @isEdit}}
<div class="absolute top-2 right-2">
<Button @type="default" @icon="trash" @helpText={{"Remove widget from the dashboard"}} @onClick={{fn this.removeWidget widget}} />
</div>
{{/if}}
</GridStackItem>
{{/if}}
{{/each}}
</GridStack>
</div>

View File

@@ -1,99 +0,0 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action, computed } from '@ember/object';
import { inject as service } from '@ember/service';
/**
* Component responsible for creating and managing the dashboard layout.
* Provides functionalities such as toggling widget float, changing grid layout, and removing widgets.
*
* @extends Component
*/
export default class DashboardCreateComponent extends Component {
/**
* Notifications service for displaying alerts or errors.
* @type {Service}
*/
@service notifications;
/**
* Tracked array to keep track of widgets that have been updated.
* @type {Array}
*/
@tracked updatedWidgets = [];
/**
* Action to toggle the floating state of widgets on the grid.
*/
@action toggleFloat() {
this.shouldFloat = !this.shouldFloat;
}
/**
* Handles changes to the grid layout, such as repositioning or resizing widgets.
* Iterates over each widget event detail and updates the corresponding widget's properties if necessary.
*
* @param {Event} event - Event containing details about the grid change.
* @action
*/
@action onChangeGrid(event) {
const { dashboard } = this.args;
event.detail.forEach((currentWidgetEvent) => {
const alreadyUpdated = this.updatedWidgets.find((item) => item.id === currentWidgetEvent.id);
if (alreadyUpdated || !this.dashboard) {
return;
}
const changedWidget = dashboard.widgets.find((widget) => widget.id === currentWidgetEvent.id);
if (!changedWidget) {
return;
}
const { x, y, w, h } = currentWidgetEvent;
const response = changedWidget.updateProperties({
grid_options: { x, y, w, h },
});
if (response) {
this.updatedWidgets.push(changedWidget);
}
});
}
/**
* Removes a specified widget from the dashboard.
* Performs a removal operation on the dashboard and handles any errors that occur during the process.
*
* @param {Object} widget - The widget object to be removed.
* @action
*/
@action removeWidget(widget) {
const { dashboard } = this.args;
if (dashboard) {
dashboard.removeWidget(widget.id).catch((error) => {
this.notifications.serverError(error);
});
}
}
/**
* Computed property that returns grid options based on the current edit state.
* Configures grid behavior such as floating, animation, and drag and resize capabilities.
*
* @computed
* @returns {Object} An object containing grid configuration options.
*/
@computed('args.isEdit') get gridOptions() {
return {
float: true,
animate: true,
acceptWidgets: true,
alwaysShowResizeHandle: this.args.isEdit,
disableDrag: !this.args.isEdit,
disableResize: !this.args.isEdit,
resizable: { handles: 'all' },
cellHeight: 30,
};
}
}

View File

@@ -0,0 +1,36 @@
{{#if this.isImpersonator}}
<EmberWormhole @to="view-header-actions">
<div class="next-user-button locale-selector-tray" ...attributes>
<BasicDropdown
class={{@wrapperClass}}
@onOpen={{@onOpen}}
@onClose={{@onClose}}
@calculatePosition={{this.calculatePosition}}
@verticalPosition={{@verticalPosition}}
@horizontalPosition={{@horizontalPosition}}
@renderInPlace={{or @renderInPlace (not (media "isMobile"))}}
as |dd|
>
<dd.Trigger class="{{@triggerClass}} local-selector-tray-trigger {{if (media 'isMobile') 'is-mobile'}}">
<div class="next-org-button-trigger flex-shrink-0 {{if dd.isOpen 'is-open'}}">
<FaIcon @icon="user-secret" @size="sm" />
</div>
</dd.Trigger>
<dd.Content class="{{@contentClass}} locale-selector-tray-content {{if (media 'isMobile') 'is-mobile'}}">
<div class="next-dd-menu {{@dropdownMenuClass}} {{if dd.isOpen 'is-open'}}">
<div class="px-1">
<a href="javascript:;" class="next-dd-item" {{on "click" this.restoreSession}}>
<div class="flex flex-row items-centerw-full">
<div class="w-6">
<FaIcon @icon="person-walking-arrow-loop-left" @size="sm" />
</div>
<div>End Impersonation</div>
</div>
</a>
</div>
</div>
</dd.Content>
</BasicDropdown>
</div>
</EmberWormhole>
{{/if}}

View File

@@ -0,0 +1,39 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { later } from '@ember/runloop';
export default class ImpersonatorTrayComponent extends Component {
@service session;
@service notifications;
@service router;
@service fetch;
get isImpersonator() {
return typeof this.session.data?.authenticated?.impersonator === 'string';
}
/**
* Restore session
*
* @memberof ConsoleAdminOrganizationsIndexUsersController
*/
@action async restoreSession() {
try {
const { token } = await this.fetch.delete('auth/impersonate');
await this.router.transitionTo('console');
this.session.manuallyAuthenticate(token);
this.notifications.info(`Ending impersonation session.`);
later(
this,
() => {
window.location.reload();
},
600
);
} catch (error) {
this.notifications.serverError(error);
}
}
}

View File

@@ -0,0 +1,12 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container pt-0i text-gray-900 dark:text-white">
<InputGroup @name="Organization name" @value={{@options.organization.name}} />
<InputGroup @name="Organization description" @value={{@options.organization.description}} />
<InputGroup @name="Organization phone number">
<PhoneInput @value={{@options.organization.phone}} @onInput={{fn (mut @options.organization.phone)}} class="form-input w-full" />
</InputGroup>
<InputGroup @name="Organization currency">
<CurrencySelect @value={{@options.organization.currency}} @onSelect={{fn (mut @options.organization.currency)}} @triggerClass="w-full form-select" />
</InputGroup>
</div>
</Modal::Default>

View File

@@ -0,0 +1,3 @@
import Component from '@glimmer/component';
export default class ModalsEditOrganizationComponent extends Component {}

View File

@@ -0,0 +1,43 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container pt-0i text-gray-900 dark:text-white">
{{#if @options.isOwner}}
{{#if @options.hasOtherMembers}}
<p>
<div class="text-base mb-2">
As the owner of
<strong>{{@options.organization.name}}</strong>, leaving the organization requires you to nominate a new owner.
</div>
<div>Please select a member from the dropdown below to transfer ownership before you can proceed.</div>
</p>
<InputGroup @name="Select a New Owner" @wrapperClass="mt-2 mb-0i">
<Select
@options={{@options.organization.users}}
@value={{@options.newOwnerId}}
@onSelect={{@options.selectNewOwner}}
@optionLabel="name"
@optionValue="id"
@placeholder="Select a member"
/>
</InputGroup>
{{else if @options.willBeDeleted}}
<p>
<div class="text-base mb-2">
You are the sole owner of
<strong>{{@options.organization.name}}</strong>.
</div>
<div>By leaving, the organization will be permanently deleted along with all its data.</div>
<div>Are you sure you want to proceed?</div>
</p>
<p class="mt-3"><em>This action cannot be undone.</em></p>
{{/if}}
{{else}}
<p>
<div class="text-base mb-2">
Are you sure you want to leave the organization
<strong>{{@options.organization.name}}</strong>?
</div>
<div>You will no longer have access to its resources and settings.</div>
</p>
{{/if}}
</div>
</Modal::Default>

View File

@@ -0,0 +1,3 @@
import Component from '@glimmer/component';
export default class ModalsLeaveOrganizationComponent extends Component {}

View File

@@ -4,25 +4,8 @@ import { tracked } from '@glimmer/tracking';
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;
/**

View File

@@ -5,53 +5,12 @@ import { action } from '@ember/object';
import pathToRoute from '@fleetbase/ember-core/utils/path-to-route';
export default class AuthLoginController extends Controller {
/**
* Inject the `forgotPassword` controller
*
* @var {Controller}
*/
@controller('auth.forgot-password') forgotPasswordController;
/**
* Inject the `notifications` service
*
* @var {Service}
*/
@service notifications;
/**
* Inject the `urlSearchParams` service
*
* @var {Service}
*/
@service urlSearchParams;
/**
* Inject the `session` service
*
* @var {Service}
*/
@service session;
/**
* Inject the `router` service
*
* @var {Service}
*/
@service router;
/**
* Inject the `intl` service
*
* @var {Service}
*/
@service intl;
/**
* Inject the `fetch` service
*
* @var {Service}
*/
@service fetch;
/**
@@ -110,8 +69,20 @@ export default class AuthLoginController extends Controller {
*/
@tracked failedAttempts = 0;
/**
* Authentication token.
*
* @memberof AuthLoginController
*/
@tracked token;
/**
* Action to login user.
*
* @param {Event} event
* @return {void}
* @memberof AuthLoginController
*/
@action async login(event) {
// firefox patch
event.preventDefault();

View File

@@ -4,32 +4,9 @@ import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
export default class AuthResetPasswordController extends Controller {
/**
* Inject the `fetch` service
*
* @memberof AuthResetPasswordController
*/
@service fetch;
/**
* Inject the `notifications` service
*
* @memberof AuthResetPasswordController
*/
@service notifications;
/**
* Inject the `router` service
*
* @memberof AuthResetPasswordController
*/
@service router;
/**
* Inject the `intl` service
*
* @memberof AuthResetPasswordController
*/
@service intl;
/**

View File

@@ -3,118 +3,27 @@ import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { later } from '@ember/runloop';
import { not } from '@ember/object/computed';
import { task } from 'ember-concurrency';
export default class AuthVerificationController extends Controller {
/**
* Inject the `fetch` service
*
* @memberof OnboardIndexController
*/
@service fetch;
/**
* Inject the `notifications` service
*
* @memberof OnboardIndexController
*/
@service notifications;
/**
* Inject the `modalsManager` service
*
* @memberof OnboardIndexController
*/
@service modalsManager;
/**
* Inject the `currentUser` service
*
* @memberof OnboardIndexController
*/
@service currentUser;
/**
* Inject the `router` service
*
* @memberof OnboardIndexController
*/
@service router;
/**
* Inject the `session` service
*
* @memberof OnboardIndexController
*/
@service session;
@service intl;
/**
* The session paramerer.
*
* @memberof OnboardVerifyEmailController
*/
/** props */
@tracked hello;
/**
* The token paramerer.
*
* @memberof OnboardVerifyEmailController
*/
@tracked token;
/**
* The loading state of the verification request.
*
* @memberof OnboardVerifyEmailController
*/
@tracked isLoading = false;
/**
* Validation state tracker.
*
* @memberof OnboardVerifyEmailController
*/
@tracked isReadyToSubmit = false;
/**
* The request timeout to trigger alternative verification options.
*
* @memberof OnboardVerifyEmailController
*/
@tracked waitTimeout = 1000 * 60 * 1.25;
/**
* Determines if Fleetbase is still awaiting verification after a certain time.
*
* @memberof OnboardVerifyEmailController
*/
@tracked stillWaiting = false;
/**
* the input code.
*
* @memberof OnboardVerifyEmailController
*/
@tracked code;
@tracked email;
@tracked isReadyToSubmit = false;
@tracked waitTimeout = 1000 * 60 * 1.25;
@tracked stillWaiting = false;
@tracked queryParams = ['hello', 'token', 'code'];
/**
* The query param for the session token.
*
* @memberof OnboardVerifyEmailController
*/
@tracked queryParams = ['hello', 'token'];
/**
* The boolean opposite of `isReadyToSubmit`
*
* @memberof OnboardVerifyEmailController
*/
@not('isReadyToSubmit') isNotReadyToSubmit;
/**
* Creates an instance of OnboardVerifyEmailController.
* @memberof OnboardVerifyEmailController
*/
constructor() {
super(...arguments);
@@ -127,21 +36,10 @@ export default class AuthVerificationController extends Controller {
);
}
/**
* Allow user to manually trigger no code received prompt.
*
* @memberof AuthVerificationController
*/
@action onDidntReceiveCode() {
this.stillWaiting = true;
}
/**
* Validates the input
*
* @param {InputEvent} { target: { value } }
* @memberof OnboardVerifyEmailController
*/
@action validateInput({ target: { value } }) {
if (value.length > 5) {
this.isReadyToSubmit = true;
@@ -150,12 +48,6 @@ export default class AuthVerificationController extends Controller {
}
}
/**
* Validates input on the first render
*
* @param {HTMLElement} el
* @memberof AuthVerificationController
*/
@action validateInitInput(el) {
const value = el.value;
if (value.length > 5) {
@@ -165,89 +57,70 @@ export default class AuthVerificationController extends Controller {
}
}
/**
* Submits to verify code.
*
* @return {Promise}
* @memberof OnboardVerifyEmailController
*/
@action verifyCode() {
const { token, code, email } = this;
@task *verifyCode() {
try {
const { status, token } = yield this.fetch.post('auth/verify-email', { token: this.token, code: this.code, email: this.email, authenticate: true });
if (status === 'ok') {
this.notifications.success('Email successfully verified!');
this.isLoading = true;
if (token) {
this.notifications.info(`Welcome to ${this.intl.t('app.name')}`);
this.session.manuallyAuthenticate(token);
return this.fetch
.post('auth/verify-email', { token, code, email, authenticate: true })
.then(({ status, token }) => {
if (status === 'ok') {
this.notifications.success('Email successfully verified!');
if (token) {
this.notifications.info('Welcome to Fleetbase!');
this.session.manuallyAuthenticate(token);
return this.router.transitionTo('console');
}
return this.router.transitionTo('auth.login');
return this.router.transitionTo('console');
}
})
.catch((error) => {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
return this.router.transitionTo('auth.login');
}
} catch (error) {
this.notifications.serverError(error);
}
}
/**
* Action to resend verification code by SMS.
*
* @memberof OnboardVerifyEmailController
*/
@action resendBySms() {
this.modalsManager.show('modals/verify-by-sms', {
title: 'Verify Account by Phone',
acceptButtonText: 'Send',
phone: this.currentUser.phone,
confirm: (modal) => {
confirm: async (modal) => {
modal.startLoading();
const phone = modal.getOption('phone');
if (!phone) {
this.notifications.error('No phone number provided.');
}
return this.fetch
.post('onboard/send-verification-sms', { phone, session: this.hello })
.then(() => {
this.notifications.success('Verification code SMS sent!');
})
.catch((error) => {
this.notifications.serverError(error);
});
try {
await this.fetch.post('onboard/send-verification-sms', { phone, session: this.hello });
this.notifications.success('Verification code SMS sent!');
modal.done();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
});
}
/**
* Action to resend verification code by email.
*
* @memberof OnboardVerifyEmailController
*/
@action resendEmail() {
this.modalsManager.show('modals/resend-verification-email', {
title: 'Resend Verification Code',
acceptButtonText: 'Send',
email: this.currentUser.email,
confirm: (modal) => {
confirm: async (modal) => {
modal.startLoading();
const email = modal.getOption('email');
if (!email) {
this.notifications.error('No email number provided.');
}
return this.fetch
.post('onboard/send-verification-email', { email, session: this.hello })
.then(() => {
this.notifications.success('Verification code email sent!');
})
.catch((error) => {
this.notifications.serverError(error);
});
try {
await this.fetch.post('onboard/send-verification-email', { email, session: this.hello });
this.notifications.success('Verification code email sent!');
modal.done();
} catch (error) {
this.notifications.serverError(error);
modal.stopLoading();
}
},
});
}

View File

@@ -50,7 +50,7 @@ export default class ConsoleController extends Controller {
*
* @var {Array}
*/
@tracked hiddenSidebarRoutes = ['console.home', 'console.notifications'];
@tracked hiddenSidebarRoutes = ['console.home', 'console.notifications', 'console.virtual'];
/**
* Menu items to be added to the main header navigation bar.
@@ -155,13 +155,13 @@ 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();
}
/**
* Action to invalidate and log user out
* Action to create or join an organization.
*
* @void
*/

View File

@@ -12,32 +12,9 @@ import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
* @extends Controller
*/
export default class ConsoleAccountAuthController extends Controller {
/**
* Service for handling data fetching.
*
* @type {fetch}
*/
@service fetch;
/**
* Service for displaying notifications.
*
* @type {notifications}
*/
@service notifications;
/**
* Service for managing application routing.
*
* @type {router}
*/
@service router;
/**
* Service for managing modals.
*
* @type {router}
*/
@service modalsManager;
/**
@@ -187,14 +164,12 @@ export default class ConsoleAccountAuthController extends Controller {
* @param {Object} twoFaSettings - User-specific two-factor authentication settings.
*/
@task *saveUserTwoFaSettings(twoFaSettings = {}) {
yield this.fetch
.post('users/two-fa', { twoFaSettings })
.then(() => {
this.notifications.success('2FA Settings saved successfully.');
})
.catch((error) => {
this.notifications.serverError(error);
});
try {
yield this.fetch.post('users/two-fa', { twoFaSettings });
this.notifications.success('2FA Settings saved successfully.');
} catch (error) {
this.notifications.serverError(error);
}
}
/**
@@ -203,13 +178,17 @@ export default class ConsoleAccountAuthController extends Controller {
* @method loadUserTwoFaSettings
*/
@task *loadUserTwoFaSettings() {
const twoFaSettings = yield this.fetch.get('users/two-fa');
try {
const twoFaSettings = yield this.fetch.get('users/two-fa');
if (twoFaSettings) {
this.isUserTwoFaEnabled = twoFaSettings.enabled;
this.twoFaSettings = twoFaSettings;
}
if (twoFaSettings) {
this.isUserTwoFaEnabled = twoFaSettings.enabled;
this.twoFaSettings = twoFaSettings;
return twoFaSettings;
} catch (error) {
this.notifications.serverError(error);
}
return twoFaSettings;
}
/**
@@ -218,12 +197,16 @@ export default class ConsoleAccountAuthController extends Controller {
* @method loadSystemTwoFaConfig
*/
@task *loadSystemTwoFaConfig() {
const twoFaConfig = yield this.fetch.get('two-fa/config');
try {
const twoFaConfig = yield this.fetch.get('two-fa/config');
if (twoFaConfig) {
this.isSystemTwoFaEnabled = twoFaConfig.enabled;
this.twoFaConfig = twoFaConfig;
}
if (twoFaConfig) {
this.isSystemTwoFaEnabled = twoFaConfig.enabled;
this.twoFaConfig = twoFaConfig;
return twoFaConfig;
} catch (error) {
this.notifications.serverError(error);
}
return twoFaConfig;
}
}

View File

@@ -0,0 +1,203 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { later } from '@ember/runloop';
import { htmlSafe } from '@ember/template';
export default class ConsoleAccountOrganizationsController extends Controller {
@service currentUser;
@service modalsManager;
@service crud;
@service notifications;
@service intl;
@service fetch;
@service router;
@action async leaveOrganization(organization) {
const isOwner = this.currentUser.id === organization.owner_uuid;
const hasOtherMembers = organization.users_count > 1;
const willBeDeleted = isOwner && organization.users_count === 1;
if (this.model.length === 1) {
return this.notifications.warning('Unable to leave your only organization.');
}
if (hasOtherMembers) {
organization.loadUsers({ exclude: [this.currentUser.id] });
}
this.modalsManager.show('modals/leave-organization', {
title: isOwner ? (willBeDeleted ? 'Delete Organization' : 'Transfer Ownership and Leave') : 'Leave Organization',
acceptButtonText: isOwner ? (willBeDeleted ? 'Delete Organization' : 'Transfer Ownership and Leave') : 'Leave Organization',
acceptButtonScheme: 'danger',
acceptButtonIcon: isOwner ? (willBeDeleted ? 'trash' : 'person-walking-arrow-right') : 'person-walking-arrow-right',
acceptButtonDisabled: isOwner && hasOtherMembers,
isOwner,
hasOtherMembers,
willBeDeleted,
organization,
newOwnerId: null,
selectNewOwner: (newOwnerId) => {
this.modalsManager.setOption('newOwnerId', newOwnerId);
this.modalsManager.setOption('acceptButtonDisabled', false);
},
confirm: async (modal) => {
modal.startLoading();
if (isOwner) {
if (hasOtherMembers) {
const newOwnerId = this.modalsManager.getOption('newOwnerId');
try {
await organization.transferOwnership(newOwnerId, { leave: true });
} catch (error) {
this.notifications.serverError(error);
}
return this.router.refresh();
}
if (willBeDeleted) {
try {
await organization.destroyRecord();
} catch (error) {
this.notifications.serverError(error);
}
return this.router.refresh();
}
}
try {
await organization.leave();
} catch (error) {
this.notifications.serverError(error);
}
return this.router.refresh();
},
});
}
@action switchOrganization(organization) {
this.modalsManager.confirm({
title: this.intl.t('console.switch-organization.modal-title', { organizationName: organization.name }),
body: this.intl.t('console.switch-organization.modal-body'),
acceptButtonText: this.intl.t('console.switch-organization.modal-accept-button-text'),
acceptButtonScheme: 'primary',
confirm: async (modal) => {
modal.startLoading();
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);
}
},
});
}
@action deleteOrganization(organization) {
const isOwner = this.currentUser.id === organization.owner_uuid;
if (this.model.length === 1) {
return this.notifications.warning('Unable to delete your only organization.');
}
if (!isOwner) {
return this.notifications.warning('You do not have rights to delete this organization.');
}
this.crud.delete(organization, {
title: `Are you sure you want to delete the organization ${organization.name}?`,
body: htmlSafe(
`This action will permanently remove all data, including orders, members, and settings associated with the organization. <br /><br /><strong>This action cannot be undone.</strong>`
),
acceptButtonText: 'Delete Organization',
acceptButtonScheme: 'danger',
acceptButtonIcon: 'trash',
confirm: async (modal) => {
modal.startLoading();
try {
await organization.destroyRecord();
return this.router.refresh();
} catch (error) {
this.notifications.serverError(error);
}
},
});
}
@action editOrganization(organization) {
this.modalsManager.show('modals/edit-organization', {
title: 'Edit Organization',
acceptButtonText: 'Save Changes',
acceptButtonIcon: 'save',
isOwner: this.currentUser.id === organization.owner_uuid,
organization,
confirm: async (modal) => {
modal.startLoading();
try {
await organization.save();
return this.router.refresh();
} catch (error) {
this.notifications.serverError(error);
}
},
});
}
@action createOrganization() {
const currency = this.currentUser.currency;
const country = this.currentUser.country;
this.modalsManager.show('modals/edit-organization', {
title: 'Create Organization',
acceptButtonText: this.intl.t('common.confirm'),
acceptButtonIcon: 'check',
acceptButtonIconPrefix: 'fas',
organization: {
name: null,
decription: null,
phone: null,
currency,
country,
timezone: null,
},
confirm: async (modal) => {
modal.startLoading();
const organization = modal.getOption('organization');
const { name, description, phone, currency, country, timezone } = organization;
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 this.router.refresh();
} catch (error) {
modal.stopLoading();
return this.notifications.serverError(error);
}
},
});
}
}

View File

@@ -0,0 +1,7 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class ConsoleAccountVirtualController extends Controller {
@tracked view;
queryParams = ['view'];
}

View File

@@ -10,41 +10,10 @@ import { action } from '@ember/object';
* @extends Controller
*/
export default class ConsoleAdminOrganizationsController extends Controller {
/**
* The Ember Data service for interacting with the store.
*
* @property {Service} store
* @type {Object}
*/
@service store;
/**
* Inject the `intl` service
*
* @var {Service}
*/
@service intl;
/**
* The Ember Router service for handling transitions between routes.
*
* @property {Service} router
* @type {Object}
*/
@service router;
/**
* Inject the `filters` service
*
* @var {Service}
*/
@service filters;
/**
* Inject the `crud` service
*
* @var {Service}
*/
@service crud;
/**

View File

@@ -2,28 +2,16 @@ import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { later } from '@ember/runloop';
export default class ConsoleAdminOrganizationsIndexUsersController extends Controller {
/**
* Inject the `filters` service
*
* @var {Service}
*/
@service filters;
/**
* Inject the `intl` service
*
* @var {Service}
*/
@service intl;
/**
* Inject the `router` service
*
* @var {Service}
*/
@service router;
@service fetch;
@service notifications;
@service modalsManager;
@service session;
/**
* The current page of data being viewed
@@ -84,6 +72,10 @@ export default class ConsoleAdminOrganizationsIndexUsersController extends Contr
label: this.intl.t('common.name'),
valuePath: 'name',
},
{
label: this.intl.t('common.role'),
valuePath: 'roleName',
},
{
label: this.intl.t('common.phone-number'),
valuePath: 'phone',
@@ -97,8 +89,71 @@ export default class ConsoleAdminOrganizationsIndexUsersController extends Contr
valuePath: 'status',
cellComponent: 'table/cell/status',
},
{
label: '',
cellComponent: 'table/cell/dropdown',
ddButtonText: false,
ddButtonIcon: 'ellipsis-h',
ddButtonIconPrefix: 'fas',
ddMenuLabel: 'User Actions',
cellClassNames: 'overflow-visible',
wrapperClass: 'flex items-center justify-end mx-2',
width: '9%',
actions: [
{
label: 'Impersonate',
icon: 'user-secret',
fn: this.impersonateUser,
},
{
label: 'Change Password',
icon: 'lock-open',
fn: this.changeUserPassword,
},
],
sortable: false,
filterable: false,
resizable: false,
searchable: false,
},
];
/**
* Impersonate the selected user.
*
* @param {UserModel} user
* @memberof ConsoleAdminOrganizationsIndexUsersController
*/
@action async impersonateUser(user) {
try {
const { token } = await this.fetch.post('auth/impersonate', { user: user.id });
await this.router.transitionTo('console');
this.session.manuallyAuthenticate(token);
this.notifications.info(`Now impersonating ${user.email}...`);
later(
this,
() => {
window.location.reload();
},
600
);
} catch (error) {
this.notifications.serverError(error);
}
}
/**
* Change password for a user
*
* @void
*/
@action changeUserPassword(user) {
this.modalsManager.show('modals/change-user-password', {
keepOpen: true,
user,
});
}
/**
* Update search query param and reset page to 1
*

View File

@@ -0,0 +1,7 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class ConsoleAdminVirtualController extends Controller {
@tracked view;
queryParams = ['view'];
}

View File

@@ -1,3 +1,26 @@
import Controller from '@ember/controller';
export default class ConsoleHomeController extends Controller {}
export default class ConsoleHomeController extends Controller {
rows = [
{
name: 'Jason',
age: 24,
vehicle: 'Honda',
},
];
columns = [
{
label: 'Name',
valuePath: 'name',
},
{
label: 'Age',
valuePath: 'age',
},
{
label: 'Vehicle',
valuePath: 'vehicle',
},
];
}

View File

@@ -0,0 +1,7 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class ConsoleSettingsVirtualController extends Controller {
@tracked view;
queryParams = ['view'];
}

View File

@@ -0,0 +1,7 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class ConsoleVirtualController extends Controller {
@tracked view;
queryParams = ['view'];
}

View File

@@ -1,39 +1,40 @@
import AuthVerificationController from '../auth/verification';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { later } from '@ember/runloop';
import { not } from '@ember/object/computed';
import { task } from 'ember-concurrency';
export default class OnboardVerifyEmailController extends AuthVerificationController {
/**
* Submits to verify code.
*
* @return {Promise}
* @memberof OnboardVerifyEmailController
*/
@action verifyCode() {
const { hello, code } = this;
@service fetch;
@service notifications;
@service session;
@service currentUser;
@service router;
this.isLoading = true;
/** props */
@tracked hello;
@tracked code;
@tracked queryParams = ['hello', 'code'];
return this.fetch
.post('onboard/verify-email', { session: hello, code })
.then(({ status, token }) => {
if (status === 'ok') {
this.notifications.success('Email successfully verified!');
@task *verifyCode() {
try {
const { status, token } = yield this.fetch.post('onboard/verify-email', { session: this.hello, code: this.code });
if (status === 'ok') {
this.notifications.success('Email successfully verified!');
if (token) {
this.notifications.info('Welcome to Fleetbase!');
this.session.manuallyAuthenticate(token);
if (token) {
this.notifications.info('Welcome to Fleetbase!');
this.session.manuallyAuthenticate(token);
return this.router.transitionTo('console');
}
return this.router.transitionTo('auth.login');
return this.router.transitionTo('console');
}
})
.catch((error) => {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
return this.router.transitionTo('auth.login');
}
} catch (error) {
this.notifications.serverError(error);
}
}
}

View File

@@ -1,7 +0,0 @@
import { helper } from '@ember/component/helper';
export default helper(function spreadWidgetOptions([params]) {
const { id, options } = params;
const gridOptions = { id, ...options };
return gridOptions;
});

View File

@@ -22,7 +22,12 @@
</head>
<body>
{{content-for "body"}}
<div id="boot-loader" class="overloader">
<div class="loader-container">
<span class="fleetbase-loader" width="16" height="16"></span>
<div class="loading-message">Starting up...</div>
</div>
</div>
<script src="{{rootURL}}assets/vendor.js"></script>
<script src="{{rootURL}}assets/@fleetbase/console.js"></script>

View File

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

View File

@@ -0,0 +1,18 @@
export function initialize(application) {
const leafletService = application.lookup('service:leaflet');
if (leafletService) {
leafletService.load({
onReady: function (L) {
// This will prevent the awkward scroll bug produced by Chrome browsers
// https://github.com/Leaflet/Leaflet/issues/4125#issuecomment-356289643
L.Control.include({
_refocusOnMap: L.Util.falseFn,
});
},
});
}
}
export default {
initialize,
};

View File

@@ -1,5 +1,6 @@
import Model, { attr, belongsTo } from '@ember-data/model';
import { computed } from '@ember/object';
import { getOwner } from '@ember/application';
import { format, formatDistanceToNow } from 'date-fns';
import autoSerialize from '../utils/auto-serialize';
@@ -34,6 +35,7 @@ export default class Company extends Model {
@attr('string') slug;
/** @dates */
@attr('date') joined_at;
@attr('date') deleted_at;
@attr('date') created_at;
@attr('date') updated_at;
@@ -71,4 +73,32 @@ export default class Company extends Model {
toJSON() {
return autoSerialize(this);
}
async transferOwnership(newOwner, params = {}) {
const owner = getOwner(this);
const fetch = owner.lookup('service:fetch');
return fetch.post('companies/transfer-ownership', { company: this.id, newOwner, ...params });
}
async leave(user = null, params = {}) {
const owner = getOwner(this);
const fetch = owner.lookup('service:fetch');
return fetch.post('companies/leave', { company: this.id, user, ...params });
}
async loadUsers(params = {}) {
const owner = getOwner(this);
const fetch = owner.lookup('service:fetch');
try {
const users = await fetch.get(`companies/${this.id}/users`, { ...params }, { normalizeToEmberData: true, normalizeModelType: 'user' });
this.set('users', users);
return users;
} catch (error) {
this.set('users', []);
return [];
}
}
}

View File

@@ -83,4 +83,14 @@ export default class DashboardModel extends Model {
});
}
}
getRegistry() {
const owner = getOwner(this);
const universe = owner.lookup('service:universe');
if (universe) {
return universe.getDashboardRegistry(this.id);
}
return undefined;
}
}

View File

@@ -1,3 +1,4 @@
import { set } from '@ember/object';
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
import { computed, get } from '@ember/object';
import { not } from '@ember/object/computed';
@@ -23,12 +24,17 @@ 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('boolean') is_subscribed;
@attr('boolean') is_trialing;
@attr('raw') meta;
@attr('raw') subscription;
/** @relationships */
@belongsTo('role') role;
@@ -39,6 +45,7 @@ export default class UserModel extends Model {
@attr('date') last_seen_at;
@attr('date') phone_verified_at;
@attr('date') email_verified_at;
@attr('date') trial_ends_at;
@attr('date') last_login;
@attr('date') deleted_at;
@attr('date') created_at;
@@ -67,6 +74,17 @@ export default class UserModel extends Model {
});
}
verify() {
const owner = getOwner(this);
const fetch = owner.lookup('service:fetch');
return fetch.patch(`users/verify/${this.id}`).then((response) => {
set(this, 'email_verified_at', response.email_verified_at);
return response;
});
}
removeFromCurrentCompany() {
const owner = getOwner(this);
const fetch = owner.lookup('service:fetch');

View File

@@ -7,15 +7,18 @@ export default class Router extends EmberRouter {
}
Router.map(function () {
this.route('virtual', { path: '/:slug' });
this.route('install');
this.route('onboard', function () {
this.route('verify-email');
});
this.route('auth', function () {
this.route('login', { path: '/' });
this.route('forgot-password');
this.route('reset-password', { path: '/reset-password/:id' });
this.route('two-fa');
this.route('verification');
});
this.route('onboard', function () {
this.route('verify-email');
this.route('portal-login', { path: '/portal' });
});
this.route('invite', { path: 'join' }, function () {
this.route('for-driver', { path: '/fleet/:public_id' });
@@ -23,24 +26,23 @@ Router.map(function () {
});
this.route('console', { path: '/' }, function () {
this.route('home', { path: '/' });
this.route('extensions');
this.route('notifications');
this.route('account', function () {
this.route('virtual', { path: '/:slug/:view' });
this.route('virtual', { path: '/:slug' });
this.route('auth');
});
this.route('settings', function () {
this.route('virtual', { path: '/:slug/:view' });
this.route('virtual', { path: '/:slug' });
this.route('two-fa');
});
this.route('virtual', { path: '/:slug/:view' });
this.route('virtual', { path: '/:slug' });
this.route('admin', function () {
this.route('config', function () {
this.route('database');
this.route('cache');
this.route('filesystem');
this.route('mail');
this.route('notification-channels');
this.route('notification-channels', { path: '/push-notifications' });
this.route('queue');
this.route('services');
this.route('socket');
@@ -48,12 +50,16 @@ Router.map(function () {
this.route('branding');
this.route('notifications');
this.route('two-fa-settings');
this.route('virtual', { path: '/:slug/:view' });
this.route('virtual', { path: '/:slug' });
this.route('organizations', function () {
this.route('index', { path: '/' });
this.route('users', { path: '/:company_id' });
this.route('index', { path: '/' }, function () {
this.route('users', { path: '/:public_id/users' });
});
});
this.route('schedule-monitor', function () {
this.route('logs', { path: '/:id/logs' });
});
});
});
this.route('install');
this.route('catch', { path: '/*' });
});

View File

@@ -1,8 +1,10 @@
import Route from '@ember/routing/route';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import isElectron from '@fleetbase/ember-core/utils/is-electron';
import pathToRoute from '@fleetbase/ember-core/utils/path-to-route';
import removeBootLoader from '../utils/remove-boot-loader';
export default class ApplicationRoute extends Route {
@service session;
@@ -11,9 +13,41 @@ export default class ApplicationRoute extends Route {
@service urlSearchParams;
@service modalsManager;
@service intl;
@service currentUser;
@service router;
@service universe;
@tracked defaultTheme;
/**
* Handle the transition into the application.
*
* @memberof ApplicationRoute
*/
@action willTransition(transition) {
this.universe.callHooks('application:will-transition', this.session, this.router, transition);
}
/**
* On application route activation
*
* @memberof ApplicationRoute
* @void
*/
@action activate() {
this.initializeTheme();
this.initializeLocale();
}
/**
* The application loading event.
* Here will just run extension hooks.
*
* @memberof ApplicationRoute
*/
@action loading(transition) {
this.universe.callHooks('application:loading', this.session, this.router, transition);
}
/**
* Check the installation status of Fleetbase and transition user accordingly.
*
@@ -43,24 +77,38 @@ export default class ApplicationRoute extends Route {
* @return {Transition}
* @memberof ApplicationRoute
*/
async beforeModel() {
async beforeModel(transition) {
await this.session.setup();
await this.universe.booting();
this.universe.callHooks('application:before-model', this.session, this.router, transition);
const { isAuthenticated } = this.session;
const shift = this.urlSearchParams.get('shift');
if (isAuthenticated && shift) {
if (this.session.isAuthenticated && shift) {
return this.router.transitionTo(pathToRoute(shift));
}
}
/**
* On application route activation
* Remove boot loader if not authenticated.
*
* @memberof ApplicationRoute
* @void
*/
activate() {
afterModel() {
if (!this.session.isAuthenticated) {
removeBootLoader();
}
}
/**
* 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 +116,18 @@ export default class ApplicationRoute extends Route {
}
this.theme.initialize({ bodyClassNames, theme: this.defaultTheme });
this.intl.setLocale(['en-us']);
}
/**
* Initializes the application's locale settings based on the current user's preferences.
*
* This method retrieves the user's preferred locale using the `getOption` method from the `currentUser` service.
* If no locale is set by the user, it defaults to `'en-us'`. It then sets the application's locale by calling
* the `setLocale` method of the `intl` service with the retrieved locale.
*/
initializeLocale() {
const locale = this.currentUser.getOption('locale', 'en-us');
this.intl.setLocale([locale]);
}
/**

View File

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

View File

@@ -3,6 +3,7 @@ import { inject as service } from '@ember/service';
export default class AuthLoginRoute extends Route {
@service session;
@service universe;
/**
* If user is authentication redirect to console.
@@ -10,7 +11,8 @@ export default class AuthLoginRoute extends Route {
* @memberof AuthLoginRoute
* @void
*/
beforeModel() {
beforeModel(transition) {
this.session.prohibitAuthentication('console');
return this.universe.virtualRouteRedirect(transition, 'auth:login', 'virtual', { restoreQueryParams: true });
}
}

View File

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

View File

@@ -1,11 +1,16 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import removeBootLoader from '../utils/remove-boot-loader';
import '@fleetbase/leaflet-routing-machine';
export default class ConsoleRoute extends Route {
@service store;
@service session;
@service universe;
@service router;
@service currentUser;
@service intl;
/**
* Require authentication to access all `console` routes.
@@ -14,10 +19,35 @@ 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);
}
}
/**
* Register after model hook.
*
* @param {DS.Model} model
* @param {Transition} transition
* @memberof ConsoleRoute
*/
async afterModel(model, transition) {
this.universe.callHooks('console:after-model', this.session, this.router, model, transition);
removeBootLoader();
}
/**
* Route did complete transition.
*
* @memberof ConsoleRoute
*/
@action didTransition() {
this.universe.callHooks('console:did-transition', this.session, this.router);
}
/**

View File

@@ -0,0 +1,10 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ConsoleAccountOrganizationsRoute extends Route {
@service currentUser;
model() {
return this.currentUser.loadOrganizations();
}
}

View File

@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
export default class ConsoleAccountVirtualRoute extends Route {
@service universe;
model({ slug, view }) {
return this.universe.lookupMenuItemFromRegistry('account', slug, view);
queryParams = {
view: {
refreshModel: true,
},
};
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console:account', slug, view);
}
}

View File

@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
export default class ConsoleAdminVirtualRoute extends Route {
@service universe;
model({ slug, view }) {
return this.universe.lookupMenuItemFromRegistry('admin', slug, view);
queryParams = {
view: {
refreshModel: true,
},
};
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console:admin', slug, view);
}
}

View File

@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
export default class ConsoleSettingsVirtualRoute extends Route {
@service universe;
model({ slug, view }) {
return this.universe.lookupMenuItemFromRegistry('settings', slug, view);
queryParams = {
view: {
refreshModel: true,
},
};
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console:settings', slug, view);
}
}

View File

@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
export default class ConsoleVirtualRoute extends Route {
@service universe;
model({ slug, view }) {
queryParams = {
view: {
refreshModel: true,
},
};
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console', slug, view);
}
}

View File

@@ -0,0 +1,17 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class VirtualRoute extends Route {
@service universe;
queryParams = {
view: {
refreshModel: true,
},
};
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('auth:login', slug, view);
}
}

View File

@@ -27,6 +27,14 @@ export default class UserSerializer extends ApplicationSerializer.extend(Embedde
// delete the password always
delete json.password;
// delete verification attributes
delete json.email_verified_at;
delete json.phone_verified_at;
// delete server managed dates
delete json.deleted_at;
delete json.created_at;
delete json.updated_at;
return json;
}

View File

@@ -1,255 +0,0 @@
import Service from '@ember/service';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency-decorators';
import { action } from '@ember/object';
import { isArray } from '@ember/array';
/**
* Service for managing dashboards, including loading, creating, and deleting dashboards, as well as managing the current dashboard and widget states.
* Utilizes Ember services such as `store`, `fetch`, `notifications`, and `universe` for data management and user interaction.
*
* @extends Service
*/
export default class DashboardService extends Service {
/**
* Ember Data store service for managing model data.
* @type {Service}
*/
@service store;
/**
* Fetch service for making network requests.
* @type {Service}
*/
@service fetch;
/**
* Notifications service for displaying user notifications.
* @type {Service}
*/
@service notifications;
/**
* Universe service for accessing global application state or utility methods.
* @type {Service}
*/
@service universe;
/**
* Internationalization service.
* @type {Service}
*/
@service intl;
/**
* Tracked array of available dashboards.
* @type {Array}
*/
@tracked dashboards = [];
/**
* Tracked property representing the currently selected dashboard.
* @type {Object}
*/
@tracked currentDashboard;
/**
* Tracked boolean indicating if the dashboard is in editing mode.
* @type {boolean}
*/
@tracked isEditingDashboard = false;
/**
* Tracked boolean indicating if a widget is being added.
* @type {boolean}
*/
@tracked isAddingWidget = false;
/**
* Task for loading dashboards from the store. It sets the current dashboard and checks if adding widget is necessary.
*/
@task *loadDashboards() {
const dashboards = yield this.store.findAll('dashboard');
if (isArray(dashboards)) {
this.dashboards = dashboards.toArray();
// insert default dashboard if it's not loaded
const defaultDashboard = this._createDefaultDashboard();
if (this._isDefaultDashboardNotLoaded()) {
this.dashboards.unshiftObject(defaultDashboard);
}
// Set the current dashboard
this.currentDashboard = this._getNextDashboard();
if (this.currentDashboard && this.currentDashboard.widgets.length === 0) {
this.onAddingWidget(true);
}
}
}
/**
* Task for selecting a dashboard. Handles dashboard switching and updates the current dashboard.
* @param {Object} dashboard - The dashboard object to select.
*/
@task *selectDashboard(dashboard) {
if (dashboard.user_uuid === 'system') {
this.currentDashboard = dashboard;
yield this.fetch.post('dashboards/reset-default');
return;
}
const currentDashboard = yield this.fetch.post('dashboards/switch', { dashboard_uuid: dashboard.id }, { normalizeToEmberData: true }).catch((error) => {
this.notifications.serverError(error);
});
if (currentDashboard) {
this.currentDashboard = currentDashboard;
}
}
/**
* Task for creating a new dashboard. It handles dashboard creation, success notification, and dashboard selection.
* @param {string} name - Name of the new dashboard.
*/
@task *createDashboard(name) {
const dashboardRecord = this.store.createRecord('dashboard', { name, is_default: true });
const dashboard = yield dashboardRecord.save().catch((error) => {
this.notifications.serverError(error);
});
if (dashboard) {
this.notifications.success(this.intl.t('services.dashboard-service.create-dashboard-success-notification', { dashboardName: dashboard.name }));
this.selectDashboard.perform(dashboard);
this.dashboards.pushObject(dashboard);
}
}
/**
* Task for deleting a dashboard. Handles dashboard deletion and success notification.
* @param {Object} dashboard - The dashboard object to delete.
* @param {Object} [options={}] - Optional configuration options.
*/
@task *deleteDashboard(dashboard, options = {}) {
yield dashboard.destroyRecord().catch((error) => {
this.notification.serverError(error);
if (typeof options.onError === 'function') {
options.onError(error, dashboard);
}
});
this.notifications.success(this.intl.t('services.dashboard-service.delete-dashboard-success-notification', { dashboardName: dashboard.name }));
yield this.loadDashboards.perform();
yield this.selectDashboard.perform(this._getNextDashboard());
if (typeof options.callback === 'function') {
options.callback(this.currentDashboard);
}
}
/**
* Task for setting the current dashboard.
* @param {Object} dashboard - The dashboard object to set as current.
*/
@task *setCurrentDashboard(dashboard) {
const currentDashboard = yield this.fetch.post('dashboards/switch', { dashboard_uuid: dashboard.id }, { normalizeToEmberData: true }).catch((error) => {
this.notifications.serverError(error);
});
if (currentDashboard) {
this.currentDashboard = currentDashboard;
}
}
/**
* Action to toggle dashboard editing state.
* @param {boolean} [state=true] - State to set for editing.
*/
@action onChangeEdit(state = true) {
this.isEditingDashboard = state;
}
/**
* Action to toggle the state of adding a widget.
* @param {boolean} [state=true] - State to set for adding a widget.
*/
@action onAddingWidget(state = true) {
this.isAddingWidget = state;
}
/**
* Creates a default dashboard with predefined widgets.
* @private
* @returns {Object} The default dashboard object.
*/
_createDefaultDashboard() {
let defaultDashboard;
// check store for default dashboard
const loadedDashboars = this.store.peekAll('dashboard');
// check for default dashboard loaded in store
defaultDashboard = loadedDashboars.find((dashboard) => dashboard.id === 'system');
if (defaultDashboard) {
return defaultDashboard;
}
// create new default dashboard
defaultDashboard = this.store.createRecord('dashboard', {
id: 'system',
uuid: 'system',
name: 'Default Dashboard',
is_default: false,
user_uuid: 'system',
widgets: this._createDefaultDashboardWidgets(),
});
return defaultDashboard;
}
/**
* Creates default widgets for the default dashboard.
* @private
* @returns {Array} An array of default dashboard widgets.
*/
_createDefaultDashboardWidgets() {
const widgets = this.universe.getDefaultDashboardWidgets().map((defaultWidget) => {
return this.store.createRecord('dashboard-widget', defaultWidget);
});
return widgets;
}
/**
* Checks if default dashboard is already loaded.
* @private
* @return {Boolean}
* @memberof DashboardService
*/
_isDefaultDashboardLoaded() {
const defaultDashboard = this._createDefaultDashboard();
return this.dashboards.some((dashboard) => dashboard.id === defaultDashboard.id);
}
/**
* Checks if default dashboard is not already loaded.
* @private
* @return {Boolean}
* @memberof DashboardService
*/
_isDefaultDashboardNotLoaded() {
return !this._isDefaultDashboardLoaded();
}
/**
* Gets the current dasbhoard or next available dashboard.
*
* @return {DashboardModel}
* @memberof DashboardService
*/
_getNextDashboard() {
return this.dashboards.find((dashboard) => dashboard.is_default) || this.dashboards[0];
}
}

View File

@@ -13,16 +13,6 @@ body[data-theme='dark'] .two-fa-enforcement-alert button#two-fa-setup-button.btn
cursor: default;
}
.app-version-in-nav {
display: flex;
align-items: center;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
font-size: 0.8rem;
line-height: 1rem;
padding-left: 1rem;
padding-top: 0.2rem;
}
.fleetbase-pagination-meta-info-wrapper.within-layout-section-header {
display: flex;
flex-direction: row;
@@ -48,3 +38,32 @@ body[data-theme='dark'] .two-fa-enforcement-alert button#two-fa-setup-button.btn
body.console-admin-organizations-index-index .next-table-wrapper > table {
table-layout: auto;
}
#boot-loader {
position: absolute;
z-index: 9999999999;
inset: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
#boot-loader > .loader-container {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
#boot-loader > .loader-container > .loading-message {
margin-left: 0.5rem;
font-weight: 600;
font-size: 0.875rem;
}
body[data-theme='dark'] #boot-loader > .loader-container > .loading-message {
color: #fff;
}

View File

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

View File

@@ -1,34 +1,30 @@
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg">
<div class="mb-4">
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
<LinkTo @route="console" class="flex items-center justify-center">
<LogoIcon @size="12" class="rounded-md" />
</LinkTo>
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{if this.isSent (t "auth.forgot-password.is-sent.title") (t "auth.forgot-password.not-sent.title")}}
</h2>
</div>
{{#if this.isSent}}
<div class="flex px-3 py-2 mb-4 rounded-md shadow-sm bg-green-200">
<div>
<FaIcon @icon="check-circle" @size="lg" class="text-green-900 mr-4" />
</div>
<p class="flex-1 text-sm text-green-900 dark:text-green-900">
<InfoBlock @type="success" @icon="info-circle" @iconSize="lg" @iconClass="mt-1" class="my-6">
<p>
{{t "auth.forgot-password.is-sent.message" htmlSafe=true}}
</p>
</div>
<div class="flex flex-row mt-4">
</InfoBlock>
<div class="flex flex-row">
<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>
<FaIcon @icon="info-circle" @size="lg" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
<InfoBlock @icon="info-circle" @iconSize="lg" @iconClass="mt-1" class="my-6">
<p>
{{t "auth.forgot-password.not-sent.message" htmlSafe=true appName=(t "app.name")}}
</p>
</div>
</InfoBlock>
<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"}}

View File

@@ -1,23 +1,31 @@
<div>
<div class="mx-auto w-12 h-12">
<LogoIcon @url={{@brand.icon_url}} @size="12" class="mx-auto" />
</div>
<LinkTo @route="console" class="flex items-center justify-center">
<LogoIcon @brand={{@brand}} @size="12" class="rounded-md" />
</LinkTo>
<h2 class="mt-6 mb-3 text-3xl font-extrabold leading-9 text-center text-gray-900 dark:text-gray-100">
{{t "auth.login.title"}}
</h2>
</div>
{{#if (gte this.failedAttempts 3)}}
<div class="px-3 py-2 my-6 rounded-md shadow-sm bg-yellow-200">
<div class="flex mb-5">
<div>
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-900 mr-4" />
<div class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
<div class="flex flex-row items-start mb-2">
<div class="w-8 flex-grow-0 flex-shrink-0">
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
</div>
<div class="flex-1">
<p class="flex-1 text-sm text-yellow-100">
{{t "auth.login.failed-attempt.message" htmlSafe=true}}
</p>
</div>
<p class="flex-1 text-sm text-yellow-900 dark:yellow-red-900">
{{t "auth.login.failed-attempt.message" htmlSafe=true}}
</p>
</div>
<Button @text={{t "auth.login.failed-attempt.button-text"}} @type="warning" @onClick={{this.forgotPassword}} />
<Button
@text={{t "auth.login.failed-attempt.button-text"}}
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.forgotPassword}}
/>
</div>
{{/if}}
@@ -65,18 +73,34 @@
href="javascript:;"
{{on "click" this.forgotPassword}}
disabled={{this.isLoading}}
class="font-medium transition duration-150 ease-in-out text-sky-600 hover:text-sky-500 focus:outline-none focus:underline"
class="font-medium transition duration-150 ease-in-out text-sky-500 hover:text-sky-400 focus:outline-none focus:underline"
>
{{t "auth.login.form.forgot-password-label"}}
</a>
</div>
</div>
<div class="mt-6">
<Button @buttonType="submit" @type="primary" @text={{t "auth.login.form.sign-in-button"}} @icon="lock" @wrapperClass="btn-block" @isLoading={{this.isLoading}} @onClick={{this.login}} />
</div>
<div class="mt-3">
<Button @text={{t "auth.login.form.create-account-button"}} @wrapperClass="btn-block" @disabled={{this.isLoading}} @onClick={{fn (transition-to "onboard")}} />
<div class="mt-6 space-y-4">
<Button
@buttonType="submit"
@type="primary"
@text={{t "auth.login.form.sign-in-button"}}
@icon="lock"
@wrapperClass="btn-block"
@isLoading={{this.isLoading}}
@onClick={{this.login}}
/>
<Button @text={{t "auth.login.form.create-account-button"}} @icon="briefcase" @wrapperClass="btn-block" @disabled={{this.isLoading}} @onClick={{fn (transition-to "onboard")}} />
<RegistryYield @type="menu" @registry="auth:login" as |menuItem|>
<Button
@text={{menuItem.title}}
@icon={{menuItem.icon}}
@type={{menuItem.type}}
@wrapperClass={{menuItem.wrapperClass}}
@disabled={{this.isLoading}}
@onClick={{menuItem.onClick}}
@permission={{menuItem.permission}}
/>
</RegistryYield>
</div>
</form>

View File

@@ -1,6 +1,8 @@
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg">
<div class="mb-4">
<Image src={{this.brand.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
<LinkTo @route="console" class="flex items-center justify-center">
<LogoIcon @brand={{@brand}} @size="12" class="rounded-md" />
</LinkTo>
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "auth.reset-password.title"}}
</h2>

View File

@@ -1,5 +1,7 @@
<div class="mb-8 text-center">
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
<LinkTo @route="console" class="flex items-center justify-center">
<LogoIcon @brand={{@brand}} @size="12" class="rounded-md" />
</LinkTo>
<h2 class="text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{if this.isSent "Verification Code"}}
</h2>
@@ -20,7 +22,7 @@
<OtpInput @onInputCompleted={{this.handleOtpInput}} @size={{6}} class="w-full" />
</div>
<div id="otp-countdown-container" class="otp-countdown-container flex {{if this.isCodeExpired "flex-col" "flex-row"}} items-center justify-center min-h-12">
<div id="otp-countdown-container" class="otp-countdown-container flex {{if this.isCodeExpired 'flex-col' 'flex-row'}} items-center justify-center min-h-12">
{{#if this.countdownReady}}
<Countdown @expiry={{this.twoFactorSessionExpiresAfter}} @countdownClass="text-lg" @onCountdownEnd={{this.handleCodeExpired}} />
{{/if}}

View File

@@ -1,46 +1,75 @@
{{page-title (t "auth.verification.header-title")}}
<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"}}>
<div class="mb-6">
<LinkTo @route="console" class="flex items-center justify-center">
<LogoIcon @brand={{@brand}} @size="12" class="rounded-md" />
</LinkTo>
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "auth.verification.title"}}
</h2>
</div>
<div class="flex px-3 py-2 mb-6 rounded-md shadow-sm bg-blue-200">
<div>
<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}}
</p>
</div>
<InfoBlock @type="info" @icon="shield-halved" @iconSize="lg">
{{t "auth.verification.message-text" htmlSafe=true}}
</InfoBlock>
<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}} />
<form class="mt-8 space-y-6" {{on "submit" (perform 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}}
/>
<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.verifyCode.isRunning}}
@disabled={{not this.isReadyToSubmit}}
@onClick={{perform this.verifyCode}}
/>
<a href="javascript:;" {{on "click" this.onDidntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "auth.verification.didnt-receive-a-code"}}</a>
</div>
{{#if this.stillWaiting}}
<div class="bg-yellow-50 rounded shadow-sm border-l-4 border-yellow-400 px-4 py-2">
<div class="flex">
<div class="flex-shrink-0">
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-400" />
<div class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
<div class="flex flex-row items-start mb-2">
<div class="w-8 flex-grow-0 flex-shrink-0">
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
</div>
<div class="ml-3 flex items-center">
<span class="text-lg font-extrabold text-yellow-800">{{t "auth.verification.didnt-receive-a-code"}}</span>
<div class="flex-1">
<div class="flex-1 text-sm text-yellow-100">
<div>{{t "auth.verification.didnt-receive-a-code" htmlSafe=true}}</div>
<div>{{t "auth.verification.not-sent.alternative-choice" htmlSafe=true}}</div>
</div>
</div>
</div>
<div class="py-1">
<p class="text-yellow-700 text-sm">{{t "auth.verification.not-sent.alternative-choice"}}</p>
<div class="flex items-center mt-3">
<Button @buttonType="button" @type="warning" @wrapperClass="mr-2" @onClick={{this.resendEmail}} class="btn-warning-alert">{{t "auth.verification.not-sent.resend-email"}}</Button>
<Button @buttonType="button" @type="warning" @onClick={{this.resendBySms}} class="btn-warning-alert">{{t "auth.verification.not-sent.send-by-sms"}}</Button>
</div>
<div class="flex items-center space-x-2">
<Button
@text={{t "auth.verification.not-sent.resend-email"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.resendEmail}}
/>
<Button
@text={{t "auth.verification.not-sent.send-by-sms"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.resendBySms}}
/>
</div>
</div>
{{/if}}

View File

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

View File

@@ -16,6 +16,7 @@
</Layout::Container>
<ChatContainer />
<ConsoleWormhole />
<ImpersonatorTray />
{{!-- template-lint-disable no-potential-path-strings --}}
<RegistryYield @registry="@fleetbase/console" as |RegistryComponent|>
<RegistryComponent @controller={{this}} />

View File

@@ -3,6 +3,7 @@
<Layout::Sidebar::Panel @open={{true}} @title={{t "common.account"}}>
<Layout::Sidebar::Item @route="console.account.index" @icon="user">Profile</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.account.auth" @icon="key">Auth</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.account.organizations" @icon="building">Organizations</Layout::Sidebar::Item>
{{#each this.universe.accountMenuItems as |menuItem|}}
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.account.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
{{/each}}

View File

@@ -2,7 +2,7 @@
<Layout::Section::Header @title="Account Auth" />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<ContentPanel @title="Change Password" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<form id="change-password-form" aria-label="change-password" {{on "submit" (perform this.changePassword)}}>
@@ -31,4 +31,5 @@
{{/if}}
</div>
</div>
<Spacer @height="500px" />
</Layout::Section::Body>

View File

@@ -0,0 +1,37 @@
{{page-title "Organizations"}}
<Layout::Section::Header @title="Organizations" />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-4">
<div class="flex flex-row justify-end">
<Button @type="primary" @icon="plus" @text="Create Organization" @onClick={{this.createOrganization}} />
</div>
<ContentPanel @title="Your Organizations" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<div class="space-y-2">
{{#each @model as |organization|}}
<div class="grid grid-cols-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 rounded-lg px-3 py-2 items-center">
<div>
<div class="font-semibold">{{organization.name}}</div>
<div>Member Since: {{format-date organization.joined_at}}</div>
</div>
<div class="col-span-2 flex flex-row items-center justify-end space-x-2">
{{#let (eq organization.owner_uuid this.currentUser.id) as |isOwner|}}
<Button @type="danger" @size="xs" @icon="person-walking-arrow-right" @text="Leave" @onClick={{fn this.leaveOrganization organization}} />
{{#unless (eq this.currentUser.companyId organization.id)}}
<Button @size="xs" @icon="shuffle" @text="Switch" @onClick={{fn this.switchOrganization organization}} />
{{/unless}}
{{#if isOwner}}
<Button @size="xs" @icon="pencil" @text="Edit" @onClick={{fn this.editOrganization organization}} />
<Button @type="danger" @size="xs" @icon="trash" @text="Delete" @onClick={{fn this.deleteOrganization organization}} />
{{/if}}
{{/let}}
</div>
</div>
{{/each}}
</div>
</ContentPanel>
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{@model.title}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-">
{{component @model.component params=@model.componentParams}}
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{t "console.admin.config.database.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<Configure::Database />
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{t "console.admin.config.filesystem.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<Configure::Filesystem />
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{t "console.admin.config.mail.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<Configure::Mail />
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{t "console.admin.config.notification-channels.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<Configure::NotificationChannels />
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{t "console.admin.config.queue.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<Configure::Queue />
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{t "console.admin.config.services.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 900}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<Configure::Services />
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{t "console.admin.config.socket.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 900}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<Configure::Socket />
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,7 +2,7 @@
<Layout::Section::Header @title={{t "common.overview"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<div class="grid grid-cols-3 xs:grid-cols-1 gap-4">
<StatWidget @title={{t "console.admin.index.total-users"}} @value={{@model.total_users}} />
@@ -11,4 +11,5 @@
</div>
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{@model.title}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-">
{{component @model.component params=@model.componentParams}}
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

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

View File

@@ -1,7 +1,7 @@
<Layout::Section::Header @title={{t "console.settings.index.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<ContentPanel @title={{t "console.settings.index.title"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<form {{on "submit" this.saveSettings}}>
@@ -11,7 +11,7 @@
<PhoneInput @value={{@model.phone}} @onInput={{fn (mut @model.phone)}} class="form-input w-full" />
</InputGroup>
<InputGroup @name={{t "console.settings.index.organization-currency"}}>
<CurrencySelect @value={{@model.currency}} @onSelect={{fn (mut @model.currency)}} @triggerClass="w-full form-select" />
<CurrencySelect @currency={{@model.currency}} @onCurrencyChange={{fn (mut @model.currency)}} @triggerClass="w-full form-select" />
</InputGroup>
<InputGroup @name={{t "console.settings.index.organization-id"}} @value={{@model.public_id}} @disabled={{true}} />
<div class="mt-3 flex items-center justify-end">
@@ -65,4 +65,5 @@
</ContentPanel>
</div>
</div>
<Spacer @height="500px" />
</Layout::Section::Body>

View File

@@ -2,7 +2,7 @@
<Layout::Section::Header @title="2FA" />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<ContentPanel @title="2FA Settings" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<div class="mb-3">
@@ -33,4 +33,5 @@
</ContentPanel>
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{@model.title}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-">
{{component @model.component params=@model.componentParams}}
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -1,10 +1,6 @@
{{page-title @model.title}}
<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="max-w-3xl my-10 mx-auto space-y-">
{{component @model.component params=@model.componentParams}}
</div>
</div>
{{component @model.component params=@model.componentParams}}
<Spacer @height="300px" />
</Layout::Section::Body>

View File

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

View File

@@ -1,46 +1,75 @@
{{page-title (t "onboard.verify-email.header-title")}}
<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"}}>
<div class="mb-6">
<LinkTo @route="console" class="flex items-center justify-center">
<LogoIcon @size="12" class="rounded-md" />
</LinkTo>
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.verify-email.title"}}
</h2>
</div>
<div class="flex px-3 py-2 mb-6 rounded-md shadow-sm bg-blue-200">
<div>
<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 "onboard.verify-email.message-text" htmlSafe=true}}
</p>
</div>
<InfoBlock @type="info" @icon="shield-halved" @iconSize="lg">
{{t "onboard.verify-email.message-text" htmlSafe=true}}
</InfoBlock>
<form class="mt-8 space-y-6" {{on "submit" this.verifyCode}}>
<InputGroup @type="tel" @name={{t "onboard.verify-email.verification-input-label"}} @value={{this.code}} @helpText={{t "onboard.verify-email.verification-code-text"}} @inputClass="input-lg" {{on "input" this.validateInput}} {{did-insert this.validateInitInput}} />
<form class="mt-8 space-y-6" {{on "submit" (perform this.verifyCode)}}>
<InputGroup
@type="tel"
@name={{t "onboard.verify-email.verification-input-label"}}
@value={{this.code}}
@helpText={{t "onboard.verify-email.verification-code-text"}}
@inputClass="input-lg"
{{on "input" this.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}} />
<Button
@icon="check"
@iconPrefix="fas"
@buttonType="submit"
@type="primary"
@size="lg"
@text="Verify & Continue"
@isLoading={{this.verifyCode.isRunning}}
@disabled={{not this.isReadyToSubmit}}
@onClick={{perform this.verifyCode}}
/>
<a href="#" {{on "click" this.onDidntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "onboard.verify-email.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 "onboard.verify-email.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 "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 "auth.verification.not-sent.resend-email"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.resendEmail}}
/>
<Button
@text={{t "auth.verification.not-sent.send-by-sms"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.resendBySms}}
/>
</div>
</div>
{{/if}}

View File

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

View File

@@ -0,0 +1,6 @@
export default function removeBootLoader() {
const bootLoaderElement = document.getElementById('boot-loader');
if (bootLoaderElement && typeof bootLoaderElement.remove === 'function') {
bootLoaderElement.remove();
}
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@fleetbase/console",
"version": "0.5.7",
"version": "0.6.0",
"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.17",
"@fleetbase/ember-ui": "^0.2.24",
"@fleetbase/fleetops-engine": "^0.5.7",
"@fleetbase/storefront-engine": "^0.3.14",
"@fleetbase/dev-engine": "^0.2.6",
"@fleetbase/iam-engine": "^0.1.0",
"@fleetbase/registry-bridge-engine": "^0.0.13",
"@fleetbase/fleetops-data": "^0.1.17",
"@fleetbase/leaflet-routing-machine": "^3.2.16",
"@ember/legacy-built-in-components": "^0.4.2",
"@fleetbase/dev-engine": "^0.2.9",
"@fleetbase/ember-core": "latest",
"@fleetbase/ember-ui": "latest",
"@fleetbase/fleetops-data": "latest",
"@fleetbase/fleetops-engine": "^0.6.0",
"@fleetbase/iam-engine": "^0.1.3",
"@fleetbase/leaflet-routing-machine": "^3.2.16",
"@fleetbase/registry-bridge-engine": "^0.0.18",
"@fleetbase/storefront-engine": "^0.3.29",
"@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.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,8 +51,6 @@
"ember-radio-button": "3.0.0-beta.1",
"ember-tag-input": "^3.1.0",
"fleetbase-extensions-indexer": "^0.0.5",
"gridstack": "^7.3.0",
"patch-package": "^8.0.0",
"postcss-at-rules-variables": "^0.3.0",
"postcss-custom-properties": "^12.1.11",
"postcss-nth-list": "^1.0.2"
@@ -95,7 +91,6 @@
"ember-data": "^4.12.8",
"ember-engines": "^0.9.0",
"ember-fetch": "^8.1.2",
"ember-leaflet": "^5.1.3",
"ember-load-initializers": "^2.1.2",
"ember-modifier": "^4.2.0",
"ember-page-title": "^8.2.3",
@@ -114,7 +109,6 @@
"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.41",
@@ -143,9 +137,9 @@
},
"pnpm": {
"overrides": {
"@fleetbase/ember-core": "^0.2.17",
"@fleetbase/ember-ui": "^0.2.24",
"@fleetbase/fleetops-data": "^0.1.17"
"@fleetbase/ember-core": "latest",
"@fleetbase/ember-ui": "latest",
"@fleetbase/fleetops-data": "latest"
}
},
"prettier": {

View File

@@ -1,11 +0,0 @@
diff --git a/node_modules/ember-gridstack/addon/components/grid-stack.js b/node_modules/ember-gridstack/addon/components/grid-stack.js
index fa51392..fdabb2a 100644
--- a/node_modules/ember-gridstack/addon/components/grid-stack.js
+++ b/node_modules/ember-gridstack/addon/components/grid-stack.js
@@ -133,5 +133,6 @@ export default class GridStackComponent extends Component {
removeWidget(element, removeDOM = false, triggerEvent = true) {
triggerEvent = triggerEvent && !this.isDestroying && !this.isDestroyed;
this.gridStack?.removeWidget(element, removeDOM, triggerEvent);
+ this.gridStack?.compact();
}
}

7361
console/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -78,6 +78,8 @@ function getRouterFileContents() {
(async () => {
const extensions = await getExtensions();
const consoleExtensions = extensions.filter((extension) => !extension.fleetbase || extension.fleetbase.mount !== 'root');
const rootExtensions = extensions.filter((extension) => extension.fleetbase && extension.fleetbase.mount === 'root');
const routerFileContents = getRouterFileContents();
const ast = recast.parse(routerFileContents, { parser: babelParser });
@@ -92,10 +94,9 @@ function getRouterFileContents() {
functionExpression = arg;
}
});
if (functionExpression) {
// Check and add the new engine mounts
extensions.forEach((extension) => {
consoleExtensions.forEach((extension) => {
const mountPath = getExtensionMountPath(extension.name);
let route = mountPath;
@@ -123,8 +124,46 @@ function getRouterFileContents() {
);
}
});
}
}
return false;
// console.log(path.value.callee.property.name);
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'map') {
let functionExpression;
path.value.arguments.forEach((arg) => {
if (arg.type === 'FunctionExpression') {
functionExpression = arg;
}
});
if (functionExpression) {
rootExtensions.forEach((extension) => {
const mountPath = getExtensionMountPath(extension.name);
let route = mountPath;
if (extension.fleetbase && extension.fleetbase.route) {
route = extension.fleetbase.route;
}
const isMounted = functionExpression.body.body.some((expressionStatement) => {
return expressionStatement.expression.arguments[0].value === extension.name;
});
if (!isMounted) {
functionExpression.body.body.push(
builders.expressionStatement(
builders.callExpression(builders.memberExpression(builders.thisExpression(), builders.identifier('mount')), [
builders.literal(extension.name),
builders.objectExpression([
builders.property('init', builders.identifier('as'), builders.literal(route)),
builders.property('init', builders.identifier('path'), builders.literal(route)),
]),
])
)
);
}
});
}
}

View File

@@ -7,15 +7,18 @@ export default class Router extends EmberRouter {
}
Router.map(function () {
this.route('virtual', { path: '/:slug' });
this.route('install');
this.route('onboard', function () {
this.route('verify-email');
});
this.route('auth', function () {
this.route('login', { path: '/' });
this.route('forgot-password');
this.route('reset-password', { path: '/reset-password/:id' });
this.route('two-fa');
this.route('verification');
});
this.route('onboard', function () {
this.route('verify-email');
this.route('portal-login', { path: '/portal' });
});
this.route('invite', { path: 'join' }, function () {
this.route('for-driver', { path: '/fleet/:public_id' });
@@ -25,14 +28,15 @@ 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('organizations');
});
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');
@@ -47,7 +51,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 +62,5 @@ Router.map(function () {
});
});
});
this.route('install');
this.route('catch', { path: '/*' });
});

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