Compare commits

...

186 Commits

Author SHA1 Message Date
Ronald A. Richardson
e1ab6a3b11 use release.md for discord announcement 2025-05-22 12:06:58 +08:00
Ronald A. Richardson
c79fe67e44 ready to release, if macos doesnt build fix later 2025-05-22 11:59:09 +08:00
Ronald A. Richardson
d8adf42b24 revert back to previous curl patch 2025-05-22 11:51:05 +08:00
Ronald A. Richardson
80da5fe013 add SPC_OPT_DOWNLOAD_ARGS to attempt to fix osx build on gh runner 2025-05-22 11:25:45 +08:00
Ronald A. Richardson
06fd5e20e8 make osx build script gh runner friendly 2025-05-22 11:21:21 +08:00
Ronald A. Richardson
f04807de1e fixed upload to only run on release workflow, debug macos build on runer 2025-05-22 11:14:53 +08:00
Ronald A. Richardson
b7666eeb3e fix github workflows and setup to debug macos build 2025-05-22 10:45:37 +08:00
Ronald A. Richardson
dd895a0fd8 remove CURRENT_HASH file 2025-05-22 10:31:05 +08:00
Ronald A. Richardson
8c74c0fb99 release is almost ready 2025-05-22 10:30:10 +08:00
Ronald A. Richardson
92170c965e updated docker-compose to use latest images, patched osx binary build script, experimenting with artifact upload for binaries 2025-05-21 22:18:49 +08:00
Ronald A. Richardson
fcb3694874 added curl patch for gh runners 2025-05-21 21:33:45 +08:00
Ronald A. Richardson
aa46059bff minor tweak on php build of osx build script 2025-05-21 21:11:41 +08:00
Ronald A. Richardson
a5175bb11b fix php 8.4 detection in osx build script 2025-05-21 21:08:52 +08:00
Ronald A. Richardson
01816a1fe0 update osx build script to skip asdf install if php 8.4 is already installed 2025-05-21 21:02:04 +08:00
Ronald A. Richardson
15d500cd58 just install php via homebrew 2025-05-21 20:49:37 +08:00
Ronald A. Richardson
95d77a6ddd in osx build workflow use correct asdf commands 2025-05-21 20:45:46 +08:00
Ronald A. Richardson
eefc93e130 debug osx binary build workflow 2025-05-21 20:42:09 +08:00
Ronald A. Richardson
0f18ae85f1 debug osx binary build workflow 2025-05-21 20:35:22 +08:00
Ronald A. Richardson
a4812192da debug osx binary build workflow 2025-05-21 20:26:24 +08:00
Ronald A. Richardson
15d3c957b8 debug osx binary build workflow 2025-05-21 20:20:33 +08:00
Ronald A. Richardson
c2bd098d14 debug osx binary build workflow 2025-05-21 20:16:16 +08:00
Ronald A. Richardson
98511fd418 patch console route && debug osx action binary build 2025-05-21 20:13:14 +08:00
Ronald A. Richardson
225110c8dc attempt to patch binary build workflow 2025-05-21 19:39:04 +08:00
Ronald A. Richardson
1aa2a99763 added workflow to build fleetbase api binaries 2025-05-21 19:35:08 +08:00
Ron
6e888af772 Merge pull request #376 from fleetbase/feature/fleetbase-binary
Working static build script for a fleetbase binary (unix/linux/osx)
2025-05-21 19:24:49 +08:00
Ronald A. Richardson
d61205d898 added fleetbase config file, added ability to set tz for user and organization 2025-05-21 19:21:22 +08:00
Ronald A. Richardson
72078553cc remove hash tracking from docker build 2025-05-20 10:19:42 +08:00
Ronald A. Richardson
bfae04a645 attempt to fix current hash check in build 2025-05-20 10:16:43 +08:00
Ronald A. Richardson
c59f028755 fix docker build 2025-05-20 10:06:26 +08:00
Ronald A. Richardson
2b959db773 remove the current hash file 2025-05-19 19:36:05 +08:00
Ronald A. Richardson
a9354ccbfd removed EXPECTED_HASH checkin 2025-05-19 19:06:21 +08:00
Ronald A. Richardson
23e6d1e6b9 removed EXPECTED_HASH 2025-05-19 17:33:13 +08:00
Ronald A. Richardson
86da1bd095 Improvements to docker setups 2025-05-19 17:30:18 +08:00
Ronald A. Richardson
ae89600ae6 updated dockerhub publish workflow 2025-05-19 13:54:28 +08:00
Ronald A. Richardson
6697b79185 secured runtime config to only allow select config values to be set 2025-05-19 13:50:10 +08:00
Ronald A. Richardson
4dc9764853 v0.7.1 ~ Fleetbase console can now read in a runtime config 2025-05-19 13:12:57 +08:00
Ron
0626bc0171 Merge pull request #388 from fleetbase/dev-v0.7.0
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.0 🛠️
2025-05-16 17:19:57 +08:00
Ronald A. Richardson
a8adf3fd84 Merge branch 'dev-v0.7.0' of github.com:fleetbase/fleetbase into dev-v0.7.0 2025-05-16 16:22:18 +08:00
Ronald A. Richardson
7b8bc4a593 removed old docker settings and github auth arg 2025-05-16 16:21:22 +08:00
Ron
490f2f1b41 Merge pull request #345 from nstankov-bg/feature/translate-bulgarian
feature/translate-bulgarian
2025-05-16 16:07:25 +08:00
Ron
e1fc7850d3 Merge pull request #385 from thawaba/add-arabic-language
Add Arabic language support
2025-05-16 16:06:44 +08:00
Ronald A. Richardson
cc278bf1bb * Patched fuel report creation/ fixed coordinates input implementation for fuel report
* Added bulk assign driver
* Improved performance for order dispatch/ bulk order dispatch/ bulk assign driver
* Added new columns to order export
* Added downloadable import templates for all importable resources via Import Modal
* Patched custom field rendering for order viewing
* Patched custom field values reset after order creation
* Added notification settings to FleetOps
* Added bulk search by ID or Tracking Number for Orders
* Patched all filters and filter indicator component
* Patched issue unable to select driver after selecting facilitator
* Fixed extension booting when not authenticated
* Fixed Internal ID rendering on order view
* Added ability to filter orders without a driver
2025-05-16 16:03:26 +08:00
aanmth
af86aaba8b Add Arabic language support 2025-05-15 05:42:17 +03:00
Ronald A. Richardson
f35dcb1544 fix: update package.json version v0.6.10
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
2025-05-08 20:06:27 +08:00
Ron
29c8f4340d Merge pull request #382 from fleetbase/dev-v0.6.10
Some checks are pending
Fleetbase CI / Build and Start Docker Services (push) Waiting to run
v0.6.10 ~ Added Product Update/Create API, Added `FRONTEND_HOSTS` ENV…
2025-05-08 12:33:02 +08:00
Ronald A. Richardson
1cb833e407 v0.6.10 ~ Added Product Update/Create API, Added FRONTEND_HOSTS ENV variable, other minor patches 2025-05-08 12:24:54 +08:00
Ronald A. Richardson
e372bc6396 minor update to linux build script 2025-05-07 14:32:17 +08:00
Ronald A. Richardson
2f432d148a Remove build artifacts; add dist & downloads to .gitignore 2025-05-07 14:26:57 +08:00
Ron
41bc6e39a7 Merge pull request #380 from fleetbase/dev-v0.6.9
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
Enhancements and bug fixes for order workflow, labels, notifications,…
2025-05-01 12:15:36 +08:00
Ronald A. Richardson
5dbe2fb5bb Enhancements and bug fixes for order workflow, labels, notifications, and route optimization
- Added support for downloading labels and barcodes per package
- Fixed proof of delivery behavior to ensure accurate completion records
- Updated waypoint activity flow to rely on the `complete` flag
- Added support for setting waypoints as either pickup or dropoff
- Enabled sending notifications to order customer, driver, and facilitator
- Added events and notifications for `order.completed` and `order.failed` states
- Fixed route optimization logic and minor issues during order creation
- Normalized `meta` response structure to always return an object (never array)
- Patched issue with order config: deleting custom field categories no longer breaks config
2025-05-01 12:08:27 +08:00
Ronald A. Richardson
8f66bc12e4 Working static build script for a fleetbase binary (unix/linux) 2025-04-16 14:22:07 +08:00
Ron
313b5ea3ba Merge pull request #374 from fleetbase/dev-v0.6.8
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.6.8 ~ View Proof of Delivery in FleetOps + API to get POD
2025-04-14 16:10:34 +08:00
Ronald A. Richardson
698f5979b1 v0.6.8 ~ View Proof of Delivery in FleetOps + API to get POD 2025-04-14 16:05:10 +08:00
Ron
f6f6899650 Merge pull request #373 from fleetbase/dev-v0.6.7
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
Fix iOS Push Notifications for Production Environments
2025-04-11 12:51:25 +08:00
Ronald A. Richardson
83a7ab7338 Fix iOS Push Notifications for Production Environments 2025-04-11 12:43:09 +08:00
Ron
acf7b209af Merge pull request #372 from fleetbase/dev-v0.6.6
Patched Join Organization + Chat Push Notifications Added
2025-04-11 10:09:40 +08:00
Ronald A. Richardson
5c048a8238 Patched Join Organization + Chat Push Notifications Added 2025-04-11 10:02:33 +08:00
Ron
de00ad31db Merge pull request #371 from fleetbase/dev-v0.6.5
Some checks are pending
Fleetbase CI / Build and Start Docker Services (push) Waiting to run
🤖 Patched Navigator App Instance Linking for Android
2025-04-10 13:44:34 +08:00
Ronald A. Richardson
8fe52c6157 🤖 Patched Navigator App Instance Linking for Android + Navigator App Identifier Configurable 2025-04-10 13:34:42 +08:00
Ron
a371e055ca Merge pull request #370 from fleetbase/dev-v0.6.4
Some checks are pending
Fleetbase CI / Build and Start Docker Services (push) Waiting to run
Upgraded FleetOps to v0.6.3
2025-04-09 19:34:52 +08:00
Ronald A. Richardson
bbec73fcef Upgraded FleetOps to v0.6.3 2025-04-09 19:03:42 +08:00
Ron
838a829a11 Merge pull request #367 from fleetbase/dev-v0.6.3
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
Patched AdHoc Pinging & Push Notifications
2025-04-01 21:17:49 +08:00
Ronald A. Richardson
dbb7bc793a removed console logs 2025-04-01 21:06:16 +08:00
Ronald A. Richardson
f0fa867ef9 Patched AdHoc Pinging & Push Notifications 2025-04-01 21:02:51 +08:00
Ron
3cc64913ca Merge pull request #366 from fleetbase/hotfix/php-geos-404
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
hotfix php-geos install replace broken url with github mirror
2025-03-31 11:13:35 +08:00
Ronald A. Richardson
d034c4ad03 hotfix php-geos install replace broken url with github mirror 2025-03-31 11:05:47 +08:00
Ron
b740cf035e Merge pull request #365 from fleetbase/dev-v0.6.1
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.6.1 ~ new driver online/offline toggle endpoint, improvements to tracker data api
2025-03-27 21:27:50 +08:00
Ronald A. Richardson
cc42779efc new driver online/offline toggle endpoint, improvements to tracker data api 2025-03-27 21:25:50 +08:00
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
Nikolay Stankov
1e331d70b1 feature/translate-bulgarian 2025-01-30 08:54:08 -05: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
Ron
dc00ac3892 Merge pull request #294 from fleetbase/dev-v0.5.7
upgrade core-api
2024-08-31 15:22:56 +07:00
Ronald A. Richardson
f18ec886a7 upgrade core-api 2024-08-31 16:21:45 +08:00
Ron
f039b61d79 Merge pull request #293 from fleetbase/dev-v0.5.7
hotfix for resource loading and relation loading for organizations
2024-08-31 15:01:36 +07:00
Ronald A. Richardson
248f70e31c hotfix for resource loading and relation loading for organizations 2024-08-31 15:51:30 +08:00
Ron
6bc76a1b33 Merge pull request #292 from fleetbase/dev-v0.5.6
v0.5.6 - hotfix only load organization with valid owners - no stale org
2024-08-30 17:18:07 +07:00
Ronald A. Richardson
30695b3ebe hotfix only load organization with valid owners - no stale org 2024-08-30 18:06:42 +08:00
Ron
7ff9c24ad5 Merge pull request #288 from fleetbase/dev-v0.5.5
v0.5.5 - Documentation & README Updates
2024-08-30 14:20:57 +07:00
Ronald A. Richardson
1a9b9c06e5 fully implemented iam permission based restrictions and controls, few bugfixes and improvements 2024-08-30 15:14:56 +08:00
Ronald A. Richardson
5f949c3b7f implementing IAM permission controls, policies and roles 2024-08-13 20:26:32 +08:00
CassioDalla
211a3a9808 Create pt-br.yaml File
Added Brazilian Portuguese translation file to the project. It would be nice if another translator reviewed it to make sure everything is correct.
2024-08-10 16:04:45 -03:00
Ronald A. Richardson
0e5e4e07dd updating docs WIP, and updated README 2024-08-08 17:01:18 +08:00
Ronald A. Richardson
2eabfc4698 version 0.5.4 2024-08-06 13:51:29 +08:00
Ron
77c2c01e58 Merge pull request #286 from fleetbase/dev-v0.5.5
v0.5.5
2024-08-06 12:48:41 +07:00
Ronald A. Richardson
7c5b5b5858 again, fix depn order 2024-08-06 13:33:29 +08:00
Ronald A. Richardson
aad072cf4c reverted dependency order in package.json 2024-08-06 13:32:39 +08:00
Ronald A. Richardson
c1c6dcafd8 ordered packages 2024-08-06 13:30:34 +08:00
Ronald A. Richardson
61992ee924 fix lockfile 2024-08-06 13:28:39 +08:00
Ronald A. Richardson
b18d6197bc upgraded registry bridge and updated erd 2024-08-06 13:25:32 +08:00
Ron
04bdb52c08 Merge pull request #284 from fleetbase/dev-v0.5.3
Upgraded Registry Bridge to v0.0.11
2024-07-31 16:42:17 +07:00
Ronald A. Richardson
8e5a45dd09 Upgraded Registry Bridge to v0.0.11 2024-07-31 17:24:01 +08:00
Ron
196af155ae Merge pull request #283 from fleetbase/dev-v0.5.2
v0.5.2
2024-07-30 21:46:10 +07:00
Ronald A. Richardson
056a717d08 changed default osrm server to project-osrm router, added admin bypass for email verification, patched somethings in registry-bridge, added new UI wysiwyg component <TipTapEditor /> 2024-07-30 22:39:57 +08:00
Ronald A. Richardson
fd008d7f73 hotfix cd for putting extensions in env config 2024-07-25 19:05:34 +08:00
Ron
451c95d0f0 Merge pull request #277 from fleetbase/dev-v0.5.1
v0.5.1
2024-07-25 17:33:52 +07:00
Ronald A. Richardson
b267b303cf fix console depnds 2024-07-25 18:17:36 +08:00
Ronald A. Richardson
441b4f3f0c Extension boot patches, dependency upgrades, performance upgrades 2024-07-25 18:14:11 +08:00
Ronald A. Richardson
0e4d4a7c8c ability to provide installed extensions via environment config 2024-07-23 17:05:35 +08:00
Ronald A. Richardson
24392527e0 fix cd workflow, upgrade node 18x and pnpm 9x 2024-07-23 12:42:16 +08:00
Ronald A. Richardson
c19d838757 hotfix Dockerfile for deployments 2024-07-23 10:45:39 +08:00
Ron
3a072c1524 Merge pull request #264 from fleetbase/feature/extensions-registry
feature/extensions-registry
2024-07-23 08:48:45 +07:00
Ronald A. Richardson
0c96386cf1 few tweaks to cicd, and fix in Dockerfile 2024-07-22 16:19:07 +08:00
Ronald A. Richardson
708babb81c fix ports on docker-compose 2024-07-22 14:19:11 +08:00
Ronald A. Richardson
8c8acf1e43 docker changes 2024-07-22 14:10:56 +08:00
Ronald A. Richardson
e853e2ca22 set new registry to composer 2024-07-20 10:41:35 +08:00
Ronald A. Richardson
dacaff37ca bump version to 0.5.0 2024-07-19 20:21:47 +08:00
Ronald A. Richardson
b0460963e5 added console back to docker-compose 2024-07-19 20:15:54 +08:00
Ronald A. Richardson
9ec786d892 almost ready for release, pending updates to registry... 2024-07-19 20:14:12 +08:00
Ronald A. Richardson
76859aeb26 almost ready for release of extensions 2024-07-19 18:09:14 +08:00
Ronald A. Richardson
1764b804de remove unnecessary utils and components 2024-07-18 02:05:19 +08:00
Ronald A. Richardson
0c33018b5b wip 2024-07-18 02:02:23 +08:00
Ronald A. Richardson
72b1b9b764 Merge branch 'feature/extensions-registry' of github.com:fleetbase/fleetbase into feature/extensions-registry 2024-07-18 01:58:43 +08:00
Ronald A. Richardson
aee552f518 remove extension template 2024-07-18 01:58:13 +08:00
Ron
9967f27c83 Merge pull request #276 from fleetbase/feature-registry-extension-updates
Feature registry extension updates
2024-07-18 00:50:27 +07:00
Ron
21ecfb5d93 Merge branch 'feature/extensions-registry' into feature-registry-extension-updates 2024-07-18 00:50:16 +07:00
Ronald A. Richardson
579a369888 preparing arch change for extensions implementation 2024-07-18 01:48:55 +08:00
Ron
b06830990c Merge pull request #270 from fleetbase/update-cd-workflow-v4 2024-07-05 20:26:45 +07:00
Ronald A. Richardson
cce1699a75 wip 2024-07-02 14:36:31 +08:00
Ronald A. Richardson
6338820372 new extension injector component for installed extensions from registry bridge WIP, need to figure out mounted extensions 2024-06-20 17:30:25 +08:00
204 changed files with 32211 additions and 24852 deletions

62
.github/workflows/build-binaries.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: Fleetbase Binaries
on:
workflow_run:
workflows: ["Create Release"]
types: [completed]
permissions:
contents: write
env:
DIST_DIR: builds/dist
jobs:
build-linux:
name: Linux Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Linux binary
run: |
chmod +x ./builds/linux/build-linux.sh
./builds/linux/build-linux.sh
- name: Upload Linux binary
if: github.event_name == 'workflow_run'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.workflow_run.head_branch }}
files: |
${{ env.DIST_DIR }}/fleetbase-linux-x86_64
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-macos:
name: macOS (ARM64) Build
needs: build-linux
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Install build dependencies
run: |
brew update
brew install autoconf automake coreutils asdf php@8.4
source "$(brew --prefix asdf)/libexec/asdf.sh"
asdf plugin add php https://github.com/asdf-community/asdf-php.git
- name: Build macOS binary
run: |
chmod +x ./builds/osx/build-osx.sh
./builds/osx/build-osx.sh
- name: Upload Linux binary
if: github.event_name == 'workflow_run'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.workflow_run.head_branch }}
files: |
${{ env.DIST_DIR }}/fleetbase-darwin-arm64
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -109,13 +109,13 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
version: 8
version: 9.5.0
run_install: false
- name: Get pnpm Store Directory
@@ -132,11 +132,31 @@ 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
- name: Set Env Variables for Production
if: startsWith(github.ref, 'refs/heads/deploy/production')
run: |
echo "STRIPE_KEY=${{ secrets.STRIPE_KEY }}" >> ./environments/.env.production
echo "LOGROCKET_APP_ID=${{ secrets.LOGROCKET_APP_ID }}" >> ./environments/.env.production
echo "EXTENSIONS=@fleetbase/billing-engine,@fleetbase/internals-engine" >> ./environments/.env.production
working-directory: ./console
- name: Install dependencies

21
.github/workflows/create-release.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Create Release
on:
push:
tags:
- 'v*'
jobs:
create:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
body_path: RELEASE.md
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,39 @@
name: Discord Announcement
on:
release:
types: [published]
jobs:
discord_announcement:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Read release notes
id: notes
run: |
body="$(cat RELEASE.md)"
echo "::set-output name=message::$(echo "${body}" | base64 -w0)"
- name: Get tag name
id: tag
run: echo "::set-output name=tag::${GITHUB_REF#refs/tags/}"
- name: Decode notes
id: decode
run: |
echo "::set-output name=text::$(echo '${{ steps.notes.outputs.message }}' | base64 --decode)"
- name: Send message to Discord
uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
username: Fleetbase
content: |
@everyone
📦 **Fleetbase ${{ steps.tag.outputs.tag }} released!**
${{ steps.decode.outputs.text }}
[View full notes on GitHub](https://github.com/fleetbase/fleetbase/releases/tag/${{ steps.tag.outputs.tag }})

View File

@@ -1,48 +0,0 @@
name: Discord Announcement
on:
push:
tags:
- "v*"
jobs:
discord_announcement:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Get tag message
id: tag
run: |
echo "::set-output name=version::$(git describe --tags --abbrev=0)"
if [[ "${ACT}" == "true" ]]; then
# If running with act, use an environment variable for the tag message
echo "::set-output name=message::$(echo -e "${TAG_MESSAGE}" | base64)"
else
# If running on GitHub, use git to get the tag message
echo "::set-output name=message::$(git tag -l --format='%(contents)' $(git describe --tags --abbrev=0) | base64)"
fi
- name: Print tag message
run: echo "${{ steps.tag.outputs.message }}"
- name: Get tag name
id: get_tag
run: echo "::set-output name=tag::${GITHUB_REF/refs\/tags\//}"
- name: Decode message
id: decode
run: |
echo "Decoding message..."
echo "::set-output name=message::$(echo '${{ steps.tag.outputs.message }}' | base64 --decode)"
- name: Send message to Discord
uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: "@everyone \n📦 New Fleetbase Version ${{ steps.get_tag.outputs.tag }} Released!\n${{ steps.decode.outputs.message }} \nVersion: ${{ steps.get_tag.outputs.tag }} \n[Release Notes for ${{ steps.get_tag.outputs.tag }}](https://github.com/fleetbase/fleetbase/releases/tag/${{ steps.get_tag.outputs.tag }})"
username: Fleetbase

View File

@@ -1,4 +1,4 @@
name: Fleetbase CI/CD
name: Fleetbase GCP CI/CD
on:
push:

View File

@@ -0,0 +1,50 @@
name: Fleetbase Docker Images
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
branch:
description: 'Branch to build from'
required: false
default: 'main'
version:
description: 'Image version tag (e.g., v0.7.1-beta)'
required: false
jobs:
docker-release:
name: Build and Push Docker Images
runs-on: ubuntu-latest
env:
REGISTRY: fleetbase
VERSION: ${{ github.event.inputs.version || (github.ref_type == 'tag' && startsWith(github.ref_name, 'v') && github.ref_name) || 'manual' }}
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.branch || github.ref_name }}
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and Push Console & API Images
uses: docker/bake-action@v2
with:
push: true
targets: |
fleetbase-console
fleetbase-api
files: |
./docker-bake.hcl

14
.gitignore vendored
View File

@@ -30,7 +30,19 @@ packages/flespi
packages/loconav
packages/internals
packages/projectargus-engine
packages/customer-portal
# wip
packages/solid
solid
verdaccio
verdaccio
# asdf
.tools-versions
# binary build resources
builds/osx/frankenphp
# build artifacts
/builds/dist/
/builds/linux/spc/downloads/*
*.exe
*.dll
*.so
*.dylib

6
.gitmodules vendored
View File

@@ -36,3 +36,9 @@
[submodule "docs"]
path = docs
url = git@github.com:fleetbase/docs.git
[submodule "packages/registry-bridge"]
path = packages/registry-bridge
url = git@github.com:fleetbase/registry-bridge.git
[submodule "packages/ledger"]
path = packages/ledger
url = git@github.com:fleetbase/ledger.git

View File

@@ -13,15 +13,15 @@ We use github to host code, to track issues and feature requests, as well as acc
## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests
Pull requests are the best way to propose changes to the codebase (we use [Github Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests:
1. Fork the repo and create your branch from `master`.
1. Fork the repo and create your branch from `main`.
2. If you've added code that should be tested, add tests.
3. If you've changed APIs, update the documentation.
4. Ensure the test suite passes.
5. Make sure your code lints.
6. Issue that pull request!
## Any contributions you make will be under the MIT Software License
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
## Any contributions you make will be under the AGPL v3 Software License
In short, when you submit code changes, your submissions are understood to be under the same [AGPL v3](https://choosealicense.com/licenses/agpl-3.0/) that covers the project. Feel free to contact the maintainers if that's a concern.
## Report bugs using Github's [issues](https://github.com/fleetbase/fleetbase/issues)
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/fleetbase/fleetbase/issues), it's that easy!
@@ -41,7 +41,7 @@ We use GitHub issues to track public bugs. Report a bug by [opening a new issue]
People *love* thorough bug reports. I'm not even kidding.
## License
By contributing, you agree that your contributions will be licensed under its MIT License.
By contributing, you agree that your contributions will be licensed under its AGPL v3 Software License.
## References
This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md)

View File

@@ -1,12 +1,14 @@
{
frankenphp
frankenphp {
num_threads 24
}
order php_server before file_server
}
http://:8000 {
root * /fleetbase/api/public
encode zstd gzip
encode zstd br gzip
php_server {
resolve_root_symlink
}
}
}

19
Caddyfile.console Normal file
View File

@@ -0,0 +1,19 @@
{
frankenphp
order php_server before file_server
}
http://:8000 {
root * /fleetbase/api/public
encode zstd gzip
php_server {
resolve_root_symlink
}
}
http://:4200 {
root * /fleetbase/console/dist
try_files {path} /
encode zstd gzip
file_server
}

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

60
RELEASE.md Normal file
View File

@@ -0,0 +1,60 @@
# 🚀 Fleetbase v0.7.1 — 2025-05-22
> “Finally, official docker images and binary builds for the upcoming Omnibus.”
---
## ✨ Highlights
- Introduction to Telemetry for improving Fleetbase and setup experiences.
- Added runtime config for Fleetbase Console (`./console/fleetbase.config.json`), no more rebuilding the frontend to update config.
- Official Fleetbase API and Console docker images published. `docker compose up` now uses the official images for faster setup - no more building.
- Performance optimization and improvements to the FleetOps Order API. Faster responses.
- Patched waypoint activity flow and proof of delivery QR scanning and photo capture API.
- Added ability to view waypoint activity status and labels if no entities attached to order.
- Fixed Timezone issues in Navigator app (v2.0.5)
- Improved Navigator app performance and UX (v2.0.5) - New "Waypoint Completed" dialog.
- Improved socket implementation on FleetOps.
- Added two new order activity events for listeners `order.waypoint_activity` and `order.waypoint_completed`
- Added Navigator App email verification code fallback if SMS is unable to send (v2.0.5).
---
## ⚠️ Breaking Changes
- None 🙂
---
## 🆕 Features
- **Navigator App v2.0.5** — Improved Navigator App Performance and UX for better experience.
- **Official Docker Images** — Official Docker images on [Docker Hub](https://hub.docker.com/u/fleetbase)
- **Binary Builds** — Fleetbase API Binary for both Linux and MacOS - `sh ./fleetbase php-server`
- **Telemetry** — opt-in daily ping with anonymized usage stats for system improvements.
---
## 🐛 Fixes
- Activity flow fixed for waypoints.
- Better implementation of socket listeners in console.
- Activity events and notifications streamlined.
## ⚙️ Using the new Console Runtime Config
- Open `./console/fleetbase.config.json`
- Edit configurable properties in JSON: `API_HOST` - `SOCKETCLUSTER_HOST` - `SOCKETCLUSTER_PORT` - `OSRM_HOST`
- Save and reload Fleetbase Console in the browser.
- 💥 Boom! Config changes applied at runtime.
## 🔧 Upgrade Steps
```bash
# Pull latest version
git pull origin main --no-rebase
# Update docker
docker compose down && docker compose up -d
# Run updrade
docker exec -ti fleetbase-application-1 bash
sh deploy.sh
```
## Need help?
Join the discussion on [GitHub Discussions](https://github.com/fleetbase/fleetbase/discussions) or drop by [#fleetbase on Discord](https://discord.com/invite/HnTqQ6zAVn)

View File

@@ -9,9 +9,11 @@
"license": "AGPL-3.0-or-later",
"require": {
"php": "^8.0",
"fleetbase/core-api": "^1.4.27",
"fleetbase/fleetops-api": "^0.5.3",
"fleetbase/storefront-api": "^0.3.12",
"appstract/laravel-opcache": "^4.0",
"fleetbase/core-api": "^1.6.6",
"fleetbase/fleetops-api": "^0.6.9",
"fleetbase/registry-bridge": "^0.0.19",
"fleetbase/storefront-api": "^0.3.31",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^10.0",
"laravel/octane": "^2.3",
@@ -21,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,8 +39,8 @@
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/fleetbase/laravel-model-caching"
"type": "composer",
"url": "https://registry.fleetbase.io"
}
],
"autoload": {

4266
api/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@ return [
|
*/
'env' => env('APP_ENV', 'production'),
'env' => env('APP_ENV', env('ENVIRONMENT', 'production')),
/*
|--------------------------------------------------------------------------

View File

@@ -21,7 +21,7 @@ return [
'allowed_methods' => ['*'],
'allowed_origins' => array_filter(['http://localhost:4200', env('CONSOLE_HOST'), Utils::addWwwToUrl(env('CONSOLE_HOST'))]),
'allowed_origins' => array_filter(['http://localhost:4200', env('CONSOLE_HOST'), Utils::addWwwToUrl(env('CONSOLE_HOST')), ...Utils::arrayFrom(env('FRONTEND_HOSTS', ''))]),
'allowed_origins_patterns' => [],

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

@@ -15,6 +15,9 @@ php artisan sandbox:migrate --force
# Seed database
php artisan fleetbase:seed
# Create permissions, policies, and roles
php artisan fleetbase:create-permissions
# Restart queue
php artisan queue:restart
@@ -23,3 +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

@@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| أسطر لغة المصادقة
|--------------------------------------------------------------------------
|
| تحتوي الأسطر التالية على رسائل المصادقة التي نعرضها للمستخدم أثناء
| عمليات تسجيل الدخول أو غيرها. يمكنك تعديل هذه الرسائل حسب متطلباتك.
|
*/
'failed' => 'بيانات الاعتماد هذه غير متطابقة مع سجلاتنا.',
'password' => 'كلمة المرور التي تم إدخالها غير صحيحة.',
'throttle' => 'عدد كبير جداً من محاولات الدخول. يرجى المحاولة مرة أخرى خلال :seconds ثانية.',
];

View File

@@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| أسطر لغة الترقيم الصفحي
|--------------------------------------------------------------------------
|
| تُستخدم الأسطر التالية من قبل مكتبة الترقيم الصفحي لبناء روابط
| الترقيم البسيطة. يمكنك تعديلها كما تشاء لتخصيص العرض بما يناسب
| تطبيقك بشكل أفضل.
|
*/
'previous' => '&laquo; السابق',
'next' => 'التالي &raquo;',
];

View File

@@ -0,0 +1,21 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| أسطر لغة إعادة تعيين كلمة المرور
|--------------------------------------------------------------------------
|
| الأسطر التالية هي الرسائل الافتراضية التي يقدمها نظام إعادة تعيين
| كلمة المرور عند فشل المحاولة، مثل رمز التحقق غير صالح أو كلمة مرور جديدة غير صحيحة.
|
*/
'reset' => 'تم إعادة تعيين كلمة المرور الخاصة بك!',
'sent' => 'لقد أرسلنا رابط إعادة تعيين كلمة المرور إلى بريدك الإلكتروني!',
'throttled' => 'يرجى الانتظار قبل المحاولة مرة أخرى.',
'token' => 'رمز إعادة تعيين كلمة المرور هذا غير صالح.',
'user' => 'لا يمكننا العثور على مستخدم بهذا العنوان الإلكتروني.',
];

View File

@@ -0,0 +1,168 @@
<?php
return [
/*
|--------------------------------------------------------------------------<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => 'يجب قبول :attribute.',
'accepted_if' => 'يجب قبول :attribute عندما يكون :other يساوي :value.',
'active_url' => ':attribute ليس عنوان URL صالحًا.',
'after' => 'يجب أن يكون :attribute تاريخًا بعد :date.',
'after_or_equal' => 'يجب أن يكون :attribute تاريخًا بعد أو يساوي :date.',
'alpha' => 'يجب أن يحتوي :attribute على أحرف فقط.',
'alpha_dash' => 'يجب أن يحتوي :attribute على أحرف وأرقام وشرطات فقط.',
'alpha_num' => 'يجب أن يحتوي :attribute على أحرف وأرقام فقط.',
'array' => 'يجب أن يكون :attribute مصفوفة.',
'before' => 'يجب أن يكون :attribute تاريخًا قبل :date.',
'before_or_equal' => 'يجب أن يكون :attribute تاريخًا قبل أو يساوي :date.',
'between' => [
'numeric' => 'يجب أن يكون :attribute بين :min و :max.',
'file' => 'يجب أن يكون :attribute بين :min و :max كيلوبايت.',
'string' => 'يجب أن يكون :attribute بين :min و :max حرفًا.',
'array' => 'يجب أن يحتوي :attribute على عدد عناصر بين :min و :max.',
],
'boolean' => 'يجب أن يكون حقل :attribute صحيحًا أو خاطئًا.',
'confirmed' => 'تأكيد :attribute غير متطابق.',
'current_password' => 'كلمة المرور غير صحيحة.',
'date' => ':attribute ليس تاريخًا صالحًا.',
'date_equals' => 'يجب أن يكون :attribute تاريخًا يساوي :date.',
'date_format' => 'لا يتطابق :attribute مع الصيغة :format.',
'declined' => 'يجب رفض :attribute.',
'declined_if' => 'يجب رفض :attribute عندما يكون :other يساوي :value.',
'different' => 'يجب أن يكون :attribute و :other مختلفين.',
'digits' => 'يجب أن يتكون :attribute من :digits أرقام.',
'digits_between' => 'يجب أن يتكون :attribute من :min إلى :max أرقام.',
'dimensions' => ':attribute يحتوي على أبعاد صورة غير صالحة.',
'distinct' => 'حقل :attribute يحتوي على قيمة مكررة.',
'email' => 'يجب أن يكون :attribute عنوان بريد إلكتروني صالحًا.',
'ends_with' => 'يجب أن ينتهي :attribute بأحد القيم التالية: :values.',
'enum' => ':attribute المحدد غير صالح.',
'exists' => ':attribute المحدد غير صالح.',
'file' => 'يجب أن يكون :attribute ملفًا.',
'filled' => 'يجب أن يحتوي حقل :attribute على قيمة.',
'gt' => [
'numeric' => 'يجب أن يكون :attribute أكبر من :value.',
'file' => 'يجب أن يكون :attribute أكبر من :value كيلوبايت.',
'string' => 'يجب أن يكون :attribute أكبر من :value حرفًا.',
'array' => 'يجب أن يحتوي :attribute على أكثر من :value عنصر.',
],
'gte' => [
'numeric' => 'يجب أن يكون :attribute أكبر من أو يساوي :value.',
'file' => 'يجب أن يكون :attribute أكبر من أو يساوي :value كيلوبايت.',
'string' => 'يجب أن يكون :attribute أكبر من أو يساوي :value حرفًا.',
'array' => 'يجب أن يحتوي :attribute على :value عنصر أو أكثر.',
],
'image' => 'يجب أن يكون :attribute صورة.',
'in' => ':attribute المحدد غير صالح.',
'in_array' => 'حقل :attribute غير موجود في :other.',
'integer' => 'يجب أن يكون :attribute عددًا صحيحًا.',
'ip' => 'يجب أن يكون :attribute عنوان IP صالحًا.',
'ipv4' => 'يجب أن يكون :attribute عنوان IPv4 صالحًا.',
'ipv6' => 'يجب أن يكون :attribute عنوان IPv6 صالحًا.',
'json' => 'يجب أن يكون :attribute نصًا بصيغة JSON صالحة.',
'lt' => [
'numeric' => 'يجب أن يكون :attribute أقل من :value.',
'file' => 'يجب أن يكون :attribute أقل من :value كيلوبايت.',
'string' => 'يجب أن يكون :attribute أقل من :value حرفًا.',
'array' => 'يجب أن يحتوي :attribute على أقل من :value عنصر.',
],
'lte' => [
'numeric' => 'يجب أن يكون :attribute أقل من أو يساوي :value.',
'file' => 'يجب أن يكون :attribute أقل من أو يساوي :value كيلوبايت.',
'string' => 'يجب أن يكون :attribute أقل من أو يساوي :value حرفًا.',
'array' => 'يجب ألا يحتوي :attribute على أكثر من :value عنصر.',
],
'mac_address' => 'يجب أن يكون :attribute عنوان MAC صالحًا.',
'max' => [
'numeric' => 'يجب ألا يتجاوز :attribute :max.',
'file' => 'يجب ألا يتجاوز :attribute :max كيلوبايت.',
'string' => 'يجب ألا يتجاوز :attribute :max حرفًا.',
'array' => 'يجب ألا يحتوي :attribute على أكثر من :max عنصر.',
],
'mimes' => 'يجب أن يكون :attribute ملفًا من النوع: :values.',
'mimetypes' => 'يجب أن يكون :attribute ملفًا من النوع: :values.',
'min' => [
'numeric' => 'يجب أن يكون :attribute على الأقل :min.',
'file' => 'يجب أن يكون :attribute على الأقل :min كيلوبايت.',
'string' => 'يجب أن يكون :attribute على الأقل :min حرفًا.',
'array' => 'يجب أن يحتوي :attribute على الأقل على :min عنصر.',
],
'multiple_of' => 'يجب أن يكون :attribute مضاعفًا لـ :value.',
'not_in' => ':attribute المحدد غير صالح.',
'not_regex' => 'صيغة :attribute غير صالحة.',
'numeric' => 'يجب أن يكون :attribute رقمًا.',
'password' => 'كلمة المرور غير صحيحة.',
'present' => 'يجب أن يكون حقل :attribute موجودًا.',
'prohibited' => 'حقل :attribute محظور.',
'prohibited_if' => 'حقل :attribute محظور عندما يكون :other يساوي :value.',
'prohibited_unless' => 'حقل :attribute محظور إلا إذا كان :other ضمن :values.',
'prohibits' => 'حقل :attribute يحظر وجود :other.',
'regex' => 'صيغة :attribute غير صالحة.',
'required' => 'حقل :attribute مطلوب.',
'required_array_keys' => 'يجب أن يحتوي حقل :attribute على إدخالات لـ: :values.',
'required_if' => 'حقل :attribute مطلوب عندما يكون :other يساوي :value.',
'required_unless' => 'حقل :attribute مطلوب إلا إذا كان :other ضمن :values.',
'required_with' => 'حقل :attribute مطلوب عند وجود :values.',
'required_with_all' => 'حقل :attribute مطلوب عند وجود جميع القيم :values.',
'required_without' => 'حقل :attribute مطلوب عند عدم وجود :values.',
'required_without_all' => 'حقل :attribute مطلوب عند عدم وجود أي من القيم :values.',
'same' => 'يجب أن يتطابق :attribute مع :other.',
'size' => [
'numeric' => 'يجب أن يكون :attribute مساويًا لـ :size.',
'file' => 'يجب أن يكون :attribute مساويًا لـ :size كيلوبايت.',
'string' => 'يجب أن يكون :attribute مساويًا لـ :size حرفًا.',
'array' => 'يجب أن يحتوي :attribute على :size عنصر.',
],
'starts_with' => 'يجب أن يبدأ :attribute بأحد القيم التالية: :values.',
'string' => 'يجب أن يكون :attribute نصًا.',
'timezone' => 'يجب أن يكون :attribute منطقة زمنية صالحة.',
'unique' => 'تم استخدام :attribute مسبقًا.',
'uploaded' => 'فشل تحميل :attribute.',
'url' => 'يجب أن يكون :attribute عنوان URL صالحًا.',
'uuid' => 'يجب أن يكون :attribute UUID صالحًا.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'رسالة مخصصة',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap our attribute placeholder
| with something more reader friendly such as "E-Mail Address" instead
| of "email". This simply helps us make our message more expressive.
|
*/
'attributes' => [],
];

View File

@@ -0,0 +1,47 @@
#!/bin/bash
set -e
# Resolve the directory the script is located in
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
APP_NAME="Fleetbase"
IMAGE_NAME="fleetbase-linux-static"
CONTAINER_NAME="fleetbase-linux-build"
DIST_DIR="$ROOT_DIR/builds/dist"
BINARY_NAME="fleetbase-linux-x86_64"
DOCKERFILE="$ROOT_DIR/builds/linux/static-build.Dockerfile"
# Ensure pkg-config archive is available
SPC_DOWNLOADS_DIR="$SCRIPT_DIR/spc/downloads"
PKG_TAR="pkg-config-0.29.2.tar.gz"
PKG_URL="https://static-php-cli.fra1.digitaloceanspaces.com/static-php-cli/deps/pkg-config/${PKG_TAR}"
if [[ ! -f "${SPC_DOWNLOADS_DIR}/${PKG_TAR}" ]]; then
echo "📥 pkg-config archive missing downloading..."
mkdir -p "${SPC_DOWNLOADS_DIR}"
curl -L --retry 3 -o "${SPC_DOWNLOADS_DIR}/${PKG_TAR}" "${PKG_URL}"
else
echo "✅ pkg-config archive already present."
fi
# Build the image
echo "📦 Building static Linux binary for ${APP_NAME}..."
docker build -f "$DOCKERFILE" -t "$IMAGE_NAME" .
# Create a container from the built image
echo "📦 Creating container to extract binary..."
docker create --name "$CONTAINER_NAME" "$IMAGE_NAME"
# Make sure dist folder exist
mkdir -p "$DIST_DIR"
# Copy binary from container to local dist folder
echo "📂 Extracting binary..."
docker cp "$CONTAINER_NAME:/go/src/app/dist/frankenphp-linux-x86_64" "$DIST_DIR/$BINARY_NAME"
# Cleanup the temp container
docker rm "$CONTAINER_NAME"
echo "✅ Build complete! Binary is located at: $DIST_DIR/$BINARY_NAME"

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace SPC\builder\linux\library;
class libgeos extends LinuxLibraryBase
{
use \SPC\builder\unix\library\libgeos;
public const NAME = 'libgeos';
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace SPC\builder\unix\library;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\store\FileSystem;
trait libgeos
{
/**
* @throws FileSystemException
* @throws RuntimeException
*/
protected function build(): void
{
FileSystem::resetDir($this->source_dir . '/build');
shell()->cd($this->source_dir . '/build')
->setEnv([
'CFLAGS' => $this->getLibExtraCFlags(),
'LDFLAGS' => $this->getLibExtraLdFlags(),
'LIBS' => $this->getLibExtraLibs(),
])
->execWithEnv("cmake {$this->builder->makeCmakeArgs()} -DBUILD_SHARED_LIBS=OFF ..")
->execWithEnv("make -j{$this->builder->concurrency}")
->execWithEnv('make install');
$this->patchPkgconfPrefix(['geos.pc']);
}
}

View File

@@ -0,0 +1,92 @@
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
WORKDIR /go/src/app
# Copy Fleetbase app
COPY ../../api ./dist/app
# Set working directory to the embedded Fleetbase app
WORKDIR /go/src/app/dist/app
# Setup for production environment
ENV APP_ENV=production
ENV APP_DEBUG=false
ENV BROADCAST_DRIVER=socketcluster
ENV OSRM_HOST="https://router.project-osrm.org"
ENV REGISTRY_PREINSTALLED_EXTENSIONS=true
# Optional: Ensure writable storage
RUN chmod -R 775 bootstrap/cache storage
# Set permissions for deploy script
RUN chmod +x ./deploy.sh
# Move back to main app directory before running build-static.sh
WORKDIR /go/src/app
# Install geos lib
RUN apk add --no-cache geos geos-dev
# Inject the libgeos library handlers
COPY ./builds/linux/spc/libgeos-linux.php ./dist/static-php-cli/src/SPC/builder/linux/library/libgeos.php
COPY ./builds/linux/spc/libgeos-unix.php ./dist/static-php-cli/src/SPC/builder/unix/library/libgeos.php
# Patch source.json to add geos extension source
RUN jq '. + {"php-geos": {"type": "url", "url": "https://github.com/libgeos/php-geos/archive/dfe1ab17b0f155cc315bc13c75689371676e02e1.zip", "license": [{"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/MIT-LICENSE"}, {"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/LGPL-2"}]}}' \
./dist/static-php-cli/config/source.json > ./dist/static-php-cli/config/source.tmp.json && \
mv ./dist/static-php-cli/config/source.tmp.json ./dist/static-php-cli/config/source.json
# Pathc source.json to add libgeos library
RUN jq '. + {"libgeos": {"type": "url", "url": "https://download.osgeo.org/geos/geos-3.12.1.tar.bz2", "filename": "geos-3.12.1.tar.bz2", "extract": "geos-3.12.1", "build-dir": "build", "license": [{"type": "file", "path": "COPYING"}]}}' \
./dist/static-php-cli/config/source.json > ./dist/static-php-cli/config/source.tmp.json && \
mv ./dist/static-php-cli/config/source.tmp.json ./dist/static-php-cli/config/source.json
# Patch ext.json to add geos extension dynamically
RUN jq '. + {"geos": {"type": "external", "arg-type": "enable", "source": "php-geos", "lib-depends": ["libgeos"]}}' \
./dist/static-php-cli/config/ext.json > ./dist/static-php-cli/config/ext.tmp.json && \
mv ./dist/static-php-cli/config/ext.tmp.json ./dist/static-php-cli/config/ext.json
# Patch lib.json to add libgeos
RUN jq '. + {"libgeos": {"source": "libgeos", "static-libs-unix": ["libgeos.a", "libgeos_c.a"]}}' \
./dist/static-php-cli/config/lib.json > ./dist/static-php-cli/config/lib.tmp.json && \
mv ./dist/static-php-cli/config/lib.tmp.json ./dist/static-php-cli/config/lib.json
# Install dependencies for SPC CLI
WORKDIR /go/src/app/dist/static-php-cli
RUN composer install --no-dev -a
# Set PHP extensions to be built (including geos!)
ENV PHP_EXTENSIONS="pdo_mysql,gd,bcmath,redis,intl,zip,gmp,apcu,opcache,imagick,sockets,pcntl,geos,iconv,mbstring,fileinfo,ctype,tokenizer,simplexml,dom,filter,session"
ENV PHP_EXTENSION_LIBS="libgeos,libzip,bzip2,libxml2,openssl,zlib"
# Force SPC to use the local source version (not download binary)
ENV SPC_REL_TYPE=source
# Debug build
ENV SPC_LOG_LEVEL=debug
# Skip compression
ENV NO_COMPRESS=1
# set PHP version
ENV PHP_VERSION=8.2
# Move to the app directory
WORKDIR /go/src/app
# Make sure pkg-config is available within the static build container
COPY ./builds/linux/spc/downloads/pkg-config-0.29.2.tar.gz ./dist/static-php-cli/downloads/pkg-config-0.29.2.tar.gz
# Pre-build pkg-config using the existing tarball
RUN apk add --no-cache build-base && \
tar -xzf ./dist/static-php-cli/downloads/pkg-config-0.29.2.tar.gz -C /tmp && \
cd /tmp/pkg-config-0.29.2 && \
./configure --with-internal-glib --prefix=/go/src/app/dist/static-php-cli/build/bin && \
make && make install && \
rm -rf /tmp/pkg-config-0.29.2
# Do not run git pull
RUN sed -i 's/^[ \t]*git pull/# git pull/' ./build-static.sh
# Build the FrankenPHP static binary
RUN EMBED=dist/app ./build-static.sh

188
builds/osx/build-osx.sh Executable file
View File

@@ -0,0 +1,188 @@
#!/bin/bash
set -e
log() {
echo -e "\033[1;34m[🔧 $1]\033[0m"
}
log_success() {
echo -e "\033[1;32m[✅ $1]\033[0m"
}
log_warn() {
echo -e "\033[1;33m[⚠️ $1]\033[0m"
}
log_error() {
echo -e "\033[1;31m[❌ $1]\033[0m"
}
# Define base paths
log "Resolving directories..."
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
OSX_DIR="$ROOT_DIR/builds/osx"
DIST_DIR="$ROOT_DIR/builds/dist"
APP_DIR="$ROOT_DIR/api"
BREW_PREFIX="/opt/homebrew"
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
BINARY_NAME="fleetbase-$OS-$ARCH"
log "Binary will be: $BINARY_NAME"
# Setup PHP 8.4
log "Detecting current PHP version..."
ORIGINAL_PHP_PATH="$(which php)"
ORIGINAL_PHP_VERSION="$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION.".".PHP_RELEASE_VERSION;' 2>/dev/null)"
IS_ASDF_MANAGED=false
if [[ "$ORIGINAL_PHP_PATH" == *".asdf"* ]]; then
IS_ASDF_MANAGED=true
fi
# 🔁 Trap to restore PHP when script exits
trap 'if [ "$IS_ASDF_MANAGED" = true ]; then
log "Restoring asdf-managed PHP version: $ORIGINAL_PHP_VERSION"
asdf set php "$ORIGINAL_PHP_VERSION" || true
log "Reverted to PHP $(php -v | head -n 1)"
else
log "Unsetting asdf set to restore system PHP"
asdf set php system || true
log "Reverted to PHP $(php -v | head -n 1)"
fi' EXIT
log "Detected PHP version: $ORIGINAL_PHP_VERSION"
log "Detected PHP binary: $ORIGINAL_PHP_PATH"
# ───────────────────────────────────────────────────────────────────────────────
# If the *current* php is already 8.4.x, we skip the entire asdf install step
# ───────────────────────────────────────────────────────────────────────────────
if [[ "$ORIGINAL_PHP_PATH" == "$BREW_PREFIX/bin/php" && "$ORIGINAL_PHP_VERSION" =~ ^8\.4\. ]]; then
log "Homebrew PHP $ORIGINAL_PHP_VERSION detected at $ORIGINAL_PHP_PATH — skipping asdf build/install."
else
# Only install under asdf if we dont already have 8.4.0 installed
log "No Homebrew PHP 8.4 detected (found $ORIGINAL_PHP_PATH $ORIGINAL_PHP_VERSION), using asdf to build/install."
if ! asdf list php | grep -q "8.4.0"; then
# Use brew to install required dependencies for asdf php management
log "Checking and installing Homebrew packages required for PHP 8.4 build..."
for pkg in autoconf automake bison freetype gd gettext icu4c krb5 libedit libiconv libjpeg libpng libxml2 libzip pkg-config re2c zlib sqlite3 libsodium oniguruma openssl@3 nasm; do
if ! brew list "$pkg" &>/dev/null; then
log_warn "$pkg not found. Installing..."
arch -arm64 brew install "$pkg"
else
log "$pkg already installed. Skipping."
fi
done
# Set necessary env flags/paths for PHP build on OSX ARM64
export CPPFLAGS="-I$BREW_PREFIX/opt/oniguruma/include -I$BREW_PREFIX/opt/libsodium/include -I$BREW_PREFIX/opt/bzip2/include -I$BREW_PREFIX/opt/zlib/include -I$BREW_PREFIX/opt/openssl@3/include -I$BREW_PREFIX/opt/libxml2/include -I$BREW_PREFIX/opt/libedit/include -I$BREW_PREFIX/opt/curl/include -I$BREW_PREFIX/opt/sqlite3/include -I$BREW_PREFIX/opt/freetype/include -I$BREW_PREFIX/opt/jpeg/include -I$BREW_PREFIX/opt/libpng/include -I$BREW_PREFIX/opt/libzip/include"
export LDFLAGS="-L$BREW_PREFIX/opt/openssl@3/lib -lssl -lcrypto -lz -L$BREW_PREFIX/opt/oniguruma/lib -lonig -L$BREW_PREFIX/opt/libsodium/lib -lsodium -L$BREW_PREFIX/opt/bzip2/lib -Wl,-rpath,$BREW_PREFIX/opt/bzip2/lib -lbz2 -L$BREW_PREFIX/opt/zlib/lib -L$BREW_PREFIX/opt/openssl@3/lib -L$BREW_PREFIX/opt/libxml2/lib -L$BREW_PREFIX/opt/libedit/lib -L$BREW_PREFIX/opt/sqlite3/lib -lsqlite3 -L$BREW_PREFIX/opt/curl/lib -lcurl -L$BREW_PREFIX/opt/freetype/lib -L$BREW_PREFIX/opt/jpeg/lib -L$BREW_PREFIX/opt/libpng/lib -L$BREW_PREFIX/opt/libzip/lib -lzip -lz"
export PKG_CONFIG_PATH="$BREW_PREFIX/opt/openssl/lib/pkgconfig:$BREW_PREFIX/opt/oniguruma/lib/pkgconfig:$BREW_PREFIX/opt/libsodium/lib/pkgconfig:$BREW_PREFIX/opt/libzip/lib/pkgconfig:$BREW_PREFIX/opt/gd/lib/pkgconfig:$BREW_PREFIX/opt/zlib/lib/pkgconfig:$BREW_PREFIX/opt/openssl@3/lib/pkgconfig:$BREW_PREFIX/opt/libxml2/lib/pkgconfig:$BREW_PREFIX/opt/curl/lib/pkgconfig:$BREW_PREFIX/opt/sqlite3/lib/pkgconfig:$BREW_PREFIX/opt/freetype/lib/pkgconfig:$BREW_PREFIX/opt/jpeg/lib/pkgconfig:$BREW_PREFIX/opt/libpng/lib/pkgconfig"
export PHP_CONFIGURE_OPTIONS="--with-openssl=$(brew --prefix openssl) --with-iconv=$(brew --prefix libiconv)"
log "Installing PHP 8.4.0 with asdf..."
asdf install php 8.4.0 --verbose
else
log "asdf already has PHP 8.4.0 installed, skipping"
fi
log "Switching to PHP 8.4.0 with asdf set..."
asdf set php 8.4.0 --home
fi
# Clone FrankenPHP
if [ ! -d "$OSX_DIR/frankenphp" ]; then
log "Cloning FrankenPHP..."
git clone https://github.com/dunglas/frankenphp "$OSX_DIR/frankenphp"
else
log_warn "FrankenPHP already cloned. Skipping."
fi
cd "$OSX_DIR/frankenphp"
# Patch build script
log "Patching build-static.sh to skip git pull..."
sed -i '' 's/^[ \t]*git pull/# git pull/' ./build-static.sh
# Set environment variables
log "Exporting build environment variables..."
export PHP_VERSION=8.2
export PHP_EXTENSIONS="pdo_mysql,gd,bcmath,redis,intl,zip,gmp,apcu,opcache,imagick,sockets,pcntl,geos,iconv,mbstring,fileinfo,ctype,tokenizer,simplexml,dom,filter,session"
export PHP_EXTENSION_LIBS="libgeos,libzip,bzip2,libxml2,openssl,zlib"
export SPC_REL_TYPE=source
export NO_COMPRESS=1
export SPC_OPT_BUILD_ARGS="--debug"
export CMAKE_OSX_ARCHITECTURES=arm64
# Clone and prepare static-php-cli in dist/
STATIC_PHP_CLI_DIR="$OSX_DIR/frankenphp/dist/static-php-cli"
if [ ! -d "$STATIC_PHP_CLI_DIR" ]; then
log "Cloning static-php-cli into dist/..."
git clone https://github.com/crazywhalecc/static-php-cli.git "$STATIC_PHP_CLI_DIR"
else
log_warn "static-php-cli already exists in dist/. Skipping clone."
fi
# Inject libgeos support
log "Injecting libgeos patch files..."
cp "$ROOT_DIR/builds/osx/spc/libgeos-unix.php" "$STATIC_PHP_CLI_DIR/src/SPC/builder/unix/library/libgeos.php"
cp "$ROOT_DIR/builds/osx/spc/libgeos-macos.php" "$STATIC_PHP_CLI_DIR/src/SPC/builder/macos/library/libgeos.php"
cp "$ROOT_DIR/builds/osx/spc/UnixBuilderBase-macos.php" "$STATIC_PHP_CLI_DIR/src/SPC/builder/unix/UnixBuilderBase.php"
# Patch SPC config
log "Patching SPC config files (source.json, ext.json, lib.json)..."
jq '. + {"php-geos": {"type": "url", "url": "https://github.com/libgeos/php-geos/archive/dfe1ab17b0f155cc315bc13c75689371676e02e1.zip", "license": [{"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/MIT-LICENSE"}, {"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/LGPL-2"}]}}' \
"$STATIC_PHP_CLI_DIR/config/source.json" > "$STATIC_PHP_CLI_DIR/config/source.tmp.json" && \
mv "$STATIC_PHP_CLI_DIR/config/source.tmp.json" "$STATIC_PHP_CLI_DIR/config/source.json"
jq '. + {"libgeos": {"type": "url", "url": "https://download.osgeo.org/geos/geos-3.12.1.tar.bz2", "filename": "geos-3.12.1.tar.bz2", "extract": "geos-3.12.1", "build-dir": "build", "license": [{"type": "file", "path": "COPYING"}]}}' \
"$STATIC_PHP_CLI_DIR/config/source.json" > "$STATIC_PHP_CLI_DIR/config/source.tmp.json" && \
mv "$STATIC_PHP_CLI_DIR/config/source.tmp.json" "$STATIC_PHP_CLI_DIR/config/source.json"
jq '. + {"libgeos": {"source": "libgeos", "static-libs-unix": ["libgeos.a", "libgeos_c.a"]}}' \
"$STATIC_PHP_CLI_DIR/config/lib.json" > "$STATIC_PHP_CLI_DIR/config/lib.tmp.json" && \
mv "$STATIC_PHP_CLI_DIR/config/lib.tmp.json" "$STATIC_PHP_CLI_DIR/config/lib.json"
jq '. + {"geos": {"type": "external", "arg-type": "enable", "source": "php-geos", "lib-depends": ["libgeos"]}}' \
"$STATIC_PHP_CLI_DIR/config/ext.json" > "$STATIC_PHP_CLI_DIR/config/ext.tmp.json" && \
mv "$STATIC_PHP_CLI_DIR/config/ext.tmp.json" "$STATIC_PHP_CLI_DIR/config/ext.json"
# Prepare app embed folder
log "📦 Preparing embedded app directory..."
rm -rf ./dist/app
mkdir -p ./dist/app
cp -R "$APP_DIR"/* ./dist/app/
log "Patching build-static.sh to skip git pull and composer install..."
# Skip `git pull`
sed -i '' 's/^[[:space:]]*git pull/# git pull/' "$OSX_DIR/frankenphp/build-static.sh"
# Patch add CoreServices framework for Caddy build on OSX
sed -i '' 's/-framework CoreFoundation -framework SystemConfiguration/& -framework CoreServices/' "$OSX_DIR/frankenphp/build-static.sh"
# ── work around 403 on GH macOS runners ────────────────────────────────────────
log "Patching curl to use a browser-like User-Agent (to avoid 403s)…"
curl() {
command curl -sSL -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15" "$@"
}
export -f curl
# Build the binary
log "⚙️ Running FrankenPHP build-static.sh..."
EMBED=dist/app ./build-static.sh
# Move built binary to dist
log "Moving built binary to output folder..."
mkdir -p "$DIST_DIR"
mv dist/frankenphp-mac-$ARCH "$DIST_DIR/$BINARY_NAME"
log_success "✅ macOS binary built at: $DIST_DIR/$BINARY_NAME"
# Clean up frankenphp build and app embed folder
log "🧹 Cleaning temporary app directory..."
rm -rf "$OSX_DIR/frankenphp"

View File

@@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
namespace SPC\builder\unix;
use SPC\builder\BuilderBase;
use SPC\builder\linux\LinuxBuilder;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\util\DependencyUtil;
use SPC\util\SPCConfigUtil;
abstract class UnixBuilderBase extends BuilderBase
{
/** @var string cflags */
public string $arch_c_flags;
/** @var string C++ flags */
public string $arch_cxx_flags;
/** @var string cmake toolchain file */
public string $cmake_toolchain_file;
/**
* @throws WrongUsageException
* @throws FileSystemException
*/
public function getAllStaticLibFiles(): array
{
$libs = [];
// reorder libs
foreach ($this->libs as $lib) {
foreach ($lib->getDependencies() as $dep) {
$libs[] = $dep;
}
$libs[] = $lib;
}
$libFiles = [];
$libNames = [];
// merge libs
foreach ($libs as $lib) {
if (!in_array($lib::NAME, $libNames, true)) {
$libNames[] = $lib::NAME;
array_unshift($libFiles, ...$lib->getStaticLibs());
}
}
return array_map(fn ($x) => realpath(BUILD_LIB_PATH . "/{$x}"), $libFiles);
}
/**
* Return generic cmake options when configuring cmake projects
*/
public function makeCmakeArgs(): string
{
$extra = $this instanceof LinuxBuilder ? '-DCMAKE_C_COMPILER=' . getenv('CC') . ' ' : '';
// NEW: allow env-variable override
$arch = getenv('CMAKE_OSX_ARCHITECTURES') ?: 'arm64';
return $extra .
'-DCMAKE_BUILD_TYPE=Release ' .
'-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' .
'-DCMAKE_INSTALL_BINDIR=bin ' .
'-DCMAKE_INSTALL_LIBDIR=lib ' .
'-DCMAKE_INSTALL_INCLUDEDIR=include ' .
"-DCMAKE_OSX_ARCHITECTURES={$arch} " .
"-DCMAKE_TOOLCHAIN_FILE={$this->cmake_toolchain_file}";
}
/**
* Generate configure flags
*/
public function makeAutoconfFlags(int $flag = AUTOCONF_ALL): string
{
$extra = '';
// TODO: add auto pkg-config support
if (($flag & AUTOCONF_LIBS) === AUTOCONF_LIBS) {
$extra .= 'LIBS="' . BUILD_LIB_PATH . '" ';
}
if (($flag & AUTOCONF_CFLAGS) === AUTOCONF_CFLAGS) {
$extra .= 'CFLAGS="-I' . BUILD_INCLUDE_PATH . '" ';
}
if (($flag & AUTOCONF_CPPFLAGS) === AUTOCONF_CPPFLAGS) {
$extra .= 'CPPFLAGS="-I' . BUILD_INCLUDE_PATH . '" ';
}
if (($flag & AUTOCONF_LDFLAGS) === AUTOCONF_LDFLAGS) {
$extra .= 'LDFLAGS="-L' . BUILD_LIB_PATH . '" ';
}
return $extra;
}
public function proveLibs(array $sorted_libraries): void
{
// search all supported libs
$support_lib_list = [];
$classes = FileSystem::getClassesPsr4(
ROOT_DIR . '/src/SPC/builder/' . osfamily2dir() . '/library',
'SPC\builder\\' . osfamily2dir() . '\library'
);
foreach ($classes as $class) {
if (defined($class . '::NAME') && $class::NAME !== 'unknown' && Config::getLib($class::NAME) !== null) {
$support_lib_list[$class::NAME] = $class;
}
}
// if no libs specified, compile all supported libs
if ($sorted_libraries === [] && $this->isLibsOnly()) {
$libraries = array_keys($support_lib_list);
$sorted_libraries = DependencyUtil::getLibs($libraries);
}
// add lib object for builder
foreach ($sorted_libraries as $library) {
if (!in_array(Config::getLib($library, 'type', 'lib'), ['lib', 'package'])) {
continue;
}
// if some libs are not supported (but in config "lib.json", throw exception)
if (!isset($support_lib_list[$library])) {
throw new WrongUsageException('library [' . $library . '] is in the lib.json list but not supported to compile, but in the future I will support it!');
}
$lib = new ($support_lib_list[$library])($this);
$this->addLib($lib);
}
// calculate and check dependencies
foreach ($this->libs as $lib) {
$lib->calcDependency();
}
$this->lib_list = $sorted_libraries;
}
/**
* Sanity check after build complete
*
* @throws RuntimeException
*/
protected function sanityCheck(int $build_target): void
{
// sanity check for php-cli
if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {
logger()->info('running cli sanity check');
[$ret, $output] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n -r "echo \"hello\";"');
$raw_output = implode('', $output);
if ($ret !== 0 || trim($raw_output) !== 'hello') {
throw new RuntimeException("cli failed sanity check: ret[{$ret}]. out[{$raw_output}]");
}
foreach ($this->getExts(false) as $ext) {
logger()->debug('testing ext: ' . $ext->getName());
$ext->runCliCheckUnix();
}
}
// sanity check for phpmicro
if (($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) {
$test_task = $this->getMicroTestTasks();
foreach ($test_task as $task_name => $task) {
$test_file = SOURCE_PATH . '/' . $task_name . '.exe';
if (file_exists($test_file)) {
@unlink($test_file);
}
file_put_contents($test_file, file_get_contents(SOURCE_PATH . '/php-src/sapi/micro/micro.sfx') . $task['content']);
chmod($test_file, 0755);
[$ret, $out] = shell()->execWithResult($test_file);
foreach ($task['conditions'] as $condition => $closure) {
if (!$closure($ret, $out)) {
$raw_out = trim(implode('', $out));
throw new RuntimeException("micro failed sanity check: {$task_name}, condition [{$condition}], ret[{$ret}], out[{$raw_out}]");
}
}
}
}
// sanity check for embed
if (($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED) {
logger()->info('running embed sanity check');
$sample_file_path = SOURCE_PATH . '/embed-test';
if (!is_dir($sample_file_path)) {
@mkdir($sample_file_path);
}
// copy embed test files
copy(ROOT_DIR . '/src/globals/common-tests/embed.c', $sample_file_path . '/embed.c');
copy(ROOT_DIR . '/src/globals/common-tests/embed.php', $sample_file_path . '/embed.php');
$util = new SPCConfigUtil($this);
$config = $util->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs'));
$lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}";
if (PHP_OS_FAMILY === 'Linux' && getenv('SPC_LIBC') === 'musl') {
$lens .= ' -static';
}
[$ret, $out] = shell()->cd($sample_file_path)->execWithResult(getenv('CC') . ' -o embed embed.c ' . $lens);
if ($ret !== 0) {
throw new RuntimeException('embed failed sanity check: build failed. Error message: ' . implode("\n", $out));
}
// if someone changed to --enable-embed=shared, we need to add LD_LIBRARY_PATH
if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') {
$ext_path = 'LD_LIBRARY_PATH=' . BUILD_ROOT_PATH . '/lib:$LD_LIBRARY_PATH ';
FileSystem::removeFileIfExists(BUILD_ROOT_PATH . '/lib/libphp.a');
} else {
$ext_path = '';
FileSystem::removeFileIfExists(BUILD_ROOT_PATH . '/lib/libphp.so');
}
[$ret, $output] = shell()->cd($sample_file_path)->execWithResult($ext_path . './embed');
if ($ret !== 0 || trim(implode('', $output)) !== 'hello') {
throw new RuntimeException('embed failed sanity check: run failed. Error message: ' . implode("\n", $output));
}
}
}
/**
* 将编译好的二进制文件发布到 buildroot
*
* @param int $type 发布类型
* @throws RuntimeException
* @throws FileSystemException
*/
protected function deployBinary(int $type): bool
{
$src = match ($type) {
BUILD_TARGET_CLI => SOURCE_PATH . '/php-src/sapi/cli/php',
BUILD_TARGET_MICRO => SOURCE_PATH . '/php-src/sapi/micro/micro.sfx',
BUILD_TARGET_FPM => SOURCE_PATH . '/php-src/sapi/fpm/php-fpm',
default => throw new RuntimeException('Deployment does not accept type ' . $type),
};
logger()->info('Deploying ' . $this->getBuildTypeName($type) . ' file');
FileSystem::createDir(BUILD_ROOT_PATH . '/bin');
shell()->exec('cp ' . escapeshellarg($src) . ' ' . escapeshellarg(BUILD_ROOT_PATH . '/bin/'));
return true;
}
/**
* Run php clean
*
* @throws RuntimeException
*/
protected function cleanMake(): void
{
logger()->info('cleaning up');
shell()->cd(SOURCE_PATH . '/php-src')->exec('make clean');
}
/**
* Patch phpize and php-config if needed
* @throws FileSystemException
*/
protected function patchPhpScripts(): void
{
// patch phpize
if (file_exists(BUILD_BIN_PATH . '/phpize')) {
logger()->debug('Patching phpize prefix');
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'");
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#');
}
// patch php-config
if (file_exists(BUILD_BIN_PATH . '/php-config')) {
logger()->debug('Patching php-config prefix and libs order');
$php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config');
$php_config_str = str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str);
// move mimalloc to the beginning of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str);
// move lstdc++ to the end of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str);
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace SPC\builder\macos\library;
class libgeos extends MacOSLibraryBase
{
use \SPC\builder\unix\library\libgeos;
public const NAME = 'libgeos';
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace SPC\builder\unix\library;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\store\FileSystem;
trait libgeos
{
/**
* @throws FileSystemException
* @throws RuntimeException
*/
protected function build(): void
{
FileSystem::resetDir($this->source_dir . '/build');
shell()->cd($this->source_dir . '/build')
->setEnv([
'CFLAGS' => $this->getLibExtraCFlags(),
'LDFLAGS' => $this->getLibExtraLdFlags(),
'LIBS' => $this->getLibExtraLibs(),
])
->execWithEnv("cmake {$this->builder->makeCmakeArgs()} -DBUILD_SHARED_LIBS=OFF ..")
->execWithEnv("make -j{$this->builder->concurrency}")
->execWithEnv('make install');
$this->patchPkgconfPrefix(['geos.pc']);
}
}

View File

@@ -1,24 +1,24 @@
# ---- 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 /app
WORKDIR /app
# 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
ENV PNPM_HOME=/root/.pnpm
# Set environment
ARG ENVIRONMENT=production
# Add the pnpm global bin to the PATH
ENV PATH /root/.pnpm/bin:$PATH
ENV PATH=/root/.pnpm/bin:$PATH
# Copy pnpm-lock.yaml (or package.json) into the directory /app in the container
COPY console/package.json console/pnpm-lock.yaml ./
# Copy pnpm-lock.yaml (or package.json) into the directory /console in the container
COPY package.json pnpm-lock.yaml ./
# Copy over .npmrc if applicable
COPY console/.npmr[c] ./
COPY .npmr[c] ./
# Install global dependencies
RUN npm install -g ember-cli pnpm
@@ -32,8 +32,8 @@ 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 /app
COPY console .
# Copy the console directory contents into the container at /console
COPY . .
# Build the application
RUN pnpm build --environment $ENVIRONMENT
@@ -42,13 +42,13 @@ RUN pnpm build --environment $ENVIRONMENT
FROM nginx:alpine
# Copy the built app to our served directory
COPY --from=builder /app/dist /usr/share/nginx/html
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
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Start Nginx server
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -4,6 +4,7 @@ import loadInitializers from 'ember-load-initializers';
import config from '@fleetbase/console/config/environment';
import loadExtensions from '@fleetbase/ember-core/utils/load-extensions';
import mapEngines from '@fleetbase/ember-core/utils/map-engines';
import loadRuntimeConfig from '@fleetbase/console/utils/runtime-config';
export default class App extends Application {
modulePrefix = config.modulePrefix;
@@ -20,4 +21,11 @@ export default class App extends Application {
}
}
loadInitializers(App, config.modulePrefix);
document.addEventListener('DOMContentLoaded', async () => {
await loadRuntimeConfig();
loadInitializers(App, config.modulePrefix);
let fleetbase = App.create();
fleetbase.deferReadiness();
fleetbase.boot();
});

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

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

View File

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

@@ -16,7 +16,7 @@
{{/if}}
</InputGroup>
<InputGroup @wrapperClass="mb-0i">
<Checkbox @label="APN Production" @value={{this.apn.production}} @onToggle={{fn (mut this.apn.production)}} @disabled={{this.isLoading}} />
<Checkbox @label="APN Production" @value={{this.apn.production}} @onToggle={{this.toggleApnProduction}} />
</InputGroup>
</ContentPanel>
@@ -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

@@ -32,6 +32,13 @@ export default class ConfigureNotificationChannelsComponent extends Component {
this.loadConfigValues();
}
@action toggleApnProduction(checked) {
this.apn = {
...this.apn,
production: checked,
};
}
@action removeApnFile() {
const apnConfig = this.apn;
apnConfig.private_key_file = null;

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

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

View File

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

View File

@@ -1,7 +1,9 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { alias } from '@ember/object/computed';
import { debug } from '@ember/debug';
import { task } from 'ember-concurrency';
export default class ConsoleAccountIndexController extends Controller {
@@ -40,6 +42,18 @@ export default class ConsoleAccountIndexController extends Controller {
*/
@alias('currentUser.user') user;
/**
* Available timezones for selection.
*
* @memberof ConsoleAccountIndexController
*/
@tracked timezones = [];
constructor() {
super(...arguments);
this.loadTimezones.perform();
}
/**
* Handle upload of new photo
*
@@ -116,6 +130,19 @@ export default class ConsoleAccountIndexController extends Controller {
return isPasswordValid;
}
/**
* Load all available timezones from lookup.
*
* @memberof ConsoleAccountIndexController
*/
@task *loadTimezones() {
try {
this.timezones = yield this.fetch.get('lookup/timezones');
} catch (error) {
debug(`Unable to load timezones : ${error.message}`);
}
}
/**
* Checks if any user attribute has been changed
*

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

@@ -2,6 +2,8 @@ import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { debug } from '@ember/debug';
import { task } from 'ember-concurrency';
export default class ConsoleSettingsIndexController extends Controller {
/**
@@ -25,13 +27,6 @@ export default class ConsoleSettingsIndexController extends Controller {
*/
@service fetch;
/**
* The request loading state.
*
* @memberof ConsoleSettingsIndexController
*/
@tracked isLoading = false;
/**
* the upload queue.
*
@@ -46,23 +41,32 @@ export default class ConsoleSettingsIndexController extends Controller {
*/
@tracked uploadedFiles = [];
/**
* Available timezones for selection.
*
* @memberof ConsoleAccountIndexController
*/
@tracked timezones = [];
constructor() {
super(...arguments);
this.loadTimezones.perform();
}
/**
* Save the organization settings.
*
* @memberof ConsoleSettingsIndexController
*/
@action saveSettings(event) {
event.preventDefault();
this.isLoading = true;
@task *saveSettings(event) {
event?.preventDefault();
this.model
.save()
.then(() => {
this.notifications.success('Organization changes successfully saved.');
})
.finally(() => {
this.isLoading = false;
});
try {
yield this.model.save();
this.notifications.success('Organization changes successfully saved.');
} catch (error) {
debug(`Unable to save organization settings : ${error.message}`);
}
}
/**
@@ -91,4 +95,17 @@ export default class ConsoleSettingsIndexController extends Controller {
}
);
}
/**
* Load all available timezones from lookup.
*
* @memberof ConsoleAccountIndexController
*/
@task *loadTimezones() {
try {
this.timezones = yield this.fetch.get('lookup/timezones');
} catch (error) {
debug(`Unable to load timezones : ${error.message}`);
}
}
}

View File

@@ -2,27 +2,28 @@ import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import createNotificationKey from '../../../utils/create-notification-key';
import createNotificationKey from '@fleetbase/ember-core/utils/create-notification-key';
import { task } from 'ember-concurrency';
export default class ConsoleAdminNotificationsController extends Controller {
export default class ConsoleSettingsNotificationsController extends Controller {
/**
* Inject the notifications service.
*
* @memberof ConsoleAdminNotificationsController
* @memberof ConsoleSettingsNotificationsController
*/
@service notifications;
/**
* Inject the fetch service.
*
* @memberof ConsoleAdminNotificationsController
* @memberof ConsoleSettingsNotificationsController
*/
@service fetch;
/**
* The notification settings value JSON.
*
* @memberof ConsoleAdminNotificationsController
* @memberof ConsoleSettingsNotificationsController
* @var {Object}
*/
@tracked notificationSettings = {};
@@ -30,26 +31,18 @@ export default class ConsoleAdminNotificationsController extends Controller {
/**
* Notification transport methods enabled.
*
* @memberof ConsoleAdminNotificationsController
* @memberof ConsoleSettingsNotificationsController
* @var {Array}
*/
@tracked notificationTransportMethods = ['email', 'sms'];
/**
* Tracked property for the loading state
*
* @memberof ConsoleAdminNotificationsController
* @var {Boolean}
*/
@tracked isLoading = false;
/**
* Creates an instance of ConsoleAdminNotificationsController.
* @memberof ConsoleAdminNotificationsController
* Creates an instance of ConsoleSettingsNotificationsController.
* @memberof ConsoleSettingsNotificationsController
*/
constructor() {
super(...arguments);
this.getSettings();
this.getSettings.perform();
}
/**
@@ -57,7 +50,7 @@ export default class ConsoleAdminNotificationsController extends Controller {
*
* @param {Object} notification
* @param {Array} notifiables
* @memberof ConsoleAdminNotificationsController
* @memberof ConsoleSettingsNotificationsController
*/
@action onSelectNotifiable(notification, notifiables) {
const notificationKey = createNotificationKey(notification.definition, notification.name);
@@ -83,7 +76,7 @@ export default class ConsoleAdminNotificationsController extends Controller {
* Mutates the notification settings property.
*
* @param {Object} [_notificationSettings={}]
* @memberof ConsoleAdminNotificationsController
* @memberof ConsoleSettingsNotificationsController
*/
mutateNotificationSettings(_notificationSettings = {}) {
this.notificationSettings = {
@@ -93,44 +86,32 @@ export default class ConsoleAdminNotificationsController extends Controller {
}
/**
* Save notification settings to the server.
* Save notification settings.
*
* @action
* @method saveSettings
* @returns {Promise}
* @memberof ConsoleAdminNotificationsController
* @memberof ConsoleSettingsNotificationsController
*/
@action saveSettings() {
@task *saveSettings() {
const { notificationSettings } = this;
this.isLoading = true;
return this.fetch
.post('notifications/save-settings', { notificationSettings })
.then(() => {
this.notifications.success('Notification settings successfully saved.');
})
.catch((error) => {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
try {
yield this.fetch.post('notifications/save-settings', { notificationSettings });
this.notifications.success('Notification settings successfully saved.');
} catch (error) {
this.notifications.serverError(error);
}
}
/**
* Fetches and updates notification settings asynchronously.
* Get notification settings.
*
* @returns {Promise<void>} A promise for successful retrieval and update, or an error on failure.
* @memberof ConsoleSettingsNotificationsController
*/
getSettings() {
return this.fetch
.get('notifications/get-settings')
.then(({ notificationSettings }) => {
this.notificationSettings = notificationSettings;
})
.catch((error) => {
this.notifications.serverError(error);
});
@task *getSettings() {
try {
const { notificationSettings } = yield this.fetch.get('notifications/get-settings');
this.notificationSettings = notificationSettings;
} catch (error) {
this.notifications.serverError(error);
}
}
}

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,6 +0,0 @@
import { helper } from '@ember/component/helper';
import createNotificationKey from '../utils/create-notification-key';
export default helper(function getNotificationKey([definition, name]) {
return createNotificationKey(definition, name);
});

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,17 +1,9 @@
import loadExtensions from '@fleetbase/ember-core/utils/load-extensions';
export function initialize(owner) {
const universe = owner.lookup('service:universe');
loadExtensions().then((extensions) => {
extensions.forEach((extension) => {
universe.loadEngine(extension.name).then((engineInstance) => {
if (engineInstance.base && engineInstance.base.setupExtension) {
engineInstance.base.setupExtension(owner, engineInstance, universe);
}
});
});
});
export function initialize(application) {
const universe = application.lookup('service:universe');
if (universe) {
universe.createRegistries(['@fleetbase/console', 'auth:login']);
universe.bootEngines(application);
}
}
export default {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import Model, { attr } from '@ember-data/model';
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';
import { getOwner } from '@ember/application';
@@ -23,18 +24,28 @@ export default class UserModel extends Model {
@attr('string') country;
@attr('string') ip_address;
@attr('string') slug;
@attr('string') role_name;
@attr('string') type;
@attr('string') session_status;
@attr('string') status;
@attr('string') locale;
@attr('boolean') is_online;
@attr('boolean') is_admin;
@attr('raw') types;
@attr('boolean') is_subscribed;
@attr('boolean') is_trialing;
@attr('raw') meta;
@attr('raw') subscription;
/** @relationships */
@belongsTo('role') role;
@hasMany('policy') policies;
@hasMany('permission') permissions;
/** @dates */
@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;
@@ -43,7 +54,7 @@ export default class UserModel extends Model {
/** @methods */
deactivate() {
const owner = getOwner(this);
const fetch = owner.lookup(`service:fetch`);
const fetch = owner.lookup('service:fetch');
return fetch.patch(`users/deactivate/${this.id}`).then((response) => {
this.session_status = 'inactive';
@@ -54,7 +65,7 @@ export default class UserModel extends Model {
activate() {
const owner = getOwner(this);
const fetch = owner.lookup(`service:fetch`);
const fetch = owner.lookup('service:fetch');
return fetch.patch(`users/activate/${this.id}`).then((response) => {
this.session_status = 'active';
@@ -63,29 +74,85 @@ 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`);
const fetch = owner.lookup('service:fetch');
return fetch.delete(`users/remove-from-company/${this.id}`);
}
resendInvite() {
const owner = getOwner(this);
const fetch = owner.lookup(`service:fetch`);
const fetch = owner.lookup('service:fetch');
return fetch.post(`users/resend-invite`, { user: this.id });
}
getPermissions() {
const permissions = [];
// get direct applied permissions
if (this.get('permissions')) {
permissions.pushObjects(this.get('permissions').toArray());
}
// get role permissions and role policies permissions
if (this.get('role')) {
if (this.get('role.permissions')) {
permissions.pushObjects(this.get('role.permissions').toArray());
}
if (this.get('role.policies')) {
for (let i = 0; i < this.get('role.policies').length; i++) {
const policy = this.get('role.policies').objectAt(i);
if (policy.get('permissions')) {
permissions.pushObjects(policy.get('permissions').toArray());
}
}
}
}
// get direct applied policy permissions
if (this.get('policies')) {
for (let i = 0; i < this.get('policies').length; i++) {
const policy = this.get('policies').objectAt(i);
if (policy.get('permissions')) {
permissions.pushObjects(policy.get('permissions').toArray());
}
}
}
return permissions;
}
/** @computed */
@not('isEmailVerified') emailIsNotVerified;
@not('isPhoneVerified') phoneIsNotVerified;
/** @computed */
get allPermissions() {
return this.getPermissions();
}
@computed('meta.two_factor_enabled') get isTwoFactorEnabled() {
return this.meta && this.meta.two_factor_enabled;
}
@computed('is_admin') get isAdmin() {
return this.is_admin === true;
}
@computed('types') get typesList() {
const types = Array.from(this.types);
return types.join(', ');

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,43 +1,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 {
/**
* Inject the `store` service
*
* @var {Service}
*/
@service store;
/**
* Inject the `fetch` service
*
* @var {Service}
*/
@service fetch;
/**
* Inject the `session` service
*
* @var {Service}
*/
@service session;
/**
* Inject the `intl` service
*
* @var {Service}
*/
@service intl;
/**
* Inject the `currentUser` service
*
* @var {Service}
*/
@service universe;
@service router;
@service currentUser;
@service intl;
/**
* Require authentication to access all `console` routes.
@@ -46,10 +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);
}
/**
@@ -61,54 +59,4 @@ export default class ConsoleRoute extends Route {
model() {
return this.store.findRecord('brand', 1);
}
/**
* We will use this hook to preload engines
*
* @void
*/
@action afterModel() {
this.fetchSessionInfo();
}
/**
* We will use this hook to setup controller and more
*
* @void
*/
@action setupController(controller, model) {
super.setupController(controller, model);
// Get and set user locale
this.fetch.get('users/locale').then(({ locale }) => {
this.intl.setLocale(locale);
});
// Get user organizations
this.fetch.get('auth/organizations').then((organizations) => {
this.currentUser.setOption('organizations', organizations);
controller.organizations = organizations;
});
}
/**
* Use this hook to fetch user related queries
*
* @void
*/
@action fetchSessionInfo() {
this.fetch.shouldResetCache();
this.fetch
.cachedGet(
'lookup/whois',
{},
{
expirationInterval: 60,
expirationIntervalUnit: 'minutes',
}
)
.then((whois) => {
this.currentUser.setOption('whois', whois);
});
}
}

View File

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

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

View File

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

View File

@@ -1,3 +0,0 @@
import Route from '@ember/routing/route';
export default class ConsoleExtensionsRoute extends Route {}

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