mirror of
https://github.com/fleetbase/fleetbase.git
synced 2026-01-01 03:47:07 +00:00
Compare commits
312 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e4baa34e0 | ||
|
|
d6d078df62 | ||
|
|
7252a95805 | ||
|
|
b25da51496 | ||
|
|
7d776f2bd5 | ||
|
|
ecfcec72e4 | ||
|
|
ca1741a4b2 | ||
|
|
7ba7821fed | ||
|
|
947565bcf0 | ||
|
|
2d4cc5cf66 | ||
|
|
af3c60fc7c | ||
|
|
53a87d6f38 | ||
|
|
a372b515da | ||
|
|
d7f8f87315 | ||
|
|
f8b9c0415b | ||
|
|
36673ef564 | ||
|
|
19341c81e7 | ||
|
|
b4ecf5bda9 | ||
|
|
1b714a7ef8 | ||
|
|
e41cd62ea5 | ||
|
|
1ca1342052 | ||
|
|
a5a5ddb0d5 | ||
|
|
c51f3ca6c8 | ||
|
|
a9b172081a | ||
|
|
a29ca0ecb9 | ||
|
|
6442644438 | ||
|
|
b525ad4b1a | ||
|
|
b161b415d0 | ||
|
|
e444d7994c | ||
|
|
0e44375d32 | ||
|
|
a9bb2f0166 | ||
|
|
ac02a6b6e0 | ||
|
|
4a640561d6 | ||
|
|
0238632fdd | ||
|
|
27652db9c3 | ||
|
|
5957bc6339 | ||
|
|
5eaf2039d4 | ||
|
|
d8c06ae0be | ||
|
|
c9011b3ffa | ||
|
|
520de0f6bc | ||
|
|
ba6ed235e3 | ||
|
|
9b15a6856f | ||
|
|
0a4e5b31cc | ||
|
|
0d47ffc785 | ||
|
|
777d84a7fe | ||
|
|
18fdfdf506 | ||
|
|
7e898dd54b | ||
|
|
761c752a8e | ||
|
|
74d02efaa0 | ||
|
|
aa928b43ba | ||
|
|
7ed422d893 | ||
|
|
064fa12a43 | ||
|
|
32f0b22ed1 | ||
|
|
4f87434911 | ||
|
|
498d519c49 | ||
|
|
e4b008093d | ||
|
|
c31377b194 | ||
|
|
6f397ea3cb | ||
|
|
015c24585b | ||
|
|
1f4b25faee | ||
|
|
3a193e414c | ||
|
|
9719632289 | ||
|
|
08dabaf138 | ||
|
|
7ae3ea95a2 | ||
|
|
9653cfcaf0 | ||
|
|
cb7a2fb05b | ||
|
|
fd569cfeaf | ||
|
|
0f9cd52bb4 | ||
|
|
72ce000786 | ||
|
|
c9477b78f2 | ||
|
|
affa141c9d | ||
|
|
5726eb974f | ||
|
|
ca3050905d | ||
|
|
cf2ced1512 | ||
|
|
93b7224335 | ||
|
|
9a053cfd9f | ||
|
|
56897af057 | ||
|
|
b27e485a44 | ||
|
|
94c5407387 | ||
|
|
54ac27b304 | ||
|
|
4fb596c866 | ||
|
|
3f12e98448 | ||
|
|
a1fc1e4ff8 | ||
|
|
d622b617c3 | ||
|
|
edba6c8396 | ||
|
|
a0fc1ce402 | ||
|
|
ffab66ac6c | ||
|
|
d3555c7c82 | ||
|
|
4b12efef41 | ||
|
|
c80f507720 | ||
|
|
2da7ee9c19 | ||
|
|
3f2c739810 | ||
|
|
6b2ab28ec9 | ||
|
|
658568e4ec | ||
|
|
8a487b2352 | ||
|
|
bc89218a26 | ||
|
|
5a4f7e2ae3 | ||
|
|
9fa1bf54d2 | ||
|
|
13cfe00958 | ||
|
|
6cab778f93 | ||
|
|
b98eb3adf5 | ||
|
|
5473b50c40 | ||
|
|
42df48c9b0 | ||
|
|
268749fcd9 | ||
|
|
d9f415528e | ||
|
|
76b0bfbfcd | ||
|
|
0432003163 | ||
|
|
da420f0b4a | ||
|
|
e923a89719 | ||
|
|
0742603b43 | ||
|
|
e1788a4ad6 | ||
|
|
50ae560409 | ||
|
|
2e48024949 | ||
|
|
7cb4654c86 | ||
|
|
a17aa3f5cc | ||
|
|
908f60aaac | ||
|
|
0bf1a7fadd | ||
|
|
aa1ea2de89 | ||
|
|
235f1ce80c | ||
|
|
5aa50504a4 | ||
|
|
5d1b2e1939 | ||
|
|
a1f2992f18 | ||
|
|
fc5d90189c | ||
|
|
2fee78e534 | ||
|
|
83fc794702 | ||
|
|
0252b387e2 | ||
|
|
66f669ad80 | ||
|
|
a11b77592c | ||
|
|
e5156829dc | ||
|
|
6cd7ddffcb | ||
|
|
b9adb92fc1 | ||
|
|
d81bd4e900 | ||
|
|
12e1ec2cac | ||
|
|
cbdf1d489b | ||
|
|
785bc55bb7 | ||
|
|
8a21593d9a | ||
|
|
5f2003eec5 | ||
|
|
d171d02aac | ||
|
|
dfd4ee37df | ||
|
|
27c063fbfb | ||
|
|
e05d12dd87 | ||
|
|
8e85dcff83 | ||
|
|
e38923c461 | ||
|
|
9911c96c09 | ||
|
|
284c62cd06 | ||
|
|
86378d3ede | ||
|
|
f8fd9f76fa | ||
|
|
67aa793537 | ||
|
|
1d36bf144b | ||
|
|
293f67d6d1 | ||
|
|
01e8298968 | ||
|
|
5d0ae16cfd | ||
|
|
1d003ee31e | ||
|
|
9c9f3a994e | ||
|
|
094d1d375e | ||
|
|
b0ae302e81 | ||
|
|
205fcf1480 | ||
|
|
23bf7c5ac8 | ||
|
|
8a1dee0cbd | ||
|
|
df2da8cea1 | ||
|
|
ced5e6b6fd | ||
|
|
de2cbd2ded | ||
|
|
894f4348dd | ||
|
|
2030a72a71 | ||
|
|
41d20c41c5 | ||
|
|
9976075843 | ||
|
|
b26f735fee | ||
|
|
791cc7283d | ||
|
|
21601737a1 | ||
|
|
0cd9e076b8 | ||
|
|
5fe799b708 | ||
|
|
878a70d328 | ||
|
|
4c5487b6bb | ||
|
|
599fd0e8f7 | ||
|
|
64561c85a8 | ||
|
|
c8d8be11d2 | ||
|
|
12847437b0 | ||
|
|
53eadee8a6 | ||
|
|
657de6c99d | ||
|
|
db601606c5 | ||
|
|
6ec176011a | ||
|
|
d416baf3cc | ||
|
|
d80b968eaa | ||
|
|
eef6847919 | ||
|
|
2ad569eaf6 | ||
|
|
dc18b48f6a | ||
|
|
3e93e161dc | ||
|
|
fac29308fd | ||
|
|
7b62e992eb | ||
|
|
18af3b4515 | ||
|
|
264c564ad6 | ||
|
|
dbf9a37ae9 | ||
|
|
46c4ac9dda | ||
|
|
e692bc3365 | ||
|
|
0b807011a3 | ||
|
|
80d84e1c3b | ||
|
|
08c722359a | ||
|
|
ebd9b0d8cb | ||
|
|
ec5ace214c | ||
|
|
396e0cbf36 | ||
|
|
3faac3481f | ||
|
|
36f837a4af | ||
|
|
094bd6c8ce | ||
|
|
aea35f6f5f | ||
|
|
96fa0294bc | ||
|
|
72ed388cd6 | ||
|
|
5a79a06642 | ||
|
|
010a6b0d05 | ||
|
|
6d138761ee | ||
|
|
1f59d8831b | ||
|
|
746b2142f0 | ||
|
|
343e07e24d | ||
|
|
d6094b8e29 | ||
|
|
59d7fa3de9 | ||
|
|
14c05a6c6a | ||
|
|
5c12254a58 | ||
|
|
65416833ca | ||
|
|
40e0de6e6b | ||
|
|
5986ced2c0 | ||
|
|
ca260bb94c | ||
|
|
95ef8ef3c9 | ||
|
|
df8513cf7b | ||
|
|
e2c544b0c9 | ||
|
|
77b39fccf9 | ||
|
|
eaa448f762 | ||
|
|
627522d61c | ||
|
|
201ac84dc9 | ||
|
|
78b1f31053 | ||
|
|
63cf8128c0 | ||
|
|
5d66c7a35f | ||
|
|
0603888393 | ||
|
|
cd50d349f8 | ||
|
|
e478767a60 | ||
|
|
17cb0cd274 | ||
|
|
a3289ddd41 | ||
|
|
ff8986541c | ||
|
|
04d0c126de | ||
|
|
ad43f74d5c | ||
|
|
3c013a3817 | ||
|
|
4392c7e3ff | ||
|
|
97dfbedd1a | ||
|
|
6d13f22a98 | ||
|
|
2c09c87bb6 | ||
|
|
f7f6991ef3 | ||
|
|
73acd4833c | ||
|
|
1c3c4c16a5 | ||
|
|
5d01438dc9 | ||
|
|
9d004410ee | ||
|
|
3a396f3b54 | ||
|
|
e86b970fc8 | ||
|
|
122a0d186a | ||
|
|
7e258f698f | ||
|
|
65ef642315 | ||
|
|
d79f034dbd | ||
|
|
05b7d5e112 | ||
|
|
61779ab102 | ||
|
|
a287c05380 | ||
|
|
ecc41e587e | ||
|
|
166529f9b4 | ||
|
|
189547f9de | ||
|
|
90a42e8a93 | ||
|
|
d28b1d41fb | ||
|
|
b056ef62b0 | ||
|
|
e70cbacbc2 | ||
|
|
fbd4a7490a | ||
|
|
b9b0eb308b | ||
|
|
48e1b89ec8 | ||
|
|
5e02c95b66 | ||
|
|
db8b5c4d6a | ||
|
|
aa214ccad7 | ||
|
|
cdd5524cf3 | ||
|
|
d99cefd2a4 | ||
|
|
1c58fd43c3 | ||
|
|
9226394159 | ||
|
|
9b23b39f32 | ||
|
|
5310fc3ff3 | ||
|
|
9569053f50 | ||
|
|
b08c054b99 | ||
|
|
2e517f2f95 | ||
|
|
7038d375b0 | ||
|
|
d5ea7f7790 | ||
|
|
4eb4b04121 | ||
|
|
24c5b93005 | ||
|
|
80610b9a48 | ||
|
|
9e5551972e | ||
|
|
b784f890f3 | ||
|
|
797a3d61fe | ||
|
|
d2f0bfe83e | ||
|
|
f28ad85c1a | ||
|
|
d7b0826f3f | ||
|
|
dd6008a8aa | ||
|
|
6671fefaaa | ||
|
|
7136f6195c | ||
|
|
2384887620 | ||
|
|
a2778f1552 | ||
|
|
acfda5ed1a | ||
|
|
e08255007a | ||
|
|
41761ea50e | ||
|
|
5c623819ed | ||
|
|
2bd885b1a2 | ||
|
|
88c3842441 | ||
|
|
2c2a4121a8 | ||
|
|
aadd03f14b | ||
|
|
706e94270d | ||
|
|
1dabc375f9 | ||
|
|
0efec46155 | ||
|
|
8bb2c6b65d | ||
|
|
1c0af1a119 | ||
|
|
dd65ee619b | ||
|
|
e790a0e123 | ||
|
|
723e3ca3d1 | ||
|
|
4eb706d33e |
65
.github/workflows/cd.yml
vendored
65
.github/workflows/cd.yml
vendored
@@ -58,6 +58,43 @@ jobs:
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
|
||||
- name: Resolve ECS Targets
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Detect naming scheme by checking if new cluster exists
|
||||
NEW_CLUSTER="${PROJECT}-${STACK}-cluster"
|
||||
if aws ecs describe-clusters --region "${AWS_REGION}" --clusters "${NEW_CLUSTER}" \
|
||||
--query "clusters[?status=='ACTIVE'].clusterArn" --output text 2>/dev/null | grep -q .; then
|
||||
# New scheme: use cluster suffix and service prefixes
|
||||
CLUSTER="${NEW_CLUSTER}"
|
||||
SERVICE_PREFIX="${PROJECT}-${STACK}-"
|
||||
SERVICE_BASE="api"
|
||||
else
|
||||
# Legacy scheme: no suffixes/prefixes
|
||||
CLUSTER="${PROJECT}-${STACK}"
|
||||
SERVICE_PREFIX=""
|
||||
SERVICE_BASE="app"
|
||||
fi
|
||||
|
||||
# Build service names
|
||||
API_SERVICE="${SERVICE_PREFIX}${SERVICE_BASE}"
|
||||
SCHEDULER_SERVICE="${SERVICE_PREFIX}scheduler"
|
||||
EVENTS_SERVICE="${SERVICE_PREFIX}events"
|
||||
TASK_DEF="${PROJECT}-${STACK}-${SERVICE_BASE}"
|
||||
|
||||
# Get container name from task definition
|
||||
CONTAINER_NAME="$(aws ecs describe-task-definition --task-definition "$TASK_DEF" \
|
||||
--query 'taskDefinition.containerDefinitions[0].name' --output text 2>/dev/null || echo "$SERVICE_BASE")"
|
||||
|
||||
{
|
||||
echo "CLUSTER=$CLUSTER"
|
||||
echo "API_SERVICE=$API_SERVICE"
|
||||
echo "SCHEDULER_SERVICE=$SCHEDULER_SERVICE"
|
||||
echo "EVENTS_SERVICE=$EVENTS_SERVICE"
|
||||
echo "TASK_DEF=$TASK_DEF"
|
||||
echo "CONTAINER_NAME=$CONTAINER_NAME"
|
||||
} >> "$GITHUB_ENV"
|
||||
- name: Download ecs-tool
|
||||
run: |
|
||||
wget -O ecs-tool.tar.gz https://github.com/springload/ecs-tool/releases/download/1.9.6/ecs-tool_1.9.6_linux_amd64.tar.gz && tar -xvf ecs-tool.tar.gz ecs-tool
|
||||
@@ -65,9 +102,21 @@ jobs:
|
||||
- name: Deploy the images 🚀
|
||||
run: |-
|
||||
set -eu
|
||||
# run deploy.sh script before deployments
|
||||
env "ECS_RUN.SERVICE=app" "ECS_RUN.LAUNCH_TYPE=FARGATE" ./ecs-tool run -l "ecs-tool" --image_tag '{container_name}-${{ env.VERSION }}' --cluster ${{ env.PROJECT }}-${{ env.STACK }} --task_definition ${{ env.PROJECT }}-${{ env.STACK }}-app --container_name app ./deploy.sh
|
||||
./ecs-tool deploy --image_tag '{container_name}-${{ env.VERSION }}' --cluster ${{ env.PROJECT }}-${{ env.STACK }} -s app -s scheduler -s events
|
||||
|
||||
# Run deploy.sh script before deployments
|
||||
env "ECS_RUN.SERVICE=${API_SERVICE}" "ECS_RUN.LAUNCH_TYPE=FARGATE" \
|
||||
./ecs-tool run -l "ecs-tool" \
|
||||
--image_tag '{container_name}-${{ env.VERSION }}' \
|
||||
--cluster "${CLUSTER}" \
|
||||
--task_definition "${TASK_DEF}" \
|
||||
--container_name "${CONTAINER_NAME}" \
|
||||
./deploy.sh
|
||||
|
||||
# Deploy services
|
||||
./ecs-tool deploy \
|
||||
--image_tag '{container_name}-${{ env.VERSION }}' \
|
||||
--cluster "${CLUSTER}" \
|
||||
-s "${API_SERVICE}" -s "${SCHEDULER_SERVICE}" -s "${EVENTS_SERVICE}"
|
||||
|
||||
build_frontend:
|
||||
name: Build and Deploy the Console
|
||||
@@ -148,7 +197,7 @@ jobs:
|
||||
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
|
||||
echo "EXTENSIONS=@fleetbase/billing-engine,@fleetbase/internals-engine,@fleetbase/aws-marketplace" >> ./environments/.env.production
|
||||
working-directory: ./console
|
||||
|
||||
- name: Set Env Variables for Production
|
||||
@@ -156,7 +205,8 @@ jobs:
|
||||
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
|
||||
echo "EXTENSIONS=@fleetbase/billing-engine,@fleetbase/internals-engine,@fleetbase/aws-marketplace" >> ./environments/.env.production
|
||||
echo "DISABLE_RUNTIME_CONFIG=true" >> ./environments/.env.production
|
||||
working-directory: ./console
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -175,6 +225,11 @@ jobs:
|
||||
set -u
|
||||
|
||||
DEPLOY_BUCKET=${STATIC_DEPLOY_BUCKET:-${{ env.PROJECT }}-${{ env.STACK }}}
|
||||
NEW_BUCKET="${PROJECT}-${STACK}-console"
|
||||
if aws s3api head-bucket --bucket "$NEW_BUCKET" 2>/dev/null; then
|
||||
DEPLOY_BUCKET="$NEW_BUCKET"
|
||||
fi
|
||||
|
||||
# this value will come from the dotenv above
|
||||
echo "Deploying to $DEPLOY_BUCKET"
|
||||
wget -O- https://github.com/bep/s3deploy/releases/download/v2.11.0/s3deploy_2.11.0_linux-amd64.tar.gz | tar xzv -f - s3deploy
|
||||
|
||||
53
.github/workflows/publish-docker-images.yml
vendored
53
.github/workflows/publish-docker-images.yml
vendored
@@ -48,56 +48,3 @@ jobs:
|
||||
fleetbase-api
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
|
||||
aws-marketplace-ecr-release:
|
||||
name: Build and Push to AWS ECR
|
||||
runs-on: ubuntu-latest
|
||||
needs: docker-release
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_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: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v3
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Manually log in to Amazon ECR (Marketplace-style)
|
||||
run: |
|
||||
aws ecr get-login-password --region ${{ secrets.AWS_ECR_REGION }} | \
|
||||
docker login --username AWS --password-stdin ${{ secrets.AWS_ECR_REGISTRY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Bake and Push to ECR
|
||||
uses: docker/bake-action@v2
|
||||
with:
|
||||
push: true
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
targets: |
|
||||
fleetbase-api
|
||||
fleetbase-console
|
||||
|
||||
- name: Verify ECR Images
|
||||
run: |
|
||||
aws ecr describe-images \
|
||||
--registry-id ${{ secrets.AWS_ECR_REGISTRY_ID }} \
|
||||
--repository-name fleetbase/fleetbase-api \
|
||||
--region ${{ secrets.AWS_ECR_REGION }}
|
||||
|
||||
aws ecr describe-images \
|
||||
--registry-id ${{ secrets.AWS_ECR_REGISTRY_ID }} \
|
||||
--repository-name fleetbase/fleetbase-console \
|
||||
--region ${{ secrets.AWS_ECR_REGION }}
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
.env.backup
|
||||
.phpunit.result.cache
|
||||
.pnpm-store
|
||||
.tool-versions
|
||||
docker-compose.override.yml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
@@ -16,6 +17,8 @@ api/composer.dev.json
|
||||
api/composer-install-dev.sh
|
||||
api/auth.json
|
||||
api/crontab
|
||||
api/go-crond
|
||||
api/.fleetbase-id
|
||||
act.sh
|
||||
composer-auth.json
|
||||
docker/database/*
|
||||
@@ -31,8 +34,14 @@ packages/loconav
|
||||
packages/internals
|
||||
packages/projectargus-engine
|
||||
packages/customer-portal
|
||||
# wip
|
||||
packages/solid
|
||||
packages/aws-marketplace
|
||||
packages/countries
|
||||
packages/fliit
|
||||
packages/samsara
|
||||
packages/solid*
|
||||
packages/valhalla
|
||||
packages/vroom
|
||||
solid
|
||||
verdaccio
|
||||
# asdf
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
http://:8000 {
|
||||
root * /fleetbase/api/public
|
||||
encode zstd br gzip
|
||||
|
||||
php_server {
|
||||
resolve_root_symlink
|
||||
}
|
||||
|
||||
85
README.md
85
README.md
@@ -7,11 +7,11 @@
|
||||
<p align="center" dir="auto">
|
||||
Modular logistics and supply chain operating system
|
||||
<br>
|
||||
<a href="https://docs.fleetbase.io/" rel="nofollow">Documentation</a>
|
||||
<a href="https://docs.fleetbase.io/" rel="nofollow" target="_fleetbase_docs">Documentation</a>
|
||||
·
|
||||
<a href="https://console.fleetbase.io" rel="nofollow">Cloud Version</a>
|
||||
<a href="https://console.fleetbase.io" rel="nofollow" target="_fleetbase_console">Cloud Version</a>
|
||||
·
|
||||
<a href="https://fleetbase.apichecker.com" target="_api_status" rel="nofollow">API Status</a>
|
||||
<a href="https://console.fleetbase.io/aws-marketplace" rel="nofollow" target="_aws_marketplace">Deploy on AWS</a>
|
||||
·
|
||||
<a href="https://tally.so/r/3NBpAW" rel="nofollow">Book a Demo</a>
|
||||
·
|
||||
@@ -25,7 +25,34 @@
|
||||
Fleetbase is a modular logistics and supply chain operating system designed to streamline management, planning, optimization, and operational control across various sectors of the supply chain industry.
|
||||
|
||||
<p align="center" dir="auto">
|
||||
<img src="https://github.com/fleetbase/fleetbase/assets/816371/125348c9-c88a-49fe-b098-9abec9d7dff8" alt="Fleetbase Console" width="1200" style="max-width: 100%;" />
|
||||
<img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/fleetbase_overview.png" alt="Fleetbase Console" width="1200" style="max-width: 100%;" />
|
||||
</p>
|
||||
|
||||
## Visual Feature Showcase
|
||||
|
||||
<p align="center" dir="auto">
|
||||
<img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/order-board-kanban.png" alt="Fleetbase Order Board" width="1200" style="max-width: 100%;" />
|
||||
<em>Visualize and manage your orders with a dynamic Kanban board.</em>
|
||||
</p>
|
||||
|
||||
<p align="center" dir="auto">
|
||||
<img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/order-workflow-config.png" alt="Fleetbase Order Workflow Configuration" width="1200" style="max-width: 100%;" />
|
||||
<em>Create custom order flows and automation with the intuitive workflow builder.</em>
|
||||
</p>
|
||||
|
||||
<p align="center" dir="auto">
|
||||
<img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/order-map-view.png" alt="Fleetbase Order Map View" width="1200" style="max-width: 100%;" />
|
||||
<em>Track individual orders in real-time on an interactive map.</em>
|
||||
</p>
|
||||
|
||||
<p align="center" dir="auto">
|
||||
<img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/live-map-tracking.png" alt="Fleetbase Live Map Tracking" width="1200" style="max-width: 100%;" />
|
||||
<em>Get a complete overview of your fleet and active orders on a live map.</em>
|
||||
</p>
|
||||
|
||||
<p align="center" dir="auto">
|
||||
<img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/fleet-map-zones.png" alt="Fleetbase Fleet Map with Zones" width="1200" style="max-width: 100%;" />
|
||||
<em>Define and manage service areas and zones for your fleet.</em>
|
||||
</p>
|
||||
|
||||
**Quickstart**
|
||||
@@ -39,6 +66,7 @@ cd fleetbase && ./scripts/docker-install.sh
|
||||
|
||||
- [Features](#-features)
|
||||
- [Install](#-install)
|
||||
- [Deploy on AWS](#-deploy-on-aws-in-one-click)
|
||||
- [Extensions](#-extensions)
|
||||
- [Apps](#-apps)
|
||||
- [Roadmap](#-roadmap)
|
||||
@@ -97,10 +125,10 @@ Next copy this value to the `APP_KEY` environment variable in the application co
|
||||
|
||||
**Routing:** Fleetbase ships with a default OSRM server hosted by `[router.project-osrm.org](https://router.project-osrm.org)` but you’re able to use your own or any other OSRM compatible server. You can modify this in the `console/environments` directory by modifying the .env file of the environment you’re deploying and setting the `OSRM_HOST` to the OSRM server for Fleetbase to use.
|
||||
|
||||
**Services:** There are a few environment variables which need to be set for Fleetbase to function with full features. If you’re deploying with docker then it’s easiest to just create a `docker-compose.override.yml` and supply the environment variables in this file.
|
||||
**Services:** There are a few environment variables which need to be set for Fleetbase to function with full features. If you're deploying with docker then it's easiest to just create a `docker-compose.override.yml` and supply the environment variables in this file.
|
||||
|
||||
```yaml
|
||||
version: “3.8”
|
||||
version: "3.8"
|
||||
services:
|
||||
application:
|
||||
environment:
|
||||
@@ -113,10 +141,50 @@ services:
|
||||
TWILIO_SID:
|
||||
TWILIO_TOKEN:
|
||||
TWILIO_FROM:
|
||||
|
||||
socket:
|
||||
environment:
|
||||
# IMPORTANT: Configure WebSocket origins for security
|
||||
# Development (localhost only - include WebSocket protocols):
|
||||
SOCKETCLUSTER_OPTIONS: '{"origins":"http://localhost:*,https://localhost:*,ws://localhost:*,wss://localhost:*"}'
|
||||
# Production (replace with your actual domain):
|
||||
# SOCKETCLUSTER_OPTIONS: '{"origins":"https://yourdomain.com:*,wss://yourdomain.com:*"}'
|
||||
```
|
||||
|
||||
**WebSocket Security:** The `SOCKETCLUSTER_OPTIONS` environment variable controls which domains can connect to the WebSocket server. Always restrict origins to your specific domains in production to prevent security vulnerabilities.
|
||||
|
||||
You can learn more about full installation, and configuration in the [official documentation](https://docs.fleetbase.io/getting-started/install).
|
||||
|
||||
## 🚀 Deploy on AWS in One Click
|
||||
|
||||
Deploy your complete Fleetbase logistics platform on AWS with enterprise-grade security, scalability, and reliability. No DevOps expertise required!
|
||||
|
||||
[](https://console.fleetbase.io/aws-marketplace)
|
||||
|
||||
### ✨ What You Get
|
||||
|
||||
- **Complete AWS Infrastructure**: ECS Fargate, RDS MySQL, ElastiCache Redis, S3, CloudFront, and more
|
||||
- **25-Minute Setup**: From zero to production-ready logistics platform
|
||||
- **Enterprise Security**: VPC isolation, encrypted storage, secrets management
|
||||
- **Auto-Scaling**: Handle traffic spikes with ECS Fargate auto-scaling
|
||||
- **High Availability**: Multi-AZ deployment with 99.9% uptime SLA
|
||||
- **Cost Optimized**: Pay-as-you-use with optimized resource allocation
|
||||
|
||||
### 🏗️ Infrastructure Included
|
||||
|
||||
Your AWS deployment includes a complete, production-ready infrastructure stack:
|
||||
|
||||
- **Compute**: ECS Fargate cluster with auto-scaling services
|
||||
- **Database**: RDS MySQL 8.0 with automated backups and Multi-AZ support
|
||||
- **Cache**: ElastiCache Redis for high-performance caching
|
||||
- **Storage**: S3 object storage with CloudFront CDN for global distribution
|
||||
- **Networking**: VPC with private subnets, NAT gateways, and security groups
|
||||
- **Load Balancing**: Application Load Balancer with SSL certificates
|
||||
- **Monitoring**: CloudWatch logs, container insights, and health monitoring
|
||||
- **Messaging**: SQS message queues for background job processing
|
||||
|
||||
[**🚀 Deploy Now**](https://console.fleetbase.io/aws-marketplace) | [**📖 Learn More**](https://docs.fleetbase.io/category/deploying/aws)
|
||||
|
||||
# 🧩 Extensions
|
||||
|
||||
Extensions are modular components that enhance the functionality of your Fleetbase instance. They allow you to add new features, customize existing behavior, or integrate with external systems.
|
||||
@@ -149,8 +217,8 @@ Fleetbase offers a few open sourced apps which are built on Fleetbase which can
|
||||
## 🛣️ Roadmap
|
||||
1. **Inventory and Warehouse Management** ~ Pallet will be Fleetbase’s first official extension for WMS & Inventory.
|
||||
2. **Accounting and Invoicing** ~ Ledger will be Fleetbase’s first official extension accounting and invoicing.
|
||||
3. **Fleetbase for Desktop** ~ Desktop builds for OSX and Windows.
|
||||
4. **Custom Maps and Routing Engines** ~ Feature to enable easy integrations with custom maps and routing engines like Google Maps or Mapbox etc…
|
||||
3. **AI** ~ AI Agent intrgation for system and workflows.
|
||||
4. **Dynamic Rules System** ~ Trigger events, tasks jobs from a rule builder on resources.
|
||||
|
||||
## 🪲 Bugs and 💡 Feature Requests
|
||||
|
||||
@@ -186,3 +254,4 @@ Get updates on Fleetbase's development and chat with the project maintainers and
|
||||
# License & Copyright
|
||||
|
||||
Fleetbase is made available under the terms of the <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank">GNU Affero General Public License 3.0 (AGPL 3.0)</a>. For other licenses <a href="mailto:hello@fleetbase.io" target="_blank">contact us</a>.
|
||||
|
||||
|
||||
14
RELEASE.md
14
RELEASE.md
@@ -1,11 +1,13 @@
|
||||
# 🚀 Fleetbase v0.7.7 — 2025-08-09
|
||||
# 🚀 Fleetbase v0.7.25 — 2025-12-29
|
||||
|
||||
> “Added ability to configure rate limiting”
|
||||
> "New SMS service to support multiple SMS providers + framework improvements"
|
||||
|
||||
---
|
||||
|
||||
## ✨ Highlights
|
||||
- Ability to configure rate limiting
|
||||
- Removed `window.Fleetbase` for improved frontend security
|
||||
- Improved query optimizations
|
||||
- Added new SMS service to support multiple SMS providers
|
||||
|
||||
---
|
||||
|
||||
@@ -15,16 +17,20 @@
|
||||
---
|
||||
|
||||
## 🔧 Upgrade Steps
|
||||
|
||||
```bash
|
||||
# Pull latest version
|
||||
git pull origin main --no-rebase
|
||||
|
||||
# Update docker
|
||||
docker compose pull
|
||||
docker compose down && docker compose up -d
|
||||
|
||||
# Run deploy script
|
||||
docker compose exec application bash -c "./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)
|
||||
Join the discussion on [GitHub Discussions](https://github.com/fleetbase/ember-ui/discussions) or drop by [#fleetbase on Discord](https://discord.com/invite/HnTqQ6zAVn)
|
||||
|
||||
100
TRANSLATING.md
Normal file
100
TRANSLATING.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Contributing to Fleetbase Translations
|
||||
|
||||
First off, thank you for considering contributing to Fleetbase translations! Your efforts help make Fleetbase accessible to a global audience. This guide will walk you through the process of adding or updating language translations for the Fleetbase platform and its various extensions.
|
||||
|
||||
## Understanding the Structure
|
||||
|
||||
Fleetbase is a modular system. The main application, known as Fleetbase Console, has its own set of translations. Additionally, each extension (like FleetOps or Storefront) also contains its own translation files. This means that to provide a complete translation for a specific language, you may need to contribute to multiple repositories.
|
||||
|
||||
- **Main Application (`fleetbase/fleetbase`)**: Contains the core translation files for the Fleetbase Console.
|
||||
- **Extensions/Modules**: Each extension has its own repository and its own set of translation files.
|
||||
|
||||
## File Format and Location
|
||||
|
||||
All translation files are in the **YAML** format (`.yaml` or `.yml`). The base language for all translations is American English (`en-us.yaml`).
|
||||
|
||||
- In the main `fleetbase/fleetbase` repository, the translation files are located at `./console/translations/`.
|
||||
- In each extension repository, the translation files are located at `./translations/`.
|
||||
|
||||
Translation files are named using the language and region code, for example:
|
||||
|
||||
- `en-us.yaml` (American English)
|
||||
- `fr-fr.yaml` (French, France)
|
||||
- `zh-cn.yaml` (Chinese, Simplified)
|
||||
|
||||
## How to Contribute Translations
|
||||
|
||||
Follow these steps to contribute a new translation or update an existing one.
|
||||
|
||||
### Step 1: Fork and Clone the Repository
|
||||
|
||||
First, you need to fork the repository you want to contribute to. This could be the main `fleetbase/fleetbase` repository or one of the extension repositories. After forking, clone it to your local machine.
|
||||
|
||||
### Step 2: Create or Update a Language File
|
||||
|
||||
Navigate to the appropriate translations directory (`./console/translations/` or `./translations/`).
|
||||
|
||||
- **To add a new language**: Copy the `en-us.yaml` file and rename it to your target language code (e.g., `es-es.yaml`).
|
||||
- **To update an existing language**: Open the existing language file. You can compare it with `en-us.yaml` to find missing keys or phrases that need updating.
|
||||
|
||||
### Step 3: Translate the Content
|
||||
|
||||
Open the YAML file in a text editor. You will see a structure of nested keys and values.
|
||||
|
||||
```yaml
|
||||
# Example from en-us.yaml
|
||||
common:
|
||||
new: New
|
||||
create: Create
|
||||
delete-selected-count: Delete {count} Selected
|
||||
```
|
||||
|
||||
When translating, you should:
|
||||
|
||||
- **Only translate the values**, not the keys. For example, in `new: New`, you would only translate `New`.
|
||||
- **Keep placeholders intact**. Some phrases contain placeholders like `{count}` or `{resource}`. These should not be translated. They are used by the application to insert dynamic values.
|
||||
|
||||
Here is an example of the French translation for the keys above:
|
||||
|
||||
```yaml
|
||||
# Example from fr-fr.yaml
|
||||
common:
|
||||
new: Nouveau
|
||||
create: Créer
|
||||
delete-selected-count: Supprimer {count} sélectionné(s)
|
||||
```
|
||||
|
||||
### Step 4: Submit a Pull Request
|
||||
|
||||
Once you have finished translating, commit your changes and push them to your forked repository. Then, open a pull request to the original Fleetbase repository.
|
||||
|
||||
- Make sure your pull request has a clear title and description of the changes you made.
|
||||
- If you are translating an extension, you may need to submit a pull request to the extension's repository. If your changes also affect the main console, a separate PR to the `fleetbase/fleetbase` repository might be necessary.
|
||||
|
||||
Your contribution will be reviewed by the Fleetbase team, and once approved, it will be merged into the project.
|
||||
|
||||
## Translation Repositories
|
||||
|
||||
Here is a list of the primary repositories that accept translation contributions:
|
||||
|
||||
| Repository | Translation Path |
|
||||
| ---------------------------------------- | ----------------------------- |
|
||||
| [fleetbase/fleetbase][1] | `./console/translations/` |
|
||||
| [fleetbase/fleetops][2] | `./translations/` |
|
||||
| [fleetbase/storefront][3] | `./translations/` |
|
||||
| [fleetbase/dev-engine][4] | `./translations/` |
|
||||
| [fleetbase/iam-engine][5] | `./translations/` |
|
||||
| [fleetbase/pallet][6] | `./translations/` |
|
||||
| [fleetbase/ledger][7] | `./translations/` |
|
||||
| [fleetbase/registry-bridge][8] | `./translations/` |
|
||||
|
||||
[1]: https://github.com/fleetbase/fleetbase
|
||||
[2]: https://github.com/fleetbase/fleetops
|
||||
[3]: https://github.com/fleetbase/storefront
|
||||
[4]: https://github.com/fleetbase/dev-engine
|
||||
[5]: https://github.com/fleetbase/iam-engine
|
||||
[6]: https://github.com/fleetbase/pallet
|
||||
[7]: https://github.com/fleetbase/ledger
|
||||
[8]: https://github.com/fleetbase/registry-bridge
|
||||
|
||||
Thank you again for your contribution to the Fleetbase community!
|
||||
@@ -40,7 +40,6 @@ class Kernel extends HttpKernel
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'throttle:api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -7,31 +7,51 @@
|
||||
"laravel"
|
||||
],
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fleetbase Pte Ltd.",
|
||||
"email": "hello@fleetbase.io"
|
||||
},
|
||||
{
|
||||
"name": "Ronald A. Richardson",
|
||||
"email": "ron@fleetbase.io"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"php": ">=8.0 <=8.2.28",
|
||||
"appstract/laravel-opcache": "^4.0",
|
||||
"fleetbase/core-api": "^1.6.13",
|
||||
"fleetbase/fleetops-api": "^0.6.16",
|
||||
"fleetbase/registry-bridge": "^0.0.19",
|
||||
"fleetbase/storefront-api": "^0.4.0",
|
||||
"fleetbase/aws-marketplace": "^0.0.8",
|
||||
"fleetbase/billing-api": "^0.1.12",
|
||||
"fleetbase/core-api": "^1.6.32",
|
||||
"fleetbase/customer-portal-api": "^0.0.10",
|
||||
"fleetbase/fleetops-api": "^0.6.32",
|
||||
"fleetbase/flespi-integration": "^0.1.16",
|
||||
"fleetbase/internals-api": "^0.0.23",
|
||||
"fleetbase/registry-bridge": "^0.1.2",
|
||||
"fleetbase/samsara-api": "^0.0.3",
|
||||
"fleetbase/solid-api": "^0.0.7",
|
||||
"fleetbase/storefront-api": "^0.4.10",
|
||||
"fleetbase/valhalla-api": "^0.0.3",
|
||||
"fleetbase/vroom-api": "^0.0.3",
|
||||
"guzzlehttp/guzzle": "^7.0.1",
|
||||
"laravel/framework": "^10.0",
|
||||
"laravel/octane": "^2.3",
|
||||
"laravel/tinker": "^2.9",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"maatwebsite/excel": "^3.1",
|
||||
"maennchen/zipstream-php": "3.1.2",
|
||||
"phpoffice/phpspreadsheet": "^1.28",
|
||||
"predis/predis": "^2.1",
|
||||
"psr/http-factory-implementation": "*",
|
||||
"resend/resend-php": "^0.14.0",
|
||||
"s-ichikawa/laravel-sendgrid-driver": "^4.0",
|
||||
"stripe/stripe-php": "13.13.0",
|
||||
"symfony/mailgun-mailer": "^7.1",
|
||||
"symfony/postmark-mailer": "^7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"spatie/laravel-ignition": "^2.0",
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"kitloong/laravel-migrations-generator": "^6.10",
|
||||
"laravel/sail": "^1.0.1",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
"nunomaduro/collision": "^7.0",
|
||||
@@ -41,6 +61,14 @@
|
||||
{
|
||||
"type": "composer",
|
||||
"url": "https://registry.fleetbase.io"
|
||||
},
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/fleetbase/aws-marketplace"
|
||||
},
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/fleetbase/internals"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
@@ -78,15 +106,6 @@
|
||||
],
|
||||
"clean-logs": [
|
||||
"composer run-script clear-logs"
|
||||
],
|
||||
"dock": [
|
||||
"docker exec -it fleetbase_os_application_1 /usr/bin/tmux -u new"
|
||||
],
|
||||
"dock-server": [
|
||||
"docker exec -it fleetbase_os_httpd_1 /bin/sh"
|
||||
],
|
||||
"tunnel": [
|
||||
"ngrok http --region=ap --hostname=fleetbase.ap.ngrok.io 8000"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
|
||||
3885
api/composer.lock
generated
3885
api/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'paths' => ['/*', 'sanctum/csrf-cookie'],
|
||||
'paths' => ['*', 'sanctum/csrf-cookie'],
|
||||
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
@@ -27,7 +27,7 @@ return [
|
||||
|
||||
'allowed_headers' => ['*'],
|
||||
|
||||
'exposed_headers' => ['x-compressed-json', 'access-console-sandbox', 'access-console-sandbox-key'],
|
||||
'exposed_headers' => ['x-compressed-json', 'access-console-sandbox', 'access-console-sandbox-key', 'content-disposition'],
|
||||
|
||||
'max_age' => 0,
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ return [
|
||||
'channels' => [
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => ['single'],
|
||||
'channels' => ['single', 'stdout'],
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
|
||||
@@ -66,6 +66,18 @@ return [
|
||||
|
||||
'resend' => [],
|
||||
|
||||
'microsoft-graph' => [
|
||||
'transport' => 'microsoft-graph',
|
||||
'client_id' => env('MICROSOFT_GRAPH_CLIENT_ID'),
|
||||
'client_secret' => env('MICROSOFT_GRAPH_CLIENT_SECRET'),
|
||||
'tenant_id' => env('MICROSOFT_GRAPH_TENANT_ID'),
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@fleetbase.io'),
|
||||
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Fleetbase')),
|
||||
],
|
||||
'save_to_sent_items' => env('MAIL_SAVE_TO_SENT_ITEMS', false),
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -t -i'),
|
||||
|
||||
@@ -105,8 +105,8 @@ return [
|
||||
OperationTerminated::class => [
|
||||
FlushOnce::class,
|
||||
FlushTemporaryContainerInstances::class,
|
||||
// DisconnectFromDatabases::class,
|
||||
// CollectGarbage::class,
|
||||
DisconnectFromDatabases::class,
|
||||
CollectGarbage::class,
|
||||
],
|
||||
|
||||
WorkerErrorOccurred::class => [
|
||||
|
||||
@@ -43,4 +43,10 @@ return [
|
||||
'secret' => env('STRIPE_SECRET', env('STRIPE_API_SECRET')),
|
||||
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
|
||||
],
|
||||
|
||||
'callpromn' => [
|
||||
'api_key' => env('CALLPROMN_API_KEY', ''),
|
||||
'from' => env('CALLPROMN_FROM', ''),
|
||||
'base_url' => env('CALLPROMN_BASE_URL', 'https://api.messagepro.mn' ),
|
||||
],
|
||||
];
|
||||
|
||||
20
api/resources/lang/es-mx/auth.php
Normal file
20
api/resources/lang/es-mx/auth.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used during authentication for various
|
||||
| messages that we need to display to the user. You are free to modify
|
||||
| these language lines according to your application's requirements.
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => 'Estas credenciales no coinciden con nuestros registros.',
|
||||
'password' => 'La contraseña proporcionada es incorrecta.',
|
||||
'throttle' => 'Demasiados intentos de inicio de sesión. Por favor, intenta de nuevo en :seconds segundos.',
|
||||
|
||||
];
|
||||
19
api/resources/lang/es-mx/pagination.php
Normal file
19
api/resources/lang/es-mx/pagination.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used by the paginator library to build
|
||||
| the simple pagination links. You are free to change them to anything
|
||||
| you want to customize your views to better match your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« Anterior',
|
||||
'next' => 'Siguiente »',
|
||||
|
||||
];
|
||||
22
api/resources/lang/es-mx/passwords.php
Normal file
22
api/resources/lang/es-mx/passwords.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are the default lines which match reasons
|
||||
| that are given by the password broker for a password update attempt
|
||||
| has failed, such as for an invalid token or invalid new password.
|
||||
|
|
||||
*/
|
||||
|
||||
'reset' => '¡Tu contraseña ha sido restablecida!',
|
||||
'sent' => '¡Te hemos enviado por correo el enlace para restablecer tu contraseña!',
|
||||
'throttled' => 'Por favor espera antes de volver a intentar.',
|
||||
'token' => 'Este token de restablecimiento de contraseña es inválido.',
|
||||
'user' => "No podemos encontrar un usuario con esa dirección de correo electrónico.",
|
||||
|
||||
];
|
||||
163
api/resources/lang/es-mx/validation.php
Normal file
163
api/resources/lang/es-mx/validation.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?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' => 'El campo :attribute debe ser aceptado.',
|
||||
'accepted_if' => 'El campo :attribute debe ser aceptado cuando :other sea :value.',
|
||||
'active_url' => 'El campo :attribute no es una URL válida.',
|
||||
'after' => 'El campo :attribute debe ser una fecha posterior a :date.',
|
||||
'after_or_equal' => 'El campo :attribute debe ser una fecha posterior o igual a :date.',
|
||||
'alpha' => 'El campo :attribute solo debe contener letras.',
|
||||
'alpha_dash' => 'El campo :attribute solo debe contener letras, números, guiones y guiones bajos.',
|
||||
'alpha_num' => 'El campo :attribute solo debe contener letras y números.',
|
||||
'array' => 'El campo :attribute debe ser un arreglo.',
|
||||
'before' => 'El campo :attribute debe ser una fecha anterior a :date.',
|
||||
'before_or_equal' => 'El campo :attribute debe ser una fecha anterior o igual a :date.',
|
||||
'between' => [
|
||||
'numeric' => 'El campo :attribute debe estar entre :min y :max.',
|
||||
'file' => 'El campo :attribute debe estar entre :min y :max kilobytes.',
|
||||
'string' => 'El campo :attribute debe tener entre :min y :max caracteres.',
|
||||
'array' => 'El campo :attribute debe tener entre :min y :max elementos.',
|
||||
],
|
||||
'boolean' => 'El campo :attribute debe ser verdadero o falso.',
|
||||
'confirmed' => 'La confirmación de :attribute no coincide.',
|
||||
'current_password' => 'La contraseña es incorrecta.',
|
||||
'date' => 'El campo :attribute no es una fecha válida.',
|
||||
'date_equals' => 'El campo :attribute debe ser una fecha igual a :date.',
|
||||
'date_format' => 'El campo :attribute no coincide con el formato :format.',
|
||||
'declined' => 'El campo :attribute debe ser rechazado.',
|
||||
'declined_if' => 'El campo :attribute debe ser rechazado cuando :other sea :value.',
|
||||
'different' => 'El campo :attribute y :other deben ser diferentes.',
|
||||
'digits' => 'El campo :attribute debe tener :digits dígitos.',
|
||||
'digits_between' => 'El campo :attribute debe tener entre :min y :max dígitos.',
|
||||
'dimensions' => 'El campo :attribute tiene dimensiones de imagen inválidas.',
|
||||
'distinct' => 'El campo :attribute tiene un valor duplicado.',
|
||||
'email' => 'El campo :attribute debe ser una dirección de correo electrónico válida.',
|
||||
'ends_with' => 'El campo :attribute debe terminar con uno de los siguientes: :values.',
|
||||
'enum' => 'El :attribute seleccionado es inválido.',
|
||||
'exists' => 'El :attribute seleccionado es inválido.',
|
||||
'file' => 'El campo :attribute debe ser un archivo.',
|
||||
'filled' => 'El campo :attribute debe tener un valor.',
|
||||
'gt' => [
|
||||
'numeric' => 'El campo :attribute debe ser mayor que :value.',
|
||||
'file' => 'El campo :attribute debe ser mayor que :value kilobytes.',
|
||||
'string' => 'El campo :attribute debe ser mayor que :value caracteres.',
|
||||
'array' => 'El campo :attribute debe tener más de :value elementos.',
|
||||
],
|
||||
'gte' => [
|
||||
'numeric' => 'El campo :attribute debe ser mayor o igual a :value.',
|
||||
'file' => 'El campo :attribute debe ser mayor o igual a :value kilobytes.',
|
||||
'string' => 'El campo :attribute debe ser mayor o igual a :value caracteres.',
|
||||
'array' => 'El campo :attribute debe tener :value elementos o más.',
|
||||
],
|
||||
'image' => 'El campo :attribute debe ser una imagen.',
|
||||
'in' => 'El :attribute seleccionado es inválido.',
|
||||
'in_array' => 'El campo :attribute no existe en :other.',
|
||||
'integer' => 'El campo :attribute debe ser un número entero.',
|
||||
'ip' => 'El campo :attribute debe ser una dirección IP válida.',
|
||||
'ipv4' => 'El campo :attribute debe ser una dirección IPv4 válida.',
|
||||
'ipv6' => 'El campo :attribute debe ser una dirección IPv6 válida.',
|
||||
'json' => 'El campo :attribute debe ser una cadena JSON válida.',
|
||||
'lt' => [
|
||||
'numeric' => 'El campo :attribute debe ser menor que :value.',
|
||||
'file' => 'El campo :attribute debe ser menor que :value kilobytes.',
|
||||
'string' => 'El campo :attribute debe ser menor que :value caracteres.',
|
||||
'array' => 'El campo :attribute debe tener menos de :value elementos.',
|
||||
],
|
||||
'lte' => [
|
||||
'numeric' => 'El campo :attribute debe ser menor o igual a :value.',
|
||||
'file' => 'El campo :attribute debe ser menor o igual a :value kilobytes.',
|
||||
'string' => 'El campo :attribute debe ser menor o igual a :value caracteres.',
|
||||
'array' => 'El campo :attribute no debe tener más de :value elementos.',
|
||||
],
|
||||
'mac_address' => 'El campo :attribute debe ser una dirección MAC válida.',
|
||||
'max' => [
|
||||
'numeric' => 'El campo :attribute no debe ser mayor que :max.',
|
||||
'file' => 'El campo :attribute no debe ser mayor que :max kilobytes.',
|
||||
'string' => 'El campo :attribute no debe ser mayor que :max caracteres.',
|
||||
'array' => 'El campo :attribute no debe tener más de :max elementos.',
|
||||
],
|
||||
'mimes' => 'El campo :attribute debe ser un archivo de tipo: :values.',
|
||||
'mimetypes' => 'El campo :attribute debe ser un archivo de tipo: :values.',
|
||||
'min' => [
|
||||
'numeric' => 'El campo :attribute debe ser al menos :min.',
|
||||
'file' => 'El campo :attribute debe ser al menos :min kilobytes.',
|
||||
'string' => 'El campo :attribute debe tener al menos :min caracteres.',
|
||||
'array' => 'El campo :attribute debe tener al menos :min elementos.',
|
||||
],
|
||||
'multiple_of' => 'El campo :attribute debe ser un múltiplo de :value.',
|
||||
'not_in' => 'El :attribute seleccionado es inválido.',
|
||||
'not_regex' => 'El formato del campo :attribute es inválido.',
|
||||
'numeric' => 'El campo :attribute debe ser un número.',
|
||||
'password' => 'La contraseña es incorrecta.',
|
||||
'present' => 'El campo :attribute debe estar presente.',
|
||||
'prohibited' => 'El campo :attribute está prohibido.',
|
||||
'prohibited_if' => 'El campo :attribute está prohibido cuando :other sea :value.',
|
||||
'prohibited_unless' => 'El campo :attribute está prohibido a menos que :other esté en :values.',
|
||||
'prohibits' => 'El campo :attribute prohíbe que :other esté presente.',
|
||||
'regex' => 'El formato del campo :attribute es inválido.',
|
||||
'required' => 'El campo :attribute es obligatorio.',
|
||||
'required_array_keys' => 'El campo :attribute debe contener entradas para: :values.',
|
||||
'required_if' => 'El campo :attribute es obligatorio cuando :other sea :value.',
|
||||
'required_unless' => 'El campo :attribute es obligatorio a menos que :other esté en :values.',
|
||||
'required_with' => 'El campo :attribute es obligatorio cuando :values está presente.',
|
||||
'required_with_all' => 'El campo :attribute es obligatorio cuando :values están presentes.',
|
||||
'required_without' => 'El campo :attribute es obligatorio cuando :values no está presente.',
|
||||
'required_without_all' => 'El campo :attribute es obligatorio cuando ninguno de :values están presentes.',
|
||||
'same' => 'El campo :attribute y :other deben coincidir.',
|
||||
'size' => [
|
||||
'numeric' => 'El campo :attribute debe ser :size.',
|
||||
'file' => 'El campo :attribute debe ser :size kilobytes.',
|
||||
'string' => 'El campo :attribute debe tener :size caracteres.',
|
||||
'array' => 'El campo :attribute debe contener :size elementos.',
|
||||
],
|
||||
'starts_with' => 'El campo :attribute debe comenzar con uno de los siguientes: :values.',
|
||||
'string' => 'El campo :attribute debe ser una cadena de texto.',
|
||||
'timezone' => 'El campo :attribute debe ser una zona horaria válida.',
|
||||
'unique' => 'El campo :attribute ya ha sido tomado.',
|
||||
'uploaded' => 'El campo :attribute falló al subir.',
|
||||
'url' => 'El campo :attribute debe ser una URL válida.',
|
||||
'uuid' => 'El campo :attribute debe ser un UUID válido.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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-message',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => [],
|
||||
|
||||
];
|
||||
15
api/resources/lang/fa/auth.php
Normal file
15
api/resources/lang/fa/auth.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| خطوط زبان احراز هویت
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| خطوط زبان زیر در طول احراز هویت برای پیامهای مختلفی که باید به کاربر نمایش دهیم استفاده میشوند.
|
||||
| شما میتوانید این خطوط زبان را بر اساس نیازهای برنامه خود تغییر دهید.
|
||||
|
|
||||
*/
|
||||
'failed' => 'این اطلاعات ورود با سوابق ما مطابقت ندارد.',
|
||||
'password' => 'رمز عبور ارائهشده نادرست است.',
|
||||
'throttle' => 'تعداد تلاشهای ورود بیش از حد زیاد است. لطفاً پس از :seconds ثانیه دوباره تلاش کنید.',
|
||||
];
|
||||
18
api/resources/lang/fa/pagination.php
Normal file
18
api/resources/lang/fa/pagination.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| خطوط زبان صفحهبندی
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| خطوط زبان زیر توسط کتابخانه صفحهبندی برای ساخت لینکهای صفحهبندی ساده استفاده میشوند.
|
||||
| شما میتوانید این خطوط را به دلخواه تغییر دهید تا با نیازهای برنامه خود سازگار شوند.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« قبلی',
|
||||
'next' => 'بعدی »',
|
||||
|
||||
];
|
||||
22
api/resources/lang/fa/passwords.php
Normal file
22
api/resources/lang/fa/passwords.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| خطوط زبان بازنشانی رمز عبور
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| خطوط زبان زیر خطوط پیشفرضی هستند که با دلایلی که توسط کارگزار رمز عبور
|
||||
| برای تلاشهای ناموفق بهروزرسانی رمز عبور ارائه میشوند، مطابقت دارند،
|
||||
| مانند توکن نامعتبر یا رمز عبور جدید نامعتبر.
|
||||
|
|
||||
*/
|
||||
|
||||
'reset' => 'رمز عبور شما بازنشانی شد!',
|
||||
'sent' => 'لینک بازنشانی رمز عبور به ایمیل شما ارسال شد!',
|
||||
'throttled' => 'لطفاً قبل از تلاش مجدد صبر کنید.',
|
||||
'token' => 'این توکن بازنشانی رمز عبور نامعتبر است.',
|
||||
'user' => 'کاربری با این آدرس ایمیل یافت نشد.',
|
||||
|
||||
];
|
||||
163
api/resources/lang/fa/validation.php
Normal file
163
api/resources/lang/fa/validation.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| خطوط زبان اعتبارسنجی
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| خطوط زبان زیر شامل پیامهای خطای پیشفرض استفادهشده توسط کلاس اعتبارسنجی هستند.
|
||||
| برخی از این قوانین نسخههای متعددی دارند، مانند قوانین مربوط به اندازه.
|
||||
| شما میتوانید این پیامها را در اینجا به دلخواه تنظیم کنید.
|
||||
|
|
||||
*/
|
||||
|
||||
'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 باید true یا false باشد.',
|
||||
'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 معتبر باشد.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| خطوط زبان اعتبارسنجی سفارشی
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| در اینجا میتوانید پیامهای اعتبارسنجی سفارشی برای ویژگیها را با استفاده از
|
||||
| قرارداد "attribute.rule" برای نامگذاری خطوط مشخص کنید. این کار امکان
|
||||
| تعیین سریع یک خط زبان سفارشی برای یک قانون خاص ویژگی را فراهم میکند.
|
||||
|
|
||||
*/
|
||||
|
||||
'custom' => [
|
||||
'attribute-name' => [
|
||||
'rule-name' => 'پیام سفارشی',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| ویژگیهای اعتبارسنجی سفارشی
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| خطوط زبان زیر برای جایگزینی placeholder ویژگیهای ما با چیزی کاربرپسندتر
|
||||
| مانند "آدرس ایمیل" به جای "email" استفاده میشوند. این کار به ما کمک میکند
|
||||
| پیامهایمان را گویاتر کنیم.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [],
|
||||
|
||||
];
|
||||
@@ -1,4 +1,5 @@
|
||||
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
|
||||
# FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
|
||||
FROM --platform=linux/amd64 docker.io/dunglas/frankenphp:static-builder@sha256:821526b776a26502735d83890cc0a0d579348c510ba6c777df0762cb1c50d967
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ export CMAKE_OSX_ARCHITECTURES=arm64
|
||||
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"
|
||||
git clone --depth 1 --branch 2.5.2 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
|
||||
|
||||
4
console/.gitignore
vendored
4
console/.gitignore
vendored
@@ -26,3 +26,7 @@
|
||||
|
||||
# broccoli-debug
|
||||
/DEBUG/
|
||||
|
||||
# Auto-generated extension files
|
||||
/app/extensions/
|
||||
/app/utils/extension-loaders.js
|
||||
|
||||
@@ -2,9 +2,7 @@ import Application from '@ember/application';
|
||||
import Resolver from 'ember-resolver';
|
||||
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';
|
||||
import './deprecation-workflow';
|
||||
|
||||
export default class App extends Application {
|
||||
modulePrefix = config.modulePrefix;
|
||||
@@ -12,20 +10,6 @@ export default class App extends Application {
|
||||
Resolver = Resolver;
|
||||
extensions = [];
|
||||
engines = {};
|
||||
|
||||
async ready() {
|
||||
const extensions = await loadExtensions();
|
||||
|
||||
this.extensions = extensions;
|
||||
this.engines = mapEngines(extensions);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadRuntimeConfig();
|
||||
loadInitializers(App, config.modulePrefix);
|
||||
|
||||
let fleetbase = App.create();
|
||||
fleetbase.deferReadiness();
|
||||
fleetbase.boot();
|
||||
});
|
||||
loadInitializers(App, config.modulePrefix);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ContentPanel @title="Filesystem" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="Filesystem" @open={{true}} @wrapperClass="bordered-classic">
|
||||
<InputGroup @name="Driver" @helpText="Select the default filesystem driver for Fleetbase to use.">
|
||||
<Select @options={{this.disks}} @value={{this.driver}} @onSelect={{this.setDriver}} @placeholder="Select filesystem driver" class="w-full" disabled={{this.isLoading}} />
|
||||
</InputGroup>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ContentPanel @title="Mail" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="Mail" @open={{true}} @wrapperClass="bordered-classic">
|
||||
<InputGroup @name="Mailer" @helpText="Select the default mailer for Fleetbase to use.">
|
||||
<Select @options={{this.mailers}} @value={{this.mailer}} @onSelect={{this.setMailer}} @placeholder="Select mailer" class="w-full" />
|
||||
</InputGroup>
|
||||
@@ -13,6 +13,14 @@
|
||||
<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}}
|
||||
{{#if (eq this.mailer "microsoft-graph")}}
|
||||
<InputGroup @name="Client ID" @value={{this.microsoftGraphClient_id}} disabled={{this.loadConfigValues.isRunning}} />
|
||||
<InputGroup @name="Client Secret" @value={{this.microsoftGraphClient_secret}} disabled={{this.loadConfigValues.isRunning}} />
|
||||
<InputGroup @name="Tenant ID" @value={{this.microsoftGraphTenant_id}} disabled={{this.loadConfigValues.isRunning}} />
|
||||
<InputGroup>
|
||||
<Toggle @isToggled={{this.microsoftGraphSave_to_sent_items}} @onToggle={{fn (mut this.microsoftGraphSave_to_sent_items)}} @label="Save to sent items" />
|
||||
</InputGroup>
|
||||
{{/if}}
|
||||
{{#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}} />
|
||||
|
||||
@@ -26,6 +26,10 @@ export default class ConfigureMailComponent extends Component {
|
||||
@tracked postmarkToken = null;
|
||||
@tracked sendgridApi_key = null;
|
||||
@tracked resendKey = null;
|
||||
@tracked microsoftGraphClient_id = null;
|
||||
@tracked microsoftGraphClient_secret = null;
|
||||
@tracked microsoftGraphTenant_id = null;
|
||||
@tracked microsoftGraphSave_to_sent_items = false;
|
||||
|
||||
/**
|
||||
* Creates an instance of ConfigureFilesystemComponent.
|
||||
@@ -64,6 +68,19 @@ export default class ConfigureMailComponent extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
@action serializeMicrosoftGraphConfig() {
|
||||
return {
|
||||
client_id: this.microsoftGraphClient_id,
|
||||
client_secret: this.microsoftGraphClient_secret,
|
||||
tenant_id: this.microsoftGraphTenant_id,
|
||||
save_to_sent_items: this.microsoftGraphSave_to_sent_items,
|
||||
from: {
|
||||
address: this.fromAddress,
|
||||
name: this.fromName,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@action serializeMailgunConfig() {
|
||||
return {
|
||||
domain: this.mailgunDomain,
|
||||
@@ -112,6 +129,7 @@ export default class ConfigureMailComponent extends Component {
|
||||
postmark: this.serializePostmarkConfig(),
|
||||
sendgrid: this.serializeSendgridConfig(),
|
||||
resend: this.serializeResendConfig(),
|
||||
microsoftGraph: this.serializeMicrosoftGraphConfig(),
|
||||
});
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
@@ -131,6 +149,7 @@ export default class ConfigureMailComponent extends Component {
|
||||
postmark: this.serializePostmarkConfig(),
|
||||
sendgrid: this.serializeSendgridConfig(),
|
||||
resend: this.serializeResendConfig(),
|
||||
microsoftGraph: this.serializeMicrosoftGraphConfig(),
|
||||
});
|
||||
this.notifications.success('Mail configuration saved.');
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ContentPanel @title="APN Configutation" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="APN Configutation" @open={{true}} @wrapperClass="bordered-classic">
|
||||
<InputGroup @name="APN Key ID" @value={{this.apn.key_id}} disabled={{this.isLoading}} />
|
||||
<InputGroup @name="APN Team ID" @value={{this.apn.team_id}} disabled={{this.isLoading}} />
|
||||
<InputGroup @name="APN App Bundle ID" @value={{this.apn.app_bundle_id}} disabled={{this.isLoading}} />
|
||||
@@ -20,7 +20,7 @@
|
||||
</InputGroup>
|
||||
</ContentPanel>
|
||||
|
||||
<ContentPanel @title="Firebase Configutation" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="Firebase Configutation" @open={{true}} @wrapperClass="bordered-classic">
|
||||
<InputGroup @wrapperClass="flex flex-row items-center mb-0i">
|
||||
<UploadButton @name="firebase-service-account" @accept="text/plain,text/javascript,application/json" @onFileAdded={{this.uploadFirebaseCredentials}} @buttonText="Upload Service Account JSON" @icon="upload" class="w-auto m-0i mt-0i" />
|
||||
{{#if this.firebase.credentials_file}}
|
||||
@@ -33,7 +33,7 @@
|
||||
</InputGroup>
|
||||
</ContentPanel>
|
||||
|
||||
<ContentPanel @title="Test Push Notification" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-900">
|
||||
<ContentPanel @title="Test Push Notification" @open={{true}} @wrapperClass="bordered-classic">
|
||||
{{#if this.testResponse}}
|
||||
<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'}}" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ContentPanel @title="Queue" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="Queue" @open={{true}} @wrapperClass="bordered-classic">
|
||||
<InputGroup @name="Driver" @helpText="Select the default queue driver for Fleetbase to use.">
|
||||
<Select @options={{this.connections}} @value={{this.driver}} @onSelect={{this.setDriver}} @placeholder="Select queue driver" disabled={{this.isLoading}} class="w-full" />
|
||||
</InputGroup>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<ContentPanel @title="AWS" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="AWS" @open={{true}} @wrapperClass="bordered-classic">
|
||||
<InputGroup @name="AWS Access Key" @value={{this.awsKey}} disabled={{this.isLoading}} />
|
||||
<InputGroup @name="AWS Access Secret" @value={{this.awsSecret}} disabled={{this.isLoading}} />
|
||||
<InputGroup @name="AWS Region" @value={{this.awsRegion}} disabled={{this.isLoading}} />
|
||||
</ContentPanel>
|
||||
|
||||
<ContentPanel @title="Google Maps" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="Google Maps" @open={{true}} @wrapperClass="bordered-classic">
|
||||
<InputGroup @name="Google Maps API Key" @value={{this.googleMapsApiKey}} disabled={{this.isLoading}} />
|
||||
<InputGroup @name="Google Maps Locale" @value={{this.googleMapsLocale}} disabled={{this.isLoading}} />
|
||||
</ContentPanel>
|
||||
|
||||
<ContentPanel @title="Twilio" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="Twilio" @open={{true}} @wrapperClass="bordered-classic">
|
||||
<InputGroup @name="Twilio SID" @value={{this.twilioSid}} disabled={{this.isLoading}} />
|
||||
<InputGroup @name="Twilio Token" @value={{this.twilioToken}} disabled={{this.isLoading}} />
|
||||
<InputGroup @name="Twilio From" @value={{this.twilioFrom}} disabled={{this.isLoading}} />
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
</ContentPanel>
|
||||
|
||||
<ContentPanel @title="Sentry" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="Sentry" @open={{true}} @wrapperClass="bordered-classic">
|
||||
<InputGroup @name="Sentry DSN" @value={{this.sentryDsn}} disabled={{this.isLoading}} />
|
||||
{{#if this.sentryTestResponse}}
|
||||
<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">
|
||||
@@ -36,7 +36,7 @@
|
||||
<Button @wrapperClass="mt-3" @icon="plug" @text="Test Sentry Config" @onClick={{perform this.testSentry}} @isLoading={{this.testSentry.isRunning}} @disabled={{not this.sentryDsn}} />
|
||||
</ContentPanel>
|
||||
|
||||
<ContentPanel @title="IP Info" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="IP Info" @open={{true}} @wrapperClass="bordered-classic">
|
||||
<InputGroup @name="IP Info API Key" @value={{this.ipinfoApiKey}} disabled={{this.isLoading}} />
|
||||
</ContentPanel>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ContentPanel @title="SocketCluster Connection" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="SocketCluster Connection" @open={{true}} @wrapperClass="bordered-classic">
|
||||
<p class="mb-4">The SocketCluster configuration cannot be changed at this time.</p>
|
||||
<div id="output" class="font-mono rounded-lg max-h-full px-6 py-4 overflow-y-scroll bg-black shadow-inner dark:shadow-none">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<Overlay @isOpen={{@isOpen}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width={{or this.width @width "400px"}}>
|
||||
<Overlay::Header @title={{t "component.dashboard-widget-panel.select-widgets"}} @hideStatusDot={{true}} @titleWrapperClass="leading-5">
|
||||
<div class="flex flex-1 justify-end">
|
||||
<Button @type="default" @icon="times" @helpText={{t "component.dashboard-widget-panel.close-and-save"}} @onClick={{this.onPressClose}} />
|
||||
</div>
|
||||
</Overlay::Header>
|
||||
|
||||
<Overlay::Body @wrapperClass="new-service-rate-overlay-body px-4 space-y-4 pt-4">
|
||||
<div class="grid grid-cols-1 gap-4 text-xs dark:text-gray-100">
|
||||
{{#each this.availableWidgets as |widget|}}
|
||||
<div
|
||||
class="rounded-lg border border-gray-300 bg-white dark:border-gray-700 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-300 ease-in-out shadow-md px-4 py-2 cursor-pointer"
|
||||
{{on "click" (fn this.addWidgetToDashboard widget)}}
|
||||
>
|
||||
<div class="flex flex-row items-center leading-6 mb-2.5">
|
||||
<div class="w-8 flex items-center justify-start">
|
||||
<FaIcon @icon={{widget.icon}} class="text-lg text-gray-600 dark:text-gray-300" />
|
||||
</div>
|
||||
<p class="text-sm truncate font-semibold dark:text-gray-100 text-gray-800">
|
||||
{{t "component.dashboard-widget-panel.widget-name" widgetName=widget.name}}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs dark:text-gray-100 text-gray-800">{{widget.description}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Overlay::Body>
|
||||
</Overlay>
|
||||
@@ -1,60 +0,0 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class DashboardWidgetPanelComponent extends Component {
|
||||
@service universe;
|
||||
@tracked availableWidgets = [];
|
||||
@tracked dashboard;
|
||||
@tracked isOpen = true;
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Constructs the component and applies initial state.
|
||||
*/
|
||||
constructor(owner, { dashboard }) {
|
||||
super(...arguments);
|
||||
|
||||
this.availableWidgets = this.universe.getDashboardWidgets();
|
||||
this.dashboard = dashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the overlay context.
|
||||
*
|
||||
* @action
|
||||
* @param {OverlayContextObject} overlayContext
|
||||
*/
|
||||
@action setOverlayContext(overlayContext) {
|
||||
this.context = overlayContext;
|
||||
|
||||
if (typeof this.args.onLoad === 'function') {
|
||||
this.args.onLoad(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
@action addWidgetToDashboard(widget) {
|
||||
// If widget is a component definition/class
|
||||
if (typeof widget.component === 'function') {
|
||||
widget.component = widget.component.name;
|
||||
}
|
||||
|
||||
this.args.dashboard.addWidget(widget).catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles cancel button press.
|
||||
*
|
||||
* @action
|
||||
*/
|
||||
@action onPressClose() {
|
||||
this.isOpen = false;
|
||||
|
||||
if (typeof this.args.onClose === 'function') {
|
||||
this.args.onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="px-4 py-2.5">
|
||||
{{#if this.isLoading}}
|
||||
{{#if this.loadBlogPosts.isRunning}}
|
||||
<Spinner />
|
||||
{{else}}
|
||||
<ul class="space-y-2">
|
||||
|
||||
@@ -1,28 +1,42 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
import { storageFor } from 'ember-local-storage';
|
||||
import { add, isPast } from 'date-fns';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class FleetbaseBlogComponent extends Component {
|
||||
@storageFor('local-cache') localCache;
|
||||
@service fetch;
|
||||
@tracked posts = [];
|
||||
@tracked isLoading = false;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.loadBlogPosts();
|
||||
this.loadBlogPosts.perform();
|
||||
}
|
||||
|
||||
@action loadBlogPosts() {
|
||||
this.isLoading = true;
|
||||
@task *loadBlogPosts() {
|
||||
// Check if cached data and expiration are available
|
||||
const cachedData = this.localCache.get('fleetbase-blog-data');
|
||||
const expiration = this.localCache.get('fleetbase-blog-data-expiration');
|
||||
|
||||
return this.fetch
|
||||
.get('lookup/fleetbase-blog')
|
||||
.then((response) => {
|
||||
this.posts = response;
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
// Check if the cached data is still valid
|
||||
if (cachedData && isArray(cachedData) && expiration && !isPast(new Date(expiration))) {
|
||||
// Use cached data
|
||||
this.posts = cachedData;
|
||||
} else {
|
||||
// Fetch new data
|
||||
try {
|
||||
const data = yield this.fetch.get('lookup/fleetbase-blog');
|
||||
this.posts = isArray(data) ? data : [];
|
||||
if (data) {
|
||||
this.localCache.set('fleetbase-blog-data', data);
|
||||
this.localCache.set('fleetbase-blog-data-expiration', add(new Date(), { hours: 6 }));
|
||||
}
|
||||
} catch (err) {
|
||||
debug('Failed to load blog: ' + err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export default class GithubCardComponent extends Component {
|
||||
this.data = cachedData;
|
||||
} else {
|
||||
// Fetch new data
|
||||
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase');
|
||||
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase', { cache: 'default' });
|
||||
if (response.ok) {
|
||||
this.data = yield response.json();
|
||||
this.localCache.set('fleetbase-github-data', this.data);
|
||||
@@ -72,7 +72,7 @@ export default class GithubCardComponent extends Component {
|
||||
this.tags = cachedTags;
|
||||
} else {
|
||||
// Fetch new tags
|
||||
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase/tags');
|
||||
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase/tags', { cache: 'default' });
|
||||
if (response.ok) {
|
||||
this.tags = yield response.json();
|
||||
this.localCache.set('fleetbase-github-tags', this.tags);
|
||||
|
||||
61
console/app/components/onboarding/form.hbs
Normal file
61
console/app/components/onboarding/form.hbs
Normal file
@@ -0,0 +1,61 @@
|
||||
<div class="flex items-center justify-center h-screen min-h-screen px-4 py-12 bg-gray-50 dark:bg-gray-900 sm:px-6 lg:px-8 overflow-y-scroll">
|
||||
<div class="w-full max-w-md h-screen flex items-center justify-center py-4">
|
||||
<div class="bg-white dark:bg-gray-800 py-5 px-4 shadow rounded-lg w-full">
|
||||
<div class="mb-4">
|
||||
<Image src={{@brand.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">
|
||||
<div>
|
||||
<FaIcon @icon="hand-spock" @size="lg" class="text-blue-900 mr-4" />
|
||||
</div>
|
||||
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
|
||||
{{t "onboard.index.welcome-title" htmlSafe=true companyName=(t "app.name")}}
|
||||
{{t "onboard.index.welcome-text"}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form {{on "submit" (perform this.onboard)}}>
|
||||
{{#if this.error}}
|
||||
<InfoBlock @icon="exclamation-triangle" @text={{this.error}} class="mb-6 px-3 py-2 bg-red-300 text-red-900" @textClass="text-red-900" />
|
||||
{{/if}}
|
||||
<InputGroup @name={{t "onboard.index.full-name"}} @value={{this.name}} @helpText={{t "onboard.index.full-name-help-text"}} @inputClass="input-lg" />
|
||||
<InputGroup @name={{t "onboard.index.your-email"}} @type="email" @value={{this.email}} @helpText={{t "onboard.index.your-email-help-text"}} @inputClass="input-lg" />
|
||||
<InputGroup @name={{t "onboard.index.phone"}} @helpText={{t "onboard.index.phone-help-text"}}>
|
||||
<PhoneInput @onInput={{fn (mut this.phone)}} class="form-input input-lg w-full" />
|
||||
</InputGroup>
|
||||
<InputGroup @name={{t "onboard.index.organization-name"}} @value={{this.organization_name}} @helpText={{t "onboard.index.organization-help-text"}} @inputClass="input-lg" />
|
||||
<InputGroup @name={{t "onboard.index.password"}} @value={{this.password}} @type="password" @helpText={{t "onboard.index.password-help-text"}} @inputClass="input-lg" />
|
||||
<InputGroup
|
||||
@name={{t "onboard.index.confirm-password"}}
|
||||
@value={{this.password_confirmation}}
|
||||
@type="password"
|
||||
@helpText={{t "onboard.index.confirm-password-help-text"}}
|
||||
@inputClass="input-lg"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-end mt-5">
|
||||
<Button
|
||||
@buttonType="submit"
|
||||
@icon="check"
|
||||
@iconPrefix="fas"
|
||||
@type="primary"
|
||||
@size="lg"
|
||||
@text={{t "onboard.index.continue-button-text"}}
|
||||
@isLoading={{this.onboard.isRunning}}
|
||||
@disabled={{not this.filled}}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<RegistryYield @registry="onboard" as |YieldedComponent ctx|>
|
||||
<YieldedComponent @context={{ctx}} />
|
||||
</RegistryYield>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
77
console/app/components/onboarding/form.js
Normal file
77
console/app/components/onboarding/form.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action, getProperties } from '@ember/object';
|
||||
import { isBlank } from '@ember/utils';
|
||||
import { task } from 'ember-concurrency';
|
||||
import OnboardValidations from '../../validations/onboard';
|
||||
import lookupValidator from 'ember-changeset-validations';
|
||||
import Changeset from 'ember-changeset';
|
||||
|
||||
export default class OnboardingFormComponent extends Component {
|
||||
@service fetch;
|
||||
@service session;
|
||||
@service router;
|
||||
@service notifications;
|
||||
@service urlSearchParams;
|
||||
@tracked name;
|
||||
@tracked email;
|
||||
@tracked phone;
|
||||
@tracked organization_name;
|
||||
@tracked password;
|
||||
@tracked password_confirmation;
|
||||
@tracked error;
|
||||
|
||||
get filled() {
|
||||
// eslint-disable-next-line ember/no-get
|
||||
const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation');
|
||||
return Object.values(input).every((val) => !isBlank(val));
|
||||
}
|
||||
|
||||
@task *onboard(event) {
|
||||
event?.preventDefault?.();
|
||||
|
||||
// eslint-disable-next-line ember/no-get
|
||||
const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation');
|
||||
const changeset = new Changeset(input, lookupValidator(OnboardValidations), OnboardValidations);
|
||||
|
||||
yield changeset.validate();
|
||||
|
||||
if (changeset.get('isInvalid')) {
|
||||
const errorMessage = changeset.errors.firstObject.validation.firstObject;
|
||||
|
||||
this.notifications.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set user timezone
|
||||
input.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
try {
|
||||
const { status, skipVerification, token, session } = yield this.fetch.post('onboard/create-account', input);
|
||||
if (status !== 'success') {
|
||||
this.notifications.error('Onboard failed');
|
||||
return;
|
||||
}
|
||||
|
||||
// save session
|
||||
this.args.context.persist('session', session);
|
||||
|
||||
if (skipVerification === true && token) {
|
||||
// only manually authenticate if skip verification
|
||||
this.session.isOnboarding().manuallyAuthenticate(token);
|
||||
|
||||
yield this.router.transitionTo('console');
|
||||
return this.notifications.success('Welcome to Fleetbase!');
|
||||
} else {
|
||||
this.args.orchestrator.next();
|
||||
this.urlSearchParams.setParamsToCurrentUrl({
|
||||
step: this.args.orchestrator?.current?.id,
|
||||
session,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
this.notifications.serverError(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
console/app/components/onboarding/verify-email.hbs
Normal file
82
console/app/components/onboarding/verify-email.hbs
Normal file
@@ -0,0 +1,82 @@
|
||||
{{page-title (t "onboard.verify-email.header-title")}}
|
||||
|
||||
<div class="flex items-center justify-center h-screen min-h-screen px-4 py-12 bg-gray-50 dark:bg-gray-900 sm:px-6 lg:px-8 overflow-y-scroll">
|
||||
<div class="w-full max-w-md h-screen flex items-center justify-center py-4">
|
||||
{{#if this.initialized}}
|
||||
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
|
||||
<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>
|
||||
|
||||
<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" (perform this.verify)}}>
|
||||
<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.verification.validateInput}}
|
||||
{{did-insert this.verification.validateInput}}
|
||||
/>
|
||||
|
||||
<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.verify.isRunning}}
|
||||
@disabled={{not this.verification.ready}}
|
||||
/>
|
||||
<a href="#" {{on "click" this.verification.didntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "onboard.verify-email.didnt-receive-a-code"}}</a>
|
||||
</div>
|
||||
|
||||
{{#if this.verification.waiting}}
|
||||
<div class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
|
||||
<div class="flex flex-row items-start mb-2">
|
||||
<div class="w-8 flex-grow-0 flex-shrink-0">
|
||||
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex-1 text-sm text-yellow-100">
|
||||
<div>{{t "auth.verification.didnt-receive-a-code" htmlSafe=true}}</div>
|
||||
<div>{{t "auth.verification.not-sent.alternative-choice" htmlSafe=true}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Button
|
||||
@text={{t "auth.verification.not-sent.resend-email"}}
|
||||
@buttonType="button"
|
||||
@type="link"
|
||||
class="text-yellow-100"
|
||||
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
|
||||
@onClick={{this.verification.resendEmail}}
|
||||
/>
|
||||
<Button
|
||||
@text={{t "auth.verification.not-sent.send-by-sms"}}
|
||||
@buttonType="button"
|
||||
@type="link"
|
||||
class="text-yellow-100"
|
||||
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
|
||||
@onClick={{this.verification.resendBySms}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
53
console/app/components/onboarding/verify-email.js
Normal file
53
console/app/components/onboarding/verify-email.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { later, next } from '@ember/runloop';
|
||||
import { not } from '@ember/object/computed';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class OnboardingVerifyEmailComponent extends Component {
|
||||
@service('session') authSession;
|
||||
@service('user-verification') verification;
|
||||
@service fetch;
|
||||
@service notifications;
|
||||
@service router;
|
||||
@service urlSearchParams;
|
||||
@tracked code;
|
||||
@tracked session;
|
||||
@tracked initialized = false;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
next(() => this.#initialize());
|
||||
}
|
||||
|
||||
#initialize() {
|
||||
this.code = this.urlSearchParams.get('code');
|
||||
this.session = this.args.context.get('session') ?? this.urlSearchParams.get('session');
|
||||
this.initialized = true;
|
||||
this.verification.start();
|
||||
}
|
||||
|
||||
@task *verify(event) {
|
||||
event?.preventDefault?.();
|
||||
|
||||
try {
|
||||
const { status, token } = yield this.fetch.post('onboard/verify-email', { session: this.session, code: this.code });
|
||||
if (status === 'ok') {
|
||||
this.notifications.success('Email successfully verified!');
|
||||
|
||||
if (token) {
|
||||
this.notifications.info('Welcome to Fleetbase!');
|
||||
this.authSession.manuallyAuthenticate(token);
|
||||
|
||||
return this.router.transitionTo('console');
|
||||
}
|
||||
|
||||
return this.router.transitionTo('auth.login');
|
||||
}
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
console/app/components/onboarding/yield.hbs
Normal file
13
console/app/components/onboarding/yield.hbs
Normal file
@@ -0,0 +1,13 @@
|
||||
<section class="onboarding step-host">
|
||||
{{#if this.initialized}}
|
||||
{{#if this.orchestrator.wrapper}}
|
||||
{{component (lazy-engine-component this.orchestrator.wrapper) currentStepComponent=this.currentComponent context=this.context orchestrator=this.orchestrator brand=@brand}}
|
||||
{{else if this.currentComponent}}
|
||||
{{component (lazy-engine-component this.currentComponent) context=this.context orchestrator=this.orchestrator brand=@brand}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="flex items-center justify-center min-h-24">
|
||||
<Spinner />
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
27
console/app/components/onboarding/yield.js
Normal file
27
console/app/components/onboarding/yield.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { next } from '@ember/runloop';
|
||||
|
||||
export default class OnboardingYieldComponent extends Component {
|
||||
@service('onboarding-orchestrator') orchestrator;
|
||||
@service('onboarding-context') context;
|
||||
@tracked initialized = false;
|
||||
|
||||
get currentComponent() {
|
||||
return this.orchestrator.current && this.orchestrator.current.component;
|
||||
}
|
||||
|
||||
constructor(owner, { step, session, code }) {
|
||||
super(...arguments);
|
||||
next(() => this.#initialize(step, session, code));
|
||||
}
|
||||
|
||||
#initialize(step, session, code) {
|
||||
if (step) this.orchestrator.goto(step);
|
||||
if (session) this.context.persist('session', session);
|
||||
if (code) this.context.set('code', code);
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { inject as service } from '@ember/service';
|
||||
import { later } from '@ember/runloop';
|
||||
import { action } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
import { dasherize } from '@ember/string';
|
||||
import first from '@fleetbase/ember-core/utils/first';
|
||||
|
||||
export default class ConsoleController extends Controller {
|
||||
@@ -16,67 +17,19 @@ export default class ConsoleController extends Controller {
|
||||
@service intl;
|
||||
@service universe;
|
||||
@service abilities;
|
||||
|
||||
/**
|
||||
* Authenticated user organizations.
|
||||
*
|
||||
* @var {Array}
|
||||
*/
|
||||
@service sidebar;
|
||||
@tracked organizations = [];
|
||||
|
||||
/**
|
||||
* Sidebar Context Controls
|
||||
*
|
||||
* @var {SidebarContext}
|
||||
*/
|
||||
@tracked sidebarContext;
|
||||
|
||||
/**
|
||||
* State of sidebar toggle icon
|
||||
*
|
||||
* @var {SidebarContext}
|
||||
*/
|
||||
@tracked sidebarToggleEnabled = true;
|
||||
|
||||
/**
|
||||
* The sidebar toggle state.
|
||||
*
|
||||
* @var {SidebarContext}
|
||||
*/
|
||||
@tracked sidebarToggleState = {};
|
||||
|
||||
/**
|
||||
* Routes which should hide the sidebar menu.
|
||||
*
|
||||
* @var {Array}
|
||||
*/
|
||||
@tracked hiddenSidebarRoutes = ['console.home', 'console.notifications', 'console.virtual'];
|
||||
|
||||
/**
|
||||
* Menu items to be added to the main header navigation bar.
|
||||
*
|
||||
* @memberof ConsoleController
|
||||
*/
|
||||
@tracked menuItems = [];
|
||||
|
||||
/**
|
||||
* Menu items to be added to the user dropdown menu located in the header.
|
||||
*
|
||||
* @memberof ConsoleController
|
||||
*/
|
||||
@tracked userMenuItems = [];
|
||||
|
||||
/**
|
||||
* Menu items to be added to the organization dropdown menu located in the header.
|
||||
*
|
||||
* @memberof ConsoleController
|
||||
*/
|
||||
@tracked organizationMenuItems = [];
|
||||
|
||||
/**
|
||||
* Creates an instance of ConsoleController.
|
||||
* @memberof ConsoleController
|
||||
*/
|
||||
get currentRouteClass() {
|
||||
return dasherize(this.router.currentRouteName.replace(/\./g, ' '));
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.router.on('routeDidChange', (transition) => {
|
||||
@@ -89,17 +42,17 @@ export default class ConsoleController extends Controller {
|
||||
|
||||
// Hide the sidebar if the current route is in hiddenSidebarRoutes
|
||||
if (shouldHideSidebar) {
|
||||
this.sidebarContext.hideNow();
|
||||
this.sidebar.hideNow();
|
||||
this.sidebarToggleEnabled = false;
|
||||
return; // Exit early as no further action is required
|
||||
}
|
||||
|
||||
// If the sidebar was manually closed and not on a hidden route, keep it closed
|
||||
if (isSidebarManuallyClosed) {
|
||||
this.sidebarContext.hideNow();
|
||||
this.sidebar.hideNow();
|
||||
} else {
|
||||
// Otherwise, show the sidebar
|
||||
this.sidebarContext.show();
|
||||
this.sidebar.show();
|
||||
}
|
||||
|
||||
// Ensure toggle is enabled unless on a hidden route
|
||||
@@ -134,7 +87,7 @@ export default class ConsoleController extends Controller {
|
||||
this.universe.trigger('sidebarContext.available', sidebarContext);
|
||||
|
||||
if (this.hiddenSidebarRoutes.includes(this.router.currentRouteName)) {
|
||||
this.sidebarContext.hideNow();
|
||||
this.sidebar.hideNow();
|
||||
this.sidebarToggleEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@ import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleAccountController extends Controller {
|
||||
/**
|
||||
* Inject the `universe` service.
|
||||
*
|
||||
* @memberof ConsoleAdminController
|
||||
*/
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ export default class ConsoleAccountIndexController extends Controller {
|
||||
subject_uuid: this.user.id,
|
||||
subject_type: 'user',
|
||||
type: 'user_avatar',
|
||||
resize: 'md'
|
||||
},
|
||||
(uploadedFile) => {
|
||||
this.user.setProperties({
|
||||
|
||||
@@ -2,10 +2,6 @@ import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleAdminController extends Controller {
|
||||
/**
|
||||
* Inject the `universe` service.
|
||||
*
|
||||
* @memberof ConsoleAdminController
|
||||
*/
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@ import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleSettingsController extends Controller {
|
||||
/**
|
||||
* INject the `universe` service
|
||||
*
|
||||
* @memberof ConsoleSettingsController
|
||||
*/
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
}
|
||||
|
||||
@@ -6,35 +6,13 @@ import createNotificationKey from '@fleetbase/ember-core/utils/create-notificati
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class ConsoleSettingsNotificationsController extends Controller {
|
||||
/**
|
||||
* Inject the notifications service.
|
||||
*
|
||||
* @memberof ConsoleSettingsNotificationsController
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Inject the fetch service.
|
||||
*
|
||||
* @memberof ConsoleSettingsNotificationsController
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* The notification settings value JSON.
|
||||
*
|
||||
* @memberof ConsoleSettingsNotificationsController
|
||||
* @var {Object}
|
||||
*/
|
||||
@service store;
|
||||
@service currentUser;
|
||||
@tracked notificationSettings = {};
|
||||
|
||||
/**
|
||||
* Notification transport methods enabled.
|
||||
*
|
||||
* @memberof ConsoleSettingsNotificationsController
|
||||
* @var {Array}
|
||||
*/
|
||||
@tracked notificationTransportMethods = ['email', 'sms'];
|
||||
@tracked company;
|
||||
|
||||
/**
|
||||
* Creates an instance of ConsoleSettingsNotificationsController.
|
||||
@@ -45,6 +23,40 @@ export default class ConsoleSettingsNotificationsController extends Controller {
|
||||
this.getSettings.perform();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the "Alphanumeric Sender ID" feature for the current company.
|
||||
*
|
||||
* Updates the company's `options` object by setting the
|
||||
* `alpha_numeric_sender_id_enabled` flag. This controls whether the
|
||||
* organization uses a custom alphanumeric sender ID when sending SMS.
|
||||
*
|
||||
* @action
|
||||
* @param {boolean} enabled - Whether the feature should be enabled or disabled.
|
||||
* @returns {void}
|
||||
*/
|
||||
@action toggleAlphaNumericSenderId(enabled) {
|
||||
const currentOptions = this.company.options ?? {};
|
||||
this.company.set('options', { ...currentOptions, alpha_numeric_sender_id_enabled: enabled });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Alphanumeric Sender ID string for the current company.
|
||||
*
|
||||
* Reads the input's value from the event and updates the company's `options`
|
||||
* object by setting the `alpha_numeric_sender_id` field. This value represents
|
||||
* the sender name that will appear in outbound SMS messages (subject to carrier
|
||||
* support and restrictions).
|
||||
*
|
||||
* @action
|
||||
* @param {Event} event - Input event containing the alphanumeric sender ID value.
|
||||
* @returns {void}
|
||||
*/
|
||||
@action setAlphaNumericSenderId(event) {
|
||||
const value = event.target.value;
|
||||
const currentOptions = this.company.options ?? {};
|
||||
this.company.set('options', { ...currentOptions, alpha_numeric_sender_id: value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Selectes notifiables for settings.
|
||||
*
|
||||
@@ -94,7 +106,8 @@ export default class ConsoleSettingsNotificationsController extends Controller {
|
||||
const { notificationSettings } = this;
|
||||
|
||||
try {
|
||||
yield this.fetch.post('notifications/save-settings', { notificationSettings });
|
||||
yield this.fetch.post('notifications/save-settings', { notificationSettings: notificationSettings ?? {} });
|
||||
yield this.saveCompanyOptions.perform();
|
||||
this.notifications.success('Notification settings successfully saved.');
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
@@ -114,4 +127,26 @@ export default class ConsoleSettingsNotificationsController extends Controller {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the updated company options to the backend.
|
||||
*
|
||||
* This ember-concurrency task attempts to persist the company's modified
|
||||
* `options` object by calling `company.save()`. If the request fails, a server
|
||||
* error notification is displayed. No action is taken if no company is loaded.
|
||||
*
|
||||
* @task
|
||||
* @generator
|
||||
* @yields {Promise} Resolves when the save request completes.
|
||||
* @returns {Promise<void>} Task completion state.
|
||||
*/
|
||||
@task *saveCompanyOptions() {
|
||||
if (!this.company) return;
|
||||
|
||||
try {
|
||||
yield this.company.save();
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,151 +1,8 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action, getProperties } from '@ember/object';
|
||||
import OnboardValidations from '../../validations/onboard';
|
||||
import lookupValidator from 'ember-changeset-validations';
|
||||
import Changeset from 'ember-changeset';
|
||||
|
||||
export default class OnboardIndexController extends Controller {
|
||||
/**
|
||||
* Inject the `fetch` service
|
||||
*
|
||||
* @memberof OnboardIndexController
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* Inject the `session` service
|
||||
*
|
||||
* @memberof OnboardIndexController
|
||||
*/
|
||||
@service session;
|
||||
|
||||
/**
|
||||
* Inject the `router` service
|
||||
*
|
||||
* @memberof OnboardIndexController
|
||||
*/
|
||||
@service router;
|
||||
|
||||
/**
|
||||
* Inject the `notifications` service
|
||||
*
|
||||
* @memberof OnboardIndexController
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* The name input field.
|
||||
*
|
||||
* @memberof OnboardIndexController
|
||||
*/
|
||||
@tracked name;
|
||||
|
||||
/**
|
||||
* The email input field.
|
||||
*
|
||||
* @memberof OnboardIndexController
|
||||
*/
|
||||
@tracked email;
|
||||
|
||||
/**
|
||||
* The phone input field.
|
||||
*
|
||||
* @memberof OnboardIndexController
|
||||
*/
|
||||
@tracked phone;
|
||||
|
||||
/**
|
||||
* The organization_name input field.
|
||||
*
|
||||
* @memberof OnboardIndexController
|
||||
*/
|
||||
@tracked organization_name;
|
||||
|
||||
/**
|
||||
* The password input field.
|
||||
*
|
||||
* @memberof OnboardIndexController
|
||||
*/
|
||||
@tracked password;
|
||||
|
||||
/**
|
||||
* The name password confirmation field.
|
||||
*
|
||||
* @memberof OnboardIndexController
|
||||
*/
|
||||
@tracked password_confirmation;
|
||||
|
||||
/**
|
||||
* The property for error message.
|
||||
*
|
||||
* @memberof OnboardIndexController
|
||||
*/
|
||||
@tracked error;
|
||||
|
||||
/**
|
||||
* The loading state of the onboard request.
|
||||
*
|
||||
* @memberof OnboardIndexController
|
||||
*/
|
||||
@tracked isLoading = false;
|
||||
|
||||
/**
|
||||
* The ready state for the form.
|
||||
*
|
||||
* @memberof OnboardIndexController
|
||||
*/
|
||||
@tracked readyToSubmit = false;
|
||||
|
||||
/**
|
||||
* Start the onboard process.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @memberof OnboardIndexController
|
||||
*/
|
||||
@action async startOnboard(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// eslint-disable-next-line ember/no-get
|
||||
const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation');
|
||||
const changeset = new Changeset(input, lookupValidator(OnboardValidations), OnboardValidations);
|
||||
|
||||
await changeset.validate();
|
||||
|
||||
if (changeset.get('isInvalid')) {
|
||||
const errorMessage = changeset.errors.firstObject.validation.firstObject;
|
||||
|
||||
this.notifications.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set user timezone
|
||||
input.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
return this.fetch
|
||||
.post('onboard/create-account', input)
|
||||
.then(({ status, skipVerification, token, session }) => {
|
||||
if (status === 'success') {
|
||||
if (skipVerification === true && token) {
|
||||
// only manually authenticate if skip verification
|
||||
this.session.isOnboarding().manuallyAuthenticate(token);
|
||||
|
||||
return this.router.transitionTo('console').then(() => {
|
||||
this.notifications.success('Welcome to Fleetbase!');
|
||||
});
|
||||
}
|
||||
|
||||
return this.router.transitionTo('onboard.verify-email', { queryParams: { hello: session } });
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
@tracked step;
|
||||
@tracked session;
|
||||
@tracked code;
|
||||
}
|
||||
|
||||
7
console/app/controllers/virtual.js
Normal file
7
console/app/controllers/virtual.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class VirtualController extends Controller {
|
||||
@tracked view;
|
||||
queryParams = ['view'];
|
||||
}
|
||||
9
console/app/deprecation-workflow.js
Normal file
9
console/app/deprecation-workflow.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import setupDeprecationWorkflow from 'ember-cli-deprecation-workflow';
|
||||
|
||||
setupDeprecationWorkflow({
|
||||
workflow: [
|
||||
{ handler: 'silence', matchId: 'ember-concurrency.deprecate-decorator-task' },
|
||||
{ handler: 'silence', matchId: 'new-helper-names' },
|
||||
{ handler: 'silence', matchId: 'ember-data:deprecate-non-strict-relationships' },
|
||||
],
|
||||
});
|
||||
@@ -10,9 +10,11 @@
|
||||
|
||||
{{content-for "head"}}
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-chrome-192x192.png" />
|
||||
<link rel="icon" type="image/png" sizes="256x256" href="/favicon/android-chrome-256x256.png" />
|
||||
<link rel="manifest" href="/favicon/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css">
|
||||
|
||||
44
console/app/initializers/load-intl-polyfills.js
Normal file
44
console/app/initializers/load-intl-polyfills.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import translations from 'ember-intl/translations';
|
||||
import { all } from 'rsvp';
|
||||
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
function langOf(tag = 'en') {
|
||||
return String(tag).toLowerCase().split('-')[0];
|
||||
}
|
||||
|
||||
async function loadBasePolyfills() {
|
||||
await import('@formatjs/intl-numberformat/polyfill-force');
|
||||
await import('@formatjs/intl-pluralrules/polyfill-force');
|
||||
await import('@formatjs/intl-datetimeformat/polyfill-force');
|
||||
await import('@formatjs/intl-relativetimeformat/polyfill-force');
|
||||
}
|
||||
|
||||
async function loadLocaleData(lang) {
|
||||
return all([
|
||||
import(`@formatjs/intl-numberformat/locale-data/${lang}.js`),
|
||||
import(`@formatjs/intl-pluralrules/locale-data/${lang}.js`),
|
||||
import(`@formatjs/intl-datetimeformat/locale-data/${lang}.js`),
|
||||
import(`@formatjs/intl-relativetimeformat/locale-data/${lang}.js`),
|
||||
]);
|
||||
}
|
||||
|
||||
export function initialize(application) {
|
||||
if (!isBrowser) return;
|
||||
|
||||
// Build-time list of locales from the generated module
|
||||
const locales = translations.map(([locale]) => String(locale));
|
||||
const langs = [...new Set(locales.map(langOf))];
|
||||
|
||||
application.deferReadiness();
|
||||
(async () => {
|
||||
await loadBasePolyfills();
|
||||
await all(langs.map(loadLocaleData));
|
||||
application.advanceReadiness();
|
||||
})();
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'load-intl-polyfills',
|
||||
initialize,
|
||||
};
|
||||
41
console/app/initializers/load-runtime-config.js
Normal file
41
console/app/initializers/load-runtime-config.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import loadRuntimeConfig from '@fleetbase/console/utils/runtime-config';
|
||||
import { debug } from '@ember/debug';
|
||||
|
||||
/**
|
||||
* Load Runtime Config Initializer
|
||||
*
|
||||
* Loads runtime configuration from fleetbase.config.json before the application boots.
|
||||
* This must run first to ensure all config is available for other initializers.
|
||||
*
|
||||
* Uses `before` to ensure it runs before any other initializers.
|
||||
*
|
||||
* @export
|
||||
* @param {Application} application
|
||||
*/
|
||||
export function initialize(application) {
|
||||
const startTime = performance.now();
|
||||
debug('[Runtime Config] Loading runtime configuration...');
|
||||
|
||||
// Defer readiness until config is loaded
|
||||
application.deferReadiness();
|
||||
(async () => {
|
||||
try {
|
||||
await loadRuntimeConfig();
|
||||
const endTime = performance.now();
|
||||
debug(`[Runtime Config] Runtime config loaded in ${(endTime - startTime).toFixed(2)}ms`);
|
||||
application.advanceReadiness();
|
||||
} catch (error) {
|
||||
console.error('[Runtime Config] Failed to load runtime config:', error);
|
||||
// Still advance readiness to prevent hanging
|
||||
application.advanceReadiness();
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'load-runtime-config',
|
||||
initialize,
|
||||
// Run after intl polyfills are loaded, before socketcluster
|
||||
after: 'load-intl-polyfills',
|
||||
before: 'load-socketcluster-client',
|
||||
};
|
||||
38
console/app/instance-initializers/apply-router-fix.js
Normal file
38
console/app/instance-initializers/apply-router-fix.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import applyRouterFix from '@fleetbase/console/utils/router-refresh-patch';
|
||||
import { debug } from '@ember/debug';
|
||||
|
||||
/**
|
||||
* Apply Router Fix Instance Initializer
|
||||
*
|
||||
* Applies the Fleetbase router refresh bug fix patch.
|
||||
* This patches the Ember router to handle dynamic segments correctly
|
||||
* when refreshing routes with query parameters.
|
||||
*
|
||||
* Runs as an instance-initializer because it needs access to the
|
||||
* application instance and router service.
|
||||
*
|
||||
* Bug: https://github.com/emberjs/ember.js/issues/19260
|
||||
*
|
||||
* @export
|
||||
* @param {ApplicationInstance} appInstance
|
||||
*/
|
||||
export function initialize(appInstance) {
|
||||
const startTime = performance.now();
|
||||
debug('[Initializing Router Patch] Applying router refresh bug fix...');
|
||||
|
||||
try {
|
||||
applyRouterFix(appInstance);
|
||||
|
||||
const endTime = performance.now();
|
||||
debug(`[Initializing Router Patch] Router fix applied in ${(endTime - startTime).toFixed(2)}ms`);
|
||||
} catch (error) {
|
||||
console.error('[Initializing Router Patch] Failed to apply router fix:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'apply-router-fix',
|
||||
initialize,
|
||||
// Run before extension loading to ensure router is patched early
|
||||
before: 'load-extensions',
|
||||
};
|
||||
20
console/app/instance-initializers/initialize-registries.js
Normal file
20
console/app/instance-initializers/initialize-registries.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { debug } from '@ember/debug';
|
||||
|
||||
/**
|
||||
* Create console-specific registries
|
||||
* Runs after extensions are loaded
|
||||
*/
|
||||
export function initialize(appInstance) {
|
||||
const registryService = appInstance.lookup('service:universe/registry-service');
|
||||
|
||||
debug('[Initializing Registries] Creating console registries...');
|
||||
|
||||
// Create console-specific registries
|
||||
registryService.createRegistries(['@fleetbase/console', 'auth:login']);
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'initialize-registries',
|
||||
after: 'load-extensions',
|
||||
initialize,
|
||||
};
|
||||
@@ -1,36 +1,47 @@
|
||||
import { Widget } from '@fleetbase/ember-core/contracts';
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
import { debug } from '@ember/debug';
|
||||
|
||||
export function initialize(application) {
|
||||
const universe = application.lookup('service:universe');
|
||||
const defaultWidgets = [
|
||||
{
|
||||
widgetId: 'fleetbase-blog',
|
||||
/**
|
||||
* Register dashboard and widgets for FleetbaseConsole
|
||||
* Runs after extensions are loaded
|
||||
*/
|
||||
export function initialize(appInstance) {
|
||||
const widgetService = appInstance.lookup('service:universe/widget-service');
|
||||
|
||||
debug('[Initializing Widgets] Registering console dashboard and widgets...');
|
||||
|
||||
// Register the console dashboard
|
||||
widgetService.registerDashboard('dashboard');
|
||||
|
||||
// Create widget definitions
|
||||
const widgets = [
|
||||
new Widget({
|
||||
id: 'fleetbase-blog',
|
||||
name: 'Fleetbase Blog',
|
||||
description: 'Lists latest news and events from the Fleetbase official team.',
|
||||
icon: 'newspaper',
|
||||
component: 'fleetbase-blog',
|
||||
grid_options: { w: 8, h: 9, minW: 8, minH: 9 },
|
||||
options: {
|
||||
title: 'Fleetbase Blog',
|
||||
},
|
||||
},
|
||||
{
|
||||
widgetId: 'fleetbase-github-card',
|
||||
default: true,
|
||||
}),
|
||||
new Widget({
|
||||
id: 'fleetbase-github-card',
|
||||
name: 'Github Card',
|
||||
description: 'Displays current Github stats from the official Fleetbase repo.',
|
||||
icon: faGithub,
|
||||
component: 'github-card',
|
||||
grid_options: { w: 4, h: 8, minW: 4, minH: 8 },
|
||||
options: {
|
||||
title: 'Github Card',
|
||||
},
|
||||
},
|
||||
default: true,
|
||||
}),
|
||||
];
|
||||
|
||||
universe.registerDefaultDashboardWidgets(defaultWidgets);
|
||||
universe.registerDashboardWidgets(defaultWidgets);
|
||||
// Register widgets
|
||||
widgetService.registerWidgets('dashboard', widgets);
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'initialize-widgets',
|
||||
after: 'load-extensions',
|
||||
initialize,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
export function initialize(application) {
|
||||
const universe = application.lookup('service:universe');
|
||||
if (universe) {
|
||||
universe.createRegistries(['@fleetbase/console', 'auth:login']);
|
||||
universe.bootEngines(application);
|
||||
/**
|
||||
* Load extensions from the API using ExtensionManager
|
||||
* This must run before other initializers that depend on extensions
|
||||
*/
|
||||
export async function initialize(appInstance) {
|
||||
const application = appInstance.application;
|
||||
const extensionManager = appInstance.lookup('service:universe/extension-manager');
|
||||
|
||||
try {
|
||||
await extensionManager.loadExtensions(application);
|
||||
} catch (error) {
|
||||
console.error('[load-extensions] Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'load-extensions',
|
||||
initialize,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export function initialize(owner) {
|
||||
const registry = owner.lookup('service:onboarding-registry');
|
||||
if (registry) {
|
||||
const defaultFlow = {
|
||||
id: 'default@v1',
|
||||
entry: 'signup',
|
||||
steps: [
|
||||
{ id: 'signup', component: 'onboarding/form', next: 'verify-email' },
|
||||
{ id: 'verify-email', component: 'onboarding/verify-email' },
|
||||
],
|
||||
};
|
||||
|
||||
registry.registerFlow(defaultFlow);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize,
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
export function initialize(appInstance) {
|
||||
// Look up UniverseService and set the application instance
|
||||
const universeService = appInstance.lookup('service:universe');
|
||||
if (universeService) {
|
||||
universeService.setApplicationInstance(appInstance);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize
|
||||
};
|
||||
16
console/app/instance-initializers/setup-extensions.js
Normal file
16
console/app/instance-initializers/setup-extensions.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Setup extensions by loading and executing their extension.js files
|
||||
* Runs after extensions are loaded from API
|
||||
*/
|
||||
export async function initialize(appInstance) {
|
||||
const universe = appInstance.lookup('service:universe');
|
||||
const extensionManager = appInstance.lookup('service:universe/extension-manager');
|
||||
|
||||
await extensionManager.setupExtensions(appInstance, universe);
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'setup-extensions',
|
||||
after: ['load-extensions', 'initialize-registries', 'initialize-widgets'],
|
||||
initialize,
|
||||
};
|
||||
20
console/app/models/activity.js
Normal file
20
console/app/models/activity.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
|
||||
export default class ActivityModel extends Model {
|
||||
@attr('string') uuid;
|
||||
@attr('string') log_name;
|
||||
@attr('string') description;
|
||||
@attr('string') company_id;
|
||||
@attr('string') subject_id;
|
||||
@attr('string') subject_type;
|
||||
@attr('string') humanized_subject_type;
|
||||
@attr('string') event;
|
||||
@attr('string') causer_id;
|
||||
@attr('string') causer_type;
|
||||
@attr('string') humanized_causer_type;
|
||||
@attr('object') properties;
|
||||
@attr('object') causer;
|
||||
@attr('object') subject;
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
}
|
||||
311
console/app/models/alert.js
Normal file
311
console/app/models/alert.js
Normal file
@@ -0,0 +1,311 @@
|
||||
import Model, { attr, belongsTo } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { format, formatDistanceToNow, differenceInMinutes } from 'date-fns';
|
||||
|
||||
export default class AlertModel extends Model {
|
||||
/** @attributes */
|
||||
@attr('string') type;
|
||||
@attr('string') severity;
|
||||
@attr('string') status;
|
||||
@attr('string') subject_type;
|
||||
@attr('string') subject_uuid;
|
||||
@attr('string') message;
|
||||
|
||||
/** @json attributes */
|
||||
@attr() rule;
|
||||
@attr() context;
|
||||
@attr() meta;
|
||||
|
||||
/** @dates */
|
||||
@attr('date') triggered_at;
|
||||
@attr('date') acknowledged_at;
|
||||
@attr('date') resolved_at;
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@attr('date') deleted_at;
|
||||
|
||||
/** @relationships */
|
||||
@belongsTo('company') company;
|
||||
@belongsTo('user', { inverse: null }) acknowledgedBy;
|
||||
@belongsTo('user', { inverse: null }) resolvedBy;
|
||||
|
||||
/** @computed - Date formatting */
|
||||
@computed('triggered_at') get triggeredAgo() {
|
||||
if (!this.triggered_at) return 'Unknown';
|
||||
return formatDistanceToNow(this.triggered_at) + ' ago';
|
||||
}
|
||||
|
||||
@computed('triggered_at') get triggeredAt() {
|
||||
if (!this.triggered_at) return 'Unknown';
|
||||
return format(this.triggered_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('acknowledged_at') get acknowledgedAgo() {
|
||||
if (!this.acknowledged_at) return null;
|
||||
return formatDistanceToNow(this.acknowledged_at) + ' ago';
|
||||
}
|
||||
|
||||
@computed('acknowledged_at') get acknowledgedAt() {
|
||||
if (!this.acknowledged_at) return 'Not acknowledged';
|
||||
return format(this.acknowledged_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('resolved_at') get resolvedAgo() {
|
||||
if (!this.resolved_at) return null;
|
||||
return formatDistanceToNow(this.resolved_at) + ' ago';
|
||||
}
|
||||
|
||||
@computed('resolved_at') get resolvedAt() {
|
||||
if (!this.resolved_at) return 'Not resolved';
|
||||
return format(this.resolved_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAgo() {
|
||||
return formatDistanceToNow(this.updated_at) + ' ago';
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAgo() {
|
||||
return formatDistanceToNow(this.created_at) + ' ago';
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
/** @computed - Status checks */
|
||||
@computed('acknowledged_at') get isAcknowledged() {
|
||||
return !!this.acknowledged_at;
|
||||
}
|
||||
|
||||
@computed('resolved_at') get isResolved() {
|
||||
return !!this.resolved_at;
|
||||
}
|
||||
|
||||
@computed('isAcknowledged', 'isResolved') get isPending() {
|
||||
return !this.isAcknowledged && !this.isResolved;
|
||||
}
|
||||
|
||||
@computed('isAcknowledged', 'isResolved') get isActive() {
|
||||
return this.isAcknowledged && !this.isResolved;
|
||||
}
|
||||
|
||||
/** @computed - Duration calculations */
|
||||
@computed('triggered_at', 'acknowledged_at') get acknowledgmentDurationMinutes() {
|
||||
if (!this.triggered_at || !this.acknowledged_at) return null;
|
||||
return differenceInMinutes(new Date(this.acknowledged_at), new Date(this.triggered_at));
|
||||
}
|
||||
|
||||
@computed('triggered_at', 'resolved_at') get resolutionDurationMinutes() {
|
||||
if (!this.triggered_at || !this.resolved_at) return null;
|
||||
return differenceInMinutes(new Date(this.resolved_at), new Date(this.triggered_at));
|
||||
}
|
||||
|
||||
@computed('triggered_at') get ageMinutes() {
|
||||
if (!this.triggered_at) return 0;
|
||||
return differenceInMinutes(new Date(), new Date(this.triggered_at));
|
||||
}
|
||||
|
||||
@computed('acknowledgmentDurationMinutes') get acknowledgmentDurationFormatted() {
|
||||
if (!this.acknowledgmentDurationMinutes) return null;
|
||||
|
||||
const minutes = this.acknowledgmentDurationMinutes;
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
if (minutes < 1440) return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
||||
return `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
|
||||
}
|
||||
|
||||
@computed('resolutionDurationMinutes') get resolutionDurationFormatted() {
|
||||
if (!this.resolutionDurationMinutes) return null;
|
||||
|
||||
const minutes = this.resolutionDurationMinutes;
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
if (minutes < 1440) return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
||||
return `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
|
||||
}
|
||||
|
||||
@computed('ageMinutes') get ageFormatted() {
|
||||
const minutes = this.ageMinutes;
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
if (minutes < 1440) return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
||||
return `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
|
||||
}
|
||||
|
||||
/** @computed - Severity styling */
|
||||
@computed('severity') get severityBadgeClass() {
|
||||
const severityClasses = {
|
||||
critical: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
|
||||
high: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300',
|
||||
medium: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
|
||||
low: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
|
||||
info: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
|
||||
};
|
||||
return severityClasses[this.severity] || severityClasses['info'];
|
||||
}
|
||||
|
||||
@computed('severity') get severityIcon() {
|
||||
const severityIcons = {
|
||||
critical: 'fas fa-exclamation-circle',
|
||||
high: 'fas fa-exclamation-triangle',
|
||||
medium: 'fas fa-exclamation',
|
||||
low: 'fas fa-info-circle',
|
||||
info: 'fas fa-info',
|
||||
};
|
||||
return severityIcons[this.severity] || severityIcons['info'];
|
||||
}
|
||||
|
||||
@computed('severity') get severityColor() {
|
||||
const severityColors = {
|
||||
critical: 'text-red-600 dark:text-red-400',
|
||||
high: 'text-orange-600 dark:text-orange-400',
|
||||
medium: 'text-yellow-600 dark:text-yellow-400',
|
||||
low: 'text-blue-600 dark:text-blue-400',
|
||||
info: 'text-gray-600 dark:text-gray-400',
|
||||
};
|
||||
return severityColors[this.severity] || severityColors['info'];
|
||||
}
|
||||
|
||||
/** @computed - Status styling */
|
||||
@computed('status', 'isAcknowledged', 'isResolved') get statusBadgeClass() {
|
||||
if (this.isResolved) {
|
||||
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';
|
||||
}
|
||||
if (this.isAcknowledged) {
|
||||
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';
|
||||
}
|
||||
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';
|
||||
}
|
||||
|
||||
@computed('status', 'isAcknowledged', 'isResolved') get statusText() {
|
||||
if (this.isResolved) return 'Resolved';
|
||||
if (this.isAcknowledged) return 'Acknowledged';
|
||||
return 'Pending';
|
||||
}
|
||||
|
||||
@computed('status', 'isAcknowledged', 'isResolved') get statusIcon() {
|
||||
if (this.isResolved) return 'fas fa-check-circle';
|
||||
if (this.isAcknowledged) return 'fas fa-eye';
|
||||
return 'fas fa-bell';
|
||||
}
|
||||
|
||||
/** @computed - Type styling */
|
||||
@computed('type') get typeIcon() {
|
||||
const typeIcons = {
|
||||
maintenance: 'fas fa-wrench',
|
||||
temperature: 'fas fa-thermometer-half',
|
||||
fuel: 'fas fa-gas-pump',
|
||||
speed: 'fas fa-tachometer-alt',
|
||||
location: 'fas fa-map-marker-alt',
|
||||
system: 'fas fa-cog',
|
||||
security: 'fas fa-shield-alt',
|
||||
performance: 'fas fa-chart-line',
|
||||
compliance: 'fas fa-clipboard-check',
|
||||
};
|
||||
return typeIcons[this.type] || 'fas fa-bell';
|
||||
}
|
||||
|
||||
@computed('type') get typeBadgeClass() {
|
||||
const typeClasses = {
|
||||
maintenance: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300',
|
||||
temperature: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
|
||||
fuel: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
|
||||
speed: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
|
||||
location: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
||||
system: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
|
||||
security: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300',
|
||||
performance: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300',
|
||||
compliance: 'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300',
|
||||
};
|
||||
return typeClasses[this.type] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
|
||||
}
|
||||
|
||||
/** @computed - Subject information */
|
||||
@computed('subject_type') get subjectTypeFormatted() {
|
||||
if (!this.subject_type) return 'Unknown';
|
||||
|
||||
// Convert from model class name to human readable
|
||||
const typeMap = {
|
||||
vehicle: 'Vehicle',
|
||||
driver: 'Driver',
|
||||
order: 'Order',
|
||||
device: 'Device',
|
||||
asset: 'Asset',
|
||||
maintenance: 'Maintenance',
|
||||
fuel_report: 'Fuel Report',
|
||||
};
|
||||
|
||||
return typeMap[this.subject_type] || this.subject_type.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
}
|
||||
|
||||
/** @computed - Priority and urgency */
|
||||
@computed('severity', 'ageMinutes') get urgencyLevel() {
|
||||
const severityWeight = {
|
||||
critical: 4,
|
||||
high: 3,
|
||||
medium: 2,
|
||||
low: 1,
|
||||
info: 0,
|
||||
};
|
||||
|
||||
const weight = severityWeight[this.severity] || 0;
|
||||
const ageHours = this.ageMinutes / 60;
|
||||
|
||||
// Calculate urgency based on severity and age
|
||||
if (weight >= 3 && ageHours > 1) return 'urgent';
|
||||
if (weight >= 2 && ageHours > 4) return 'urgent';
|
||||
if (weight >= 3) return 'high';
|
||||
if (weight >= 2) return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
@computed('urgencyLevel') get urgencyBadgeClass() {
|
||||
const urgencyClasses = {
|
||||
urgent: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300 animate-pulse',
|
||||
high: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300',
|
||||
medium: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
|
||||
low: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
||||
};
|
||||
return urgencyClasses[this.urgencyLevel] || urgencyClasses['low'];
|
||||
}
|
||||
|
||||
/** @computed - Context information */
|
||||
@computed('context') get hasContext() {
|
||||
return !!(this.context && Object.keys(this.context).length > 0);
|
||||
}
|
||||
|
||||
@computed('rule') get hasRule() {
|
||||
return !!(this.rule && Object.keys(this.rule).length > 0);
|
||||
}
|
||||
|
||||
@computed('context.location') get hasLocation() {
|
||||
return !!this.context?.location;
|
||||
}
|
||||
|
||||
@computed('context.value', 'rule.{operator,threshold}') get thresholdExceeded() {
|
||||
if (!this.context?.value || !this.rule?.threshold) return null;
|
||||
|
||||
const value = parseFloat(this.context.value);
|
||||
const threshold = parseFloat(this.rule.threshold);
|
||||
const operator = this.rule.operator || '>';
|
||||
|
||||
switch (operator) {
|
||||
case '>':
|
||||
return value > threshold;
|
||||
case '<':
|
||||
return value < threshold;
|
||||
case '>=':
|
||||
return value >= threshold;
|
||||
case '<=':
|
||||
return value <= threshold;
|
||||
case '==':
|
||||
return value === threshold;
|
||||
case '!=':
|
||||
return value !== threshold;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,10 @@ export default class CategoryModel extends Model {
|
||||
@hasMany('category', { inverse: 'parent' }) subcategories;
|
||||
@tracked parent_category;
|
||||
|
||||
/** Array<CustomFieldModel> attached at runtime for rendering */
|
||||
@tracked customFields = [];
|
||||
@tracked isEditing = false;
|
||||
|
||||
/** @attributes */
|
||||
@attr('string') owner_type;
|
||||
@attr('string') name;
|
||||
@@ -46,7 +50,7 @@ export default class CategoryModel extends Model {
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAtShort() {
|
||||
@@ -58,7 +62,7 @@ export default class CategoryModel extends Model {
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAtShort() {
|
||||
|
||||
@@ -31,7 +31,7 @@ export default class CommentModel extends Model {
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAgo() {
|
||||
@@ -39,6 +39,6 @@ export default class CommentModel extends Model {
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class Company extends Model {
|
||||
@attr('string') logo_url;
|
||||
@attr('string') backdrop_url;
|
||||
@attr('string') description;
|
||||
@attr('raw') options;
|
||||
@attr('object') options;
|
||||
@attr('number') users_count;
|
||||
@attr('string') type;
|
||||
@attr('string') currency;
|
||||
@@ -50,7 +50,7 @@ export default class Company extends Model {
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAtShort() {
|
||||
@@ -62,7 +62,7 @@ export default class Company extends Model {
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAtShort() {
|
||||
|
||||
@@ -41,7 +41,7 @@ export default class CustomFieldValueModel extends Model {
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAgo() {
|
||||
@@ -49,6 +49,6 @@ export default class CustomFieldValueModel extends Model {
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export default class CustomFieldModel extends Model {
|
||||
/** @attributes */
|
||||
@attr('string') name;
|
||||
@attr('string') description;
|
||||
@attr('string') for;
|
||||
@attr('string') help_text;
|
||||
@attr('string') label;
|
||||
@attr('string') type;
|
||||
@@ -30,12 +31,20 @@ export default class CustomFieldModel extends Model {
|
||||
@attr('date') deleted_at;
|
||||
|
||||
/** @computed */
|
||||
@computed('type') get valueType() {
|
||||
if (this.type === 'file-upload') return 'file';
|
||||
if (this.type === 'date-time-input') return 'date';
|
||||
if (this.type === 'model-select') return 'model';
|
||||
|
||||
return 'text';
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAgo() {
|
||||
return formatDistanceToNow(this.created_at);
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAgo() {
|
||||
@@ -43,6 +52,6 @@ export default class CustomFieldModel extends Model {
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export default class DashboardWidgetModel extends Model {
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAtShort() {
|
||||
@@ -37,7 +37,7 @@ export default class DashboardWidgetModel extends Model {
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAtShort() {
|
||||
|
||||
@@ -13,7 +13,11 @@ export default class DashboardModel extends Model {
|
||||
|
||||
/** @attributes */
|
||||
@attr('string') name;
|
||||
@attr('string') extension;
|
||||
@attr('boolean') is_default;
|
||||
@attr('array') tags;
|
||||
@attr('object') options;
|
||||
@attr('object') meta;
|
||||
|
||||
/** @dates */
|
||||
@attr('date') created_at;
|
||||
@@ -25,7 +29,7 @@ export default class DashboardModel extends Model {
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAtShort() {
|
||||
@@ -37,7 +41,7 @@ export default class DashboardModel extends Model {
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAtShort() {
|
||||
|
||||
@@ -111,7 +111,7 @@ export default class ExtensionModel extends Model {
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAtShort() {
|
||||
@@ -123,7 +123,7 @@ export default class ExtensionModel extends Model {
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAtShort() {
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class FileModel extends Model {
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('content_type') get isVideo() {
|
||||
|
||||
@@ -26,7 +26,7 @@ export default class GroupModel extends Model {
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAgo() {
|
||||
@@ -34,6 +34,6 @@ export default class GroupModel extends Model {
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ export default class NotificationModel extends Model {
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('read_at') get readAt() {
|
||||
return format(this.read_at, 'PPP p');
|
||||
return format(this.read_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('read_at') get isRead() {
|
||||
|
||||
@@ -132,6 +132,6 @@ export default class PermissionModel extends Model {
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export default class PolicyModel extends Model {
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAgo() {
|
||||
@@ -45,6 +45,6 @@ export default class PolicyModel extends Model {
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
}
|
||||
|
||||
519
console/app/models/report.js
Normal file
519
console/app/models/report.js
Normal file
@@ -0,0 +1,519 @@
|
||||
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { isPresent, isEmpty } from '@ember/utils';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
|
||||
export default class ReportModel extends Model {
|
||||
/** @ids */
|
||||
@attr('string') public_id;
|
||||
@attr('string') company_uuid;
|
||||
@attr('string') created_by_uuid;
|
||||
@attr('string') category_uuid;
|
||||
@attr('string') subject_uuid;
|
||||
|
||||
/** @attributes */
|
||||
@attr('string') subject_type;
|
||||
@attr('string') title;
|
||||
@attr('string') description;
|
||||
@attr('date') period_start;
|
||||
@attr('date') period_end;
|
||||
@attr('date') last_executed_at;
|
||||
@attr('number') execution_time;
|
||||
@attr('number') row_count;
|
||||
@attr('boolean') is_scheduled;
|
||||
@attr('boolean') is_generated;
|
||||
@attr('string') status;
|
||||
@attr('string') type;
|
||||
@attr('raw') export_formats;
|
||||
@attr('raw') schedule_config;
|
||||
@attr('raw') data;
|
||||
@attr('raw') result_columns;
|
||||
@attr('raw') query_config;
|
||||
@attr('raw') tags;
|
||||
@attr('raw') options;
|
||||
@attr('raw') meta;
|
||||
@attr('string') status;
|
||||
|
||||
/** @dates */
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
|
||||
/** @relationships */
|
||||
// @belongsTo('company') company;
|
||||
// @belongsTo('user') createdBy;
|
||||
// @hasMany('report-execution') executions;
|
||||
// @hasMany('report-audit-log') auditLogs;
|
||||
|
||||
fillResult(result = {}) {
|
||||
this.setProperties({
|
||||
result_columns: result?.columns ?? [],
|
||||
data: result?.data ?? [],
|
||||
meta: result?.meta ?? {},
|
||||
row_count: result?.meta?.total_rows ?? 0,
|
||||
execution_time: result?.meta?.execution_time_ms ?? -1,
|
||||
last_executed_at: new Date(),
|
||||
is_generated: true,
|
||||
});
|
||||
}
|
||||
|
||||
/** @computed */
|
||||
@computed('updated_at') get updatedAgo() {
|
||||
return formatDistanceToNow(this.updated_at);
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAgo() {
|
||||
return formatDistanceToNow(this.created_at);
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('query_config.columns.length', 'query_config.table.name') get hasValidConfig() {
|
||||
return (
|
||||
isPresent(this.query_config) &&
|
||||
isPresent(this.query_config.table) &&
|
||||
isPresent(this.query_config.table.name) &&
|
||||
isArray(this.query_config.columns) &&
|
||||
this.query_config.columns.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
@computed('query_config.table.name') get tableName() {
|
||||
return this.query_config?.table?.name || '';
|
||||
}
|
||||
|
||||
@computed('query_config.table.label', 'tableName') get tableLabel() {
|
||||
return this.query_config?.table?.label || this.tableName;
|
||||
}
|
||||
|
||||
@computed('query_config.columns.[]') get selectedColumns() {
|
||||
return this.query_config?.columns || [];
|
||||
}
|
||||
|
||||
@computed('selectedColumns.[]', 'query_config.joins.[]') get totalSelectedColumns() {
|
||||
let count = this.selectedColumns.length;
|
||||
|
||||
// Add columns from joins
|
||||
if (isArray(this.query_config?.joins)) {
|
||||
this.query_config.joins.forEach((join) => {
|
||||
if (isArray(join.selectedColumns)) {
|
||||
count += join.selectedColumns.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@computed('query_config.joins.[]') get hasJoins() {
|
||||
return isArray(this.query_config?.joins) && this.query_config.joins.length > 0;
|
||||
}
|
||||
|
||||
@computed('hasJoins', 'query_config.joins.[]') get joinedTables() {
|
||||
if (!this.hasJoins) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.query_config.joins.map((join) => ({
|
||||
table: join.table,
|
||||
label: join.label || join.table,
|
||||
type: join.type,
|
||||
columnsCount: join.selectedColumns?.length || 0,
|
||||
}));
|
||||
}
|
||||
|
||||
@computed('query_config.conditions.[]') get hasConditions() {
|
||||
return isArray(this.query_config?.conditions) && this.query_config.conditions.length > 0;
|
||||
}
|
||||
|
||||
@computed('hasConditions', 'query_config.conditions.[]') get conditionsCount() {
|
||||
if (!this.hasConditions) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.countConditionsRecursively(this.query_config.conditions);
|
||||
}
|
||||
|
||||
@computed('query_config.groupBy.[]') get hasGrouping() {
|
||||
return isArray(this.query_config?.groupBy) && this.query_config.groupBy.length > 0;
|
||||
}
|
||||
|
||||
@computed('query_config.sortBy.[]') get hasSorting() {
|
||||
return isArray(this.query_config?.sortBy) && this.query_config.sortBy.length > 0;
|
||||
}
|
||||
|
||||
@computed('query_config.limit') get hasLimit() {
|
||||
return isPresent(this.query_config?.limit) && this.query_config.limit > 0;
|
||||
}
|
||||
|
||||
@computed('conditionsCount', 'hasGrouping', 'hasJoins', 'joinedTables.length', 'totalSelectedColumns') get complexity() {
|
||||
let score = 0;
|
||||
|
||||
score += this.totalSelectedColumns;
|
||||
score += this.hasJoins ? this.joinedTables.length * 3 : 0;
|
||||
score += this.conditionsCount * 2;
|
||||
score += this.hasGrouping ? 5 : 0;
|
||||
|
||||
if (score < 10) {
|
||||
return 'simple';
|
||||
} else if (score < 25) {
|
||||
return 'moderate';
|
||||
} else {
|
||||
return 'complex';
|
||||
}
|
||||
}
|
||||
|
||||
@computed('complexity', 'totalSelectedColumns', 'joinedTables.length') get estimatedPerformance() {
|
||||
if (this.complexity === 'simple' && this.totalSelectedColumns <= 10) {
|
||||
return 'fast';
|
||||
} else if (this.complexity === 'moderate' && this.joinedTables.length <= 2) {
|
||||
return 'moderate';
|
||||
} else {
|
||||
return 'slow';
|
||||
}
|
||||
}
|
||||
|
||||
@computed('last_executed_at') get lastExecutedDisplay() {
|
||||
if (!this.last_executed_at) {
|
||||
return 'Never executed';
|
||||
}
|
||||
|
||||
return this.last_executed_at.toLocaleDateString() + ' ' + this.last_executed_at.toLocaleTimeString();
|
||||
}
|
||||
|
||||
@computed('average_execution_time') get averageExecutionTimeDisplay() {
|
||||
if (!this.average_execution_time) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
if (this.average_execution_time < 1000) {
|
||||
return `${Math.round(this.average_execution_time)}ms`;
|
||||
} else {
|
||||
return `${(this.average_execution_time / 1000).toFixed(2)}s`;
|
||||
}
|
||||
}
|
||||
|
||||
@computed('execution_count') get executionCountDisplay() {
|
||||
return this.execution_count || 0;
|
||||
}
|
||||
|
||||
@computed('last_result_count') get lastResultCountDisplay() {
|
||||
if (this.last_result_count === null || this.last_result_count === undefined) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
return this.last_result_count.toLocaleString();
|
||||
}
|
||||
|
||||
@computed('export_formats.[]') get availableExportFormats() {
|
||||
return this.export_formats || ['csv', 'excel', 'json'];
|
||||
}
|
||||
|
||||
@computed('tags.[]') get tagsList() {
|
||||
return this.tags || [];
|
||||
}
|
||||
|
||||
@computed('shared_with.[]') get sharedWithList() {
|
||||
return this.shared_with || [];
|
||||
}
|
||||
|
||||
@computed('is_scheduled', 'next_scheduled_run', 'schedule_frequency', 'schedule_timezone') get scheduleInfo() {
|
||||
if (!this.is_scheduled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
frequency: this.schedule_frequency,
|
||||
nextRun: this.next_scheduled_run,
|
||||
timezone: this.schedule_timezone || 'UTC',
|
||||
};
|
||||
}
|
||||
|
||||
@computed('hasConditions', 'query_config.conditions.[]') get conditionsSummary() {
|
||||
if (!this.hasConditions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.extractConditionsSummary(this.query_config.conditions);
|
||||
}
|
||||
|
||||
@computed('status') get statusDisplay() {
|
||||
const statusMap = {
|
||||
draft: 'Draft',
|
||||
active: 'Active',
|
||||
archived: 'Archived',
|
||||
error: 'Error',
|
||||
};
|
||||
|
||||
return statusMap[this.status] || this.status;
|
||||
}
|
||||
|
||||
@computed('status') get statusClass() {
|
||||
const statusClasses = {
|
||||
draft: 'status-draft',
|
||||
active: 'status-active',
|
||||
archived: 'status-archived',
|
||||
error: 'status-error',
|
||||
};
|
||||
|
||||
return statusClasses[this.status] || 'status-unknown';
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
countConditionsRecursively(conditions) {
|
||||
let count = 0;
|
||||
|
||||
if (!isArray(conditions)) {
|
||||
return count;
|
||||
}
|
||||
|
||||
conditions.forEach((condition) => {
|
||||
if (condition.conditions) {
|
||||
count += this.countConditionsRecursively(condition.conditions);
|
||||
} else {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
extractConditionsSummary(conditions, summary = []) {
|
||||
if (!isArray(conditions)) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
conditions.forEach((condition) => {
|
||||
if (condition.conditions) {
|
||||
this.extractConditionsSummary(condition.conditions, summary);
|
||||
} else if (condition.field && condition.operator) {
|
||||
summary.push({
|
||||
field: condition.field.label || condition.field.name,
|
||||
operator: condition.operator.label || condition.operator.value,
|
||||
value: condition.value,
|
||||
table: condition.field.table || this.tableName,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
// API methods for interacting with the new backend
|
||||
async execute() {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post(this.id ? `reports/${this.id}/execute` : 'reports/execute-query', { query_config: this.query_config });
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// API methods for interacting with the new backend
|
||||
async executeQuery() {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post('reports/execute-query', { query_config: this.query_config });
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async export(format = 'csv', options = {}) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post(`reports/${this.id}/export`, {
|
||||
format,
|
||||
options,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async validate() {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post('reports/validate-query', {
|
||||
query_config: this.query_config,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async analyze() {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post('reports/analyze-query', {
|
||||
query_config: this.query_config,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Static methods for direct query operations
|
||||
static async executeQuery(queryConfig) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post('reports/execute-query', {
|
||||
query_config: queryConfig,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async exportQuery(queryConfig, format = 'csv', options = {}) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch('reports/export-query', {
|
||||
query_config: queryConfig,
|
||||
format,
|
||||
options,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async validateQuery(queryConfig) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post('reports/validate-query', { query_config: queryConfig });
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async analyzeQuery(queryConfig) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const response = await fetch.post('reports/analyze-query', { query_config: queryConfig });
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getTables() {
|
||||
try {
|
||||
const { tables } = await fetch.get('reports/tables');
|
||||
return tables;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getTableSchema(tableName) {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const { schema } = await fetch.get(`reports/tables/${tableName}/schema`);
|
||||
return schema;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async getExportFormats() {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup('service:fetch');
|
||||
|
||||
try {
|
||||
const { formats } = await fetch.get('reports/export-formats');
|
||||
return formats;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility methods for frontend display
|
||||
getComplexityBadgeClass() {
|
||||
const complexityClasses = {
|
||||
simple: 'badge-success',
|
||||
moderate: 'badge-warning',
|
||||
complex: 'badge-danger',
|
||||
};
|
||||
|
||||
return complexityClasses[this.complexity] || 'badge-secondary';
|
||||
}
|
||||
|
||||
getPerformanceBadgeClass() {
|
||||
const performanceClasses = {
|
||||
fast: 'badge-success',
|
||||
moderate: 'badge-warning',
|
||||
slow: 'badge-danger',
|
||||
};
|
||||
|
||||
return performanceClasses[this.estimatedPerformance] || 'badge-secondary';
|
||||
}
|
||||
|
||||
getQuerySummary() {
|
||||
const parts = [];
|
||||
|
||||
parts.push(`${this.totalSelectedColumns} columns from ${this.tableLabel}`);
|
||||
|
||||
if (this.hasJoins) {
|
||||
parts.push(`${this.joinedTables.length} joins`);
|
||||
}
|
||||
|
||||
if (this.hasConditions) {
|
||||
parts.push(`${this.conditionsCount} conditions`);
|
||||
}
|
||||
|
||||
if (this.hasGrouping) {
|
||||
parts.push('grouped');
|
||||
}
|
||||
|
||||
if (this.hasSorting) {
|
||||
parts.push('sorted');
|
||||
}
|
||||
|
||||
if (this.hasLimit) {
|
||||
parts.push(`limited to ${this.query_config.limit} rows`);
|
||||
}
|
||||
|
||||
return parts.join(', ');
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export default class RoleModel extends Model {
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAtShort() {
|
||||
@@ -50,7 +50,7 @@ export default class RoleModel extends Model {
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAtShort() {
|
||||
|
||||
18
console/app/models/schedule-availability.js
Normal file
18
console/app/models/schedule-availability.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
|
||||
export default class ScheduleAvailabilityModel extends Model {
|
||||
@attr('string') subject_uuid;
|
||||
@attr('string') subject_type;
|
||||
@attr('date') start_at;
|
||||
@attr('date') end_at;
|
||||
@attr('boolean', { defaultValue: true }) is_available;
|
||||
@attr('number') preference_level;
|
||||
@attr('string') rrule;
|
||||
@attr('string') reason;
|
||||
@attr('string') notes;
|
||||
@attr('object') meta;
|
||||
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@attr('date') deleted_at;
|
||||
}
|
||||
23
console/app/models/schedule-constraint.js
Normal file
23
console/app/models/schedule-constraint.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import Model, { attr, belongsTo } from '@ember-data/model';
|
||||
|
||||
export default class ScheduleConstraintModel extends Model {
|
||||
@attr('string') company_uuid;
|
||||
@attr('string') subject_uuid;
|
||||
@attr('string') subject_type;
|
||||
@attr('string') name;
|
||||
@attr('string') description;
|
||||
@attr('string') type;
|
||||
@attr('string') category;
|
||||
@attr('string') constraint_key;
|
||||
@attr('string') constraint_value;
|
||||
@attr('string') jurisdiction;
|
||||
@attr('number', { defaultValue: 0 }) priority;
|
||||
@attr('boolean', { defaultValue: true }) is_active;
|
||||
@attr('object') meta;
|
||||
|
||||
@belongsTo('company') company;
|
||||
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@attr('date') deleted_at;
|
||||
}
|
||||
23
console/app/models/schedule-item.js
Normal file
23
console/app/models/schedule-item.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import Model, { attr, belongsTo } from '@ember-data/model';
|
||||
|
||||
export default class ScheduleItemModel extends Model {
|
||||
@attr('string') public_id;
|
||||
@attr('string') schedule_uuid;
|
||||
@attr('string') assignee_uuid;
|
||||
@attr('string') assignee_type;
|
||||
@attr('string') resource_uuid;
|
||||
@attr('string') resource_type;
|
||||
@attr('date') start_at;
|
||||
@attr('date') end_at;
|
||||
@attr('number') duration;
|
||||
@attr('date') break_start_at;
|
||||
@attr('date') break_end_at;
|
||||
@attr('string', { defaultValue: 'pending' }) status;
|
||||
@attr('object') meta;
|
||||
|
||||
@belongsTo('schedule') schedule;
|
||||
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@attr('date') deleted_at;
|
||||
}
|
||||
22
console/app/models/schedule-template.js
Normal file
22
console/app/models/schedule-template.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Model, { attr, belongsTo } from '@ember-data/model';
|
||||
|
||||
export default class ScheduleTemplateModel extends Model {
|
||||
@attr('string') public_id;
|
||||
@attr('string') company_uuid;
|
||||
@attr('string') subject_uuid;
|
||||
@attr('string') subject_type;
|
||||
@attr('string') name;
|
||||
@attr('string') description;
|
||||
@attr('string') start_time;
|
||||
@attr('string') end_time;
|
||||
@attr('number') duration;
|
||||
@attr('number') break_duration;
|
||||
@attr('string') rrule;
|
||||
@attr('object') meta;
|
||||
|
||||
@belongsTo('company') company;
|
||||
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@attr('date') deleted_at;
|
||||
}
|
||||
27
console/app/models/schedule.js
Normal file
27
console/app/models/schedule.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import Model, { attr, hasMany, belongsTo } from '@ember-data/model';
|
||||
|
||||
export default class ScheduleModel extends Model {
|
||||
/** @ids */
|
||||
@attr('string') public_id;
|
||||
@attr('string') company_uuid;
|
||||
@attr('string') subject_uuid;
|
||||
@attr('string') subject_type;
|
||||
|
||||
/** @attributes */
|
||||
@attr('string') name;
|
||||
@attr('string') description;
|
||||
@attr('date') start_date;
|
||||
@attr('date') end_date;
|
||||
@attr('string') timezone;
|
||||
@attr('string', { defaultValue: 'draft' }) status;
|
||||
@attr('object') meta;
|
||||
|
||||
/** @relationships */
|
||||
@hasMany('schedule-item') items;
|
||||
@belongsTo('company') company;
|
||||
|
||||
/** @dates */
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@attr('date') deleted_at;
|
||||
}
|
||||
@@ -186,7 +186,7 @@ export default class UserModel extends Model {
|
||||
if (!isValid(this.updated_at)) {
|
||||
return '-';
|
||||
}
|
||||
return format(this.updated_at, 'PPP p');
|
||||
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAtShort() {
|
||||
@@ -207,7 +207,7 @@ export default class UserModel extends Model {
|
||||
if (!isValid(this.created_at)) {
|
||||
return '-';
|
||||
}
|
||||
return format(this.created_at, 'PPP p');
|
||||
return format(this.created_at, 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAtShort() {
|
||||
|
||||
@@ -7,6 +7,8 @@ import pathToRoute from '@fleetbase/ember-core/utils/path-to-route';
|
||||
import removeBootLoader from '../utils/remove-boot-loader';
|
||||
|
||||
export default class ApplicationRoute extends Route {
|
||||
@service('universe/hook-service') hookService;
|
||||
@service('universe/extension-manager') extensionManager;
|
||||
@service session;
|
||||
@service theme;
|
||||
@service fetch;
|
||||
@@ -15,7 +17,6 @@ export default class ApplicationRoute extends Route {
|
||||
@service intl;
|
||||
@service currentUser;
|
||||
@service router;
|
||||
@service universe;
|
||||
@tracked defaultTheme;
|
||||
|
||||
/**
|
||||
@@ -24,7 +25,7 @@ export default class ApplicationRoute extends Route {
|
||||
* @memberof ApplicationRoute
|
||||
*/
|
||||
@action willTransition(transition) {
|
||||
this.universe.callHooks('application:will-transition', this.session, this.router, transition);
|
||||
this.hookService.execute('application:will-transition', this.session, this.router, transition);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +46,7 @@ export default class ApplicationRoute extends Route {
|
||||
* @memberof ApplicationRoute
|
||||
*/
|
||||
@action loading(transition) {
|
||||
this.universe.callHooks('application:loading', this.session, this.router, transition);
|
||||
this.hookService.execute('application:loading', this.session, this.router, transition);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,9 +80,9 @@ export default class ApplicationRoute extends Route {
|
||||
*/
|
||||
async beforeModel(transition) {
|
||||
await this.session.setup();
|
||||
await this.universe.booting();
|
||||
await this.extensionManager.waitForBoot();
|
||||
|
||||
this.universe.callHooks('application:before-model', this.session, this.router, transition);
|
||||
this.hookService.execute('application:before-model', this.session, this.router, transition);
|
||||
|
||||
const shift = this.urlSearchParams.get('shift');
|
||||
if (this.session.isAuthenticated && shift) {
|
||||
@@ -95,9 +96,7 @@ export default class ApplicationRoute extends Route {
|
||||
* @memberof ApplicationRoute
|
||||
*/
|
||||
afterModel() {
|
||||
if (!this.session.isAuthenticated) {
|
||||
removeBootLoader();
|
||||
}
|
||||
if (!this.session.isAuthenticated) removeBootLoader();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,11 +121,11 @@ export default class ApplicationRoute extends Route {
|
||||
* Initializes the application's locale settings based on the current user's preferences.
|
||||
*
|
||||
* This method retrieves the user's preferred locale using the `getOption` method from the `currentUser` service.
|
||||
* If no locale is set by the user, it defaults to `'en-us'`. It then sets the application's locale by calling
|
||||
* If no locale is set by the user, it defaults to `'en-US'`. It then sets the application's locale by calling
|
||||
* the `setLocale` method of the `intl` service with the retrieved locale.
|
||||
*/
|
||||
initializeLocale() {
|
||||
const locale = this.currentUser.getOption('locale', 'en-us');
|
||||
const locale = this.currentUser.getOption('locale', 'en-US');
|
||||
this.intl.setLocale([locale]);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import removeBootLoader from '../utils/remove-boot-loader';
|
||||
import '@fleetbase/leaflet-routing-machine';
|
||||
|
||||
export default class ConsoleRoute extends Route {
|
||||
@service('universe/hook-service') hookService;
|
||||
@service store;
|
||||
@service session;
|
||||
@service universe;
|
||||
@service router;
|
||||
@service currentUser;
|
||||
@service intl;
|
||||
@@ -22,7 +22,7 @@ export default class ConsoleRoute extends Route {
|
||||
async beforeModel(transition) {
|
||||
await this.session.requireAuthentication(transition, 'auth.login');
|
||||
|
||||
this.universe.callHooks('console:before-model', this.session, this.router, transition);
|
||||
this.hookService.execute('console:before-model', this.session, this.router, transition);
|
||||
|
||||
if (this.session.isAuthenticated) {
|
||||
return this.session.promiseCurrentUser(transition);
|
||||
@@ -37,7 +37,7 @@ export default class ConsoleRoute extends Route {
|
||||
* @memberof ConsoleRoute
|
||||
*/
|
||||
async afterModel(model, transition) {
|
||||
this.universe.callHooks('console:after-model', this.session, this.router, model, transition);
|
||||
this.hookService.execute('console:after-model', this.session, this.router, model, transition);
|
||||
removeBootLoader();
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export default class ConsoleRoute extends Route {
|
||||
* @memberof ConsoleRoute
|
||||
*/
|
||||
@action didTransition() {
|
||||
this.universe.callHooks('console:did-transition', this.session, this.router);
|
||||
this.hookService.execute('console:did-transition', this.session, this.router);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleAccountVirtualRoute extends Route {
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
|
||||
queryParams = {
|
||||
@@ -12,6 +13,6 @@ export default class ConsoleAccountVirtualRoute extends Route {
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console:account', slug, view);
|
||||
return this.menuService.lookupMenuItem('console:account', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleAdminVirtualRoute extends Route {
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
|
||||
queryParams = {
|
||||
@@ -12,6 +13,6 @@ export default class ConsoleAdminVirtualRoute extends Route {
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console:admin', slug, view);
|
||||
return this.menuService.lookupMenuItem('console:admin', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import groupBy from '@fleetbase/ember-core/utils/group-by';
|
||||
|
||||
export default class ConsoleSettingsNotificationsRoute extends Route {
|
||||
@service fetch;
|
||||
@service currentUser;
|
||||
|
||||
model() {
|
||||
return hash({
|
||||
@@ -13,10 +14,11 @@ export default class ConsoleSettingsNotificationsRoute extends Route {
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller, { registry, notifiables }) {
|
||||
async setupController(controller, { registry, notifiables }) {
|
||||
super.setupController(...arguments);
|
||||
|
||||
controller.groupedNotifications = groupBy(registry, 'package');
|
||||
controller.notifiables = notifiables;
|
||||
controller.company = await this.currentUser.loadCompany();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleSettingsVirtualRoute extends Route {
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
|
||||
queryParams = {
|
||||
@@ -12,6 +13,6 @@ export default class ConsoleSettingsVirtualRoute extends Route {
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console:settings', slug, view);
|
||||
return this.menuService.lookupMenuItem('console:settings', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleVirtualRoute extends Route {
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
|
||||
queryParams = {
|
||||
@@ -12,6 +13,6 @@ export default class ConsoleVirtualRoute extends Route {
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('console', slug, view);
|
||||
return this.menuService.lookupMenuItem('console', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,17 @@ import { inject as service } from '@ember/service';
|
||||
|
||||
export default class OnboardIndexRoute extends Route {
|
||||
@service store;
|
||||
@service('onboarding-orchestrator') orchestrator;
|
||||
|
||||
queryParams = {
|
||||
step: { refreshModel: false },
|
||||
session: { refreshModel: false },
|
||||
code: { refreshModel: false },
|
||||
};
|
||||
|
||||
beforeModel() {
|
||||
this.orchestrator.start();
|
||||
}
|
||||
|
||||
model() {
|
||||
return this.store.findRecord('brand', 1);
|
||||
|
||||
@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class VirtualRoute extends Route {
|
||||
@service('universe/menu-service') menuService;
|
||||
@service universe;
|
||||
|
||||
queryParams = {
|
||||
@@ -12,6 +13,6 @@ export default class VirtualRoute extends Route {
|
||||
|
||||
model({ slug }, transition) {
|
||||
const view = this.universe.getViewFromTransition(transition);
|
||||
return this.universe.lookupMenuItemFromRegistry('auth:login', slug, view);
|
||||
return this.menuService.lookupMenuItem('auth:login', slug, view);
|
||||
}
|
||||
}
|
||||
|
||||
13
console/app/serializers/activity.js
Normal file
13
console/app/serializers/activity.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
|
||||
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
|
||||
|
||||
export default class CategorySerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
|
||||
/**
|
||||
* Embedded relationship attributes
|
||||
*
|
||||
* @var {Object}
|
||||
*/
|
||||
get attrs() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
4
console/app/serializers/alert.js
Normal file
4
console/app/serializers/alert.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
|
||||
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
|
||||
|
||||
export default class AlertSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {}
|
||||
4
console/app/serializers/report.js
Normal file
4
console/app/serializers/report.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
|
||||
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
|
||||
|
||||
export default class ReportSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {}
|
||||
@@ -4,8 +4,6 @@ import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
|
||||
export default class UserSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
|
||||
/**
|
||||
* Embedded relationship attributes
|
||||
*
|
||||
* @var {Object}
|
||||
*/
|
||||
get attrs() {
|
||||
return {
|
||||
@@ -16,22 +14,45 @@ export default class UserSerializer extends ApplicationSerializer.extend(Embedde
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize serializer so that the password is never sent to the server via Ember Data
|
||||
* Prevent partial payloads from overwriting fully-loaded
|
||||
* user records in the store.
|
||||
*
|
||||
* @param {Snapshot} snapshot
|
||||
* @param {Object} options
|
||||
* @return {Object} json
|
||||
* This runs ONLY on incoming data.
|
||||
*/
|
||||
normalize(modelClass, resourceHash, prop) {
|
||||
let normalized = super.normalize(modelClass, resourceHash, prop);
|
||||
|
||||
// Existing user already loaded in the store?
|
||||
let existing = this.store.peekRecord(normalized.data.type, normalized.data.id);
|
||||
|
||||
if (existing) {
|
||||
let attrs = normalized.data.attributes || {};
|
||||
|
||||
for (let key in attrs) {
|
||||
if (attrs[key] === null || attrs[key] === undefined || key === 'avatar_url') {
|
||||
delete attrs[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize serializer so that sensitive or server-managed
|
||||
* fields are never sent to the backend.
|
||||
*/
|
||||
serialize() {
|
||||
const json = super.serialize(...arguments);
|
||||
|
||||
// delete the password always
|
||||
// Never send password
|
||||
delete json.password;
|
||||
// delete verification attributes
|
||||
|
||||
// Verification flags
|
||||
delete json.email_verified_at;
|
||||
delete json.phone_verified_at;
|
||||
|
||||
// delete server managed dates
|
||||
// Server-managed timestamps
|
||||
delete json.deleted_at;
|
||||
delete json.created_at;
|
||||
delete json.updated_at;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user