mirror of
https://github.com/fleetbase/fleetbase.git
synced 2025-12-31 19:37:11 +00:00
Compare commits
132 Commits
feature/fu
...
v0.7.18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9f415528e | ||
|
|
76b0bfbfcd | ||
|
|
0432003163 | ||
|
|
da420f0b4a | ||
|
|
e923a89719 | ||
|
|
0742603b43 | ||
|
|
e1788a4ad6 | ||
|
|
7cb4654c86 | ||
|
|
a17aa3f5cc | ||
|
|
0bf1a7fadd | ||
|
|
aa1ea2de89 | ||
|
|
235f1ce80c | ||
|
|
5aa50504a4 | ||
|
|
5d1b2e1939 | ||
|
|
fc5d90189c | ||
|
|
2fee78e534 | ||
|
|
83fc794702 | ||
|
|
66f669ad80 | ||
|
|
a11b77592c | ||
|
|
e5156829dc | ||
|
|
6cd7ddffcb | ||
|
|
cbdf1d489b | ||
|
|
785bc55bb7 | ||
|
|
d171d02aac | ||
|
|
dfd4ee37df | ||
|
|
27c063fbfb | ||
|
|
8e85dcff83 | ||
|
|
e38923c461 | ||
|
|
9911c96c09 | ||
|
|
284c62cd06 | ||
|
|
f8fd9f76fa | ||
|
|
67aa793537 | ||
|
|
5d0ae16cfd | ||
|
|
1d003ee31e | ||
|
|
9c9f3a994e | ||
|
|
b0ae302e81 | ||
|
|
205fcf1480 | ||
|
|
23bf7c5ac8 | ||
|
|
ada7e0df92 | ||
|
|
f3bc42ace5 | ||
|
|
b91cbed080 | ||
|
|
9870b11a71 | ||
|
|
1d62dbca6b | ||
|
|
db3bf46a02 | ||
|
|
ec053f1d13 | ||
|
|
030ec2494d | ||
|
|
fe56bcac85 | ||
|
|
8b118d1ad9 | ||
|
|
724c1b49ab | ||
|
|
8e5b2e1ae3 | ||
|
|
e141d4d3a3 | ||
|
|
ab2e102e28 | ||
|
|
723deff398 | ||
|
|
fd9adc3961 | ||
|
|
4244a04052 | ||
|
|
e3c60a2232 | ||
|
|
1eaeb2c46e | ||
|
|
1d64d18b8b | ||
|
|
1124ecb56c | ||
|
|
672f3d51ca | ||
|
|
cd5af8dfc8 | ||
|
|
1a0073eae0 | ||
|
|
d24b1d6fbe | ||
|
|
ebbc4b2bf8 | ||
|
|
b531c18d65 | ||
|
|
fded8b24df | ||
|
|
98d082c780 | ||
|
|
d905943511 | ||
|
|
5c73b6e76d | ||
|
|
cedf96fc97 | ||
|
|
854fa2e680 | ||
|
|
91b01c8a17 | ||
|
|
a4033db36c | ||
|
|
c54ef7fb30 | ||
|
|
b5ec15f0bb | ||
|
|
1f609dd882 | ||
|
|
01883da5a2 | ||
|
|
d2ab5b8a94 | ||
|
|
dca23f7e3f | ||
|
|
d94dff7fbb | ||
|
|
e1ab6a3b11 | ||
|
|
c79fe67e44 | ||
|
|
d8adf42b24 | ||
|
|
80da5fe013 | ||
|
|
06fd5e20e8 | ||
|
|
f04807de1e | ||
|
|
b7666eeb3e | ||
|
|
dd895a0fd8 | ||
|
|
8c74c0fb99 | ||
|
|
92170c965e | ||
|
|
fcb3694874 | ||
|
|
aa46059bff | ||
|
|
a5175bb11b | ||
|
|
01816a1fe0 | ||
|
|
15d500cd58 | ||
|
|
95d77a6ddd | ||
|
|
eefc93e130 | ||
|
|
0f18ae85f1 | ||
|
|
a4812192da | ||
|
|
15d3c957b8 | ||
|
|
c2bd098d14 | ||
|
|
98511fd418 | ||
|
|
225110c8dc | ||
|
|
1aa2a99763 | ||
|
|
6e888af772 | ||
|
|
d61205d898 | ||
|
|
72078553cc | ||
|
|
bfae04a645 | ||
|
|
c59f028755 | ||
|
|
2b959db773 | ||
|
|
a9354ccbfd | ||
|
|
23e6d1e6b9 | ||
|
|
86da1bd095 | ||
|
|
ae89600ae6 | ||
|
|
6697b79185 | ||
|
|
4dc9764853 | ||
|
|
0626bc0171 | ||
|
|
a8adf3fd84 | ||
|
|
7b8bc4a593 | ||
|
|
490f2f1b41 | ||
|
|
e1fc7850d3 | ||
|
|
cc278bf1bb | ||
|
|
af86aaba8b | ||
|
|
f35dcb1544 | ||
|
|
29c8f4340d | ||
|
|
1cb833e407 | ||
|
|
e372bc6396 | ||
|
|
2f432d148a | ||
|
|
41bc6e39a7 | ||
|
|
5dbe2fb5bb | ||
|
|
8f66bc12e4 | ||
|
|
1e331d70b1 |
64
.github/workflows/build-binaries.yml
vendored
Normal file
64
.github/workflows/build-binaries.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Build Fleetbase Binaries
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows: ["Create Release"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
DIST_DIR: builds/dist
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
name: Linux Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build Linux binary
|
||||
run: |
|
||||
chmod +x ./builds/linux/build-linux.sh
|
||||
./builds/linux/build-linux.sh
|
||||
- name: Upload Linux binary
|
||||
if: github.event_name == 'workflow_run'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.workflow_run.head_branch }}
|
||||
files: |
|
||||
${{ env.DIST_DIR }}/fleetbase-linux-x86_64
|
||||
draft: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-macos:
|
||||
name: macOS (ARM64) Build
|
||||
needs: build-linux
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
brew update
|
||||
brew install autoconf automake coreutils asdf php@8.4
|
||||
source "$(brew --prefix asdf)/libexec/asdf.sh"
|
||||
asdf plugin add php https://github.com/asdf-community/asdf-php.git
|
||||
- name: Build macOS binary
|
||||
run: |
|
||||
chmod +x ./builds/osx/build-osx.sh
|
||||
./builds/osx/build-osx.sh
|
||||
- name: Upload Linux binary
|
||||
if: github.event_name == 'workflow_run'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.workflow_run.head_branch }}
|
||||
files: |
|
||||
${{ env.DIST_DIR }}/fleetbase-darwin-arm64
|
||||
draft: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
60
.github/workflows/cd.yml
vendored
60
.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
|
||||
@@ -175,6 +224,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
|
||||
|
||||
21
.github/workflows/create-release.yml
vendored
Normal file
21
.github/workflows/create-release.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Create Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
create:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Publish GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: ${{ github.ref_name }}
|
||||
body_path: RELEASE.md
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
61
.github/workflows/discord-announcement.yml
vendored
Normal file
61
.github/workflows/discord-announcement.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: Discord Announcement
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Create Release"]
|
||||
types: [completed]
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Release tag to announce (e.g. v0.7.1)"
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
discord_announcement:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# 1️⃣ Figure out which tag we’re talking about
|
||||
- id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
TAG="${{ github.event.inputs.tag }}"
|
||||
else
|
||||
TAG="${{ github.event.workflow_run.head_branch }}"
|
||||
fi
|
||||
echo "TAG=$TAG" >> "$GITHUB_ENV"
|
||||
|
||||
# 2️⃣ Check out the exact commit for that tag
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ env.TAG }}
|
||||
fetch-depth: 1
|
||||
|
||||
# 3️⃣ Stash RELEASE.md in an env var (one atomic write → no EOF error)
|
||||
- id: prep-body
|
||||
shell: bash
|
||||
run: |
|
||||
body=$(<RELEASE.md)
|
||||
max=4000
|
||||
[[ ${#body} -gt $max ]] && body="${body:0:$max}…" # add ellipsis if trimmed
|
||||
{
|
||||
echo "body<<EOF"
|
||||
echo "$body"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# 4️⃣ Fire the webhook
|
||||
- uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
username: Fleetbase
|
||||
content: |
|
||||
@everyone
|
||||
📦 **Fleetbase ${{ env.TAG }} released!**
|
||||
<https://github.com/${{ github.repository }}/releases/tag/${{ env.TAG }}>
|
||||
embed-title: "Fleetbase ${{ env.TAG }} — release notes"
|
||||
embed-url: "https://github.com/fleetbase/fleetbase/releases/tag/${{ env.TAG }}"
|
||||
embed-description: ${{ steps.prep-body.outputs.body }}
|
||||
embed-color: 4362730 # 0x4291EA (Fleetbase Blue)
|
||||
48
.github/workflows/discord_announcement.yml
vendored
48
.github/workflows/discord_announcement.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Discord Announcement
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
discord_announcement:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get tag message
|
||||
id: tag
|
||||
run: |
|
||||
echo "::set-output name=version::$(git describe --tags --abbrev=0)"
|
||||
if [[ "${ACT}" == "true" ]]; then
|
||||
# If running with act, use an environment variable for the tag message
|
||||
echo "::set-output name=message::$(echo -e "${TAG_MESSAGE}" | base64)"
|
||||
else
|
||||
# If running on GitHub, use git to get the tag message
|
||||
echo "::set-output name=message::$(git tag -l --format='%(contents)' $(git describe --tags --abbrev=0) | base64)"
|
||||
fi
|
||||
|
||||
- name: Print tag message
|
||||
run: echo "${{ steps.tag.outputs.message }}"
|
||||
|
||||
- name: Get tag name
|
||||
id: get_tag
|
||||
run: echo "::set-output name=tag::${GITHUB_REF/refs\/tags\//}"
|
||||
|
||||
- name: Decode message
|
||||
id: decode
|
||||
run: |
|
||||
echo "Decoding message..."
|
||||
echo "::set-output name=message::$(echo '${{ steps.tag.outputs.message }}' | base64 --decode)"
|
||||
|
||||
- name: Send message to Discord
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: "@everyone \n📦 New Fleetbase Version ${{ steps.get_tag.outputs.tag }} Released!\n${{ steps.decode.outputs.message }} \nVersion: ${{ steps.get_tag.outputs.tag }} \n[Release Notes for ${{ steps.get_tag.outputs.tag }}](https://github.com/fleetbase/fleetbase/releases/tag/${{ steps.get_tag.outputs.tag }})"
|
||||
username: Fleetbase
|
||||
2
.github/workflows/gcp-cd.yml
vendored
2
.github/workflows/gcp-cd.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Fleetbase CI/CD
|
||||
name: Fleetbase GCP CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
50
.github/workflows/publish-docker-images.yml
vendored
Normal file
50
.github/workflows/publish-docker-images.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: Fleetbase Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'Branch to build from'
|
||||
required: false
|
||||
default: 'main'
|
||||
version:
|
||||
description: 'Image version tag (e.g., v0.7.1-beta)'
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
docker-release:
|
||||
name: Build and Push Docker Images
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
REGISTRY: fleetbase
|
||||
VERSION: ${{ github.event.inputs.version || (github.ref_type == 'tag' && startsWith(github.ref_name, 'v') && github.ref_name) || 'manual' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch || github.ref_name }}
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build and Push Console & API Images
|
||||
uses: docker/bake-action@v2
|
||||
with:
|
||||
push: true
|
||||
targets: |
|
||||
fleetbase-console
|
||||
fleetbase-api
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
24
.gitignore
vendored
24
.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,7 +34,24 @@ 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
|
||||
verdaccio
|
||||
# asdf
|
||||
.tools-versions
|
||||
# binary build resources
|
||||
builds/osx/frankenphp
|
||||
# build artifacts
|
||||
/builds/dist/
|
||||
/builds/linux/spc/downloads/*
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
96
README.md
96
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,23 +25,48 @@
|
||||
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**
|
||||
|
||||
```bash
|
||||
git clone git@github.com:fleetbase/fleetbase.git
|
||||
cd fleetbase
|
||||
docker-compose up -d
|
||||
docker exec -ti fleetbase-application-1 bash
|
||||
sh deploy.sh
|
||||
cd fleetbase && ./scripts/docker-install.sh
|
||||
```
|
||||
|
||||
## 📖 Table of contents
|
||||
|
||||
- [Features](#-features)
|
||||
- [Install](#-install)
|
||||
- [Deploy on AWS](#-deploy-on-aws-in-one-click)
|
||||
- [Extensions](#-extensions)
|
||||
- [Apps](#-apps)
|
||||
- [Roadmap](#-roadmap)
|
||||
@@ -75,10 +100,7 @@ Make sure you have both the latest versions of docker and docker-compose install
|
||||
|
||||
```bash
|
||||
git clone git@github.com:fleetbase/fleetbase.git
|
||||
cd fleetbase
|
||||
docker-compose up -d
|
||||
docker exec -ti fleetbase-application-1 bash
|
||||
sh deploy.sh
|
||||
cd fleetbase && ./scripts/docker-install.sh
|
||||
```
|
||||
|
||||
### Accessing Fleetbase
|
||||
@@ -89,7 +111,17 @@ Fleetbase API: http://localhost:8000
|
||||
|
||||
### Additional Configurations
|
||||
|
||||
**CORS:** If you’re installing directly on a server you may need to add your IP address or domain to the `api/config/cors.php` file in the `allowed_hosts` array.
|
||||
**CORS:** If you’re installing directly on a server you will need to configure the environment variables to the application container:
|
||||
```
|
||||
CONSOLE_HOST=http://{yourhost}:4200
|
||||
```
|
||||
If you have additional applications or frontends you can use the environment variable `FRONTEND_HOSTS` to add a comma delimited list of additioal frontend hosts.
|
||||
|
||||
**Application Key** If you get an issue about a missing application key just run:
|
||||
```bash
|
||||
docker compose exec application bash -c "php artisan key:generate --show"
|
||||
```
|
||||
Next copy this value to the `APP_KEY` environment variable in the application container and restart.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -100,6 +132,7 @@ version: “3.8”
|
||||
services:
|
||||
application:
|
||||
environment:
|
||||
CONSOLE_HOST: http://localhost:4200
|
||||
MAIL_MAILER: (ses, smtp, mailgun, postmark, sendgrid)
|
||||
OSRM_HOST: https://router.project-osrm.org
|
||||
IPINFO_API_KEY:
|
||||
@@ -108,11 +141,40 @@ services:
|
||||
TWILIO_SID:
|
||||
TWILIO_TOKEN:
|
||||
TWILIO_FROM:
|
||||
CONSOLE_HOST: http://localhost:4200
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -145,9 +207,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. **Binary Builds** ~ Run Fleetbase from a single binary.
|
||||
4. **Fleetbase for Desktop** ~ Desktop builds for OSX and Windows.
|
||||
5. **Custom Maps and Routing Engines** ~ Feature to enable easy integrations with custom maps and routing engines like Google Maps or Mapbox etc…
|
||||
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
|
||||
|
||||
@@ -183,3 +244,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>.
|
||||
|
||||
|
||||
34
RELEASE.md
Normal file
34
RELEASE.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# 🚀 Fleetbase v0.7.18 — 2025-11-10
|
||||
|
||||
> "Hotfix IAM user validation, make online/offline toggle silent"
|
||||
|
||||
---
|
||||
|
||||
## ✨ Highlights
|
||||
- Hotfix validateRequest implementation to not rewrite request params
|
||||
- Hotfix user validation password optional for creation
|
||||
- Made online/offline endpoint for drivers silent
|
||||
- Hotfix QPay payment gateway on Storefront + ebarimt reciept fix
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Breaking Changes
|
||||
- None
|
||||
|
||||
---
|
||||
|
||||
## 🔧 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)
|
||||
@@ -40,7 +40,6 @@ class Kernel extends HttpKernel
|
||||
],
|
||||
|
||||
'api' => [
|
||||
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
||||
'throttle:api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
@@ -17,17 +15,15 @@ class RouteServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->configureRateLimiting();
|
||||
|
||||
$this->routes(
|
||||
function () {
|
||||
Route::get(
|
||||
'/status',
|
||||
function () {
|
||||
'/health',
|
||||
function (Request $request) {
|
||||
return response()->json(
|
||||
[
|
||||
'status' => 'ok',
|
||||
'time' => microtime(true) - LARAVEL_START
|
||||
'time' => microtime(true) - $request->attributes->get('request_start_time')
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -35,19 +31,4 @@ class RouteServiceProvider extends ServiceProvider
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the rate limiters for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function configureRateLimiting()
|
||||
{
|
||||
RateLimiter::for(
|
||||
'api',
|
||||
function (Request $request) {
|
||||
return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,31 +7,42 @@
|
||||
"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",
|
||||
"appstract/laravel-opcache": "^4.0",
|
||||
"fleetbase/core-api": "^1.6.2",
|
||||
"fleetbase/fleetops-api": "^0.6.5",
|
||||
"fleetbase/registry-bridge": "^0.0.18",
|
||||
"fleetbase/storefront-api": "^0.3.30",
|
||||
"fleetbase/core-api": "^1.6.24",
|
||||
"fleetbase/fleetops-api": "^0.6.25",
|
||||
"fleetbase/registry-bridge": "^0.1.0",
|
||||
"fleetbase/storefront-api": "^0.4.6",
|
||||
"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",
|
||||
@@ -78,15 +89,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": {
|
||||
|
||||
3030
api/composer.lock
generated
3030
api/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
'env' => env('APP_ENV', env('ENVIRONMENT', 'production')),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -17,11 +17,11 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'paths' => ['/*', 'sanctum/csrf-cookie'],
|
||||
'paths' => ['*', 'sanctum/csrf-cookie'],
|
||||
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
'allowed_origins' => array_filter(['http://localhost:4200', env('CONSOLE_HOST'), Utils::addWwwToUrl(env('CONSOLE_HOST'))]),
|
||||
'allowed_origins' => array_filter(['http://localhost:4200', env('CONSOLE_HOST'), Utils::addWwwToUrl(env('CONSOLE_HOST')), ...Utils::arrayFrom(env('FRONTEND_HOSTS', ''))]),
|
||||
|
||||
'allowed_origins_patterns' => [],
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use Fleetbase\Support\Utils;
|
||||
use Laravel\Octane\Contracts\OperationTerminated;
|
||||
use Laravel\Octane\Events\RequestHandled;
|
||||
use Laravel\Octane\Events\RequestReceived;
|
||||
@@ -192,6 +193,7 @@ return [
|
||||
'routes',
|
||||
'composer.lock',
|
||||
'.env',
|
||||
...Utils::arrayFrom(env('OCTANE_WATCH_DIRS'))
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
19
api/resources/lang/ar/auth.php
Normal file
19
api/resources/lang/ar/auth.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| أسطر لغة المصادقة
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| تحتوي الأسطر التالية على رسائل المصادقة التي نعرضها للمستخدم أثناء
|
||||
| عمليات تسجيل الدخول أو غيرها. يمكنك تعديل هذه الرسائل حسب متطلباتك.
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => 'بيانات الاعتماد هذه غير متطابقة مع سجلاتنا.',
|
||||
'password' => 'كلمة المرور التي تم إدخالها غير صحيحة.',
|
||||
'throttle' => 'عدد كبير جداً من محاولات الدخول. يرجى المحاولة مرة أخرى خلال :seconds ثانية.',
|
||||
|
||||
];
|
||||
19
api/resources/lang/ar/pagination.php
Normal file
19
api/resources/lang/ar/pagination.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| أسطر لغة الترقيم الصفحي
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| تُستخدم الأسطر التالية من قبل مكتبة الترقيم الصفحي لبناء روابط
|
||||
| الترقيم البسيطة. يمكنك تعديلها كما تشاء لتخصيص العرض بما يناسب
|
||||
| تطبيقك بشكل أفضل.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« السابق',
|
||||
'next' => 'التالي »',
|
||||
|
||||
];
|
||||
21
api/resources/lang/ar/passwords.php
Normal file
21
api/resources/lang/ar/passwords.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| أسطر لغة إعادة تعيين كلمة المرور
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| الأسطر التالية هي الرسائل الافتراضية التي يقدمها نظام إعادة تعيين
|
||||
| كلمة المرور عند فشل المحاولة، مثل رمز التحقق غير صالح أو كلمة مرور جديدة غير صحيحة.
|
||||
|
|
||||
*/
|
||||
|
||||
'reset' => 'تم إعادة تعيين كلمة المرور الخاصة بك!',
|
||||
'sent' => 'لقد أرسلنا رابط إعادة تعيين كلمة المرور إلى بريدك الإلكتروني!',
|
||||
'throttled' => 'يرجى الانتظار قبل المحاولة مرة أخرى.',
|
||||
'token' => 'رمز إعادة تعيين كلمة المرور هذا غير صالح.',
|
||||
'user' => 'لا يمكننا العثور على مستخدم بهذا العنوان الإلكتروني.',
|
||||
|
||||
];
|
||||
168
api/resources/lang/ar/validation.php
Normal file
168
api/resources/lang/ar/validation.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines contain the default error messages used by
|
||||
| the validator class. Some of these rules have multiple versions such
|
||||
| as the size rules. Feel free to tweak each of these messages here.
|
||||
|
|
||||
*/
|
||||
|
||||
'accepted' => 'يجب قبول :attribute.',
|
||||
'accepted_if' => 'يجب قبول :attribute عندما يكون :other يساوي :value.',
|
||||
'active_url' => ':attribute ليس عنوان URL صالحًا.',
|
||||
'after' => 'يجب أن يكون :attribute تاريخًا بعد :date.',
|
||||
'after_or_equal' => 'يجب أن يكون :attribute تاريخًا بعد أو يساوي :date.',
|
||||
'alpha' => 'يجب أن يحتوي :attribute على أحرف فقط.',
|
||||
'alpha_dash' => 'يجب أن يحتوي :attribute على أحرف وأرقام وشرطات فقط.',
|
||||
'alpha_num' => 'يجب أن يحتوي :attribute على أحرف وأرقام فقط.',
|
||||
'array' => 'يجب أن يكون :attribute مصفوفة.',
|
||||
'before' => 'يجب أن يكون :attribute تاريخًا قبل :date.',
|
||||
'before_or_equal' => 'يجب أن يكون :attribute تاريخًا قبل أو يساوي :date.',
|
||||
'between' => [
|
||||
'numeric' => 'يجب أن يكون :attribute بين :min و :max.',
|
||||
'file' => 'يجب أن يكون :attribute بين :min و :max كيلوبايت.',
|
||||
'string' => 'يجب أن يكون :attribute بين :min و :max حرفًا.',
|
||||
'array' => 'يجب أن يحتوي :attribute على عدد عناصر بين :min و :max.',
|
||||
],
|
||||
'boolean' => 'يجب أن يكون حقل :attribute صحيحًا أو خاطئًا.',
|
||||
'confirmed' => 'تأكيد :attribute غير متطابق.',
|
||||
'current_password' => 'كلمة المرور غير صحيحة.',
|
||||
'date' => ':attribute ليس تاريخًا صالحًا.',
|
||||
'date_equals' => 'يجب أن يكون :attribute تاريخًا يساوي :date.',
|
||||
'date_format' => 'لا يتطابق :attribute مع الصيغة :format.',
|
||||
'declined' => 'يجب رفض :attribute.',
|
||||
'declined_if' => 'يجب رفض :attribute عندما يكون :other يساوي :value.',
|
||||
'different' => 'يجب أن يكون :attribute و :other مختلفين.',
|
||||
'digits' => 'يجب أن يتكون :attribute من :digits أرقام.',
|
||||
'digits_between' => 'يجب أن يتكون :attribute من :min إلى :max أرقام.',
|
||||
'dimensions' => ':attribute يحتوي على أبعاد صورة غير صالحة.',
|
||||
'distinct' => 'حقل :attribute يحتوي على قيمة مكررة.',
|
||||
'email' => 'يجب أن يكون :attribute عنوان بريد إلكتروني صالحًا.',
|
||||
'ends_with' => 'يجب أن ينتهي :attribute بأحد القيم التالية: :values.',
|
||||
'enum' => ':attribute المحدد غير صالح.',
|
||||
'exists' => ':attribute المحدد غير صالح.',
|
||||
'file' => 'يجب أن يكون :attribute ملفًا.',
|
||||
'filled' => 'يجب أن يحتوي حقل :attribute على قيمة.',
|
||||
'gt' => [
|
||||
'numeric' => 'يجب أن يكون :attribute أكبر من :value.',
|
||||
'file' => 'يجب أن يكون :attribute أكبر من :value كيلوبايت.',
|
||||
'string' => 'يجب أن يكون :attribute أكبر من :value حرفًا.',
|
||||
'array' => 'يجب أن يحتوي :attribute على أكثر من :value عنصر.',
|
||||
],
|
||||
'gte' => [
|
||||
'numeric' => 'يجب أن يكون :attribute أكبر من أو يساوي :value.',
|
||||
'file' => 'يجب أن يكون :attribute أكبر من أو يساوي :value كيلوبايت.',
|
||||
'string' => 'يجب أن يكون :attribute أكبر من أو يساوي :value حرفًا.',
|
||||
'array' => 'يجب أن يحتوي :attribute على :value عنصر أو أكثر.',
|
||||
],
|
||||
'image' => 'يجب أن يكون :attribute صورة.',
|
||||
'in' => ':attribute المحدد غير صالح.',
|
||||
'in_array' => 'حقل :attribute غير موجود في :other.',
|
||||
'integer' => 'يجب أن يكون :attribute عددًا صحيحًا.',
|
||||
'ip' => 'يجب أن يكون :attribute عنوان IP صالحًا.',
|
||||
'ipv4' => 'يجب أن يكون :attribute عنوان IPv4 صالحًا.',
|
||||
'ipv6' => 'يجب أن يكون :attribute عنوان IPv6 صالحًا.',
|
||||
'json' => 'يجب أن يكون :attribute نصًا بصيغة JSON صالحة.',
|
||||
'lt' => [
|
||||
'numeric' => 'يجب أن يكون :attribute أقل من :value.',
|
||||
'file' => 'يجب أن يكون :attribute أقل من :value كيلوبايت.',
|
||||
'string' => 'يجب أن يكون :attribute أقل من :value حرفًا.',
|
||||
'array' => 'يجب أن يحتوي :attribute على أقل من :value عنصر.',
|
||||
],
|
||||
'lte' => [
|
||||
'numeric' => 'يجب أن يكون :attribute أقل من أو يساوي :value.',
|
||||
'file' => 'يجب أن يكون :attribute أقل من أو يساوي :value كيلوبايت.',
|
||||
'string' => 'يجب أن يكون :attribute أقل من أو يساوي :value حرفًا.',
|
||||
'array' => 'يجب ألا يحتوي :attribute على أكثر من :value عنصر.',
|
||||
],
|
||||
'mac_address' => 'يجب أن يكون :attribute عنوان MAC صالحًا.',
|
||||
'max' => [
|
||||
'numeric' => 'يجب ألا يتجاوز :attribute :max.',
|
||||
'file' => 'يجب ألا يتجاوز :attribute :max كيلوبايت.',
|
||||
'string' => 'يجب ألا يتجاوز :attribute :max حرفًا.',
|
||||
'array' => 'يجب ألا يحتوي :attribute على أكثر من :max عنصر.',
|
||||
],
|
||||
'mimes' => 'يجب أن يكون :attribute ملفًا من النوع: :values.',
|
||||
'mimetypes' => 'يجب أن يكون :attribute ملفًا من النوع: :values.',
|
||||
'min' => [
|
||||
'numeric' => 'يجب أن يكون :attribute على الأقل :min.',
|
||||
'file' => 'يجب أن يكون :attribute على الأقل :min كيلوبايت.',
|
||||
'string' => 'يجب أن يكون :attribute على الأقل :min حرفًا.',
|
||||
'array' => 'يجب أن يحتوي :attribute على الأقل على :min عنصر.',
|
||||
],
|
||||
'multiple_of' => 'يجب أن يكون :attribute مضاعفًا لـ :value.',
|
||||
'not_in' => ':attribute المحدد غير صالح.',
|
||||
'not_regex' => 'صيغة :attribute غير صالحة.',
|
||||
'numeric' => 'يجب أن يكون :attribute رقمًا.',
|
||||
'password' => 'كلمة المرور غير صحيحة.',
|
||||
'present' => 'يجب أن يكون حقل :attribute موجودًا.',
|
||||
'prohibited' => 'حقل :attribute محظور.',
|
||||
'prohibited_if' => 'حقل :attribute محظور عندما يكون :other يساوي :value.',
|
||||
'prohibited_unless' => 'حقل :attribute محظور إلا إذا كان :other ضمن :values.',
|
||||
'prohibits' => 'حقل :attribute يحظر وجود :other.',
|
||||
'regex' => 'صيغة :attribute غير صالحة.',
|
||||
'required' => 'حقل :attribute مطلوب.',
|
||||
'required_array_keys' => 'يجب أن يحتوي حقل :attribute على إدخالات لـ: :values.',
|
||||
'required_if' => 'حقل :attribute مطلوب عندما يكون :other يساوي :value.',
|
||||
'required_unless' => 'حقل :attribute مطلوب إلا إذا كان :other ضمن :values.',
|
||||
'required_with' => 'حقل :attribute مطلوب عند وجود :values.',
|
||||
'required_with_all' => 'حقل :attribute مطلوب عند وجود جميع القيم :values.',
|
||||
'required_without' => 'حقل :attribute مطلوب عند عدم وجود :values.',
|
||||
'required_without_all' => 'حقل :attribute مطلوب عند عدم وجود أي من القيم :values.',
|
||||
'same' => 'يجب أن يتطابق :attribute مع :other.',
|
||||
'size' => [
|
||||
'numeric' => 'يجب أن يكون :attribute مساويًا لـ :size.',
|
||||
'file' => 'يجب أن يكون :attribute مساويًا لـ :size كيلوبايت.',
|
||||
'string' => 'يجب أن يكون :attribute مساويًا لـ :size حرفًا.',
|
||||
'array' => 'يجب أن يحتوي :attribute على :size عنصر.',
|
||||
],
|
||||
'starts_with' => 'يجب أن يبدأ :attribute بأحد القيم التالية: :values.',
|
||||
'string' => 'يجب أن يكون :attribute نصًا.',
|
||||
'timezone' => 'يجب أن يكون :attribute منطقة زمنية صالحة.',
|
||||
'unique' => 'تم استخدام :attribute مسبقًا.',
|
||||
'uploaded' => 'فشل تحميل :attribute.',
|
||||
'url' => 'يجب أن يكون :attribute عنوان URL صالحًا.',
|
||||
'uuid' => 'يجب أن يكون :attribute UUID صالحًا.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify custom validation messages for attributes using the
|
||||
| convention "attribute.rule" to name the lines. This makes it quick to
|
||||
| specify a specific custom language line for a given attribute rule.
|
||||
|
|
||||
*/
|
||||
|
||||
'custom' => [
|
||||
'attribute-name' => [
|
||||
'rule-name' => 'رسالة مخصصة',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used to swap our attribute placeholder
|
||||
| with something more reader friendly such as "E-Mail Address" instead
|
||||
| of "email". This simply helps us make our message more expressive.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [],
|
||||
|
||||
];
|
||||
47
builds/linux/build-linux.sh
Normal file
47
builds/linux/build-linux.sh
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Resolve the directory the script is located in
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
APP_NAME="Fleetbase"
|
||||
IMAGE_NAME="fleetbase-linux-static"
|
||||
CONTAINER_NAME="fleetbase-linux-build"
|
||||
DIST_DIR="$ROOT_DIR/builds/dist"
|
||||
BINARY_NAME="fleetbase-linux-x86_64"
|
||||
DOCKERFILE="$ROOT_DIR/builds/linux/static-build.Dockerfile"
|
||||
|
||||
# Ensure pkg-config archive is available
|
||||
SPC_DOWNLOADS_DIR="$SCRIPT_DIR/spc/downloads"
|
||||
PKG_TAR="pkg-config-0.29.2.tar.gz"
|
||||
PKG_URL="https://static-php-cli.fra1.digitaloceanspaces.com/static-php-cli/deps/pkg-config/${PKG_TAR}"
|
||||
|
||||
if [[ ! -f "${SPC_DOWNLOADS_DIR}/${PKG_TAR}" ]]; then
|
||||
echo "📥 pkg-config archive missing – downloading..."
|
||||
mkdir -p "${SPC_DOWNLOADS_DIR}"
|
||||
curl -L --retry 3 -o "${SPC_DOWNLOADS_DIR}/${PKG_TAR}" "${PKG_URL}"
|
||||
else
|
||||
echo "✅ pkg-config archive already present."
|
||||
fi
|
||||
|
||||
# Build the image
|
||||
echo "📦 Building static Linux binary for ${APP_NAME}..."
|
||||
docker build -f "$DOCKERFILE" -t "$IMAGE_NAME" .
|
||||
|
||||
# Create a container from the built image
|
||||
echo "📦 Creating container to extract binary..."
|
||||
docker create --name "$CONTAINER_NAME" "$IMAGE_NAME"
|
||||
|
||||
# Make sure dist folder exist
|
||||
mkdir -p "$DIST_DIR"
|
||||
|
||||
# Copy binary from container to local dist folder
|
||||
echo "📂 Extracting binary..."
|
||||
docker cp "$CONTAINER_NAME:/go/src/app/dist/frankenphp-linux-x86_64" "$DIST_DIR/$BINARY_NAME"
|
||||
|
||||
# Cleanup the temp container
|
||||
docker rm "$CONTAINER_NAME"
|
||||
|
||||
echo "✅ Build complete! Binary is located at: $DIST_DIR/$BINARY_NAME"
|
||||
12
builds/linux/spc/libgeos-linux.php
Normal file
12
builds/linux/spc/libgeos-linux.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\builder\linux\library;
|
||||
|
||||
class libgeos extends LinuxLibraryBase
|
||||
{
|
||||
use \SPC\builder\unix\library\libgeos;
|
||||
|
||||
public const NAME = 'libgeos';
|
||||
}
|
||||
33
builds/linux/spc/libgeos-unix.php
Normal file
33
builds/linux/spc/libgeos-unix.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\builder\unix\library;
|
||||
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
use SPC\store\FileSystem;
|
||||
|
||||
trait libgeos
|
||||
{
|
||||
/**
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function build(): void
|
||||
{
|
||||
FileSystem::resetDir($this->source_dir . '/build');
|
||||
|
||||
shell()->cd($this->source_dir . '/build')
|
||||
->setEnv([
|
||||
'CFLAGS' => $this->getLibExtraCFlags(),
|
||||
'LDFLAGS' => $this->getLibExtraLdFlags(),
|
||||
'LIBS' => $this->getLibExtraLibs(),
|
||||
])
|
||||
->execWithEnv("cmake {$this->builder->makeCmakeArgs()} -DBUILD_SHARED_LIBS=OFF ..")
|
||||
->execWithEnv("make -j{$this->builder->concurrency}")
|
||||
->execWithEnv('make install');
|
||||
|
||||
$this->patchPkgconfPrefix(['geos.pc']);
|
||||
}
|
||||
}
|
||||
93
builds/linux/static-build.Dockerfile
Normal file
93
builds/linux/static-build.Dockerfile
Normal file
@@ -0,0 +1,93 @@
|
||||
# FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
|
||||
FROM --platform=linux/amd64 docker.io/dunglas/frankenphp:static-builder@sha256:821526b776a26502735d83890cc0a0d579348c510ba6c777df0762cb1c50d967
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
# Copy Fleetbase app
|
||||
COPY ../../api ./dist/app
|
||||
|
||||
# Set working directory to the embedded Fleetbase app
|
||||
WORKDIR /go/src/app/dist/app
|
||||
|
||||
# Setup for production environment
|
||||
ENV APP_ENV=production
|
||||
ENV APP_DEBUG=false
|
||||
ENV BROADCAST_DRIVER=socketcluster
|
||||
ENV OSRM_HOST="https://router.project-osrm.org"
|
||||
ENV REGISTRY_PREINSTALLED_EXTENSIONS=true
|
||||
|
||||
# Optional: Ensure writable storage
|
||||
RUN chmod -R 775 bootstrap/cache storage
|
||||
|
||||
# Set permissions for deploy script
|
||||
RUN chmod +x ./deploy.sh
|
||||
|
||||
# Move back to main app directory before running build-static.sh
|
||||
WORKDIR /go/src/app
|
||||
|
||||
# Install geos lib
|
||||
RUN apk add --no-cache geos geos-dev
|
||||
|
||||
# Inject the libgeos library handlers
|
||||
COPY ./builds/linux/spc/libgeos-linux.php ./dist/static-php-cli/src/SPC/builder/linux/library/libgeos.php
|
||||
COPY ./builds/linux/spc/libgeos-unix.php ./dist/static-php-cli/src/SPC/builder/unix/library/libgeos.php
|
||||
|
||||
# Patch source.json to add geos extension source
|
||||
RUN jq '. + {"php-geos": {"type": "url", "url": "https://github.com/libgeos/php-geos/archive/dfe1ab17b0f155cc315bc13c75689371676e02e1.zip", "license": [{"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/MIT-LICENSE"}, {"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/LGPL-2"}]}}' \
|
||||
./dist/static-php-cli/config/source.json > ./dist/static-php-cli/config/source.tmp.json && \
|
||||
mv ./dist/static-php-cli/config/source.tmp.json ./dist/static-php-cli/config/source.json
|
||||
|
||||
# Pathc source.json to add libgeos library
|
||||
RUN jq '. + {"libgeos": {"type": "url", "url": "https://download.osgeo.org/geos/geos-3.12.1.tar.bz2", "filename": "geos-3.12.1.tar.bz2", "extract": "geos-3.12.1", "build-dir": "build", "license": [{"type": "file", "path": "COPYING"}]}}' \
|
||||
./dist/static-php-cli/config/source.json > ./dist/static-php-cli/config/source.tmp.json && \
|
||||
mv ./dist/static-php-cli/config/source.tmp.json ./dist/static-php-cli/config/source.json
|
||||
|
||||
# Patch ext.json to add geos extension dynamically
|
||||
RUN jq '. + {"geos": {"type": "external", "arg-type": "enable", "source": "php-geos", "lib-depends": ["libgeos"]}}' \
|
||||
./dist/static-php-cli/config/ext.json > ./dist/static-php-cli/config/ext.tmp.json && \
|
||||
mv ./dist/static-php-cli/config/ext.tmp.json ./dist/static-php-cli/config/ext.json
|
||||
|
||||
# Patch lib.json to add libgeos
|
||||
RUN jq '. + {"libgeos": {"source": "libgeos", "static-libs-unix": ["libgeos.a", "libgeos_c.a"]}}' \
|
||||
./dist/static-php-cli/config/lib.json > ./dist/static-php-cli/config/lib.tmp.json && \
|
||||
mv ./dist/static-php-cli/config/lib.tmp.json ./dist/static-php-cli/config/lib.json
|
||||
|
||||
# Install dependencies for SPC CLI
|
||||
WORKDIR /go/src/app/dist/static-php-cli
|
||||
RUN composer install --no-dev -a
|
||||
|
||||
# Set PHP extensions to be built (including geos!)
|
||||
ENV PHP_EXTENSIONS="pdo_mysql,gd,bcmath,redis,intl,zip,gmp,apcu,opcache,imagick,sockets,pcntl,geos,iconv,mbstring,fileinfo,ctype,tokenizer,simplexml,dom,filter,session"
|
||||
ENV PHP_EXTENSION_LIBS="libgeos,libzip,bzip2,libxml2,openssl,zlib"
|
||||
|
||||
# Force SPC to use the local source version (not download binary)
|
||||
ENV SPC_REL_TYPE=source
|
||||
|
||||
# Debug build
|
||||
ENV SPC_LOG_LEVEL=debug
|
||||
|
||||
# Skip compression
|
||||
ENV NO_COMPRESS=1
|
||||
|
||||
# set PHP version
|
||||
ENV PHP_VERSION=8.2
|
||||
|
||||
# Move to the app directory
|
||||
WORKDIR /go/src/app
|
||||
|
||||
# Make sure pkg-config is available within the static build container
|
||||
COPY ./builds/linux/spc/downloads/pkg-config-0.29.2.tar.gz ./dist/static-php-cli/downloads/pkg-config-0.29.2.tar.gz
|
||||
|
||||
# Pre-build pkg-config using the existing tarball
|
||||
RUN apk add --no-cache build-base && \
|
||||
tar -xzf ./dist/static-php-cli/downloads/pkg-config-0.29.2.tar.gz -C /tmp && \
|
||||
cd /tmp/pkg-config-0.29.2 && \
|
||||
./configure --with-internal-glib --prefix=/go/src/app/dist/static-php-cli/build/bin && \
|
||||
make && make install && \
|
||||
rm -rf /tmp/pkg-config-0.29.2
|
||||
|
||||
# Do not run git pull
|
||||
RUN sed -i 's/^[ \t]*git pull/# git pull/' ./build-static.sh
|
||||
|
||||
# Build the FrankenPHP static binary
|
||||
RUN EMBED=dist/app ./build-static.sh
|
||||
188
builds/osx/build-osx.sh
Executable file
188
builds/osx/build-osx.sh
Executable file
@@ -0,0 +1,188 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
log() {
|
||||
echo -e "\033[1;34m[🔧 $1]\033[0m"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "\033[1;32m[✅ $1]\033[0m"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "\033[1;33m[⚠️ $1]\033[0m"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "\033[1;31m[❌ $1]\033[0m"
|
||||
}
|
||||
|
||||
# Define base paths
|
||||
log "Resolving directories..."
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
OSX_DIR="$ROOT_DIR/builds/osx"
|
||||
DIST_DIR="$ROOT_DIR/builds/dist"
|
||||
APP_DIR="$ROOT_DIR/api"
|
||||
BREW_PREFIX="/opt/homebrew"
|
||||
|
||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
ARCH="$(uname -m)"
|
||||
BINARY_NAME="fleetbase-$OS-$ARCH"
|
||||
|
||||
log "Binary will be: $BINARY_NAME"
|
||||
|
||||
# Setup PHP 8.4
|
||||
log "Detecting current PHP version..."
|
||||
ORIGINAL_PHP_PATH="$(which php)"
|
||||
ORIGINAL_PHP_VERSION="$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION.".".PHP_RELEASE_VERSION;' 2>/dev/null)"
|
||||
IS_ASDF_MANAGED=false
|
||||
|
||||
if [[ "$ORIGINAL_PHP_PATH" == *".asdf"* ]]; then
|
||||
IS_ASDF_MANAGED=true
|
||||
fi
|
||||
|
||||
# 🔁 Trap to restore PHP when script exits
|
||||
trap 'if [ "$IS_ASDF_MANAGED" = true ]; then
|
||||
log "Restoring asdf-managed PHP version: $ORIGINAL_PHP_VERSION"
|
||||
asdf set php "$ORIGINAL_PHP_VERSION" || true
|
||||
log "Reverted to PHP $(php -v | head -n 1)"
|
||||
else
|
||||
log "Unsetting asdf set to restore system PHP"
|
||||
asdf set php system || true
|
||||
log "Reverted to PHP $(php -v | head -n 1)"
|
||||
fi' EXIT
|
||||
|
||||
log "Detected PHP version: $ORIGINAL_PHP_VERSION"
|
||||
log "Detected PHP binary: $ORIGINAL_PHP_PATH"
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────────────
|
||||
# If the *current* php is already 8.4.x, we skip the entire asdf install step
|
||||
# ───────────────────────────────────────────────────────────────────────────────
|
||||
if [[ "$ORIGINAL_PHP_PATH" == "$BREW_PREFIX/bin/php" && "$ORIGINAL_PHP_VERSION" =~ ^8\.4\. ]]; then
|
||||
log "Homebrew PHP $ORIGINAL_PHP_VERSION detected at $ORIGINAL_PHP_PATH — skipping asdf build/install."
|
||||
else
|
||||
# Only install under asdf if we don’t already have 8.4.0 installed
|
||||
log "No Homebrew PHP 8.4 detected (found $ORIGINAL_PHP_PATH $ORIGINAL_PHP_VERSION), using asdf to build/install."
|
||||
if ! asdf list php | grep -q "8.4.0"; then
|
||||
# Use brew to install required dependencies for asdf php management
|
||||
log "Checking and installing Homebrew packages required for PHP 8.4 build..."
|
||||
|
||||
for pkg in autoconf automake bison freetype gd gettext icu4c krb5 libedit libiconv libjpeg libpng libxml2 libzip pkg-config re2c zlib sqlite3 libsodium oniguruma openssl@3 nasm; do
|
||||
if ! brew list "$pkg" &>/dev/null; then
|
||||
log_warn "$pkg not found. Installing..."
|
||||
arch -arm64 brew install "$pkg"
|
||||
else
|
||||
log "$pkg already installed. Skipping."
|
||||
fi
|
||||
done
|
||||
|
||||
# Set necessary env flags/paths for PHP build on OSX ARM64
|
||||
export CPPFLAGS="-I$BREW_PREFIX/opt/oniguruma/include -I$BREW_PREFIX/opt/libsodium/include -I$BREW_PREFIX/opt/bzip2/include -I$BREW_PREFIX/opt/zlib/include -I$BREW_PREFIX/opt/openssl@3/include -I$BREW_PREFIX/opt/libxml2/include -I$BREW_PREFIX/opt/libedit/include -I$BREW_PREFIX/opt/curl/include -I$BREW_PREFIX/opt/sqlite3/include -I$BREW_PREFIX/opt/freetype/include -I$BREW_PREFIX/opt/jpeg/include -I$BREW_PREFIX/opt/libpng/include -I$BREW_PREFIX/opt/libzip/include"
|
||||
export LDFLAGS="-L$BREW_PREFIX/opt/openssl@3/lib -lssl -lcrypto -lz -L$BREW_PREFIX/opt/oniguruma/lib -lonig -L$BREW_PREFIX/opt/libsodium/lib -lsodium -L$BREW_PREFIX/opt/bzip2/lib -Wl,-rpath,$BREW_PREFIX/opt/bzip2/lib -lbz2 -L$BREW_PREFIX/opt/zlib/lib -L$BREW_PREFIX/opt/openssl@3/lib -L$BREW_PREFIX/opt/libxml2/lib -L$BREW_PREFIX/opt/libedit/lib -L$BREW_PREFIX/opt/sqlite3/lib -lsqlite3 -L$BREW_PREFIX/opt/curl/lib -lcurl -L$BREW_PREFIX/opt/freetype/lib -L$BREW_PREFIX/opt/jpeg/lib -L$BREW_PREFIX/opt/libpng/lib -L$BREW_PREFIX/opt/libzip/lib -lzip -lz"
|
||||
export PKG_CONFIG_PATH="$BREW_PREFIX/opt/openssl/lib/pkgconfig:$BREW_PREFIX/opt/oniguruma/lib/pkgconfig:$BREW_PREFIX/opt/libsodium/lib/pkgconfig:$BREW_PREFIX/opt/libzip/lib/pkgconfig:$BREW_PREFIX/opt/gd/lib/pkgconfig:$BREW_PREFIX/opt/zlib/lib/pkgconfig:$BREW_PREFIX/opt/openssl@3/lib/pkgconfig:$BREW_PREFIX/opt/libxml2/lib/pkgconfig:$BREW_PREFIX/opt/curl/lib/pkgconfig:$BREW_PREFIX/opt/sqlite3/lib/pkgconfig:$BREW_PREFIX/opt/freetype/lib/pkgconfig:$BREW_PREFIX/opt/jpeg/lib/pkgconfig:$BREW_PREFIX/opt/libpng/lib/pkgconfig"
|
||||
export PHP_CONFIGURE_OPTIONS="--with-openssl=$(brew --prefix openssl) --with-iconv=$(brew --prefix libiconv)"
|
||||
|
||||
log "Installing PHP 8.4.0 with asdf..."
|
||||
asdf install php 8.4.0 --verbose
|
||||
else
|
||||
log "asdf already has PHP 8.4.0 installed, skipping"
|
||||
fi
|
||||
|
||||
log "Switching to PHP 8.4.0 with asdf set..."
|
||||
asdf set php 8.4.0 --home
|
||||
fi
|
||||
|
||||
# Clone FrankenPHP
|
||||
if [ ! -d "$OSX_DIR/frankenphp" ]; then
|
||||
log "Cloning FrankenPHP..."
|
||||
git clone https://github.com/dunglas/frankenphp "$OSX_DIR/frankenphp"
|
||||
else
|
||||
log_warn "FrankenPHP already cloned. Skipping."
|
||||
fi
|
||||
|
||||
cd "$OSX_DIR/frankenphp"
|
||||
|
||||
# Patch build script
|
||||
log "Patching build-static.sh to skip git pull..."
|
||||
sed -i '' 's/^[ \t]*git pull/# git pull/' ./build-static.sh
|
||||
|
||||
# Set environment variables
|
||||
log "Exporting build environment variables..."
|
||||
export PHP_VERSION=8.2
|
||||
export PHP_EXTENSIONS="pdo_mysql,gd,bcmath,redis,intl,zip,gmp,apcu,opcache,imagick,sockets,pcntl,geos,iconv,mbstring,fileinfo,ctype,tokenizer,simplexml,dom,filter,session"
|
||||
export PHP_EXTENSION_LIBS="libgeos,libzip,bzip2,libxml2,openssl,zlib"
|
||||
export SPC_REL_TYPE=source
|
||||
export NO_COMPRESS=1
|
||||
export SPC_OPT_BUILD_ARGS="--debug"
|
||||
export CMAKE_OSX_ARCHITECTURES=arm64
|
||||
|
||||
# Clone and prepare static-php-cli in dist/
|
||||
STATIC_PHP_CLI_DIR="$OSX_DIR/frankenphp/dist/static-php-cli"
|
||||
if [ ! -d "$STATIC_PHP_CLI_DIR" ]; then
|
||||
log "Cloning static-php-cli into dist/..."
|
||||
git clone --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
|
||||
|
||||
# Inject libgeos support
|
||||
log "Injecting libgeos patch files..."
|
||||
cp "$ROOT_DIR/builds/osx/spc/libgeos-unix.php" "$STATIC_PHP_CLI_DIR/src/SPC/builder/unix/library/libgeos.php"
|
||||
cp "$ROOT_DIR/builds/osx/spc/libgeos-macos.php" "$STATIC_PHP_CLI_DIR/src/SPC/builder/macos/library/libgeos.php"
|
||||
cp "$ROOT_DIR/builds/osx/spc/UnixBuilderBase-macos.php" "$STATIC_PHP_CLI_DIR/src/SPC/builder/unix/UnixBuilderBase.php"
|
||||
|
||||
# Patch SPC config
|
||||
log "Patching SPC config files (source.json, ext.json, lib.json)..."
|
||||
jq '. + {"php-geos": {"type": "url", "url": "https://github.com/libgeos/php-geos/archive/dfe1ab17b0f155cc315bc13c75689371676e02e1.zip", "license": [{"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/MIT-LICENSE"}, {"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/LGPL-2"}]}}' \
|
||||
"$STATIC_PHP_CLI_DIR/config/source.json" > "$STATIC_PHP_CLI_DIR/config/source.tmp.json" && \
|
||||
mv "$STATIC_PHP_CLI_DIR/config/source.tmp.json" "$STATIC_PHP_CLI_DIR/config/source.json"
|
||||
|
||||
jq '. + {"libgeos": {"type": "url", "url": "https://download.osgeo.org/geos/geos-3.12.1.tar.bz2", "filename": "geos-3.12.1.tar.bz2", "extract": "geos-3.12.1", "build-dir": "build", "license": [{"type": "file", "path": "COPYING"}]}}' \
|
||||
"$STATIC_PHP_CLI_DIR/config/source.json" > "$STATIC_PHP_CLI_DIR/config/source.tmp.json" && \
|
||||
mv "$STATIC_PHP_CLI_DIR/config/source.tmp.json" "$STATIC_PHP_CLI_DIR/config/source.json"
|
||||
|
||||
jq '. + {"libgeos": {"source": "libgeos", "static-libs-unix": ["libgeos.a", "libgeos_c.a"]}}' \
|
||||
"$STATIC_PHP_CLI_DIR/config/lib.json" > "$STATIC_PHP_CLI_DIR/config/lib.tmp.json" && \
|
||||
mv "$STATIC_PHP_CLI_DIR/config/lib.tmp.json" "$STATIC_PHP_CLI_DIR/config/lib.json"
|
||||
|
||||
jq '. + {"geos": {"type": "external", "arg-type": "enable", "source": "php-geos", "lib-depends": ["libgeos"]}}' \
|
||||
"$STATIC_PHP_CLI_DIR/config/ext.json" > "$STATIC_PHP_CLI_DIR/config/ext.tmp.json" && \
|
||||
mv "$STATIC_PHP_CLI_DIR/config/ext.tmp.json" "$STATIC_PHP_CLI_DIR/config/ext.json"
|
||||
|
||||
# Prepare app embed folder
|
||||
log "📦 Preparing embedded app directory..."
|
||||
rm -rf ./dist/app
|
||||
mkdir -p ./dist/app
|
||||
cp -R "$APP_DIR"/* ./dist/app/
|
||||
|
||||
log "Patching build-static.sh to skip git pull and composer install..."
|
||||
|
||||
# Skip `git pull`
|
||||
sed -i '' 's/^[[:space:]]*git pull/# git pull/' "$OSX_DIR/frankenphp/build-static.sh"
|
||||
|
||||
# Patch add CoreServices framework for Caddy build on OSX
|
||||
sed -i '' 's/-framework CoreFoundation -framework SystemConfiguration/& -framework CoreServices/' "$OSX_DIR/frankenphp/build-static.sh"
|
||||
|
||||
# ── work around 403 on GH macOS runners ────────────────────────────────────────
|
||||
log "Patching curl to use a browser-like User-Agent (to avoid 403s)…"
|
||||
curl() {
|
||||
command curl -sSL -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15" "$@"
|
||||
}
|
||||
export -f curl
|
||||
|
||||
# Build the binary
|
||||
log "⚙️ Running FrankenPHP build-static.sh..."
|
||||
EMBED=dist/app ./build-static.sh
|
||||
|
||||
# Move built binary to dist
|
||||
log "Moving built binary to output folder..."
|
||||
mkdir -p "$DIST_DIR"
|
||||
mv dist/frankenphp-mac-$ARCH "$DIST_DIR/$BINARY_NAME"
|
||||
|
||||
log_success "✅ macOS binary built at: $DIST_DIR/$BINARY_NAME"
|
||||
|
||||
# Clean up frankenphp build and app embed folder
|
||||
log "🧹 Cleaning temporary app directory..."
|
||||
rm -rf "$OSX_DIR/frankenphp"
|
||||
271
builds/osx/spc/UnixBuilderBase-macos.php
Normal file
271
builds/osx/spc/UnixBuilderBase-macos.php
Normal file
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\builder\unix;
|
||||
|
||||
use SPC\builder\BuilderBase;
|
||||
use SPC\builder\linux\LinuxBuilder;
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
use SPC\exception\WrongUsageException;
|
||||
use SPC\store\Config;
|
||||
use SPC\store\FileSystem;
|
||||
use SPC\util\DependencyUtil;
|
||||
use SPC\util\SPCConfigUtil;
|
||||
|
||||
abstract class UnixBuilderBase extends BuilderBase
|
||||
{
|
||||
/** @var string cflags */
|
||||
public string $arch_c_flags;
|
||||
|
||||
/** @var string C++ flags */
|
||||
public string $arch_cxx_flags;
|
||||
|
||||
/** @var string cmake toolchain file */
|
||||
public string $cmake_toolchain_file;
|
||||
|
||||
/**
|
||||
* @throws WrongUsageException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public function getAllStaticLibFiles(): array
|
||||
{
|
||||
$libs = [];
|
||||
|
||||
// reorder libs
|
||||
foreach ($this->libs as $lib) {
|
||||
foreach ($lib->getDependencies() as $dep) {
|
||||
$libs[] = $dep;
|
||||
}
|
||||
$libs[] = $lib;
|
||||
}
|
||||
|
||||
$libFiles = [];
|
||||
$libNames = [];
|
||||
// merge libs
|
||||
foreach ($libs as $lib) {
|
||||
if (!in_array($lib::NAME, $libNames, true)) {
|
||||
$libNames[] = $lib::NAME;
|
||||
array_unshift($libFiles, ...$lib->getStaticLibs());
|
||||
}
|
||||
}
|
||||
return array_map(fn ($x) => realpath(BUILD_LIB_PATH . "/{$x}"), $libFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return generic cmake options when configuring cmake projects
|
||||
*/
|
||||
public function makeCmakeArgs(): string
|
||||
{
|
||||
$extra = $this instanceof LinuxBuilder ? '-DCMAKE_C_COMPILER=' . getenv('CC') . ' ' : '';
|
||||
|
||||
// NEW: allow env-variable override
|
||||
$arch = getenv('CMAKE_OSX_ARCHITECTURES') ?: 'arm64';
|
||||
|
||||
return $extra .
|
||||
'-DCMAKE_BUILD_TYPE=Release ' .
|
||||
'-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' .
|
||||
'-DCMAKE_INSTALL_BINDIR=bin ' .
|
||||
'-DCMAKE_INSTALL_LIBDIR=lib ' .
|
||||
'-DCMAKE_INSTALL_INCLUDEDIR=include ' .
|
||||
"-DCMAKE_OSX_ARCHITECTURES={$arch} " .
|
||||
"-DCMAKE_TOOLCHAIN_FILE={$this->cmake_toolchain_file}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate configure flags
|
||||
*/
|
||||
public function makeAutoconfFlags(int $flag = AUTOCONF_ALL): string
|
||||
{
|
||||
$extra = '';
|
||||
// TODO: add auto pkg-config support
|
||||
if (($flag & AUTOCONF_LIBS) === AUTOCONF_LIBS) {
|
||||
$extra .= 'LIBS="' . BUILD_LIB_PATH . '" ';
|
||||
}
|
||||
if (($flag & AUTOCONF_CFLAGS) === AUTOCONF_CFLAGS) {
|
||||
$extra .= 'CFLAGS="-I' . BUILD_INCLUDE_PATH . '" ';
|
||||
}
|
||||
if (($flag & AUTOCONF_CPPFLAGS) === AUTOCONF_CPPFLAGS) {
|
||||
$extra .= 'CPPFLAGS="-I' . BUILD_INCLUDE_PATH . '" ';
|
||||
}
|
||||
if (($flag & AUTOCONF_LDFLAGS) === AUTOCONF_LDFLAGS) {
|
||||
$extra .= 'LDFLAGS="-L' . BUILD_LIB_PATH . '" ';
|
||||
}
|
||||
return $extra;
|
||||
}
|
||||
|
||||
public function proveLibs(array $sorted_libraries): void
|
||||
{
|
||||
// search all supported libs
|
||||
$support_lib_list = [];
|
||||
$classes = FileSystem::getClassesPsr4(
|
||||
ROOT_DIR . '/src/SPC/builder/' . osfamily2dir() . '/library',
|
||||
'SPC\builder\\' . osfamily2dir() . '\library'
|
||||
);
|
||||
foreach ($classes as $class) {
|
||||
if (defined($class . '::NAME') && $class::NAME !== 'unknown' && Config::getLib($class::NAME) !== null) {
|
||||
$support_lib_list[$class::NAME] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
// if no libs specified, compile all supported libs
|
||||
if ($sorted_libraries === [] && $this->isLibsOnly()) {
|
||||
$libraries = array_keys($support_lib_list);
|
||||
$sorted_libraries = DependencyUtil::getLibs($libraries);
|
||||
}
|
||||
|
||||
// add lib object for builder
|
||||
foreach ($sorted_libraries as $library) {
|
||||
if (!in_array(Config::getLib($library, 'type', 'lib'), ['lib', 'package'])) {
|
||||
continue;
|
||||
}
|
||||
// if some libs are not supported (but in config "lib.json", throw exception)
|
||||
if (!isset($support_lib_list[$library])) {
|
||||
throw new WrongUsageException('library [' . $library . '] is in the lib.json list but not supported to compile, but in the future I will support it!');
|
||||
}
|
||||
$lib = new ($support_lib_list[$library])($this);
|
||||
$this->addLib($lib);
|
||||
}
|
||||
|
||||
// calculate and check dependencies
|
||||
foreach ($this->libs as $lib) {
|
||||
$lib->calcDependency();
|
||||
}
|
||||
$this->lib_list = $sorted_libraries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanity check after build complete
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function sanityCheck(int $build_target): void
|
||||
{
|
||||
// sanity check for php-cli
|
||||
if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {
|
||||
logger()->info('running cli sanity check');
|
||||
[$ret, $output] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n -r "echo \"hello\";"');
|
||||
$raw_output = implode('', $output);
|
||||
if ($ret !== 0 || trim($raw_output) !== 'hello') {
|
||||
throw new RuntimeException("cli failed sanity check: ret[{$ret}]. out[{$raw_output}]");
|
||||
}
|
||||
|
||||
foreach ($this->getExts(false) as $ext) {
|
||||
logger()->debug('testing ext: ' . $ext->getName());
|
||||
$ext->runCliCheckUnix();
|
||||
}
|
||||
}
|
||||
|
||||
// sanity check for phpmicro
|
||||
if (($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) {
|
||||
$test_task = $this->getMicroTestTasks();
|
||||
foreach ($test_task as $task_name => $task) {
|
||||
$test_file = SOURCE_PATH . '/' . $task_name . '.exe';
|
||||
if (file_exists($test_file)) {
|
||||
@unlink($test_file);
|
||||
}
|
||||
file_put_contents($test_file, file_get_contents(SOURCE_PATH . '/php-src/sapi/micro/micro.sfx') . $task['content']);
|
||||
chmod($test_file, 0755);
|
||||
[$ret, $out] = shell()->execWithResult($test_file);
|
||||
foreach ($task['conditions'] as $condition => $closure) {
|
||||
if (!$closure($ret, $out)) {
|
||||
$raw_out = trim(implode('', $out));
|
||||
throw new RuntimeException("micro failed sanity check: {$task_name}, condition [{$condition}], ret[{$ret}], out[{$raw_out}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sanity check for embed
|
||||
if (($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED) {
|
||||
logger()->info('running embed sanity check');
|
||||
$sample_file_path = SOURCE_PATH . '/embed-test';
|
||||
if (!is_dir($sample_file_path)) {
|
||||
@mkdir($sample_file_path);
|
||||
}
|
||||
// copy embed test files
|
||||
copy(ROOT_DIR . '/src/globals/common-tests/embed.c', $sample_file_path . '/embed.c');
|
||||
copy(ROOT_DIR . '/src/globals/common-tests/embed.php', $sample_file_path . '/embed.php');
|
||||
$util = new SPCConfigUtil($this);
|
||||
$config = $util->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs'));
|
||||
$lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}";
|
||||
if (PHP_OS_FAMILY === 'Linux' && getenv('SPC_LIBC') === 'musl') {
|
||||
$lens .= ' -static';
|
||||
}
|
||||
[$ret, $out] = shell()->cd($sample_file_path)->execWithResult(getenv('CC') . ' -o embed embed.c ' . $lens);
|
||||
if ($ret !== 0) {
|
||||
throw new RuntimeException('embed failed sanity check: build failed. Error message: ' . implode("\n", $out));
|
||||
}
|
||||
// if someone changed to --enable-embed=shared, we need to add LD_LIBRARY_PATH
|
||||
if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') {
|
||||
$ext_path = 'LD_LIBRARY_PATH=' . BUILD_ROOT_PATH . '/lib:$LD_LIBRARY_PATH ';
|
||||
FileSystem::removeFileIfExists(BUILD_ROOT_PATH . '/lib/libphp.a');
|
||||
} else {
|
||||
$ext_path = '';
|
||||
FileSystem::removeFileIfExists(BUILD_ROOT_PATH . '/lib/libphp.so');
|
||||
}
|
||||
[$ret, $output] = shell()->cd($sample_file_path)->execWithResult($ext_path . './embed');
|
||||
if ($ret !== 0 || trim(implode('', $output)) !== 'hello') {
|
||||
throw new RuntimeException('embed failed sanity check: run failed. Error message: ' . implode("\n", $output));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将编译好的二进制文件发布到 buildroot
|
||||
*
|
||||
* @param int $type 发布类型
|
||||
* @throws RuntimeException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
protected function deployBinary(int $type): bool
|
||||
{
|
||||
$src = match ($type) {
|
||||
BUILD_TARGET_CLI => SOURCE_PATH . '/php-src/sapi/cli/php',
|
||||
BUILD_TARGET_MICRO => SOURCE_PATH . '/php-src/sapi/micro/micro.sfx',
|
||||
BUILD_TARGET_FPM => SOURCE_PATH . '/php-src/sapi/fpm/php-fpm',
|
||||
default => throw new RuntimeException('Deployment does not accept type ' . $type),
|
||||
};
|
||||
logger()->info('Deploying ' . $this->getBuildTypeName($type) . ' file');
|
||||
FileSystem::createDir(BUILD_ROOT_PATH . '/bin');
|
||||
shell()->exec('cp ' . escapeshellarg($src) . ' ' . escapeshellarg(BUILD_ROOT_PATH . '/bin/'));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run php clean
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function cleanMake(): void
|
||||
{
|
||||
logger()->info('cleaning up');
|
||||
shell()->cd(SOURCE_PATH . '/php-src')->exec('make clean');
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch phpize and php-config if needed
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
protected function patchPhpScripts(): void
|
||||
{
|
||||
// patch phpize
|
||||
if (file_exists(BUILD_BIN_PATH . '/phpize')) {
|
||||
logger()->debug('Patching phpize prefix');
|
||||
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'");
|
||||
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#');
|
||||
}
|
||||
// patch php-config
|
||||
if (file_exists(BUILD_BIN_PATH . '/php-config')) {
|
||||
logger()->debug('Patching php-config prefix and libs order');
|
||||
$php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config');
|
||||
$php_config_str = str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str);
|
||||
// move mimalloc to the beginning of libs
|
||||
$php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str);
|
||||
// move lstdc++ to the end of libs
|
||||
$php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str);
|
||||
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
builds/osx/spc/libgeos-macos.php
Normal file
12
builds/osx/spc/libgeos-macos.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\builder\macos\library;
|
||||
|
||||
class libgeos extends MacOSLibraryBase
|
||||
{
|
||||
use \SPC\builder\unix\library\libgeos;
|
||||
|
||||
public const NAME = 'libgeos';
|
||||
}
|
||||
33
builds/osx/spc/libgeos-unix.php
Normal file
33
builds/osx/spc/libgeos-unix.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\builder\unix\library;
|
||||
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
use SPC\store\FileSystem;
|
||||
|
||||
trait libgeos
|
||||
{
|
||||
/**
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function build(): void
|
||||
{
|
||||
FileSystem::resetDir($this->source_dir . '/build');
|
||||
|
||||
shell()->cd($this->source_dir . '/build')
|
||||
->setEnv([
|
||||
'CFLAGS' => $this->getLibExtraCFlags(),
|
||||
'LDFLAGS' => $this->getLibExtraLdFlags(),
|
||||
'LIBS' => $this->getLibExtraLibs(),
|
||||
])
|
||||
->execWithEnv("cmake {$this->builder->makeCmakeArgs()} -DBUILD_SHARED_LIBS=OFF ..")
|
||||
->execWithEnv("make -j{$this->builder->concurrency}")
|
||||
->execWithEnv('make install');
|
||||
|
||||
$this->patchPkgconfPrefix(['geos.pc']);
|
||||
}
|
||||
}
|
||||
@@ -6,19 +6,19 @@ WORKDIR /console
|
||||
|
||||
# Create the pnpm directory and set the PNPM_HOME environment variable
|
||||
RUN mkdir -p ~/.pnpm
|
||||
ENV PNPM_HOME /root/.pnpm
|
||||
ENV PNPM_HOME=/root/.pnpm
|
||||
|
||||
# Set environment
|
||||
ARG ENVIRONMENT=production
|
||||
|
||||
# Add the pnpm global bin to the PATH
|
||||
ENV PATH /root/.pnpm/bin:$PATH
|
||||
ENV PATH=/root/.pnpm/bin:$PATH
|
||||
|
||||
# Copy pnpm-lock.yaml (or package.json) into the directory /console in the container
|
||||
COPY console/package.json console/pnpm-lock.yaml ./
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# Copy over .npmrc if applicable
|
||||
COPY console/.npmr[c] ./
|
||||
COPY .npmr[c] ./
|
||||
|
||||
# Install global dependencies
|
||||
RUN npm install -g ember-cli pnpm
|
||||
@@ -33,7 +33,7 @@ RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
|
||||
RUN pnpm install
|
||||
|
||||
# Copy the console directory contents into the container at /console
|
||||
COPY console .
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN pnpm build --environment $ENVIRONMENT
|
||||
@@ -48,7 +48,7 @@ COPY --from=builder /console/dist /usr/share/nginx/html
|
||||
EXPOSE 4200
|
||||
|
||||
# Use custom nginx.conf
|
||||
COPY console/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Start Nginx server
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,57 +0,0 @@
|
||||
# ---- Build Stage ----
|
||||
FROM node:18.15.0-alpine
|
||||
|
||||
# Set the working directory in the container to /console
|
||||
WORKDIR /console
|
||||
|
||||
# Create the pnpm directory and set the PNPM_HOME environment variable
|
||||
RUN mkdir -p ~/.pnpm
|
||||
ENV PNPM_HOME /root/.pnpm
|
||||
|
||||
# Set environment
|
||||
ARG ENVIRONMENT=production
|
||||
|
||||
# Add the pnpm global bin to the PATH
|
||||
ENV PATH /root/.pnpm/bin:$PATH
|
||||
|
||||
# Copy pnpm-lock.yaml (or package.json) into the directory /console in the container
|
||||
COPY console/package.json console/pnpm-lock.yaml ./
|
||||
|
||||
# Copy over .npmrc if applicable
|
||||
COPY console/.npmr[c] ./
|
||||
|
||||
# Install global dependencies
|
||||
RUN npm install -g ember-cli pnpm
|
||||
|
||||
# Install git
|
||||
RUN apk update && apk add git openssh-client
|
||||
|
||||
# Trust GitHub's RSA host key
|
||||
RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
|
||||
|
||||
# Install app dependencies
|
||||
RUN pnpm install
|
||||
|
||||
# Copy the console directory contents into the container at /console
|
||||
COPY console .
|
||||
|
||||
# Build the application
|
||||
RUN pnpm build --environment $ENVIRONMENT
|
||||
|
||||
# # Make sure the build output is available in /console/dist
|
||||
# RUN ls -la /console/dist
|
||||
|
||||
# # ---- Serve Stage ----
|
||||
# FROM nginx:alpine
|
||||
|
||||
# # Copy the built app to our served directory
|
||||
# COPY --from=builder /console/dist /usr/share/nginx/html
|
||||
|
||||
# # Expose the port nginx is bound to
|
||||
# EXPOSE 4201
|
||||
|
||||
# # Use custom nginx.conf
|
||||
# COPY console/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# # Start Nginx server
|
||||
# CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -4,6 +4,8 @@ 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 applyRouterFix from './utils/router-refresh-patch';
|
||||
|
||||
export default class App extends Application {
|
||||
modulePrefix = config.modulePrefix;
|
||||
@@ -13,6 +15,7 @@ export default class App extends Application {
|
||||
engines = {};
|
||||
|
||||
async ready() {
|
||||
applyRouterFix(this);
|
||||
const extensions = await loadExtensions();
|
||||
|
||||
this.extensions = extensions;
|
||||
@@ -20,4 +23,11 @@ export default class App extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
loadInitializers(App, config.modulePrefix);
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadRuntimeConfig();
|
||||
loadInitializers(App, config.modulePrefix);
|
||||
|
||||
let fleetbase = App.create();
|
||||
fleetbase.deferReadiness();
|
||||
fleetbase.boot();
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class MetricComponent extends Component {
|
||||
@service fetch;
|
||||
|
||||
42
console/app/components/onboarding/form.hbs
Normal file
42
console/app/components/onboarding/form.hbs
Normal file
@@ -0,0 +1,42 @@
|
||||
<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>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
console/app/components/onboarding/verify-email.hbs
Normal file
78
console/app/components/onboarding/verify-email.hbs
Normal file
@@ -0,0 +1,78 @@
|
||||
{{page-title (t "onboard.verify-email.header-title")}}
|
||||
|
||||
{{#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}}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
console/app/components/onboarding/yield.hbs
Normal file
11
console/app/components/onboarding/yield.hbs
Normal file
@@ -0,0 +1,11 @@
|
||||
<section class="onboarding step-host">
|
||||
{{#if this.initialized}}
|
||||
{{#if this.currentComponent}}
|
||||
{{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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
|
||||
@@ -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,7 +2,7 @@ import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
import { task } from 'ember-concurrency';
|
||||
import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import { debug } from '@ember/debug';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class ConsoleAccountIndexController extends Controller {
|
||||
@@ -40,6 +42,18 @@ export default class ConsoleAccountIndexController extends Controller {
|
||||
*/
|
||||
@alias('currentUser.user') user;
|
||||
|
||||
/**
|
||||
* Available timezones for selection.
|
||||
*
|
||||
* @memberof ConsoleAccountIndexController
|
||||
*/
|
||||
@tracked timezones = [];
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.loadTimezones.perform();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle upload of new photo
|
||||
*
|
||||
@@ -116,6 +130,19 @@ export default class ConsoleAccountIndexController extends Controller {
|
||||
return isPasswordValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all available timezones from lookup.
|
||||
*
|
||||
* @memberof ConsoleAccountIndexController
|
||||
*/
|
||||
@task *loadTimezones() {
|
||||
try {
|
||||
this.timezones = yield this.fetch.get('lookup/timezones');
|
||||
} catch (error) {
|
||||
debug(`Unable to load timezones : ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any user attribute has been changed
|
||||
*
|
||||
|
||||
@@ -2,7 +2,7 @@ import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
import { task } from 'ember-concurrency';
|
||||
import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,8 @@ import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { debug } from '@ember/debug';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class ConsoleSettingsIndexController extends Controller {
|
||||
/**
|
||||
@@ -25,13 +27,6 @@ export default class ConsoleSettingsIndexController extends Controller {
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* The request loading state.
|
||||
*
|
||||
* @memberof ConsoleSettingsIndexController
|
||||
*/
|
||||
@tracked isLoading = false;
|
||||
|
||||
/**
|
||||
* the upload queue.
|
||||
*
|
||||
@@ -46,23 +41,32 @@ export default class ConsoleSettingsIndexController extends Controller {
|
||||
*/
|
||||
@tracked uploadedFiles = [];
|
||||
|
||||
/**
|
||||
* Available timezones for selection.
|
||||
*
|
||||
* @memberof ConsoleAccountIndexController
|
||||
*/
|
||||
@tracked timezones = [];
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.loadTimezones.perform();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the organization settings.
|
||||
*
|
||||
* @memberof ConsoleSettingsIndexController
|
||||
*/
|
||||
@action saveSettings(event) {
|
||||
event.preventDefault();
|
||||
this.isLoading = true;
|
||||
@task *saveSettings(event) {
|
||||
event?.preventDefault();
|
||||
|
||||
this.model
|
||||
.save()
|
||||
.then(() => {
|
||||
this.notifications.success('Organization changes successfully saved.');
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
try {
|
||||
yield this.model.save();
|
||||
this.notifications.success('Organization changes successfully saved.');
|
||||
} catch (error) {
|
||||
debug(`Unable to save organization settings : ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,4 +95,17 @@ export default class ConsoleSettingsIndexController extends Controller {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all available timezones from lookup.
|
||||
*
|
||||
* @memberof ConsoleAccountIndexController
|
||||
*/
|
||||
@task *loadTimezones() {
|
||||
try {
|
||||
this.timezones = yield this.fetch.get('lookup/timezones');
|
||||
} catch (error) {
|
||||
debug(`Unable to load timezones : ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,27 +2,28 @@ import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import createNotificationKey from '../../../utils/create-notification-key';
|
||||
import createNotificationKey from '@fleetbase/ember-core/utils/create-notification-key';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class ConsoleAdminNotificationsController extends Controller {
|
||||
export default class ConsoleSettingsNotificationsController extends Controller {
|
||||
/**
|
||||
* Inject the notifications service.
|
||||
*
|
||||
* @memberof ConsoleAdminNotificationsController
|
||||
* @memberof ConsoleSettingsNotificationsController
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Inject the fetch service.
|
||||
*
|
||||
* @memberof ConsoleAdminNotificationsController
|
||||
* @memberof ConsoleSettingsNotificationsController
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* The notification settings value JSON.
|
||||
*
|
||||
* @memberof ConsoleAdminNotificationsController
|
||||
* @memberof ConsoleSettingsNotificationsController
|
||||
* @var {Object}
|
||||
*/
|
||||
@tracked notificationSettings = {};
|
||||
@@ -30,26 +31,18 @@ export default class ConsoleAdminNotificationsController extends Controller {
|
||||
/**
|
||||
* Notification transport methods enabled.
|
||||
*
|
||||
* @memberof ConsoleAdminNotificationsController
|
||||
* @memberof ConsoleSettingsNotificationsController
|
||||
* @var {Array}
|
||||
*/
|
||||
@tracked notificationTransportMethods = ['email', 'sms'];
|
||||
|
||||
/**
|
||||
* Tracked property for the loading state
|
||||
*
|
||||
* @memberof ConsoleAdminNotificationsController
|
||||
* @var {Boolean}
|
||||
*/
|
||||
@tracked isLoading = false;
|
||||
|
||||
/**
|
||||
* Creates an instance of ConsoleAdminNotificationsController.
|
||||
* @memberof ConsoleAdminNotificationsController
|
||||
* Creates an instance of ConsoleSettingsNotificationsController.
|
||||
* @memberof ConsoleSettingsNotificationsController
|
||||
*/
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.getSettings();
|
||||
this.getSettings.perform();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +50,7 @@ export default class ConsoleAdminNotificationsController extends Controller {
|
||||
*
|
||||
* @param {Object} notification
|
||||
* @param {Array} notifiables
|
||||
* @memberof ConsoleAdminNotificationsController
|
||||
* @memberof ConsoleSettingsNotificationsController
|
||||
*/
|
||||
@action onSelectNotifiable(notification, notifiables) {
|
||||
const notificationKey = createNotificationKey(notification.definition, notification.name);
|
||||
@@ -83,7 +76,7 @@ export default class ConsoleAdminNotificationsController extends Controller {
|
||||
* Mutates the notification settings property.
|
||||
*
|
||||
* @param {Object} [_notificationSettings={}]
|
||||
* @memberof ConsoleAdminNotificationsController
|
||||
* @memberof ConsoleSettingsNotificationsController
|
||||
*/
|
||||
mutateNotificationSettings(_notificationSettings = {}) {
|
||||
this.notificationSettings = {
|
||||
@@ -93,44 +86,32 @@ export default class ConsoleAdminNotificationsController extends Controller {
|
||||
}
|
||||
|
||||
/**
|
||||
* Save notification settings to the server.
|
||||
* Save notification settings.
|
||||
*
|
||||
* @action
|
||||
* @method saveSettings
|
||||
* @returns {Promise}
|
||||
* @memberof ConsoleAdminNotificationsController
|
||||
* @memberof ConsoleSettingsNotificationsController
|
||||
*/
|
||||
@action saveSettings() {
|
||||
@task *saveSettings() {
|
||||
const { notificationSettings } = this;
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
return this.fetch
|
||||
.post('notifications/save-settings', { notificationSettings })
|
||||
.then(() => {
|
||||
this.notifications.success('Notification settings successfully saved.');
|
||||
})
|
||||
.catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
try {
|
||||
yield this.fetch.post('notifications/save-settings', { notificationSettings });
|
||||
this.notifications.success('Notification settings successfully saved.');
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and updates notification settings asynchronously.
|
||||
* Get notification settings.
|
||||
*
|
||||
* @returns {Promise<void>} A promise for successful retrieval and update, or an error on failure.
|
||||
* @memberof ConsoleSettingsNotificationsController
|
||||
*/
|
||||
getSettings() {
|
||||
return this.fetch
|
||||
.get('notifications/get-settings')
|
||||
.then(({ notificationSettings }) => {
|
||||
this.notificationSettings = notificationSettings;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
@task *getSettings() {
|
||||
try {
|
||||
const { notificationSettings } = yield this.fetch.get('notifications/get-settings');
|
||||
this.notificationSettings = notificationSettings;
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
import { task } from 'ember-concurrency';
|
||||
import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
|
||||
|
||||
export default class ConsoleSettingsTwoFaController extends Controller {
|
||||
|
||||
@@ -2,7 +2,7 @@ import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class InstallController extends Controller {
|
||||
@service fetch;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
import createNotificationKey from '../utils/create-notification-key';
|
||||
|
||||
export default helper(function getNotificationKey([definition, name]) {
|
||||
return createNotificationKey(definition, name);
|
||||
});
|
||||
41
console/app/initializers/load-intl-polyfills.js
Normal file
41
console/app/initializers/load-intl-polyfills.js
Normal file
@@ -0,0 +1,41 @@
|
||||
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 { initialize };
|
||||
@@ -14,5 +14,6 @@ export function initialize(application) {
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'load-leaflet',
|
||||
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,
|
||||
};
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -23,6 +23,7 @@ export default class UserModel extends Model {
|
||||
@attr('string') timezone;
|
||||
@attr('string') country;
|
||||
@attr('string') ip_address;
|
||||
@attr('string') aws_customer_id;
|
||||
@attr('string') slug;
|
||||
@attr('string') role_name;
|
||||
@attr('string') type;
|
||||
@@ -185,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() {
|
||||
@@ -206,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() {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import groupBy from '@fleetbase/ember-core/utils/group-by';
|
||||
|
||||
export default class ConsoleAdminNotificationsRoute extends Route {
|
||||
export default class ConsoleSettingsNotificationsRoute extends Route {
|
||||
@service fetch;
|
||||
|
||||
model() {
|
||||
@@ -14,6 +14,8 @@ export default class ConsoleAdminNotificationsRoute extends Route {
|
||||
}
|
||||
|
||||
setupController(controller, { registry, notifiables }) {
|
||||
super.setupController(...arguments);
|
||||
|
||||
controller.groupedNotifications = groupBy(registry, 'package');
|
||||
controller.notifiables = notifiables;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
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) {}
|
||||
39
console/app/services/onboarding-context.js
Normal file
39
console/app/services/onboarding-context.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import Service, { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class OnboardingContextService extends Service {
|
||||
@service appCache;
|
||||
@tracked data = {};
|
||||
|
||||
get(key) {
|
||||
return this.data[key] ?? this.appCache.get(`onboarding:context:${key}`);
|
||||
}
|
||||
|
||||
getFromCache(key) {
|
||||
return this.appCache.get(`onboarding:context:${key}`);
|
||||
}
|
||||
|
||||
set(key, value, options = {}) {
|
||||
this.data = { ...this.data, [key]: value };
|
||||
if (options?.persist === true) {
|
||||
this.appCache.set(`onboarding:context:${key}`, value);
|
||||
}
|
||||
}
|
||||
|
||||
persist(key, value) {
|
||||
this.set(key, value, { persist: true });
|
||||
}
|
||||
|
||||
del(key) {
|
||||
const { [key]: _drop, ...rest } = this.data; // eslint-disable-line no-unused-vars
|
||||
this.data = rest;
|
||||
this.appCache.set(`onboarding:context:${key}`, undefined);
|
||||
}
|
||||
|
||||
reset() {
|
||||
for (let key in this.data) {
|
||||
this.appCache.set(`onboarding:context:${key}`, undefined);
|
||||
}
|
||||
this.data = {};
|
||||
}
|
||||
}
|
||||
71
console/app/services/onboarding-orchestrator.js
Normal file
71
console/app/services/onboarding-orchestrator.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import Service from '@ember/service';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class OnboardingOrchestratorService extends Service {
|
||||
@service onboardingRegistry;
|
||||
@service onboardingContext;
|
||||
|
||||
@tracked flow = null;
|
||||
@tracked current = null;
|
||||
@tracked history = [];
|
||||
@tracked sessionId = null;
|
||||
|
||||
start(flowId = null, opts = {}) {
|
||||
const flow = this.onboardingRegistry.getFlow(flowId ?? this.onboardingRegistry.defaultFlow);
|
||||
if (!flow) throw new Error(`Onboarding flow '${flowId}' not found`);
|
||||
this.flow = flow;
|
||||
this.sessionId = opts.sessionId || null;
|
||||
this.history = [];
|
||||
this.goto(flow.entry);
|
||||
}
|
||||
|
||||
async goto(stepId) {
|
||||
if (!this.flow) throw new Error('No active onboarding flow');
|
||||
const step = this.flow.steps.find((s) => s.id === stepId);
|
||||
if (!step) throw new Error(`Step '${stepId}' not found`);
|
||||
|
||||
if (typeof step.guard === 'function' && !step.guard(this.onboardingContext)) {
|
||||
return this.next();
|
||||
}
|
||||
|
||||
if (typeof step.beforeEnter === 'function') {
|
||||
await step.beforeEnter(this.onboardingContext);
|
||||
}
|
||||
|
||||
this.current = step;
|
||||
}
|
||||
|
||||
async next() {
|
||||
if (!this.flow || !this.current) return;
|
||||
|
||||
const leaving = this.current;
|
||||
if (typeof leaving.afterLeave === 'function') {
|
||||
await leaving.afterLeave(this.onboardingContext);
|
||||
}
|
||||
|
||||
if (!this.history.includes(leaving)) this.history.push(leaving);
|
||||
|
||||
let nextId;
|
||||
if (typeof leaving.next === 'function') {
|
||||
nextId = leaving.next(this.onboardingContext);
|
||||
} else {
|
||||
nextId = leaving.next;
|
||||
}
|
||||
|
||||
if (!nextId) {
|
||||
this.current = null; // finished
|
||||
return;
|
||||
}
|
||||
|
||||
return this.goto(nextId);
|
||||
}
|
||||
|
||||
async back() {
|
||||
if (!this.flow || this.history.length === 0) return;
|
||||
const prev = this.history[this.history.length - 1];
|
||||
if (prev && prev.allowBack === false) return;
|
||||
this.history = this.history.slice(0, -1);
|
||||
await this.goto(prev.id);
|
||||
}
|
||||
}
|
||||
31
console/app/services/onboarding-registry.js
Normal file
31
console/app/services/onboarding-registry.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import Service from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class OnboardingRegistryService extends Service {
|
||||
flows = new Map();
|
||||
@tracked defaultFlow = 'default@v1';
|
||||
|
||||
useFlow(flowId) {
|
||||
this.defaultFlow = flowId;
|
||||
}
|
||||
|
||||
registerFlow(flow) {
|
||||
if (!flow || !flow.id || !flow.entry || !Array.isArray(flow.steps)) {
|
||||
throw new Error('Invalid FlowDef: id, entry, steps are required');
|
||||
}
|
||||
const ids = new Set(flow.steps.map((s) => s.id));
|
||||
if (!ids.has(flow.entry)) {
|
||||
throw new Error(`Flow '${flow.id}' entry '${flow.entry}' not found in steps`);
|
||||
}
|
||||
for (const s of flow.steps) {
|
||||
if (typeof s.next === 'string' && s.next && !ids.has(s.next)) {
|
||||
throw new Error(`Flow '${flow.id}' step '${s.id}' has unknown next '${s.next}'`);
|
||||
}
|
||||
}
|
||||
this.flows.set(flow.id, flow);
|
||||
}
|
||||
|
||||
getFlow(id) {
|
||||
return this.flows.get(id);
|
||||
}
|
||||
}
|
||||
114
console/app/services/user-verification.js
Normal file
114
console/app/services/user-verification.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import Service, { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { later } from '@ember/runloop';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class UserVerificationService extends Service {
|
||||
@service fetch;
|
||||
@service notifications;
|
||||
@service modalsManager;
|
||||
@service currentUser;
|
||||
@service router;
|
||||
@service session;
|
||||
@service intl;
|
||||
@tracked token;
|
||||
@tracked code;
|
||||
@tracked ready;
|
||||
@tracked waiting = false;
|
||||
|
||||
@action start(options = {}) {
|
||||
this.#wait(options?.timeout ?? 75000);
|
||||
}
|
||||
|
||||
@action didntReceiveCode() {
|
||||
this.waiting = true;
|
||||
}
|
||||
|
||||
@action validateInput(event) {
|
||||
const value = event instanceof HTMLElement ? event.value : (event?.target?.value ?? '');
|
||||
this.ready = value?.length > 5;
|
||||
}
|
||||
|
||||
@action resendBySms() {
|
||||
this.modalsManager.show('modals/verify-by-sms', {
|
||||
title: 'Verify Account by Phone',
|
||||
acceptButtonText: 'Send',
|
||||
phone: this.currentUser.phone,
|
||||
confirm: async (modal) => {
|
||||
modal.startLoading();
|
||||
const phone = modal.getOption('phone');
|
||||
if (!phone) {
|
||||
this.notifications.error('No phone number provided.');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.fetch.post('onboard/send-verification-sms', { phone, session: this.hello });
|
||||
this.notifications.success('Verification code SMS sent!');
|
||||
modal.done();
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
modal.stopLoading();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action resendEmail() {
|
||||
this.modalsManager.show('modals/resend-verification-email', {
|
||||
title: 'Resend Verification Code',
|
||||
acceptButtonText: 'Send',
|
||||
email: this.currentUser.email,
|
||||
confirm: async (modal) => {
|
||||
modal.startLoading();
|
||||
const email = modal.getOption('email');
|
||||
if (!email) {
|
||||
this.notifications.error('No email number provided.');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.fetch.post('onboard/send-verification-email', { email, session: this.hello });
|
||||
this.notifications.success('Verification code email sent!');
|
||||
modal.done();
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
modal.stopLoading();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@task *verifyCode() {
|
||||
try {
|
||||
const { status, token } = yield this.fetch.post('auth/verify-email', { token: this.token, code: this.code, email: this.email, authenticate: true });
|
||||
if (status === 'ok') {
|
||||
this.notifications.success('Email successfully verified!');
|
||||
|
||||
if (token) {
|
||||
this.notifications.info(`Welcome to ${this.intl.t('app.name')}`);
|
||||
this.session.manuallyAuthenticate(token);
|
||||
|
||||
return this.router.transitionTo('console');
|
||||
}
|
||||
|
||||
return this.router.transitionTo('auth.login');
|
||||
}
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
}
|
||||
|
||||
setToken(token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
setCode(code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
#wait(timeout = 75000) {
|
||||
return later(this, () => {
|
||||
this.waiting = true;
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{{page-title (t "app.name")}}
|
||||
<ModalsContainer />
|
||||
<NotificationContainer @position="top" @zindex="99999" />
|
||||
<BasicDropdownWormhole />
|
||||
<div id="application-root-wormhole"></div>
|
||||
{{outlet}}
|
||||
@@ -1,7 +1,16 @@
|
||||
{{page-title (t "app.name")}}
|
||||
<Layout::Container>
|
||||
<Layout::Header @brand={{@model}} @menuItems={{this.menuItems}} @organizationMenuItems={{this.organizationMenuItems}} @userMenuItems={{this.userMenuItems}} @onAction={{this.onAction}} @showSidebarToggle={{true}} @sidebarToggleEnabled={{this.sidebarToggleEnabled}} @onSidebarToggle={{this.onSidebarToggle}} />
|
||||
<Layout::Main>
|
||||
<Layout::Header
|
||||
@brand={{@model}}
|
||||
@menuItems={{this.menuItems}}
|
||||
@organizationMenuItems={{this.organizationMenuItems}}
|
||||
@userMenuItems={{this.userMenuItems}}
|
||||
@onAction={{this.onAction}}
|
||||
@showSidebarToggle={{true}}
|
||||
@sidebarToggleEnabled={{true}}
|
||||
@onSidebarToggle={{this.onSidebarToggle}}
|
||||
/>
|
||||
<Layout::Main class={{this.currentRouteClass}}>
|
||||
<Layout::Sidebar @onSetup={{this.setSidebarContext}}>
|
||||
<div class="next-sidebar-content-inner">
|
||||
<div role="menu" id="sidebar-menu-items">
|
||||
@@ -11,13 +20,21 @@
|
||||
<Layout::Section>
|
||||
{{outlet}}
|
||||
</Layout::Section>
|
||||
<ResourceContextPanel />
|
||||
</Layout::Main>
|
||||
<Layout::MobileNavbar @brand={{@model}} @user={{this.user}} @organizations={{this.organizations}} @menuItems={{this.menuItems}} @extensions={{this.extensions}} @onAction={{this.onAction}} />
|
||||
<Layout::MobileNavbar
|
||||
@brand={{@model}}
|
||||
@user={{this.user}}
|
||||
@organizations={{this.organizations}}
|
||||
@menuItems={{this.menuItems}}
|
||||
@extensions={{this.extensions}}
|
||||
@onAction={{this.onAction}}
|
||||
/>
|
||||
</Layout::Container>
|
||||
<ChatContainer />
|
||||
<ConsoleWormhole />
|
||||
<ImpersonatorTray />
|
||||
{{!-- template-lint-disable no-potential-path-strings --}}
|
||||
{{! template-lint-disable no-potential-path-strings }}
|
||||
<RegistryYield @registry="@fleetbase/console" as |RegistryComponent|>
|
||||
<RegistryComponent @controller={{this}} />
|
||||
</RegistryYield>
|
||||
</RegistryYield>
|
||||
@@ -4,7 +4,7 @@
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<ContentPanel @title="Change Password" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="Change Password" @open={{true}} @wrapperClass="bordered-classic">
|
||||
<form id="change-password-form" aria-label="change-password" {{on "submit" (perform this.changePassword)}}>
|
||||
<legend class="mb-3">Change Password</legend>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
@@ -16,7 +16,7 @@
|
||||
</ContentPanel>
|
||||
|
||||
{{#if this.isSystemTwoFaEnabled}}
|
||||
<ContentPanel @title="2FA Settings" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="2FA Settings" @open={{true}} @wrapperClass="bordered-classic">
|
||||
<div class="mb-3">
|
||||
{{#if this.loadUserTwoFaSettings.isIdle}}
|
||||
<TwoFaSettings @twoFaMethods={{this.methods}} @twoFaSettings={{this.twoFaSettings}} @onTwoFaToggled={{this.onTwoFaToggled}} @onTwoFaMethodSelected={{this.onTwoFaMethodSelected}} />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto">
|
||||
<ContentPanel @title={{t "common.your-profile"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title={{t "common.your-profile"}} @open={{true}} @wrapperClass="bordered-classic">
|
||||
<form class="flex flex-col md:flex-row" {{on "submit" (perform this.saveProfile)}}>
|
||||
<div class="w-32 flex flex-col justify-center mb-6 mr-6">
|
||||
<Image src={{this.user.avatar_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{this.user.name}} class="w-32 h-32 rounded-md" />
|
||||
@@ -33,9 +33,12 @@
|
||||
<PhoneInput @value={{this.user.phone}} @onInput={{fn (mut this.user.phone)}} class="form-input input-lg w-full" />
|
||||
</InputGroup>
|
||||
<InputGroup @name={{t "common.date-of-birth"}} @type="date" @value={{this.user.date_of_birth}} />
|
||||
<InputGroup @name={{t "common.timezone"}} @helpText={{t "console.account.index.timezone"}}>
|
||||
<Select @value={{this.user.timezone}} @options={{this.timezones}} @onSelect={{fn (mut this.user.timezone)}} @placeholder={{t "console.account.index.timezone"}} />
|
||||
</InputGroup>
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-end">
|
||||
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text={{t "common.save-button-text"}} @onClick={{perform this.saveProfile}} @isLoading={{not this.saveProfile.isIdle}} />
|
||||
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text={{t "common.save-changes"}} @onClick={{perform this.saveProfile}} @isLoading={{not this.saveProfile.isIdle}} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="flex flex-row justify-end">
|
||||
<Button @type="primary" @icon="plus" @text="Create Organization" @onClick={{this.createOrganization}} />
|
||||
</div>
|
||||
<ContentPanel @title="Your Organizations" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="Your Organizations" @open={{true}} @wrapperClass="bordered-classic">
|
||||
<div class="space-y-2">
|
||||
{{#each @model as |organization|}}
|
||||
<div class="grid grid-cols-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 rounded-lg px-3 py-2 items-center">
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
{{page-title "Admin"}}
|
||||
|
||||
<EmberWormhole @to="sidebar-menu-items">
|
||||
<Layout::Sidebar::Item @route="console.admin.index" @icon="rectangle-list">{{t "common.overview"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.organizations" @icon="building">{{t "common.organizations"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.branding" @icon="palette">{{t "common.branding"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.notifications" @icon="bell">{{t "common.notifications"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.two-fa-settings" @icon="shield-halved">{{t "common.2fa-config"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.index" @icon="rectangle-list">{{t "console.admin.menu.overview"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.organizations" @icon="building">{{t "console.admin.menu.organizations"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.branding" @icon="palette">{{t "console.admin.menu.branding"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.two-fa-settings" @icon="shield-halved">{{t "console.admin.menu.2fa-config"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.schedule-monitor" @icon="calendar-check">{{t "console.admin.schedule-monitor.schedule-monitor"}}</Layout::Sidebar::Item>
|
||||
{{#each this.universe.adminMenuItems as |menuItem|}}
|
||||
<Layout::Sidebar::Item
|
||||
@@ -26,12 +25,12 @@
|
||||
</Layout::Sidebar::Panel>
|
||||
{{/each}}
|
||||
<Layout::Sidebar::Panel @open={{true}} @title="System Config">
|
||||
<Layout::Sidebar::Item @route="console.admin.config.services" @icon="bell-concierge">{{t "common.services"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.config.mail" @icon="envelope">{{t "common.mail"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.config.filesystem" @icon="hard-drive">{{t "common.filesystem"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.config.queue" @icon="layer-group">{{t "common.queue"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.config.socket" @icon="plug">{{t "common.socket"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.config.notification-channels" @icon="tower-broadcast">{{t "common.push-notifications"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.config.services" @icon="bell-concierge">{{t "console.admin.menu.services"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.config.mail" @icon="envelope">{{t "console.admin.menu.mail"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.config.filesystem" @icon="hard-drive">{{t "console.admin.menu.filesystem"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.config.queue" @icon="layer-group">{{t "console.admin.menu.queue"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.config.socket" @icon="plug">{{t "console.admin.menu.socket"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.config.notification-channels" @icon="tower-broadcast">{{t "console.admin.menu.push-notifications"}}</Layout::Sidebar::Item>
|
||||
</Layout::Sidebar::Panel>
|
||||
</EmberWormhole>
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{{page-title (t "console.admin.branding.title")}}
|
||||
<Layout::Section::Header @title={{t "console.admin.branding.title"}}>
|
||||
<Button @type="primary" @size="sm" @icon="save" @text={{t "common.save-button-text"}} @onClick={{this.save}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
|
||||
<Button @type="primary" @size="sm" @icon="save" @text={{t "common.save-changes"}} @onClick={{this.save}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
|
||||
</Layout::Section::Header>
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<ContentPanel @title={{t "console.admin.branding.title"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title={{t "console.admin.branding.title"}} @open={{true}} @wrapperClass="bordered-classic">
|
||||
<form class="flex flex-col" {{on "submit" this.save}}>
|
||||
<div class="input-group">
|
||||
<label>{{t "console.admin.branding.icon-text"}}</label>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
{{page-title (concat (t "console.admin.schedule-monitor.schedule-monitor") " - " @model.name)}}
|
||||
<Overlay @isOpen={{true}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width="600px" @isResizable={{true}}>
|
||||
<Overlay::Header @title={{concat (t "console.admin.schedule-monitor.task-logs-for") @model.name}} @titleClass="max-w-400px truncate" @hideStatusDot={{true}} @titleWrapperClass="leading-5">
|
||||
<div class="flex flex-1 justify-end">
|
||||
<Button @type="default" @icon="times" @helpText={{t "common.close-and-save"}} @onClick={{this.onPressClose}} />
|
||||
</div>
|
||||
<:actions>
|
||||
<div class="flex flex-1 justify-end">
|
||||
<Button @type="default" @icon="times" @helpText={{t "common.close"}} @onClick={{this.onPressClose}} />
|
||||
</div>
|
||||
</:actions>
|
||||
</Overlay::Header>
|
||||
|
||||
<Overlay::Body>
|
||||
<div class="p-4">
|
||||
<div class="px-3 py-2">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="text-sm">{{t "console.admin.schedule-monitor.showing-last-count" count=20}}</div>
|
||||
<Button @size="xs" @icon="arrows-rotate" @onClick={{perform this.reload @model}} @isLoading={{not this.reload.isIdle}} />
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{{page-title "2FA Config"}}
|
||||
<Layout::Section::Header @title="2FA Config">
|
||||
<Button @type="primary" @size="sm" @icon="save" @text="Save Changes" @onClick={{this.saveSettings}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
|
||||
<Button @type="primary" @size="sm" @icon="save" @text={{t "common.save-changes"}} @onClick={{this.saveSettings}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
|
||||
</Layout::Section::Header>
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-4">
|
||||
<ContentPanel @title="2FA Config" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title="2FA Config" @open={{true}} @wrapperClass="bordered-classic">
|
||||
{{#if this.loadSystemTwoFaConfig.isIdle}}
|
||||
<TwoFaSettings
|
||||
@showEnforceOption={{true}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{page-title "Dashboard"}}
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<TwoFaEnforcementAlert />
|
||||
<Dashboard @sidebar={{this.sidebarContext}} class="flex items-center justify-between mb-4 mt-6 px-14" />
|
||||
<Dashboard @extension="core" @createWrapperClass="px-10" class="flex items-center justify-between mb-4 mt-6 px-14" />
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
<div id="console-home-wormhole" />
|
||||
@@ -3,6 +3,7 @@
|
||||
<Layout::Sidebar::Panel @open={{true}} @title={{t "common.settings"}}>
|
||||
<Layout::Sidebar::Item @route="console.settings.index" @icon="cog">{{t "common.organization"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.settings.two-fa" @icon="shield-halved">{{t "common.two-factor"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.settings.notifications" @icon="bell">{{t "common.notifications"}}</Layout::Sidebar::Item>
|
||||
{{#each this.universe.settingsMenuItems as |menuItem|}}
|
||||
<Layout::Sidebar::Item
|
||||
@onClick={{fn this.universe.transitionMenuItem "console.settings.virtual" menuItem}}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<ContentPanel @title={{t "console.settings.index.title"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<form {{on "submit" this.saveSettings}}>
|
||||
<ContentPanel @title={{t "console.settings.index.title"}} @open={{true}} @wrapperClass="bordered-classic">
|
||||
<form {{on "submit" (perform this.saveSettings)}}>
|
||||
<InputGroup @name={{t "console.settings.index.organization-name"}} @value={{@model.name}} />
|
||||
<InputGroup @name={{t "console.settings.index.organization-description"}} @value={{@model.description}} />
|
||||
<InputGroup @name={{t "console.settings.index.organization-phone"}}>
|
||||
@@ -13,14 +13,17 @@
|
||||
<InputGroup @name={{t "console.settings.index.organization-currency"}}>
|
||||
<CurrencySelect @currency={{@model.currency}} @onCurrencyChange={{fn (mut @model.currency)}} @triggerClass="w-full form-select" />
|
||||
</InputGroup>
|
||||
<InputGroup @name={{t "common.timezone"}} @helpText={{t "console.settings.index.organization-timezone"}}>
|
||||
<Select @value={{@model.timezone}} @options={{this.timezones}} @onSelect={{fn (mut @model.timezone)}} @placeholder={{t "console.settings.index.select-timezone"}} class="w-full form-select" />
|
||||
</InputGroup>
|
||||
<InputGroup @name={{t "console.settings.index.organization-id"}} @value={{@model.public_id}} @disabled={{true}} />
|
||||
<div class="mt-3 flex items-center justify-end">
|
||||
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text="{{t "common.save-button-text"}}" @isLoading={{this.isLoading}} />
|
||||
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text="{{t "common.save-changes"}}" @isLoading={{this.saveSettings.isRunning}} />
|
||||
</div>
|
||||
</form>
|
||||
</ContentPanel>
|
||||
|
||||
<ContentPanel @title={{t "console.settings.index.organization-branding"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title={{t "console.settings.index.organization-branding"}} @open={{true}} @wrapperClass="bordered-classic">
|
||||
<InputGroup @name={{t "console.settings.index.logo"}} @helpText={{t "console.settings.index.logo-help-text"}}>
|
||||
<div class="flex flex-row items-center">
|
||||
<Image src={{@model.logo_url}} @fallbackSrc={{config "defaultValues.placeholderImage"}} alt={{concat @model.name " logo"}} class="h-20 w-64 border dark:border-gray-900 rounded-md mr-4" />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user