mirror of
https://github.com/fleetbase/fleetbase.git
synced 2026-01-07 15:01:45 +00:00
Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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>.
|
||||
|
||||
|
||||
32
RELEASE.md
Normal file
32
RELEASE.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 🚀 Fleetbase v0.7.15 — 2025-11-01
|
||||
|
||||
> "Optimization tune-up, prevent queue blockage"
|
||||
|
||||
---
|
||||
|
||||
## ✨ Highlights
|
||||
- Create surgical optimziations around metadata option data, as well as order methods to prevent trigger lifecycle jobs.
|
||||
- Updated order `setDistanceAndTime` to prevent lifecycle job triggers, order estimation tracking limited to orders within past 2 days instead of month, now runs every 10 minutes instead of 5 minutes.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 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.3",
|
||||
"fleetbase/fleetops-api": "^0.6.6",
|
||||
"fleetbase/registry-bridge": "^0.0.18",
|
||||
"fleetbase/storefront-api": "^0.3.30",
|
||||
"fleetbase/core-api": "^1.6.22",
|
||||
"fleetbase/fleetops-api": "^0.6.23",
|
||||
"fleetbase/registry-bridge": "^0.1.0",
|
||||
"fleetbase/storefront-api": "^0.4.4",
|
||||
"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": {
|
||||
|
||||
2727
api/composer.lock
generated
2727
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' => [],
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
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) {}
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{{page-title "Push Notifications"}}
|
||||
<Layout::Section::Header @title={{t "console.admin.notifications.title"}}>
|
||||
<Button @type="primary" @size="sm" @icon="save" @text={{t "common.save-button-text"}} @onClick={{this.saveSettings}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
|
||||
{{page-title "Notifications"}}
|
||||
<Layout::Section::Header @title={{t "common.notifications"}}>
|
||||
<Button @type="primary" @size="sm" @icon="save" @text={{t "common.save-changes"}} @onClick={{perform this.saveSettings}} @disabled={{this.saveSettings.isRunning}} @isLoading={{or this.saveSettings.isRunning this.getSettings.isRunning}} />
|
||||
</Layout::Section::Header>
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-4">
|
||||
{{#each-in this.groupedNotifications as |groupName notifications|}}
|
||||
<ContentPanel @title={{concat (smart-humanize groupName) " " (t "console.admin.notifications.notification-settings") }} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<ContentPanel @title={{concat (smart-humanize groupName) " " (t "console.admin.notifications.notification-settings") }} @open={{true}} @wrapperClass="bordered-classic">
|
||||
{{#each notifications as |notification|}}
|
||||
<InputGroup @name={{titleize notification.name}} @helpText={{notification.description}}>
|
||||
<div class="fleetbase-model-select fleetbase-power-select ember-model-select">
|
||||
@@ -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="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.loadCompanyTwoFaSettings.isIdle}}
|
||||
<TwoFaSettings
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<div class="bg-white dark:bg-gray-800 py-5 px-4 shadow rounded-lg w-full">
|
||||
<div class="mb-4">
|
||||
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
|
||||
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
|
||||
{{t "onboard.index.title"}}
|
||||
</h2>
|
||||
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} height="56" class="h-10 object-contain mx-auto" />
|
||||
<div class="mt-2">
|
||||
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
|
||||
{{t "onboard.index.title"}}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex px-3 py-2 mb-4 rounded-md shadow-sm bg-blue-200">
|
||||
@@ -33,4 +35,8 @@
|
||||
<Button @icon="check" @iconPrefix="fas" @type="primary" @size="lg" @text={{t "onboard.index.continue-button-text"}} @isLoading={{this.isLoading}} @disabled={{this.readyToSubmit}} @onClick={{this.startOnboard}} />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<RegistryYield @registry="onboard" as |YieldedComponent ctx|>
|
||||
<YieldedComponent @context={{ctx}} />
|
||||
</RegistryYield>
|
||||
</div>
|
||||
46
console/app/transforms/array.js
Normal file
46
console/app/transforms/array.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
import { isArray } from '@ember/array';
|
||||
|
||||
export default class ArrayTransform extends Transform {
|
||||
deserialize(serialized) {
|
||||
if (serialized === null || serialized === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isArray(serialized)) {
|
||||
return serialized;
|
||||
}
|
||||
|
||||
if (typeof serialized !== 'string') {
|
||||
return Array.from(serialized);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(serialized);
|
||||
} catch (e) {
|
||||
// Fallback: return empty array if parsing fails
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
serialize(deserialized) {
|
||||
if (deserialized === null || deserialized === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isArray(deserialized)) {
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
if (typeof deserialized !== 'string') {
|
||||
return Array.from(deserialized);
|
||||
}
|
||||
|
||||
// Fallback: attempt to parse if it’s a string
|
||||
try {
|
||||
return JSON.parse(deserialized);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
36
console/app/transforms/object.js
Normal file
36
console/app/transforms/object.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import Transform from '@ember-data/serializer/transform';
|
||||
import isObject from '@fleetbase/ember-core/utils/is-object';
|
||||
|
||||
export default class ObjectTransform extends Transform {
|
||||
deserialize(serialized) {
|
||||
if (!serialized) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (isObject(serialized)) {
|
||||
return serialized;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(serialized);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
serialize(deserialized) {
|
||||
if (!deserialized) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (isObject(deserialized)) {
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(deserialized);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { camelize } from '@ember/string';
|
||||
|
||||
export default function createNotificationKey(definition, name) {
|
||||
const withoutSlashes = definition.replace(/[\W_]+/g, '');
|
||||
const key = `${camelize(withoutSlashes)}__${camelize(name)}`;
|
||||
|
||||
return key;
|
||||
}
|
||||
@@ -19,6 +19,8 @@ export default function getPermissionAction(permissionName) {
|
||||
'notify',
|
||||
'assign_vehicle',
|
||||
'assign_order_to',
|
||||
'assign_driver_to',
|
||||
'assign_vehicle_to',
|
||||
'dispatch_order_to',
|
||||
'dispatch',
|
||||
'assign',
|
||||
|
||||
160
console/app/utils/router-refresh-patch.js
Normal file
160
console/app/utils/router-refresh-patch.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import { debug } from '@ember/debug';
|
||||
/**
|
||||
* Fleetbase Router Refresh Bug Fix Utility
|
||||
*
|
||||
* This utility patches the Ember.js router refresh bug that causes
|
||||
* "missing params" errors when transitioning to nested routes with
|
||||
* dynamic segments while query parameters with refreshModel: true
|
||||
* are present.
|
||||
*
|
||||
* Bug: https://github.com/emberjs/ember.js/issues/19260
|
||||
*
|
||||
* @author Fleetbase Pte Ltd <hello@fleetbase.io>
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Applies the router refresh bug fix patch
|
||||
* @param {Application} application - The Ember application instance
|
||||
*/
|
||||
export function patchRouterRefresh(application) {
|
||||
if (!application || typeof application.lookup !== 'function') {
|
||||
debug('[Fleetbase Router Patch] Invalid application instance provided');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const router = application.lookup('router:main');
|
||||
|
||||
if (!router || !router._routerMicrolib) {
|
||||
debug('[Fleetbase Router Patch] Router not found or invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already patched
|
||||
if (router._routerMicrolib._fleetbaseRefreshPatched) {
|
||||
debug('[Fleetbase Router Patch] Already applied, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
const originalRefresh = router._routerMicrolib.refresh.bind(router._routerMicrolib);
|
||||
|
||||
router._routerMicrolib.refresh = function (pivotRoute) {
|
||||
const previousTransition = this.activeTransition;
|
||||
const state = previousTransition ? previousTransition[this.constructor.STATE_SYMBOL] : this.state;
|
||||
const routeInfos = state.routeInfos;
|
||||
|
||||
if (pivotRoute === undefined) {
|
||||
pivotRoute = routeInfos[0].route;
|
||||
}
|
||||
|
||||
const name = routeInfos[routeInfos.length - 1].name;
|
||||
const currentRouteInfo = routeInfos[routeInfos.length - 1];
|
||||
|
||||
// Extract current dynamic segment parameters
|
||||
const contexts = [];
|
||||
if (currentRouteInfo && currentRouteInfo.params) {
|
||||
const handlers = this.recognizer.handlersFor(name);
|
||||
const targetHandler = handlers[handlers.length - 1];
|
||||
|
||||
if (targetHandler && targetHandler.names && targetHandler.names.length > 0) {
|
||||
targetHandler.names.forEach((paramName) => {
|
||||
if (currentRouteInfo.params[paramName] !== undefined) {
|
||||
contexts.push(currentRouteInfo.params[paramName]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const NamedTransitionIntent = this.constructor.NamedTransitionIntent;
|
||||
const intent = new NamedTransitionIntent(
|
||||
this,
|
||||
name,
|
||||
pivotRoute,
|
||||
contexts, // Preserve dynamic segments instead of empty array
|
||||
this._changedQueryParams || state.queryParams
|
||||
);
|
||||
|
||||
const newTransition = this.transitionByIntent(intent, false);
|
||||
|
||||
if (previousTransition && previousTransition.urlMethod === 'replace') {
|
||||
newTransition.method(previousTransition.urlMethod);
|
||||
}
|
||||
|
||||
return newTransition;
|
||||
};
|
||||
|
||||
// Mark as patched
|
||||
router._routerMicrolib._fleetbaseRefreshPatched = true;
|
||||
|
||||
debug('[Fleetbase Router Patch] Successfully applied router refresh bug fix');
|
||||
} catch (error) {
|
||||
debug('[Fleetbase Router Patch] Failed to apply patch: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative error suppression approach for cases where patching fails
|
||||
* @param {Application} application - The Ember application instance
|
||||
*/
|
||||
export function suppressRouterRefreshErrors(application) {
|
||||
if (!application) {
|
||||
debug('[Fleetbase Router Patch] Invalid application instance for error suppression');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Global error handler for unhandled promise rejections
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
const error = event.reason;
|
||||
if (error?.message?.includes("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments")) {
|
||||
debug('[Fleetbase Router Patch] Suppressed known Ember route refresh bug:', error.message);
|
||||
event.preventDefault(); // Prevent the error from being logged
|
||||
}
|
||||
});
|
||||
|
||||
// Ember.js error handler
|
||||
if (window.Ember) {
|
||||
const originalEmberError = window.Ember.onerror;
|
||||
|
||||
window.Ember.onerror = function (error) {
|
||||
if (error?.message?.includes("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments")) {
|
||||
debug('[Fleetbase Router Patch] Suppressed known Ember route refresh bug:', error.message);
|
||||
return; // Suppress the error
|
||||
}
|
||||
|
||||
// Let other errors through
|
||||
if (originalEmberError) {
|
||||
return originalEmberError(error);
|
||||
}
|
||||
throw error;
|
||||
};
|
||||
}
|
||||
|
||||
debug('[Fleetbase Router Patch] Error suppression handlers installed');
|
||||
} catch (error) {
|
||||
debug('[Fleetbase Router Patch] Failed to install error suppression: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to apply the complete router fix
|
||||
* @param {Application} application - The Ember application instance
|
||||
* @param {Object} options - Configuration options
|
||||
* @param {boolean} options.suppressErrors - Whether to also install error suppression (default: true)
|
||||
*/
|
||||
export default function applyRouterFix(application, options = {}) {
|
||||
const { suppressErrors = true } = options;
|
||||
|
||||
debug('[Fleetbase Router Patch] Applying Ember router refresh bug fix...');
|
||||
|
||||
// Apply the main patch
|
||||
patchRouterRefresh(application);
|
||||
|
||||
// Optionally install error suppression as fallback
|
||||
if (suppressErrors) {
|
||||
suppressRouterRefreshErrors(application);
|
||||
}
|
||||
|
||||
debug('[Fleetbase Router Patch] Router fix application complete');
|
||||
}
|
||||
84
console/app/utils/runtime-config.js
Normal file
84
console/app/utils/runtime-config.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import config from '@fleetbase/console/config/environment';
|
||||
import toBoolean from '@fleetbase/ember-core/utils/to-boolean';
|
||||
import { set } from '@ember/object';
|
||||
import { debug } from '@ember/debug';
|
||||
|
||||
/**
|
||||
* Maps allowed runtime keys to internal config paths.
|
||||
*/
|
||||
const RUNTIME_CONFIG_MAP = {
|
||||
API_HOST: 'API.host',
|
||||
API_NAMESPACE: 'API.namespace',
|
||||
SOCKETCLUSTER_PATH: 'socket.path',
|
||||
SOCKETCLUSTER_HOST: 'socket.hostname',
|
||||
SOCKETCLUSTER_SECURE: 'socket.secure',
|
||||
SOCKETCLUSTER_PORT: 'socket.port',
|
||||
OSRM_HOST: 'osrm.host',
|
||||
EXTENSIONS: 'APP.extensions',
|
||||
};
|
||||
|
||||
/**
|
||||
* Coerce and sanitize runtime config values based on key.
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {*} value
|
||||
* @return {*}
|
||||
*/
|
||||
function coerceValue(key, value) {
|
||||
switch (key) {
|
||||
case 'SOCKETCLUSTER_PORT':
|
||||
return parseInt(value, 10);
|
||||
|
||||
case 'SOCKETCLUSTER_SECURE':
|
||||
return toBoolean(value);
|
||||
|
||||
case 'EXTENSIONS':
|
||||
return typeof value === 'string' ? value.split(',') : Array.from(value);
|
||||
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply runtime config overrides based on strict allowlist mapping.
|
||||
*
|
||||
* @param {Object} rawConfig
|
||||
*/
|
||||
export function applyRuntimeConfig(rawConfig = {}) {
|
||||
Object.entries(rawConfig).forEach(([key, value]) => {
|
||||
const configPath = RUNTIME_CONFIG_MAP[key];
|
||||
|
||||
if (configPath) {
|
||||
const coercedValue = coerceValue(key, value);
|
||||
set(config, configPath, coercedValue);
|
||||
} else {
|
||||
debug(`[runtime-config] Ignored unknown key: ${key}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and apply runtime config.
|
||||
*
|
||||
* @export
|
||||
* @return {void}
|
||||
*/
|
||||
export default async function loadRuntimeConfig() {
|
||||
if (config.APP.disableRuntimeConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/fleetbase.config.json?_t=${Date.now()}`, { cache: 'no-cache' });
|
||||
if (!response.ok) {
|
||||
debug('No fleetbase.config.json found, using built-in config defaults');
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeConfig = await response.json();
|
||||
applyRuntimeConfig(runtimeConfig);
|
||||
} catch (e) {
|
||||
debug(`Failed to load runtime config : ${e.message}`);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,9 @@ module.exports = function (environment) {
|
||||
},
|
||||
|
||||
APP: {
|
||||
autoboot: false,
|
||||
extensions: asArray(getenv('EXTENSIONS')),
|
||||
disableRuntimeConfig: toBoolean(getenv('DISABLE_RUNTIME_CONFIG')),
|
||||
},
|
||||
|
||||
API: {
|
||||
@@ -47,7 +49,8 @@ module.exports = function (environment) {
|
||||
|
||||
defaultValues: {
|
||||
categoryImage: getenv('DEFAULT_CATEGORY_IMAGE', 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/images/fallback-placeholder-1.png'),
|
||||
placeholderImage: getenv('DEFAULT_PLACEHOLDER_IMAGE', 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/images/fallback-placeholder-2.png'),
|
||||
placeholderImage: getenv('DEFAULT_PLACEHOLDER_IMAGE', 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/image-file-icon.png'),
|
||||
placeholderImageOld: getenv('DEFAULT_PLACEHOLDER_IMAGE_OLD', 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/images/fallback-placeholder-2.png'),
|
||||
driverImage: getenv('DEFAULT_DRIVER_IMAGE', 'https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/no-avatar.png'),
|
||||
userImage: getenv('DEFAULT_USER_IMAGE', 'https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/no-avatar.png'),
|
||||
contactImage: getenv('DEFAULT_CONTACT_IMAGE', 'https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/no-avatar.png'),
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
/** eslint-disable node/no-unpublished-require */
|
||||
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
|
||||
const FleetbaseExtensionsIndexer = require('fleetbase-extensions-indexer');
|
||||
const Funnel = require('broccoli-funnel');
|
||||
const writeFile = require('broccoli-file-creator');
|
||||
const postcssImport = require('postcss-import');
|
||||
const postcssPresetEnv = require('postcss-preset-env');
|
||||
const postcssEach = require('postcss-each');
|
||||
@@ -11,6 +13,8 @@ const postcssConditionals = require('postcss-conditionals-renewed');
|
||||
const postcssAtRulesVariables = require('postcss-at-rules-variables');
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const tailwind = require('tailwindcss');
|
||||
const toBoolean = require('./config/utils/to-boolean');
|
||||
const environment = process.env.EMBER_ENV;
|
||||
|
||||
module.exports = function (defaults) {
|
||||
const app = new EmberApp(defaults, {
|
||||
@@ -49,7 +53,7 @@ module.exports = function (defaults) {
|
||||
},
|
||||
filter: {
|
||||
enabled: true,
|
||||
plugins: [postcssAtRulesVariables, postcssMixins, postcssEach, postcssConditionals, tailwind('./tailwind.js')],
|
||||
plugins: [postcssAtRulesVariables, postcssMixins, postcssEach, postcssConditionals, tailwind('./tailwind.config.js')],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -59,19 +63,15 @@ module.exports = function (defaults) {
|
||||
});
|
||||
|
||||
let extensions = new FleetbaseExtensionsIndexer();
|
||||
let runtimeConfigTree;
|
||||
if (toBoolean(process.env.DISABLE_RUNTIME_CONFIG)) {
|
||||
runtimeConfigTree = writeFile('fleetbase.config.json', '{}');
|
||||
} else {
|
||||
runtimeConfigTree = new Funnel('.', {
|
||||
files: ['fleetbase.config.json'],
|
||||
destDir: '/',
|
||||
});
|
||||
}
|
||||
|
||||
// Use `app.import` to add additional libraries to the generated
|
||||
// output files.
|
||||
//
|
||||
// If you need to use different assets in different
|
||||
// environments, specify an object as the first parameter. That
|
||||
// object's keys should be the environment name and the values
|
||||
// should be the asset to use in that environment.
|
||||
//
|
||||
// If the library that you are including contains AMD or ES6
|
||||
// modules that you would like to import into your application
|
||||
// please specify an object with the list of modules as keys
|
||||
// along with the exports of each module as its value.
|
||||
|
||||
return app.toTree([extensions]);
|
||||
return app.toTree([extensions, runtimeConfigTree].filter(Boolean));
|
||||
};
|
||||
|
||||
3
console/fleetbase.config.json
Normal file
3
console/fleetbase.config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"API_HOST": "http://localhost:8000"
|
||||
}
|
||||
@@ -6,4 +6,18 @@ server {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html =404;
|
||||
}
|
||||
|
||||
# Serve runtime config with no cache
|
||||
location = /fleetbase.config.json {
|
||||
root /usr/share/nginx/html;
|
||||
default_type application/json;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires 0;
|
||||
try_files /fleetbase.config.json @config_fallback;
|
||||
}
|
||||
|
||||
location @config_fallback {
|
||||
return 200 '{}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fleetbase/console",
|
||||
"version": "0.6.9",
|
||||
"version": "0.7.15",
|
||||
"private": true,
|
||||
"description": "Modular logistics and supply chain operating system (LSOS)",
|
||||
"repository": "https://github.com/fleetbase/fleetbase",
|
||||
@@ -29,24 +29,28 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ember/legacy-built-in-components": "^0.4.2",
|
||||
"@fleetbase/dev-engine": "^0.2.9",
|
||||
"@fleetbase/dev-engine": "^0.2.10",
|
||||
"@fleetbase/ember-core": "latest",
|
||||
"@fleetbase/ember-ui": "latest",
|
||||
"@fleetbase/fleetops-data": "latest",
|
||||
"@fleetbase/fleetops-engine": "^0.6.6",
|
||||
"@fleetbase/iam-engine": "^0.1.3",
|
||||
"@fleetbase/leaflet-routing-machine": "^3.2.16",
|
||||
"@fleetbase/registry-bridge-engine": "^0.0.18",
|
||||
"@fleetbase/storefront-engine": "^0.3.30",
|
||||
"@fleetbase/fleetops-engine": "^0.6.23",
|
||||
"@fleetbase/iam-engine": "^0.1.4",
|
||||
"@fleetbase/leaflet-routing-machine": "^3.2.17",
|
||||
"@fleetbase/registry-bridge-engine": "^0.1.0",
|
||||
"@fleetbase/storefront-engine": "^0.4.4",
|
||||
"@formatjs/intl-datetimeformat": "^6.18.2",
|
||||
"@formatjs/intl-numberformat": "^8.15.6",
|
||||
"@formatjs/intl-pluralrules": "^5.4.6",
|
||||
"@formatjs/intl-relativetimeformat": "^11.4.13",
|
||||
"@fortawesome/ember-fontawesome": "^2.0.0",
|
||||
"ember-changeset": "4.1.2",
|
||||
"ember-changeset-validations": "4.1.2",
|
||||
"ember-composable-helpers": "^5.0.0",
|
||||
"ember-concurrency": "^3.1.1",
|
||||
"ember-concurrency": "^4.0.4",
|
||||
"ember-concurrency-decorators": "^2.0.3",
|
||||
"ember-intl": "6.3.2",
|
||||
"ember-math-helpers": "^2.18.2",
|
||||
"ember-power-select": "^7.2.0",
|
||||
"ember-maybe-in-element": "^2.1.0",
|
||||
"ember-prism": "^0.13.0",
|
||||
"ember-radio-button": "3.0.0-beta.1",
|
||||
"ember-tag-input": "^3.1.0",
|
||||
@@ -56,13 +60,13 @@
|
||||
"postcss-nth-list": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@embroider/macros": "1.16.12",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/eslint-parser": "^7.25.1",
|
||||
"@babel/plugin-proposal-decorators": "^7.24.7",
|
||||
"@ember/optional-features": "^2.1.0",
|
||||
"@ember/string": "^3.1.1",
|
||||
"@ember/test-helpers": "^3.3.1",
|
||||
"@embroider/macros": "1.16.12",
|
||||
"@fleetbase/intl-lint": "^0.0.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.4.0",
|
||||
@@ -72,6 +76,7 @@
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"broccoli-file-creator": "^2.1.1",
|
||||
"broccoli-funnel": "^3.0.8",
|
||||
"concurrently": "^8.2.2",
|
||||
"date-fns": "^2.30.0",
|
||||
|
||||
4994
console/pnpm-lock.yaml
generated
4994
console/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user