mirror of
https://github.com/fleetbase/fleetbase.git
synced 2026-01-04 05:17:09 +00:00
Compare commits
123 Commits
v0.5.10
...
dev-v0.7.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1ab6a3b11 | ||
|
|
c79fe67e44 | ||
|
|
d8adf42b24 | ||
|
|
80da5fe013 | ||
|
|
06fd5e20e8 | ||
|
|
f04807de1e | ||
|
|
b7666eeb3e | ||
|
|
dd895a0fd8 | ||
|
|
8c74c0fb99 | ||
|
|
92170c965e | ||
|
|
fcb3694874 | ||
|
|
aa46059bff | ||
|
|
a5175bb11b | ||
|
|
01816a1fe0 | ||
|
|
15d500cd58 | ||
|
|
95d77a6ddd | ||
|
|
eefc93e130 | ||
|
|
0f18ae85f1 | ||
|
|
a4812192da | ||
|
|
15d3c957b8 | ||
|
|
c2bd098d14 | ||
|
|
98511fd418 | ||
|
|
225110c8dc | ||
|
|
1aa2a99763 | ||
|
|
6e888af772 | ||
|
|
d61205d898 | ||
|
|
72078553cc | ||
|
|
bfae04a645 | ||
|
|
c59f028755 | ||
|
|
2b959db773 | ||
|
|
a9354ccbfd | ||
|
|
23e6d1e6b9 | ||
|
|
86da1bd095 | ||
|
|
ae89600ae6 | ||
|
|
6697b79185 | ||
|
|
4dc9764853 | ||
|
|
0626bc0171 | ||
|
|
a8adf3fd84 | ||
|
|
7b8bc4a593 | ||
|
|
490f2f1b41 | ||
|
|
e1fc7850d3 | ||
|
|
cc278bf1bb | ||
|
|
af86aaba8b | ||
|
|
f35dcb1544 | ||
|
|
29c8f4340d | ||
|
|
1cb833e407 | ||
|
|
e372bc6396 | ||
|
|
2f432d148a | ||
|
|
41bc6e39a7 | ||
|
|
5dbe2fb5bb | ||
|
|
8f66bc12e4 | ||
|
|
313b5ea3ba | ||
|
|
698f5979b1 | ||
|
|
f6f6899650 | ||
|
|
83a7ab7338 | ||
|
|
acf7b209af | ||
|
|
5c048a8238 | ||
|
|
de00ad31db | ||
|
|
8fe52c6157 | ||
|
|
a371e055ca | ||
|
|
bbec73fcef | ||
|
|
838a829a11 | ||
|
|
dbb7bc793a | ||
|
|
f0fa867ef9 | ||
|
|
3cc64913ca | ||
|
|
d034c4ad03 | ||
|
|
b740cf035e | ||
|
|
cc42779efc | ||
|
|
4a5422e357 | ||
|
|
21a0808b99 | ||
|
|
f6cb850219 | ||
|
|
80707774ac | ||
|
|
96318bb909 | ||
|
|
2c10f3551e | ||
|
|
41a469c983 | ||
|
|
edf7efe167 | ||
|
|
d7a2dd474a | ||
|
|
2b0c6f793d | ||
|
|
3e60479130 | ||
|
|
215d5dc42e | ||
|
|
a57467539b | ||
|
|
c612c97e43 | ||
|
|
683e93abe0 | ||
|
|
ba63441e7c | ||
|
|
879409d530 | ||
|
|
6105b575c6 | ||
|
|
1e331d70b1 | ||
|
|
57c22eccb7 | ||
|
|
989ca4d35e | ||
|
|
afb1c1dbdc | ||
|
|
0ac52bc772 | ||
|
|
50d8ffee33 | ||
|
|
015b87ba82 | ||
|
|
983d4e5bae | ||
|
|
9bc78c0bcc | ||
|
|
8f13603f4b | ||
|
|
c6bef55839 | ||
|
|
dcecbf2953 | ||
|
|
08b8566b90 | ||
|
|
7f3aa5005d | ||
|
|
5e36ac0aa2 | ||
|
|
da6e8e79ba | ||
|
|
dd1271b1ce | ||
|
|
39f00789bf | ||
|
|
ffc0d0fd1a | ||
|
|
f8196ccc03 | ||
|
|
6f1664e123 | ||
|
|
687af92752 | ||
|
|
eb3f706791 | ||
|
|
5ceb3cbc84 | ||
|
|
7792cf31e2 | ||
|
|
50f30742a8 | ||
|
|
c7b1a876f5 | ||
|
|
892eaeeca0 | ||
|
|
32f4b69697 | ||
|
|
6317c4b2e4 | ||
|
|
e7c229ece5 | ||
|
|
983a3d22b5 | ||
|
|
42105380ca | ||
|
|
8fd4a40016 | ||
|
|
331e98af20 | ||
|
|
b1d226256a | ||
|
|
f1ee8b0c99 |
62
.github/workflows/build-binaries.yml
vendored
Normal file
62
.github/workflows/build-binaries.yml
vendored
Normal 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 }}
|
||||
2
.github/workflows/cd.yml
vendored
2
.github/workflows/cd.yml
vendored
@@ -147,6 +147,7 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -154,6 +155,7 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/heads/deploy/production')
|
||||
run: |
|
||||
echo "STRIPE_KEY=${{ secrets.STRIPE_KEY }}" >> ./environments/.env.production
|
||||
echo "LOGROCKET_APP_ID=${{ secrets.LOGROCKET_APP_ID }}" >> ./environments/.env.production
|
||||
echo "EXTENSIONS=@fleetbase/billing-engine,@fleetbase/internals-engine" >> ./environments/.env.production
|
||||
working-directory: ./console
|
||||
|
||||
|
||||
21
.github/workflows/create-release.yml
vendored
Normal file
21
.github/workflows/create-release.yml
vendored
Normal 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 }}
|
||||
39
.github/workflows/discord-announcement.yml
vendored
Normal file
39
.github/workflows/discord-announcement.yml
vendored
Normal 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 }})
|
||||
48
.github/workflows/discord_announcement.yml
vendored
48
.github/workflows/discord_announcement.yml
vendored
@@ -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
|
||||
2
.github/workflows/gcp-cd.yml
vendored
2
.github/workflows/gcp-cd.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Fleetbase CI/CD
|
||||
name: Fleetbase GCP CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
50
.github/workflows/publish-docker-images.yml
vendored
Normal file
50
.github/workflows/publish-docker-images.yml
vendored
Normal 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
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -34,4 +34,15 @@ 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
|
||||
@@ -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>
|
||||
|
||||
60
RELEASE.md
Normal file
60
RELEASE.md
Normal 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)
|
||||
@@ -9,10 +9,11 @@
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"fleetbase/core-api": "^1.5.10",
|
||||
"fleetbase/fleetops-api": "^0.5.8",
|
||||
"fleetbase/registry-bridge": "^0.0.15",
|
||||
"fleetbase/storefront-api": "^0.3.15",
|
||||
"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",
|
||||
@@ -22,7 +23,10 @@
|
||||
"phpoffice/phpspreadsheet": "^1.28",
|
||||
"predis/predis": "^2.1",
|
||||
"psr/http-factory-implementation": "*",
|
||||
"s-ichikawa/laravel-sendgrid-driver": "^4.0"
|
||||
"resend/resend-php": "^0.14.0",
|
||||
"s-ichikawa/laravel-sendgrid-driver": "^4.0",
|
||||
"symfony/mailgun-mailer": "^7.1",
|
||||
"symfony/postmark-mailer": "^7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"spatie/laravel-ignition": "^2.0",
|
||||
@@ -34,10 +38,6 @@
|
||||
"phpunit/phpunit": "^10.0"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/fleetbase/laravel-model-caching"
|
||||
},
|
||||
{
|
||||
"type": "composer",
|
||||
"url": "https://registry.fleetbase.io"
|
||||
|
||||
3353
api/composer.lock
generated
3353
api/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
'env' => env('APP_ENV', env('ENVIRONMENT', 'production')),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -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' => [],
|
||||
|
||||
|
||||
@@ -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
30
api/config/opcache.php
Normal 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',
|
||||
],
|
||||
];
|
||||
@@ -34,6 +34,10 @@ 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')),
|
||||
|
||||
@@ -26,6 +26,14 @@ php artisan schedule-monitor:sync
|
||||
|
||||
# Clear cache
|
||||
php artisan cache:clear
|
||||
php artisan route:clear
|
||||
|
||||
# Optimize
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
|
||||
# Initialize registry
|
||||
php artisan registry:init
|
||||
|
||||
# Restart octane
|
||||
# php artisan octane:reload
|
||||
|
||||
19
api/resources/lang/ar/auth.php
Normal file
19
api/resources/lang/ar/auth.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| أسطر لغة المصادقة
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| تحتوي الأسطر التالية على رسائل المصادقة التي نعرضها للمستخدم أثناء
|
||||
| عمليات تسجيل الدخول أو غيرها. يمكنك تعديل هذه الرسائل حسب متطلباتك.
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => 'بيانات الاعتماد هذه غير متطابقة مع سجلاتنا.',
|
||||
'password' => 'كلمة المرور التي تم إدخالها غير صحيحة.',
|
||||
'throttle' => 'عدد كبير جداً من محاولات الدخول. يرجى المحاولة مرة أخرى خلال :seconds ثانية.',
|
||||
|
||||
];
|
||||
19
api/resources/lang/ar/pagination.php
Normal file
19
api/resources/lang/ar/pagination.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| أسطر لغة الترقيم الصفحي
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| تُستخدم الأسطر التالية من قبل مكتبة الترقيم الصفحي لبناء روابط
|
||||
| الترقيم البسيطة. يمكنك تعديلها كما تشاء لتخصيص العرض بما يناسب
|
||||
| تطبيقك بشكل أفضل.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« السابق',
|
||||
'next' => 'التالي »',
|
||||
|
||||
];
|
||||
21
api/resources/lang/ar/passwords.php
Normal file
21
api/resources/lang/ar/passwords.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| أسطر لغة إعادة تعيين كلمة المرور
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| الأسطر التالية هي الرسائل الافتراضية التي يقدمها نظام إعادة تعيين
|
||||
| كلمة المرور عند فشل المحاولة، مثل رمز التحقق غير صالح أو كلمة مرور جديدة غير صحيحة.
|
||||
|
|
||||
*/
|
||||
|
||||
'reset' => 'تم إعادة تعيين كلمة المرور الخاصة بك!',
|
||||
'sent' => 'لقد أرسلنا رابط إعادة تعيين كلمة المرور إلى بريدك الإلكتروني!',
|
||||
'throttled' => 'يرجى الانتظار قبل المحاولة مرة أخرى.',
|
||||
'token' => 'رمز إعادة تعيين كلمة المرور هذا غير صالح.',
|
||||
'user' => 'لا يمكننا العثور على مستخدم بهذا العنوان الإلكتروني.',
|
||||
|
||||
];
|
||||
168
api/resources/lang/ar/validation.php
Normal file
168
api/resources/lang/ar/validation.php
Normal 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' => [],
|
||||
|
||||
];
|
||||
47
builds/linux/build-linux.sh
Normal file
47
builds/linux/build-linux.sh
Normal 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"
|
||||
12
builds/linux/spc/libgeos-linux.php
Normal file
12
builds/linux/spc/libgeos-linux.php
Normal 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';
|
||||
}
|
||||
33
builds/linux/spc/libgeos-unix.php
Normal file
33
builds/linux/spc/libgeos-unix.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
92
builds/linux/static-build.Dockerfile
Normal file
92
builds/linux/static-build.Dockerfile
Normal 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
188
builds/osx/build-osx.sh
Executable 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 don’t 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"
|
||||
271
builds/osx/spc/UnixBuilderBase-macos.php
Normal file
271
builds/osx/spc/UnixBuilderBase-macos.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
builds/osx/spc/libgeos-macos.php
Normal file
12
builds/osx/spc/libgeos-macos.php
Normal 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';
|
||||
}
|
||||
33
builds/osx/spc/libgeos-unix.php
Normal file
33
builds/osx/spc/libgeos-unix.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
# ---- Build Stage ----
|
||||
FROM node:18.15.0-alpine
|
||||
FROM node:18.15.0-alpine as builder
|
||||
|
||||
# Set the working directory in the container to /console
|
||||
WORKDIR /console
|
||||
|
||||
# Create the pnpm directory and set the PNPM_HOME environment variable
|
||||
RUN mkdir -p ~/.pnpm
|
||||
ENV PNPM_HOME /root/.pnpm
|
||||
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 /console in the container
|
||||
COPY console/package.json console/pnpm-lock.yaml ./
|
||||
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
|
||||
@@ -33,25 +33,22 @@ RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
|
||||
RUN pnpm install
|
||||
|
||||
# Copy the console directory contents into the container at /console
|
||||
COPY console .
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN pnpm build --environment $ENVIRONMENT
|
||||
|
||||
# # Make sure the build output is available in /console/dist
|
||||
# RUN ls -la /console/dist
|
||||
# ---- Serve Stage ----
|
||||
FROM nginx:alpine
|
||||
|
||||
# # ---- Serve Stage ----
|
||||
# FROM nginx:alpine
|
||||
# Copy the built app to our served directory
|
||||
COPY --from=builder /console/dist /usr/share/nginx/html
|
||||
|
||||
# # Copy the built app to our served directory
|
||||
# COPY --from=builder /console/dist /usr/share/nginx/html
|
||||
# Expose the port nginx is bound to
|
||||
EXPOSE 4200
|
||||
|
||||
# # Expose the port nginx is bound to
|
||||
# EXPOSE 4201
|
||||
# Use custom nginx.conf
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# # Use custom nginx.conf
|
||||
# COPY console/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# # Start Nginx server
|
||||
# CMD ["nginx", "-g", "daemon off;"]
|
||||
# Start Nginx server
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,54 +0,0 @@
|
||||
# ---- Build Stage ----
|
||||
FROM node:18.15.0-alpine as builder
|
||||
|
||||
# Set the working directory in the container to /console
|
||||
WORKDIR /console
|
||||
|
||||
# Create the pnpm directory and set the PNPM_HOME environment variable
|
||||
RUN mkdir -p ~/.pnpm
|
||||
ENV PNPM_HOME /root/.pnpm
|
||||
|
||||
# Set environment
|
||||
ARG ENVIRONMENT=production
|
||||
|
||||
# Add the pnpm global bin to the PATH
|
||||
ENV PATH /root/.pnpm/bin:$PATH
|
||||
|
||||
# Copy pnpm-lock.yaml (or package.json) into the directory /console in the container
|
||||
COPY console/package.json console/pnpm-lock.yaml ./
|
||||
|
||||
# Copy over .npmrc if applicable
|
||||
COPY console/.npmr[c] ./
|
||||
|
||||
# Install global dependencies
|
||||
RUN npm install -g ember-cli pnpm
|
||||
|
||||
# Install git
|
||||
RUN apk update && apk add git openssh-client
|
||||
|
||||
# Trust GitHub's RSA host key
|
||||
RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
|
||||
|
||||
# Install app dependencies
|
||||
RUN pnpm install
|
||||
|
||||
# Copy the console directory contents into the container at /console
|
||||
COPY console .
|
||||
|
||||
# Build the application
|
||||
RUN pnpm build --environment $ENVIRONMENT
|
||||
|
||||
# ---- Serve Stage ----
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy the built app to our served directory
|
||||
COPY --from=builder /console/dist /usr/share/nginx/html
|
||||
|
||||
# Expose the port nginx is bound to
|
||||
EXPOSE 4200
|
||||
|
||||
# Use custom nginx.conf
|
||||
COPY console/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Start Nginx server
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<InputGroup @name="Twilio Token" @value={{this.twilioToken}} disabled={{this.isLoading}} />
|
||||
<InputGroup @name="Twilio From" @value={{this.twilioFrom}} disabled={{this.isLoading}} />
|
||||
{{#if this.twilioTestResponse}}
|
||||
<div class="animate-pulse flex flex-row items-center rounded-lg border {{if (eq this.twilioTestResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
|
||||
<div class="flex flex-row items-center rounded-lg border {{if (eq this.twilioTestResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
|
||||
<FaIcon @icon={{if (eq this.twilioTestResponse.status 'error') 'triangle-exclamation' 'circle-check'}} class="mr-1.5 {{if (eq this.twilioTestResponse.status 'error') 'text-red-200' 'text-green-200'}}" />
|
||||
<span class="text-xs">{{this.this.twilioTestResponse.message}}</span>
|
||||
</div>
|
||||
@@ -28,7 +28,7 @@
|
||||
<ContentPanel @title="Sentry" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<InputGroup @name="Sentry DSN" @value={{this.sentryDsn}} disabled={{this.isLoading}} />
|
||||
{{#if this.sentryTestResponse}}
|
||||
<div class="animate-pulse flex flex-row items-center rounded-lg border {{if (eq this.sentryTestResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
|
||||
<div class="flex flex-row items-center rounded-lg border {{if (eq this.sentryTestResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
|
||||
<FaIcon @icon={{if (eq this.sentryTestResponse.status 'error') 'triangle-exclamation' 'circle-check'}} class="mr-1.5 {{if (eq this.sentryTestResponse.status 'error') 'text-red-200' 'text-green-200'}}" />
|
||||
<span class="text-xs">{{this.this.sentryTestResponse.message}}</span>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
36
console/app/components/impersonator-tray.hbs
Normal file
36
console/app/components/impersonator-tray.hbs
Normal 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}}
|
||||
39
console/app/components/impersonator-tray.js
Normal file
39
console/app/components/impersonator-tray.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
console/app/components/modals/edit-organization.hbs
Normal file
12
console/app/components/modals/edit-organization.hbs
Normal 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>
|
||||
3
console/app/components/modals/edit-organization.js
Normal file
3
console/app/components/modals/edit-organization.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
export default class ModalsEditOrganizationComponent extends Component {}
|
||||
43
console/app/components/modals/leave-organization.hbs
Normal file
43
console/app/components/modals/leave-organization.hbs
Normal 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>
|
||||
3
console/app/components/modals/leave-organization.js
Normal file
3
console/app/components/modals/leave-organization.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
export default class ModalsLeaveOrganizationComponent extends Component {}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export default class ConsoleController extends Controller {
|
||||
*
|
||||
* @var {Array}
|
||||
*/
|
||||
@tracked hiddenSidebarRoutes = ['console.home', 'console.notifications'];
|
||||
@tracked hiddenSidebarRoutes = ['console.home', 'console.notifications', 'console.virtual'];
|
||||
|
||||
/**
|
||||
* Menu items to be added to the main header navigation bar.
|
||||
@@ -161,7 +161,7 @@ export default class ConsoleController extends Controller {
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to invalidate and log user out
|
||||
* Action to create or join an organization.
|
||||
*
|
||||
* @void
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
203
console/app/controllers/console/account/organizations.js
Normal file
203
console/app/controllers/console/account/organizations.js
Normal 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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export function initialize (application) {
|
||||
export function initialize(application) {
|
||||
const universe = application.lookup('service:universe');
|
||||
if (universe) {
|
||||
universe.createRegistries(['@fleetbase/console', 'auth:login']);
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { set } from '@ember/object';
|
||||
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
|
||||
import { computed, get } from '@ember/object';
|
||||
import { not } from '@ember/object/computed';
|
||||
@@ -30,7 +31,10 @@ export default class UserModel extends Model {
|
||||
@attr('string') locale;
|
||||
@attr('boolean') is_online;
|
||||
@attr('boolean') is_admin;
|
||||
@attr('boolean') is_subscribed;
|
||||
@attr('boolean') is_trialing;
|
||||
@attr('raw') meta;
|
||||
@attr('raw') subscription;
|
||||
|
||||
/** @relationships */
|
||||
@belongsTo('role') role;
|
||||
@@ -41,6 +45,7 @@ export default class UserModel extends Model {
|
||||
@attr('date') last_seen_at;
|
||||
@attr('date') phone_verified_at;
|
||||
@attr('date') email_verified_at;
|
||||
@attr('date') trial_ends_at;
|
||||
@attr('date') last_login;
|
||||
@attr('date') deleted_at;
|
||||
@attr('date') created_at;
|
||||
@@ -69,6 +74,17 @@ export default class UserModel extends Model {
|
||||
});
|
||||
}
|
||||
|
||||
verify() {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
return fetch.patch(`users/verify/${this.id}`).then((response) => {
|
||||
set(this, 'email_verified_at', response.email_verified_at);
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
removeFromCurrentCompany() {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
@@ -4,6 +4,7 @@ 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;
|
||||
@@ -18,13 +19,12 @@ export default class ApplicationRoute extends Route {
|
||||
@tracked defaultTheme;
|
||||
|
||||
/**
|
||||
* The application loading event.
|
||||
* Here will just run extension hooks.
|
||||
* Handle the transition into the application.
|
||||
*
|
||||
* @memberof ApplicationRoute
|
||||
*/
|
||||
@action loading (transition) {
|
||||
this.universe.callHooks('application:loading', this.session, this.router, transition);
|
||||
@action willTransition(transition) {
|
||||
this.universe.callHooks('application:will-transition', this.session, this.router, transition);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,6 +38,16 @@ export default class ApplicationRoute extends Route {
|
||||
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.
|
||||
*
|
||||
@@ -79,6 +89,17 @@ export default class ApplicationRoute extends Route {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove boot loader if not authenticated.
|
||||
*
|
||||
* @memberof ApplicationRoute
|
||||
*/
|
||||
afterModel() {
|
||||
if (!this.session.isAuthenticated) {
|
||||
removeBootLoader();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the application's theme settings, applying necessary class names and default theme configurations.
|
||||
*
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class AuthForgotPasswordRoute extends Route {
|
||||
@service store;
|
||||
|
||||
queryParams = {
|
||||
email: {
|
||||
refreshModel: false,
|
||||
},
|
||||
};
|
||||
|
||||
model() {
|
||||
return this.store.findRecord('brand', 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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 {
|
||||
@@ -27,6 +29,27 @@ export default class ConsoleRoute extends Route {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the branding settings.
|
||||
*
|
||||
|
||||
10
console/app/routes/console/account/organizations.js
Normal file
10
console/app/routes/console/account/organizations.js
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import groupBy from '@fleetbase/ember-core/utils/group-by';
|
||||
|
||||
export default class ConsoleAdminNotificationsRoute extends Route {
|
||||
export default class ConsoleSettingsNotificationsRoute extends Route {
|
||||
@service fetch;
|
||||
|
||||
model() {
|
||||
@@ -14,6 +14,8 @@ export default class ConsoleAdminNotificationsRoute extends Route {
|
||||
}
|
||||
|
||||
setupController(controller, { registry, notifiables }) {
|
||||
super.setupController(...arguments);
|
||||
|
||||
controller.groupedNotifications = groupBy(registry, 'package');
|
||||
controller.notifiables = notifiables;
|
||||
}
|
||||
@@ -38,3 +38,32 @@ body[data-theme='dark'] .two-fa-enforcement-alert button#two-fa-setup-button.btn
|
||||
body.console-admin-organizations-index-index .next-table-wrapper > table {
|
||||
table-layout: auto;
|
||||
}
|
||||
|
||||
#boot-loader {
|
||||
position: absolute;
|
||||
z-index: 9999999999;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#boot-loader > .loader-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#boot-loader > .loader-container > .loading-message {
|
||||
margin-left: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
body[data-theme='dark'] #boot-loader > .loader-container > .loading-message {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg">
|
||||
<div class="mb-4">
|
||||
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
|
||||
<LinkTo @route="console" class="flex items-center justify-center">
|
||||
<LogoIcon @size="12" class="rounded-md" />
|
||||
</LinkTo>
|
||||
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
|
||||
{{if this.isSent (t "auth.forgot-password.is-sent.title") (t "auth.forgot-password.not-sent.title")}}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{{#if this.isSent}}
|
||||
<div class="flex px-3 py-2 mb-4 rounded-md shadow-sm bg-green-200">
|
||||
<div>
|
||||
<FaIcon @icon="check-circle" @size="lg" class="text-green-900 mr-4" />
|
||||
</div>
|
||||
<p class="flex-1 text-sm text-green-900 dark:text-green-900">
|
||||
<InfoBlock @type="success" @icon="info-circle" @iconSize="lg" @iconClass="mt-1" class="my-6">
|
||||
<p>
|
||||
{{t "auth.forgot-password.is-sent.message" htmlSafe=true}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-row mt-4">
|
||||
</InfoBlock>
|
||||
<div class="flex flex-row">
|
||||
<Button @icon="check" @type="primary" @text={{t "common.continue"}} @onClick={{transition-to "auth.login"}} />
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="flex px-3 py-2 mb-6 rounded-md shadow-sm bg-blue-200">
|
||||
<div>
|
||||
<FaIcon @icon="info-circle" @size="lg" class="text-blue-900 mr-4" />
|
||||
</div>
|
||||
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
|
||||
<InfoBlock @icon="info-circle" @iconSize="lg" @iconClass="mt-1" class="my-6">
|
||||
<p>
|
||||
{{t "auth.forgot-password.not-sent.message" htmlSafe=true appName=(t "app.name")}}
|
||||
</p>
|
||||
</div>
|
||||
</InfoBlock>
|
||||
|
||||
<form class="space-y-6" {{on "submit" (perform this.sendSecureLink)}}>
|
||||
<div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div>
|
||||
<div class="mx-auto w-12 h-12">
|
||||
<LogoIcon @size="12" class="mx-auto rounded-sm" />
|
||||
</div>
|
||||
<LinkTo @route="console" class="flex items-center justify-center">
|
||||
<LogoIcon @brand={{@brand}} @size="12" class="rounded-md" />
|
||||
</LinkTo>
|
||||
<h2 class="mt-6 mb-3 text-3xl font-extrabold leading-9 text-center text-gray-900 dark:text-gray-100">
|
||||
{{t "auth.login.title"}}
|
||||
</h2>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg">
|
||||
<div class="mb-4">
|
||||
<Image src={{this.brand.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
|
||||
<LinkTo @route="console" class="flex items-center justify-center">
|
||||
<LogoIcon @brand={{@brand}} @size="12" class="rounded-md" />
|
||||
</LinkTo>
|
||||
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
|
||||
{{t "auth.reset-password.title"}}
|
||||
</h2>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<div class="mb-8 text-center">
|
||||
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
|
||||
<LinkTo @route="console" class="flex items-center justify-center">
|
||||
<LogoIcon @brand={{@brand}} @size="12" class="rounded-md" />
|
||||
</LinkTo>
|
||||
<h2 class="text-lg font-extrabold text-gray-900 dark:text-white truncate">
|
||||
{{if this.isSent "Verification Code"}}
|
||||
</h2>
|
||||
@@ -20,7 +22,7 @@
|
||||
<OtpInput @onInputCompleted={{this.handleOtpInput}} @size={{6}} class="w-full" />
|
||||
</div>
|
||||
|
||||
<div id="otp-countdown-container" class="otp-countdown-container flex {{if this.isCodeExpired "flex-col" "flex-row"}} items-center justify-center min-h-12">
|
||||
<div id="otp-countdown-container" class="otp-countdown-container flex {{if this.isCodeExpired 'flex-col' 'flex-row'}} items-center justify-center min-h-12">
|
||||
{{#if this.countdownReady}}
|
||||
<Countdown @expiry={{this.twoFactorSessionExpiresAfter}} @countdownClass="text-lg" @onCountdownEnd={{this.handleCodeExpired}} />
|
||||
{{/if}}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
{{page-title (t "auth.verification.header-title")}}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
|
||||
<div class="mb-8">
|
||||
<img class="mx-auto h-12 w-auto" src="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} />
|
||||
<div class="mb-6">
|
||||
<LinkTo @route="console" class="flex items-center justify-center">
|
||||
<LogoIcon @brand={{@brand}} @size="12" class="rounded-md" />
|
||||
</LinkTo>
|
||||
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
|
||||
{{t "auth.verification.title"}}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex px-3 py-2 mb-6 rounded-md shadow-sm bg-blue-200">
|
||||
<div>
|
||||
<FaIcon @icon="shield-check" @size="lg" class="text-blue-900 mr-4" />
|
||||
</div>
|
||||
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
|
||||
{{t "auth.verification.message-text" htmlSafe=true}}
|
||||
</p>
|
||||
</div>
|
||||
<InfoBlock @type="info" @icon="shield-halved" @iconSize="lg">
|
||||
{{t "auth.verification.message-text" htmlSafe=true}}
|
||||
</InfoBlock>
|
||||
|
||||
<form class="mt-8 space-y-6" {{on "submit" this.verifyCode}}>
|
||||
<form class="mt-8 space-y-6" {{on "submit" (perform this.verifyCode)}}>
|
||||
<InputGroup
|
||||
@type="tel"
|
||||
@name={{t "auth.verification.verification-input-label"}}
|
||||
@@ -36,9 +33,9 @@
|
||||
@type="primary"
|
||||
@size="lg"
|
||||
@text="Verify & Continue"
|
||||
@isLoading={{this.isLoading}}
|
||||
@disabled={{this.isNotReadyToSubmit}}
|
||||
@onClick={{this.verifyCode}}
|
||||
@isLoading={{this.verifyCode.isRunning}}
|
||||
@disabled={{not this.isReadyToSubmit}}
|
||||
@onClick={{perform this.verifyCode}}
|
||||
/>
|
||||
<a href="javascript:;" {{on "click" this.onDidntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "auth.verification.didnt-receive-a-code"}}</a>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
</Layout::Container>
|
||||
<ChatContainer />
|
||||
<ConsoleWormhole />
|
||||
<ImpersonatorTray />
|
||||
{{!-- template-lint-disable no-potential-path-strings --}}
|
||||
<RegistryYield @registry="@fleetbase/console" as |RegistryComponent|>
|
||||
<RegistryComponent @controller={{this}} />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<Layout::Sidebar::Panel @open={{true}} @title={{t "common.account"}}>
|
||||
<Layout::Sidebar::Item @route="console.account.index" @icon="user">Profile</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.account.auth" @icon="key">Auth</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.account.organizations" @icon="building">Organizations</Layout::Sidebar::Item>
|
||||
{{#each this.universe.accountMenuItems as |menuItem|}}
|
||||
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.account.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
|
||||
{{/each}}
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
<PhoneInput @value={{this.user.phone}} @onInput={{fn (mut this.user.phone)}} class="form-input input-lg w-full" />
|
||||
</InputGroup>
|
||||
<InputGroup @name={{t "common.date-of-birth"}} @type="date" @value={{this.user.date_of_birth}} />
|
||||
<InputGroup @name={{t "common.timezone"}} @helpText={{t "console.account.index.timezone"}}>
|
||||
<Select @value={{this.user.timezone}} @options={{this.timezones}} @onSelect={{fn (mut this.user.timezone)}} @placeholder={{t "console.account.index.timezone"}} />
|
||||
</InputGroup>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-end">
|
||||
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text={{t "common.save-button-text"}} @onClick={{perform this.saveProfile}} @isLoading={{not this.saveProfile.isIdle}} />
|
||||
|
||||
37
console/app/templates/console/account/organizations.hbs
Normal file
37
console/app/templates/console/account/organizations.hbs
Normal file
@@ -0,0 +1,37 @@
|
||||
{{page-title "Organizations"}}
|
||||
<Layout::Section::Header @title="Organizations" />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-4">
|
||||
<div class="flex flex-row justify-end">
|
||||
<Button @type="primary" @icon="plus" @text="Create Organization" @onClick={{this.createOrganization}} />
|
||||
</div>
|
||||
<ContentPanel @title="Your Organizations" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<div class="space-y-2">
|
||||
{{#each @model as |organization|}}
|
||||
<div class="grid grid-cols-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 rounded-lg px-3 py-2 items-center">
|
||||
<div>
|
||||
<div class="font-semibold">{{organization.name}}</div>
|
||||
<div>Member Since: {{format-date organization.joined_at}}</div>
|
||||
</div>
|
||||
<div class="col-span-2 flex flex-row items-center justify-end space-x-2">
|
||||
{{#let (eq organization.owner_uuid this.currentUser.id) as |isOwner|}}
|
||||
<Button @type="danger" @size="xs" @icon="person-walking-arrow-right" @text="Leave" @onClick={{fn this.leaveOrganization organization}} />
|
||||
{{#unless (eq this.currentUser.companyId organization.id)}}
|
||||
<Button @size="xs" @icon="shuffle" @text="Switch" @onClick={{fn this.switchOrganization organization}} />
|
||||
{{/unless}}
|
||||
{{#if isOwner}}
|
||||
<Button @size="xs" @icon="pencil" @text="Edit" @onClick={{fn this.editOrganization organization}} />
|
||||
<Button @type="danger" @size="xs" @icon="trash" @text="Delete" @onClick={{fn this.deleteOrganization organization}} />
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</ContentPanel>
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -4,7 +4,6 @@
|
||||
<Layout::Sidebar::Item @route="console.admin.index" @icon="rectangle-list">{{t "common.overview"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.organizations" @icon="building">{{t "common.organizations"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.branding" @icon="palette">{{t "common.branding"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.notifications" @icon="bell">{{t "common.notifications"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.two-fa-settings" @icon="shield-halved">{{t "common.2fa-config"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.schedule-monitor" @icon="calendar-check">{{t "console.admin.schedule-monitor.schedule-monitor"}}</Layout::Sidebar::Item>
|
||||
{{#each this.universe.adminMenuItems as |menuItem|}}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<Layout::Sidebar::Panel @open={{true}} @title={{t "common.settings"}}>
|
||||
<Layout::Sidebar::Item @route="console.settings.index" @icon="cog">{{t "common.organization"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.settings.two-fa" @icon="shield-halved">{{t "common.two-factor"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.settings.notifications" @icon="bell">{{t "common.notifications"}}</Layout::Sidebar::Item>
|
||||
{{#each this.universe.settingsMenuItems as |menuItem|}}
|
||||
<Layout::Sidebar::Item
|
||||
@onClick={{fn this.universe.transitionMenuItem "console.settings.virtual" menuItem}}
|
||||
|
||||
@@ -4,18 +4,21 @@
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<ContentPanel @title={{t "console.settings.index.title"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<form {{on "submit" this.saveSettings}}>
|
||||
<form {{on "submit" (perform this.saveSettings)}}>
|
||||
<InputGroup @name={{t "console.settings.index.organization-name"}} @value={{@model.name}} />
|
||||
<InputGroup @name={{t "console.settings.index.organization-description"}} @value={{@model.description}} />
|
||||
<InputGroup @name={{t "console.settings.index.organization-phone"}}>
|
||||
<PhoneInput @value={{@model.phone}} @onInput={{fn (mut @model.phone)}} class="form-input w-full" />
|
||||
</InputGroup>
|
||||
<InputGroup @name={{t "console.settings.index.organization-currency"}}>
|
||||
<CurrencySelect @value={{@model.currency}} @onSelect={{fn (mut @model.currency)}} @triggerClass="w-full form-select" />
|
||||
<CurrencySelect @currency={{@model.currency}} @onCurrencyChange={{fn (mut @model.currency)}} @triggerClass="w-full form-select" />
|
||||
</InputGroup>
|
||||
<InputGroup @name={{t "common.timezone"}} @helpText={{t "console.settings.index.organization-timezone"}}>
|
||||
<Select @value={{@model.timezone}} @options={{this.timezones}} @onSelect={{fn (mut @model.timezone)}} @placeholder={{t "console.settings.index.select-timezone"}} class="w-full form-select" />
|
||||
</InputGroup>
|
||||
<InputGroup @name={{t "console.settings.index.organization-id"}} @value={{@model.public_id}} @disabled={{true}} />
|
||||
<div class="mt-3 flex items-center justify-end">
|
||||
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text="{{t "common.save-button-text"}}" @isLoading={{this.isLoading}} />
|
||||
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text="{{t "common.save-button-text"}}" @isLoading={{this.saveSettings.isRunning}} />
|
||||
</div>
|
||||
</form>
|
||||
</ContentPanel>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{page-title "Push Notifications"}}
|
||||
<Layout::Section::Header @title={{t "console.admin.notifications.title"}}>
|
||||
<Button @type="primary" @size="sm" @icon="save" @text={{t "common.save-button-text"}} @onClick={{this.saveSettings}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
|
||||
{{page-title "Notifications"}}
|
||||
<Layout::Section::Header @title={{t "common.notifications"}}>
|
||||
<Button @type="primary" @size="sm" @icon="save" @text={{t "common.save-button-text"}} @onClick={{perform this.saveSettings}} @disabled={{this.saveSettings.isRunning}} @isLoading={{or this.saveSettings.isRunning this.getSettings.isRunning}} />
|
||||
</Layout::Section::Header>
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
@@ -1,11 +1,6 @@
|
||||
{{page-title @model.title}}
|
||||
<Layout::Section::Header @title={{@model.title}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-">
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
</div>
|
||||
</div>
|
||||
{{component @model.component params=@model.componentParams}}
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -1,9 +1,11 @@
|
||||
<div class="bg-white dark:bg-gray-800 py-5 px-4 shadow rounded-lg w-full">
|
||||
<div class="mb-4">
|
||||
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
|
||||
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
|
||||
{{t "onboard.index.title"}}
|
||||
</h2>
|
||||
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} height="56" class="h-10 object-contain mx-auto" />
|
||||
<div class="mt-2">
|
||||
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
|
||||
{{t "onboard.index.title"}}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex px-3 py-2 mb-4 rounded-md shadow-sm bg-blue-200">
|
||||
|
||||
@@ -1,27 +1,42 @@
|
||||
{{page-title (t "onboard.verify-email.header-title")}}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
|
||||
<div class="mb-8">
|
||||
<img class="mx-auto h-12 w-auto " src="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}}>
|
||||
<div class="mb-6">
|
||||
<LinkTo @route="console" class="flex items-center justify-center">
|
||||
<LogoIcon @size="12" class="rounded-md" />
|
||||
</LinkTo>
|
||||
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
|
||||
{{t "onboard.verify-email.title"}}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex px-3 py-2 mb-6 rounded-md shadow-sm bg-blue-200">
|
||||
<div>
|
||||
<FaIcon @icon="shield-check" @size="lg" class="text-blue-900 mr-4" />
|
||||
</div>
|
||||
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
|
||||
{{t "onboard.verify-email.message-text" htmlSafe=true}}
|
||||
</p>
|
||||
</div>
|
||||
<InfoBlock @type="info" @icon="shield-halved" @iconSize="lg">
|
||||
{{t "onboard.verify-email.message-text" htmlSafe=true}}
|
||||
</InfoBlock>
|
||||
|
||||
<form class="mt-8 space-y-6" {{on "submit" this.verifyCode}}>
|
||||
<InputGroup @type="tel" @name={{t "onboard.verify-email.verification-input-label"}} @value={{this.code}} @helpText={{t "onboard.verify-email.verification-code-text"}} @inputClass="input-lg" {{on "input" this.validateInput}} {{did-insert this.validateInitInput}} />
|
||||
<form class="mt-8 space-y-6" {{on "submit" (perform this.verifyCode)}}>
|
||||
<InputGroup
|
||||
@type="tel"
|
||||
@name={{t "onboard.verify-email.verification-input-label"}}
|
||||
@value={{this.code}}
|
||||
@helpText={{t "onboard.verify-email.verification-code-text"}}
|
||||
@inputClass="input-lg"
|
||||
{{on "input" this.validateInput}}
|
||||
{{did-insert this.validateInitInput}}
|
||||
/>
|
||||
|
||||
<div class="flex flex-row items-center space-x-4">
|
||||
<Button @icon="check" @iconPrefix="fas" @buttonType="submit" @type="primary" @size="lg" @text="Verify & Continue" @isLoading={{this.isLoading}} @disabled={{this.isNotReadyToSubmit}} @onClick={{this.verifyCode}} />
|
||||
<Button
|
||||
@icon="check"
|
||||
@iconPrefix="fas"
|
||||
@buttonType="submit"
|
||||
@type="primary"
|
||||
@size="lg"
|
||||
@text="Verify & Continue"
|
||||
@isLoading={{this.verifyCode.isRunning}}
|
||||
@disabled={{not this.isReadyToSubmit}}
|
||||
@onClick={{perform this.verifyCode}}
|
||||
/>
|
||||
<a href="#" {{on "click" this.onDidntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "onboard.verify-email.didnt-receive-a-code"}}</a>
|
||||
</div>
|
||||
|
||||
@@ -33,14 +48,14 @@
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex-1 text-sm text-yellow-100">
|
||||
<div>{{t "onboard.verify-email.didnt-receive-a-code" htmlSafe=true}}</div>
|
||||
<div>{{t "onboard.verify-email.not-sent.alternative-choice" htmlSafe=true}}</div>
|
||||
<div>{{t "auth.verification.didnt-receive-a-code" htmlSafe=true}}</div>
|
||||
<div>{{t "auth.verification.not-sent.alternative-choice" htmlSafe=true}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Button
|
||||
@text={{t "onboard.verify-email.not-sent.resend-email"}}
|
||||
@text={{t "auth.verification.not-sent.resend-email"}}
|
||||
@buttonType="button"
|
||||
@type="link"
|
||||
class="text-yellow-100"
|
||||
@@ -48,7 +63,7 @@
|
||||
@onClick={{this.resendEmail}}
|
||||
/>
|
||||
<Button
|
||||
@text={{t "onboard.verify-email.not-sent.send-by-sms"}}
|
||||
@text={{t "auth.verification.not-sent.send-by-sms"}}
|
||||
@buttonType="button"
|
||||
@type="link"
|
||||
class="text-yellow-100"
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { camelize } from '@ember/string';
|
||||
|
||||
export default function createNotificationKey(definition, name) {
|
||||
const withoutSlashes = definition.replace(/[\W_]+/g, '');
|
||||
const key = `${camelize(withoutSlashes)}__${camelize(name)}`;
|
||||
|
||||
return key;
|
||||
}
|
||||
@@ -19,6 +19,8 @@ export default function getPermissionAction(permissionName) {
|
||||
'notify',
|
||||
'assign_vehicle',
|
||||
'assign_order_to',
|
||||
'assign_driver_to',
|
||||
'assign_vehicle_to',
|
||||
'dispatch_order_to',
|
||||
'dispatch',
|
||||
'assign',
|
||||
|
||||
6
console/app/utils/remove-boot-loader.js
Normal file
6
console/app/utils/remove-boot-loader.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default function removeBootLoader() {
|
||||
const bootLoaderElement = document.getElementById('boot-loader');
|
||||
if (bootLoaderElement && typeof bootLoaderElement.remove === 'function') {
|
||||
bootLoaderElement.remove();
|
||||
}
|
||||
}
|
||||
84
console/app/utils/runtime-config.js
Normal file
84
console/app/utils/runtime-config.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import config from '@fleetbase/console/config/environment';
|
||||
import toBoolean from '@fleetbase/ember-core/utils/to-boolean';
|
||||
import { set } from '@ember/object';
|
||||
import { debug } from '@ember/debug';
|
||||
|
||||
/**
|
||||
* Maps allowed runtime keys to internal config paths.
|
||||
*/
|
||||
const RUNTIME_CONFIG_MAP = {
|
||||
API_HOST: 'API.host',
|
||||
API_NAMESPACE: 'API.namespace',
|
||||
SOCKETCLUSTER_PATH: 'socket.path',
|
||||
SOCKETCLUSTER_HOST: 'socket.hostname',
|
||||
SOCKETCLUSTER_SECURE: 'socket.secure',
|
||||
SOCKETCLUSTER_PORT: 'socket.port',
|
||||
OSRM_HOST: 'osrm.host',
|
||||
EXTENSIONS: 'APP.extensions',
|
||||
};
|
||||
|
||||
/**
|
||||
* Coerce and sanitize runtime config values based on key.
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {*} value
|
||||
* @return {*}
|
||||
*/
|
||||
function coerceValue(key, value) {
|
||||
switch (key) {
|
||||
case 'SOCKETCLUSTER_PORT':
|
||||
return parseInt(value, 10);
|
||||
|
||||
case 'SOCKETCLUSTER_SECURE':
|
||||
return toBoolean(value);
|
||||
|
||||
case 'EXTENSIONS':
|
||||
return typeof value === 'string' ? value.split(',') : Array.from(value);
|
||||
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply runtime config overrides based on strict allowlist mapping.
|
||||
*
|
||||
* @param {Object} rawConfig
|
||||
*/
|
||||
export function applyRuntimeConfig(rawConfig = {}) {
|
||||
Object.entries(rawConfig).forEach(([key, value]) => {
|
||||
const configPath = RUNTIME_CONFIG_MAP[key];
|
||||
|
||||
if (configPath) {
|
||||
const coercedValue = coerceValue(key, value);
|
||||
set(config, configPath, coercedValue);
|
||||
} else {
|
||||
debug(`[runtime-config] Ignored unknown key: ${key}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and apply runtime config.
|
||||
*
|
||||
* @export
|
||||
* @return {void}
|
||||
*/
|
||||
export default async function loadRuntimeConfig() {
|
||||
if (config.APP.disableRuntimeConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/fleetbase.config.json?_t=${Date.now()}`, { cache: 'no-cache' });
|
||||
if (!response.ok) {
|
||||
debug('No fleetbase.config.json found, using built-in config defaults');
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeConfig = await response.json();
|
||||
applyRuntimeConfig(runtimeConfig);
|
||||
} catch (e) {
|
||||
debug(`Failed to load runtime config : ${e.message}`);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,9 @@ module.exports = function (environment) {
|
||||
},
|
||||
|
||||
APP: {
|
||||
autoboot: false,
|
||||
extensions: asArray(getenv('EXTENSIONS')),
|
||||
disableRuntimeConfig: toBoolean(getenv('DISABLE_RUNTIME_CONFIG'))
|
||||
},
|
||||
|
||||
API: {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
/** eslint-disable node/no-unpublished-require */
|
||||
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
|
||||
const FleetbaseExtensionsIndexer = require('fleetbase-extensions-indexer');
|
||||
const Funnel = require('broccoli-funnel');
|
||||
const postcssImport = require('postcss-import');
|
||||
const postcssPresetEnv = require('postcss-preset-env');
|
||||
const postcssEach = require('postcss-each');
|
||||
@@ -11,6 +12,7 @@ const postcssConditionals = require('postcss-conditionals-renewed');
|
||||
const postcssAtRulesVariables = require('postcss-at-rules-variables');
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const tailwind = require('tailwindcss');
|
||||
const environment = process.env.EMBER_ENV;
|
||||
|
||||
module.exports = function (defaults) {
|
||||
const app = new EmberApp(defaults, {
|
||||
@@ -59,19 +61,13 @@ module.exports = function (defaults) {
|
||||
});
|
||||
|
||||
let extensions = new FleetbaseExtensionsIndexer();
|
||||
let config;
|
||||
if (environment === 'development') {
|
||||
config = new Funnel('.', {
|
||||
files: ['fleetbase.config.json'],
|
||||
destDir: '/',
|
||||
});
|
||||
}
|
||||
|
||||
// Use `app.import` to add additional libraries to the generated
|
||||
// output files.
|
||||
//
|
||||
// If you need to use different assets in different
|
||||
// environments, specify an object as the first parameter. That
|
||||
// object's keys should be the environment name and the values
|
||||
// should be the asset to use in that environment.
|
||||
//
|
||||
// If the library that you are including contains AMD or ES6
|
||||
// modules that you would like to import into your application
|
||||
// please specify an object with the list of modules as keys
|
||||
// along with the exports of each module as its value.
|
||||
|
||||
return app.toTree([extensions]);
|
||||
return app.toTree([extensions, config].filter(Boolean));
|
||||
};
|
||||
|
||||
3
console/fleetbase.config.json
Normal file
3
console/fleetbase.config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"API_HOST": "http://localhost:8000"
|
||||
}
|
||||
@@ -6,4 +6,18 @@ server {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
|
||||
# Serve runtime config with no cache
|
||||
location = /fleetbase.config.json {
|
||||
root /usr/share/nginx/html;
|
||||
default_type application/json;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires 0;
|
||||
try_files /fleetbase.config.json @config_fallback;
|
||||
}
|
||||
|
||||
location @config_fallback {
|
||||
return 200 '{}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fleetbase/console",
|
||||
"version": "0.5.10",
|
||||
"version": "0.7.1",
|
||||
"private": true,
|
||||
"description": "Modular logistics and supply chain operating system (LSOS)",
|
||||
"repository": "https://github.com/fleetbase/fleetbase",
|
||||
@@ -29,18 +29,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ember/legacy-built-in-components": "^0.4.2",
|
||||
"@fleetbase/dev-engine": "^0.2.7",
|
||||
"@fleetbase/ember-core": "^0.2.21",
|
||||
"@fleetbase/ember-ui": "^0.2.34",
|
||||
"@fleetbase/fleetops-data": "^0.1.18",
|
||||
"@fleetbase/fleetops-engine": "^0.5.8",
|
||||
"@fleetbase/iam-engine": "^0.1.1",
|
||||
"@fleetbase/ember-core": "latest",
|
||||
"@fleetbase/ember-ui": "latest",
|
||||
"@fleetbase/fleetops-data": "latest",
|
||||
"@fleetbase/fleetops-engine": "^0.6.9",
|
||||
"@fleetbase/iam-engine": "^0.1.3",
|
||||
"@fleetbase/dev-engine": "^0.2.9",
|
||||
"@fleetbase/registry-bridge-engine": "^0.0.19",
|
||||
"@fleetbase/storefront-engine": "^0.3.31",
|
||||
"@fleetbase/leaflet-routing-machine": "^3.2.16",
|
||||
"@fleetbase/registry-bridge-engine": "^0.0.15",
|
||||
"@fleetbase/storefront-engine": "^0.3.15",
|
||||
"@fortawesome/ember-fontawesome": "^2.0.0",
|
||||
"ember-changeset": "^4.1.2",
|
||||
"ember-changeset-validations": "^4.1.1",
|
||||
"ember-changeset": "4.1.2",
|
||||
"ember-changeset-validations": "4.1.2",
|
||||
"ember-composable-helpers": "^5.0.0",
|
||||
"ember-concurrency": "^3.1.1",
|
||||
"ember-concurrency-decorators": "^2.0.3",
|
||||
@@ -56,6 +56,7 @@
|
||||
"postcss-nth-list": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@embroider/macros": "1.16.12",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/eslint-parser": "^7.25.1",
|
||||
"@babel/plugin-proposal-decorators": "^7.24.7",
|
||||
@@ -127,7 +128,7 @@
|
||||
"stylelint-prettier": "^4.1.0",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"tracked-built-ins": "^3.3.0",
|
||||
"webpack": "^5.94.0"
|
||||
"webpack": "^5.98.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
@@ -137,9 +138,9 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@fleetbase/ember-core": "^0.2.21",
|
||||
"@fleetbase/ember-ui": "^0.2.34",
|
||||
"@fleetbase/fleetops-data": "^0.1.18"
|
||||
"@fleetbase/ember-core": "latest",
|
||||
"@fleetbase/ember-ui": "latest",
|
||||
"@fleetbase/fleetops-data": "latest"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
|
||||
7012
console/pnpm-lock.yaml
generated
7012
console/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -30,10 +30,12 @@ Router.map(function () {
|
||||
this.route('account', function () {
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('auth');
|
||||
this.route('organizations');
|
||||
});
|
||||
this.route('settings', function () {
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('two-fa');
|
||||
this.route('notifications');
|
||||
});
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('admin', function () {
|
||||
@@ -48,7 +50,6 @@ Router.map(function () {
|
||||
this.route('socket');
|
||||
});
|
||||
this.route('branding');
|
||||
this.route('notifications');
|
||||
this.route('two-fa-settings');
|
||||
this.route('virtual', { path: '/:slug' });
|
||||
this.route('organizations', function () {
|
||||
@@ -62,5 +63,4 @@ Router.map(function () {
|
||||
});
|
||||
});
|
||||
this.route('catch', { path: '/*' });
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | impersonator-tray', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.set('myAction', function(val) { ... });
|
||||
|
||||
await render(hbs`<ImpersonatorTray />`);
|
||||
|
||||
assert.dom().hasText('');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
<ImpersonatorTray>
|
||||
template block text
|
||||
</ImpersonatorTray>
|
||||
`);
|
||||
|
||||
assert.dom().hasText('template block text');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | modals/edit-organization', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.set('myAction', function(val) { ... });
|
||||
|
||||
await render(hbs`<Modals::EditOrganization />`);
|
||||
|
||||
assert.dom().hasText('');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
<Modals::EditOrganization>
|
||||
template block text
|
||||
</Modals::EditOrganization>
|
||||
`);
|
||||
|
||||
assert.dom().hasText('template block text');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | modals/leave-organization', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.set('myAction', function(val) { ... });
|
||||
|
||||
await render(hbs`<Modals::LeaveOrganization />`);
|
||||
|
||||
assert.dom().hasText('');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
<Modals::LeaveOrganization>
|
||||
template block text
|
||||
</Modals::LeaveOrganization>
|
||||
`);
|
||||
|
||||
assert.dom().hasText('template block text');
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Helper | get-notification-key', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it renders', async function (assert) {
|
||||
this.set('inputValue', '1234');
|
||||
|
||||
await render(hbs`{{get-notification-key this.inputValue}}`);
|
||||
|
||||
assert.dom(this.element).hasText('1234');
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Controller | console/admin/notifications', function (hooks) {
|
||||
module('Unit | Controller | console/account/organizations', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:console/admin/notifications');
|
||||
let controller = this.owner.lookup('controller:console/account/organizations');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user