mirror of
https://github.com/fleetbase/fleetbase.git
synced 2026-01-10 16:29:07 +00:00
Compare commits
142 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00b037a385 | ||
|
|
151fea2505 | ||
|
|
08f901d865 | ||
|
|
116873a1ce | ||
|
|
20a1793447 | ||
|
|
72ab83dc7a | ||
|
|
171e365ba6 | ||
|
|
009f2d6e53 | ||
|
|
4a59318feb | ||
|
|
8920039b40 | ||
|
|
cd9be05714 | ||
|
|
fbe35545e9 | ||
|
|
56ab967d7a | ||
|
|
79604c7981 | ||
|
|
312eb1aa6f | ||
|
|
6572a59120 | ||
|
|
fa536c6183 | ||
|
|
d4626be332 | ||
|
|
ffc54ecdbb | ||
|
|
9bf097b88b | ||
|
|
52c8df8b66 | ||
|
|
908e0eb9ee | ||
|
|
6438138913 | ||
|
|
aee06a2146 | ||
|
|
ccacc6c597 | ||
|
|
62c396b789 | ||
|
|
295da5f331 | ||
|
|
e775ccc2c8 | ||
|
|
bd0759881f | ||
|
|
830ae69b1d | ||
|
|
3b9a80866f | ||
|
|
76badbf949 | ||
|
|
f968556585 | ||
|
|
6d01eab305 | ||
|
|
9406446db1 | ||
|
|
565db7bbce | ||
|
|
003cb467e8 | ||
|
|
8e1b281e77 | ||
|
|
af4507cc87 | ||
|
|
2a2c7d8426 | ||
|
|
077a4298b9 | ||
|
|
af840d30d0 | ||
|
|
a253c0c6f7 | ||
|
|
0fc1d6068d | ||
|
|
6993510d08 | ||
|
|
4fcf09c1b8 | ||
|
|
f89596a74b | ||
|
|
d89e93f248 | ||
|
|
2f0c15bc93 | ||
|
|
ae6c07006b | ||
|
|
d754641493 | ||
|
|
bb0d706006 | ||
|
|
652bc363e1 | ||
|
|
363309af61 | ||
|
|
83791ea91c | ||
|
|
8aae8cd025 | ||
|
|
6fb0353813 | ||
|
|
c73c8e0f54 | ||
|
|
84ddf730c2 | ||
|
|
9da0d6b5c4 | ||
|
|
52bf61b3be | ||
|
|
9ff3d9e85f | ||
|
|
b6bad3212d | ||
|
|
a43b86807b | ||
|
|
9cc0e35ac3 | ||
|
|
2e555b8dc3 | ||
|
|
948ba4aa6d | ||
|
|
46bd448ec7 | ||
|
|
e588242cb9 | ||
|
|
58600a357a | ||
|
|
4dd8a7b2ff | ||
|
|
54803909a4 | ||
|
|
e437ee04fb | ||
|
|
80d2ebb544 | ||
|
|
300e157bc1 | ||
|
|
26509008dc | ||
|
|
4119eb1717 | ||
|
|
27661a3888 | ||
|
|
a4f69409fd | ||
|
|
da9caf57f1 | ||
|
|
acfa34b09f | ||
|
|
356b5076e5 | ||
|
|
038382928b | ||
|
|
f459f2e7e3 | ||
|
|
7f9b09f673 | ||
|
|
440040fecb | ||
|
|
bc31bf4c4c | ||
|
|
def361c2af | ||
|
|
be382fcbd3 | ||
|
|
f42de06dcf | ||
|
|
dc2b4e1aee | ||
|
|
c7348766df | ||
|
|
e67149db89 | ||
|
|
4765bcfbd9 | ||
|
|
019be89e06 | ||
|
|
db0c56d8f0 | ||
|
|
8de660d51a | ||
|
|
67f22aafbe | ||
|
|
1ff89ca16a | ||
|
|
c6777efb2d | ||
|
|
7d1f594f87 | ||
|
|
da6485987a | ||
|
|
b623d613f1 | ||
|
|
d1a77c95d0 | ||
|
|
bc7c6c12ad | ||
|
|
50bfe55583 | ||
|
|
bd1a61912f | ||
|
|
9d49730d52 | ||
|
|
fd38f2bef6 | ||
|
|
0fa3a25d59 | ||
|
|
0917d7cce9 | ||
|
|
d33aa21418 | ||
|
|
683353517d | ||
|
|
27b1af6c40 | ||
|
|
ab8cbf9ea6 | ||
|
|
b2c44842ce | ||
|
|
f9e6c8d50d | ||
|
|
6cd0625753 | ||
|
|
9bb2ea15e0 | ||
|
|
575e4e0952 | ||
|
|
f2058e0d58 | ||
|
|
9669e917ab | ||
|
|
2b6a200811 | ||
|
|
1d92764958 | ||
|
|
13db00c39f | ||
|
|
eedeb8f209 | ||
|
|
bac95cca00 | ||
|
|
520e9771b4 | ||
|
|
75742de7ac | ||
|
|
8628695b0b | ||
|
|
74fd369854 | ||
|
|
5d43df0e1b | ||
|
|
c96a3eb2bf | ||
|
|
4424ed1754 | ||
|
|
6c8b09db61 | ||
|
|
8d5e649646 | ||
|
|
6844e61dd4 | ||
|
|
6a68c85d94 | ||
|
|
2e8a6de7ea | ||
|
|
3a72cacc5c | ||
|
|
fc1e610a54 | ||
|
|
59f01832ef |
@@ -14,6 +14,7 @@ concourse/
|
||||
infra/*
|
||||
vagrant/*
|
||||
docker/Dockerfile
|
||||
docker/database/
|
||||
deploy/*
|
||||
media/*
|
||||
data/*
|
||||
@@ -23,4 +24,4 @@ docker-compose-prod.yml
|
||||
docker-compose.yml
|
||||
$virtualenv.tar.gz
|
||||
$node_modules.tar.gz
|
||||
docker-compose.override.yml
|
||||
docker-compose.override.yml
|
||||
|
||||
190
.github/workflows/cd.yml
vendored
190
.github/workflows/cd.yml
vendored
@@ -2,14 +2,15 @@ name: Fleetbase CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "deploy/*" ]
|
||||
branches: ["deploy/*"]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
PROJECT: ${{ secrets.PROJECT }}
|
||||
GITHUB_AUTH_KEY: ${{ secrets._GITHUB_AUTH_TOKEN }}
|
||||
|
||||
jobs:
|
||||
build_service:
|
||||
@@ -17,59 +18,52 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # This is required for requesting the JWT
|
||||
contents: read # This is required for actions/checkout
|
||||
contents: read # This is required for actions/checkout
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Set Dynamic ENV Vars
|
||||
run: |
|
||||
- name: Set Dynamic ENV Vars
|
||||
run: |
|
||||
SHORT_COMMIT=$(echo $GITHUB_SHA | cut -c -8)
|
||||
echo "VERSION=${SHORT_COMMIT}" >> $GITHUB_ENV
|
||||
echo "STACK=$(basename $GITHUB_REF)" >> $GITHUB_ENV
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_NUMBER }}:role/${{ env.PROJECT }}-${{ env.STACK }}-deployer
|
||||
role-session-name: github
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_NUMBER }}:role/${{ env.PROJECT }}-${{ env.STACK }}-deployer
|
||||
role-session-name: github
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v1
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v1
|
||||
|
||||
- name: Prepare Composer Auth Secret
|
||||
run: |
|
||||
if [[ -n "${{ secrets._GITHUB_AUTH_TOKEN }}" ]]; then
|
||||
echo '{"github-oauth": {"github.com": "'${{ secrets._GITHUB_AUTH_TOKEN }}'"}}' > composer-auth.json
|
||||
else
|
||||
echo '{}' > composer-auth.json
|
||||
fi
|
||||
- name: Build and Release
|
||||
uses: docker/bake-action@v2
|
||||
env:
|
||||
REGISTRY: ${{ steps.login-ecr.outputs.registry }}/${{ env.PROJECT }}-${{ env.STACK }}
|
||||
VERSION: ${{ env.VERSION }}
|
||||
GITHUB_AUTH_KEY: ${{ env.GITHUB_AUTH_KEY }}
|
||||
CACHE: type=gha
|
||||
with:
|
||||
push: true
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
|
||||
- name: Build and Release
|
||||
uses: docker/bake-action@v2
|
||||
env:
|
||||
REGISTRY: ${{ steps.login-ecr.outputs.registry }}/${{ env.PROJECT }}-${{ env.STACK }}
|
||||
VERSION: ${{ env.VERSION }}
|
||||
CACHE: type=gha
|
||||
with:
|
||||
push: true
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
|
||||
- name: Download ecs-tool
|
||||
run: |
|
||||
- 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
|
||||
|
||||
- name: Deploy the images 🚀
|
||||
run: |-
|
||||
|
||||
- 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
|
||||
@@ -81,29 +75,29 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # This is required for requesting the JWT
|
||||
contents: read # This is required for actions/checkout
|
||||
contents: read # This is required for actions/checkout
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set Dynamic ENV Vars
|
||||
run: |
|
||||
- name: Set Dynamic ENV Vars
|
||||
run: |
|
||||
SHORT_COMMIT=$(echo $GITHUB_SHA | cut -c -8)
|
||||
echo "VERSION=${SHORT_COMMIT}" >> $GITHUB_ENV
|
||||
echo "STACK=$(basename $GITHUB_REF)" >> $GITHUB_ENV
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_NUMBER }}:role/${{ env.PROJECT }}-${{ env.STACK }}-deployer
|
||||
role-session-name: github
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_NUMBER }}:role/${{ env.PROJECT }}-${{ env.STACK }}-deployer
|
||||
role-session-name: github
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Get infra-provided configuration
|
||||
run: |
|
||||
- name: Get infra-provided configuration
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
wget -O- https://github.com/springload/ssm-parent/releases/download/1.8.0/ssm-parent_1.8.0_linux_amd64.tar.gz | tar xvzf - ssm-parent
|
||||
@@ -112,52 +106,52 @@ jobs:
|
||||
# remove double quotes and pipe into the env
|
||||
cat /tmp/dotenv.file | sed -e 's/"//g' >> $GITHUB_ENV
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
- uses: pnpm/action-setup@v2
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm Store Directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
- name: Get pnpm Store Directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm Cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm Cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Check for _GITHUB_AUTH_TOKEN and create .npmrc
|
||||
run: |
|
||||
if [[ -n "${{ secrets._GITHUB_AUTH_TOKEN }}" ]]; then
|
||||
echo "//npm.pkg.github.com/:_authToken=${{ secrets._GITHUB_AUTH_TOKEN }}" > .npmrc
|
||||
fi
|
||||
working-directory: ./console
|
||||
- name: Check for _GITHUB_AUTH_TOKEN and create .npmrc
|
||||
run: |
|
||||
if [[ -n "${{ secrets._GITHUB_AUTH_TOKEN }}" ]]; then
|
||||
echo "//npm.pkg.github.com/:_authToken=${{ secrets._GITHUB_AUTH_TOKEN }}" > .npmrc
|
||||
fi
|
||||
working-directory: ./console
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
working-directory: ./console
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
working-directory: ./console
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
set -eu
|
||||
- name: Build
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
pnpm build
|
||||
working-directory: ./console
|
||||
|
||||
- name: Deploy Console 🚀
|
||||
run: |
|
||||
pnpm build --environment production
|
||||
working-directory: ./console
|
||||
|
||||
- name: Deploy Console 🚀
|
||||
run: |
|
||||
set -u
|
||||
|
||||
DEPLOY_BUCKET=${STATIC_DEPLOY_BUCKET:-${{ env.PROJECT }}-${{ env.STACK }}}
|
||||
|
||||
2
.github/workflows/gcp-cd.yml
vendored
2
.github/workflows/gcp-cd.yml
vendored
@@ -173,7 +173,7 @@ jobs:
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
pnpm build
|
||||
pnpm build --environment production
|
||||
working-directory: ./console
|
||||
|
||||
- name: Deploy Console 🚀
|
||||
|
||||
11
COMMERCIAL_LICENSE.md
Normal file
11
COMMERCIAL_LICENSE.md
Normal file
@@ -0,0 +1,11 @@
|
||||
FLEETBASE DUAL LICENSE
|
||||
|
||||
COPYRIGHT (C) 2024 FLEETBASE PTE LTD.
|
||||
|
||||
PERMISSION IS HEREBY GRANTED, FREE OF CHARGE, TO ANY PERSON OBTAINING A COPY OF THIS SOFTWARE AND ASSOCIATED DOCUMENTATION FILES (THE "SOFTWARE"), TO USE THE SOFTWARE FOR NON-COMMERCIAL PURPOSES ONLY. NON-COMMERCIAL PURPOSES INCLUDE INTERNAL OPERATIONS, ACADEMIC RESEARCH, PERSONAL PROJECTS, OR ANY OTHER USE THAT IS NOT INTENDED FOR COMMERCIAL GAIN.
|
||||
|
||||
FOR VERSIONS 0.4.10 ONWARDS, YOU ARE PERMITTED TO USE THE SOFTWARE FOR NON-COMMERCIAL PURPOSES FREE OF CHARGE. HOWEVER, COMMERCIAL USE OF THIS SOFTWARE, INCLUDING BUT NOT LIMITED TO BUILDING SAAS PLATFORMS, OFFERING SERVICES TO THIRD PARTIES, OR INTEGRATING WITH COMMERCIAL PRODUCTS, REQUIRES THE PURCHASE OF A COMMERCIAL LICENSE FROM FLEETBASE PTE LTD.
|
||||
|
||||
FOR INQUIRIES REGARDING COMMERCIAL LICENSING OR ANY OTHER QUESTIONS RELATED TO FLEETBASE, PLEASE CONTACT HELLO@FLEETBASE.IO.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
12
Caddyfile
Normal file
12
Caddyfile
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
frankenphp
|
||||
order php_server before file_server
|
||||
}
|
||||
|
||||
http://:8000 {
|
||||
root * /fleetbase/api/public
|
||||
encode zstd gzip
|
||||
php_server {
|
||||
resolve_root_symlink
|
||||
}
|
||||
}
|
||||
21
LICENSE
21
LICENSE
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Fleetbase Pte Ltd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT LICENSE
|
||||
|
||||
COPYRIGHT (C) 2023 FLEETBASE PTE LTD
|
||||
|
||||
PERMISSION IS HEREBY GRANTED, FREE OF CHARGE, TO ANY PERSON OBTAINING A COPY
|
||||
OF THIS SOFTWARE AND ASSOCIATED DOCUMENTATION FILES (THE "SOFTWARE"), TO DEAL
|
||||
IN THE SOFTWARE WITHOUT RESTRICTION, INCLUDING WITHOUT LIMITATION THE RIGHTS
|
||||
TO USE, COPY, MODIFY, MERGE, PUBLISH, DISTRIBUTE, SUBLICENSE, AND/OR SELL
|
||||
COPIES OF THE SOFTWARE, AND TO PERMIT PERSONS TO WHOM THE SOFTWARE IS
|
||||
FURNISHED TO DO SO, SUBJECT TO THE FOLLOWING CONDITIONS:
|
||||
|
||||
THE ABOVE COPYRIGHT NOTICE AND THIS PERMISSION NOTICE SHALL BE INCLUDED IN ALL
|
||||
COPIES OR SUBSTANTIAL PORTIONS OF THE SOFTWARE.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
5
api/.gitignore
vendored
5
api/.gitignore
vendored
@@ -13,4 +13,7 @@ npm-debug.log
|
||||
yarn-error.log
|
||||
/.idea
|
||||
/.vscode
|
||||
.composer.dev.json
|
||||
.composer.dev.json
|
||||
/caddy
|
||||
frankenphp
|
||||
frankenphp-worker.php
|
||||
|
||||
@@ -16,7 +16,7 @@ class Kernel extends HttpKernel
|
||||
protected $middleware = [
|
||||
// \App\Http\Middleware\TrustHosts::class,
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
\Fruitcake\Cors\HandleCors::class,
|
||||
\Illuminate\Http\Middleware\HandleCors::class,
|
||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
|
||||
@@ -8,29 +8,29 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^7.3|^8.0",
|
||||
"fleetbase/core-api": "^1.3.13",
|
||||
"fleetbase/fleetops-api": "^0.4.4",
|
||||
"fleetbase/storefront-api": "^0.2.9",
|
||||
"fruitcake/laravel-cors": "^2.0",
|
||||
"php": "^8.0",
|
||||
"fleetbase/core-api": "^1.4.12",
|
||||
"fleetbase/fleetops-api": "^0.4.18",
|
||||
"fleetbase/storefront-api": "^0.3.5",
|
||||
"guzzlehttp/guzzle": "^7.0.1",
|
||||
"laravel/framework": "^8.75",
|
||||
"laravel/sanctum": "^2.11",
|
||||
"laravel/tinker": "^2.5",
|
||||
"league/flysystem-aws-s3-v3": "^1.0",
|
||||
"laravel/framework": "^10.0",
|
||||
"laravel/octane": "^2.3",
|
||||
"laravel/tinker": "^2.9",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"maatwebsite/excel": "^3.1",
|
||||
"phpoffice/phpspreadsheet": "^1.28",
|
||||
"predis/predis": "^2.1",
|
||||
"psr/http-factory-implementation": "*"
|
||||
"psr/http-factory-implementation": "*",
|
||||
"s-ichikawa/laravel-sendgrid-driver": "^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"facade/ignition": "^2.5",
|
||||
"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": "^5.10",
|
||||
"phpunit/phpunit": "^9.5.10"
|
||||
"nunomaduro/collision": "^7.0",
|
||||
"phpunit/phpunit": "^10.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -91,6 +91,6 @@
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
|
||||
7756
api/composer.lock
generated
7756
api/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -21,13 +21,13 @@ return [
|
||||
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
'allowed_origins' => array_filter(['http://localhost:4200', env('CONSOLE_HOST'), Utils::addWwwToUrl(env('CONSOLE_URL'))]),
|
||||
'allowed_origins' => array_filter(['http://localhost:4200', env('CONSOLE_HOST'), Utils::addWwwToUrl(env('CONSOLE_HOST'))]),
|
||||
|
||||
'allowed_origins_patterns' => [],
|
||||
|
||||
'allowed_headers' => ['*'],
|
||||
|
||||
'exposed_headers' => [],
|
||||
'exposed_headers' => ['x-compressed-json', 'access-console-sandbox', 'access-console-sandbox-key'],
|
||||
|
||||
'max_age' => 0,
|
||||
|
||||
|
||||
223
api/config/octane.php
Normal file
223
api/config/octane.php
Normal file
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
use Laravel\Octane\Contracts\OperationTerminated;
|
||||
use Laravel\Octane\Events\RequestHandled;
|
||||
use Laravel\Octane\Events\RequestReceived;
|
||||
use Laravel\Octane\Events\RequestTerminated;
|
||||
use Laravel\Octane\Events\TaskReceived;
|
||||
use Laravel\Octane\Events\TaskTerminated;
|
||||
use Laravel\Octane\Events\TickReceived;
|
||||
use Laravel\Octane\Events\TickTerminated;
|
||||
use Laravel\Octane\Events\WorkerErrorOccurred;
|
||||
use Laravel\Octane\Events\WorkerStarting;
|
||||
use Laravel\Octane\Events\WorkerStopping;
|
||||
use Laravel\Octane\Listeners\CollectGarbage;
|
||||
use Laravel\Octane\Listeners\DisconnectFromDatabases;
|
||||
use Laravel\Octane\Listeners\EnsureUploadedFilesAreValid;
|
||||
use Laravel\Octane\Listeners\EnsureUploadedFilesCanBeMoved;
|
||||
use Laravel\Octane\Listeners\FlushOnce;
|
||||
use Laravel\Octane\Listeners\FlushTemporaryContainerInstances;
|
||||
use Laravel\Octane\Listeners\FlushUploadedFiles;
|
||||
use Laravel\Octane\Listeners\ReportException;
|
||||
use Laravel\Octane\Listeners\StopWorkerIfNecessary;
|
||||
use Laravel\Octane\Octane;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Octane Server
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the default "server" that will be used by Octane
|
||||
| when starting, restarting, or stopping your server via the CLI. You
|
||||
| are free to change this to the supported server of your choosing.
|
||||
|
|
||||
| Supported: "roadrunner", "swoole", "frankenphp"
|
||||
|
|
||||
*/
|
||||
|
||||
'server' => env('OCTANE_SERVER', 'frankenphp'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Force HTTPS
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When this configuration value is set to "true", Octane will inform the
|
||||
| framework that all absolute links must be generated using the HTTPS
|
||||
| protocol. Otherwise your links may be generated using plain HTTP.
|
||||
|
|
||||
*/
|
||||
|
||||
'https' => env('OCTANE_HTTPS', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Octane Listeners
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All of the event listeners for Octane's events are defined below. These
|
||||
| listeners are responsible for resetting your application's state for
|
||||
| the next request. You may even add your own listeners to the list.
|
||||
|
|
||||
*/
|
||||
|
||||
'listeners' => [
|
||||
WorkerStarting::class => [
|
||||
EnsureUploadedFilesAreValid::class,
|
||||
EnsureUploadedFilesCanBeMoved::class,
|
||||
],
|
||||
|
||||
RequestReceived::class => [
|
||||
...Octane::prepareApplicationForNextOperation(),
|
||||
...Octane::prepareApplicationForNextRequest(),
|
||||
//
|
||||
],
|
||||
|
||||
RequestHandled::class => [
|
||||
//
|
||||
],
|
||||
|
||||
RequestTerminated::class => [
|
||||
// FlushUploadedFiles::class,
|
||||
],
|
||||
|
||||
TaskReceived::class => [
|
||||
...Octane::prepareApplicationForNextOperation(),
|
||||
//
|
||||
],
|
||||
|
||||
TaskTerminated::class => [
|
||||
//
|
||||
],
|
||||
|
||||
TickReceived::class => [
|
||||
...Octane::prepareApplicationForNextOperation(),
|
||||
//
|
||||
],
|
||||
|
||||
TickTerminated::class => [
|
||||
//
|
||||
],
|
||||
|
||||
OperationTerminated::class => [
|
||||
FlushOnce::class,
|
||||
FlushTemporaryContainerInstances::class,
|
||||
// DisconnectFromDatabases::class,
|
||||
// CollectGarbage::class,
|
||||
],
|
||||
|
||||
WorkerErrorOccurred::class => [
|
||||
ReportException::class,
|
||||
StopWorkerIfNecessary::class,
|
||||
],
|
||||
|
||||
WorkerStopping::class => [
|
||||
//
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Warm / Flush Bindings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The bindings listed below will either be pre-warmed when a worker boots
|
||||
| or they will be flushed before every new request. Flushing a binding
|
||||
| will force the container to resolve that binding again when asked.
|
||||
|
|
||||
*/
|
||||
|
||||
'warm' => [
|
||||
...Octane::defaultServicesToWarm(),
|
||||
],
|
||||
|
||||
'flush' => [
|
||||
//
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Octane Swoole Tables
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| While using Swoole, you may define additional tables as required by the
|
||||
| application. These tables can be used to store data that needs to be
|
||||
| quickly accessed by other workers on the particular Swoole server.
|
||||
|
|
||||
*/
|
||||
|
||||
'tables' => [
|
||||
'example:1000' => [
|
||||
'name' => 'string:1000',
|
||||
'votes' => 'int',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Octane Swoole Cache Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| While using Swoole, you may leverage the Octane cache, which is powered
|
||||
| by a Swoole table. You may set the maximum number of rows as well as
|
||||
| the number of bytes per row using the configuration options below.
|
||||
|
|
||||
*/
|
||||
|
||||
'cache' => [
|
||||
'rows' => 1000,
|
||||
'bytes' => 10000,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| File Watching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following list of files and directories will be watched when using
|
||||
| the --watch option offered by Octane. If any of the directories and
|
||||
| files are changed, Octane will automatically reload your workers.
|
||||
|
|
||||
*/
|
||||
|
||||
'watch' => [
|
||||
'app',
|
||||
'bootstrap',
|
||||
'config',
|
||||
'database',
|
||||
'public/**/*.php',
|
||||
'resources/**/*.php',
|
||||
'routes',
|
||||
'composer.lock',
|
||||
'.env',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Garbage Collection Threshold
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When executing long-lived PHP scripts such as Octane, memory can build
|
||||
| up before being cleared by PHP. You can force Octane to run garbage
|
||||
| collection if your application consumes this amount of megabytes.
|
||||
|
|
||||
*/
|
||||
|
||||
'garbage' => 50,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maximum Execution Time
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following setting configures the maximum execution time for requests
|
||||
| being handled by Octane. You may set this value to 0 to indicate that
|
||||
| there isn't a specific time limit on Octane request execution time.
|
||||
|
|
||||
*/
|
||||
|
||||
'max_execution_time' => 30,
|
||||
|
||||
];
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
|
||||
# Exit the script as soon as a command fails
|
||||
set -e
|
||||
@@ -16,4 +16,4 @@ php artisan sandbox:migrate --force
|
||||
php artisan fleetbase:seed
|
||||
|
||||
# Restart queue
|
||||
php artisan queue:restart
|
||||
php artisan queue:restart
|
||||
|
||||
@@ -8,6 +8,9 @@ WORKDIR /app
|
||||
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
|
||||
|
||||
@@ -33,7 +36,7 @@ RUN pnpm install
|
||||
COPY console .
|
||||
|
||||
# Build the application
|
||||
RUN pnpm build
|
||||
RUN pnpm build --environment $ENVIRONMENT
|
||||
|
||||
# ---- Serve Stage ----
|
||||
FROM nginx:alpine
|
||||
|
||||
@@ -7,6 +7,31 @@
|
||||
<InputGroup @name="S3 URL" @value={{this.s3Url}} disabled={{this.isLoading}} />
|
||||
<InputGroup @name="S3 Endpoint" @value={{this.s3Endpoint}} disabled={{this.isLoading}} />
|
||||
{{/if}}
|
||||
{{#if (eq this.driver "gcs")}}
|
||||
{{#if this.isGoogleCloudStorageEnvConfigued}}
|
||||
<div class="border border-yellow-900 shadow-sm rounded-lg bg-yellow-200 mb-4">
|
||||
<div class="px-3 py-2 text-sm text-yellow-900 flex items-center">
|
||||
<FaIcon @icon="triangle-exclamation" @size="md" class="mr-1.5" />
|
||||
Warning! GCS is already configured in the server environment. Changing values below may break this.
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<InputGroup @name="GCS Bucket" @value={{this.gcsBucket}} disabled={{this.isLoading}} />
|
||||
<InputGroup @name="GCS Service Account Key File" @wrapperClass="">
|
||||
<div class="flex flex-row items-center mb-0i">
|
||||
<UploadButton @name="firebase-service-account" @accept="text/plain,text/javascript,application/json" @onFileAdded={{this.uploadGcsCredentialsFile}} @buttonText="Upload Service Account JSON" @icon="upload" class="w-auto m-0i mt-0i" />
|
||||
{{#if this.gcsCredentialsFile}}
|
||||
<div class="ml-2.5 text-sm dark:text-white text-black flex flex-row items-center border border-blue-500 rounded-lg px-2 py-0.5 -mt-1">
|
||||
<FaIcon @icon="file-text" @size="sm" class="mr-2 dark:text-white text-black" />
|
||||
<span>{{this.gcsCredentialsFile.original_filename}}</span>
|
||||
<a href="javascript:;" class="text-red-500 ml-2" {{on "click" this.removeGcsCredentialsFile}}>
|
||||
<FaIcon @icon="times" class="text-red-500" />
|
||||
</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</InputGroup>
|
||||
{{/if}}
|
||||
{{#if this.testResponse}}
|
||||
<div class="animate-pulse flex flex-row items-center rounded-lg border {{if (eq this.testResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
|
||||
<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'}}" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { action } from '@ember/object';
|
||||
export default class ConfigureFilesystemComponent extends Component {
|
||||
@service fetch;
|
||||
@service notifications;
|
||||
@service currentUser;
|
||||
@tracked isLoading = false;
|
||||
@tracked testResponse;
|
||||
@tracked disks = [];
|
||||
@@ -13,6 +14,10 @@ export default class ConfigureFilesystemComponent extends Component {
|
||||
@tracked s3Bucket = null;
|
||||
@tracked s3Url = null;
|
||||
@tracked s3Endpoint = null;
|
||||
@tracked gcsBucket = null;
|
||||
@tracked gcsCredentialsFileId = null;
|
||||
@tracked gcsCredentialsFile = null;
|
||||
@tracked isGoogleCloudStorageEnvConfigued = false;
|
||||
|
||||
/**
|
||||
* Creates an instance of ConfigureFilesystemComponent.
|
||||
@@ -59,6 +64,8 @@ export default class ConfigureFilesystemComponent extends Component {
|
||||
url: this.s3Url,
|
||||
endpoint: this.s3Endpoint,
|
||||
},
|
||||
gcsCredentialsFileId: this.gcsCredentialsFileId,
|
||||
gcsBucket: this.gcsBucket,
|
||||
})
|
||||
.then(() => {
|
||||
this.notifications.success('Filesystem configuration saved.');
|
||||
@@ -82,4 +89,31 @@ export default class ConfigureFilesystemComponent extends Component {
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action removeGcsCredentialsFile() {
|
||||
this.gcsCredentialsFileId = undefined;
|
||||
this.gcsCredentialsFile = undefined;
|
||||
}
|
||||
|
||||
@action uploadGcsCredentialsFile(file) {
|
||||
try {
|
||||
this.fetch.uploadFile.perform(
|
||||
file,
|
||||
{
|
||||
path: 'gcs',
|
||||
subject_uuid: this.currentUser.companyId,
|
||||
subject_type: 'company',
|
||||
type: 'gcs_credentials',
|
||||
},
|
||||
(uploadedFile) => {
|
||||
console.log('uploadedFile', uploadedFile);
|
||||
this.gcsCredentialsFileId = uploadedFile.id;
|
||||
this.gcsCredentialsFile = uploadedFile;
|
||||
console.log('this.gcsCredentialsFile', this.gcsCredentialsFile);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<Textarea class="form-input w-full" @value={{this.apn.private_key_content}} placeholder="APN Private Key" rows="10" disabled={{this.isLoading}} />
|
||||
</InputGroup> --}}
|
||||
<InputGroup @wrapperClass="flex flex-row items-center">
|
||||
<UploadButton @name="apn-key" @accept="text/plain,application/x-pem-file,application/x-pkcs12,application/x-x509-ca-cert,.p12,.pem,.p8" @onFileAdded={{this.uploadApnKey}} @buttonText="Upload P8 Key File" @uploadIcon="upload" class="w-auto m-0i mt-0i" />
|
||||
<UploadButton @name="apn-key" @accept="text/plain,application/x-pem-file,application/x-pkcs12,application/x-x509-ca-cert,.p12,.pem,.p8" @onFileAdded={{this.uploadApnKey}} @buttonText="Upload P8 Key File" @icon="upload" class="w-auto m-0i mt-0i" />
|
||||
{{#if this.apn.private_key_file}}
|
||||
<div class="ml-2.5 text-sm dark:text-white text-black flex flex-row items-center border border-blue-500 rounded-lg px-2 py-0.5 -mt-1">
|
||||
<FaIcon @icon="file-text" @size="sm" class="mr-2 dark:text-white text-black" />
|
||||
@@ -15,16 +15,32 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroup @wrapperClass="mb-0i">
|
||||
<Checkbox @label="APN Production" @value={{this.apn.production}} @onToggle={{fn (mut this.apn.production)}} @disabled={{this.isLoading}} />
|
||||
</InputGroup>
|
||||
</ContentPanel>
|
||||
|
||||
<ContentPanel @title="Firebase Configutation" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<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}}
|
||||
<div class="ml-2.5 text-sm dark:text-white text-black flex flex-row items-center border border-blue-500 rounded-lg px-2 py-0.5 -mt-1">
|
||||
<FaIcon @icon="file-text" @size="sm" class="mr-2 dark:text-white text-black" />
|
||||
<span>{{this.firebase.credentials_file.original_filename}}</span>
|
||||
<a href="javascript:;" class="text-red-500 ml-2" {{on "click" this.removeFirebaseCredentialsFile}}><FaIcon @icon="times" class="text-red-500" /></a>
|
||||
</div>
|
||||
{{/if}}
|
||||
</InputGroup>
|
||||
</ContentPanel>
|
||||
|
||||
<ContentPanel @title="Test Push Notification" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-900">
|
||||
{{#if this.testResponse}}
|
||||
<div class="animate-pulse flex flex-row items-center rounded-lg border {{if (eq this.testResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
|
||||
<FaIcon @icon={{if (eq this.testResponse.status 'error') 'triangle-exclamation' 'circle-check'}} class="mr-1.5 {{if (eq this.testResponse.status 'error') 'text-red-200' 'text-green-200'}}" />
|
||||
<span class="text-xs">{{this.this.testResponse.message}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="mt-3 rounded-lg bg-gray-900 shadow-inner p-3">
|
||||
<div class="">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="flex flex-row items-center">
|
||||
<div class="text-sm w-40">Title:</div>
|
||||
@@ -47,6 +63,8 @@
|
||||
</div>
|
||||
</ContentPanel>
|
||||
|
||||
<Spacer @height="300px" />
|
||||
|
||||
<EmberWormhole @to="next-view-section-subheader-actions">
|
||||
<Button @type="primary" @size="sm" @icon="save" @text="Save Changes" @onClick={{this.save}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
|
||||
</EmberWormhole>
|
||||
@@ -18,14 +18,13 @@ export default class ConfigureNotificationChannelsComponent extends Component {
|
||||
team_id: '',
|
||||
app_bundle_id: '',
|
||||
private_key_path: '',
|
||||
_private_key_path: '',
|
||||
private_key_file_id: '',
|
||||
private_key_file: null,
|
||||
production: true,
|
||||
};
|
||||
@tracked fcm = {
|
||||
firebase_credentials_json: '',
|
||||
firebase_database_url: '',
|
||||
firebase_project_name: '',
|
||||
@tracked firebase = {
|
||||
credentials: '',
|
||||
};
|
||||
|
||||
constructor() {
|
||||
@@ -38,26 +37,36 @@ export default class ConfigureNotificationChannelsComponent extends Component {
|
||||
apnConfig.private_key_file = null;
|
||||
apnConfig.private_key_file_id = '';
|
||||
apnConfig.private_key_path = '';
|
||||
apnConfig._private_key_path = '';
|
||||
|
||||
this.apn = apnConfig;
|
||||
}
|
||||
|
||||
@action removeFirebaseCredentialsFile() {
|
||||
const firebaseConfig = this.firebase;
|
||||
firebaseConfig.credentials_file = null;
|
||||
firebaseConfig.credentials_file_id = '';
|
||||
firebaseConfig.credentials = '';
|
||||
|
||||
this.firebase = firebaseConfig;
|
||||
}
|
||||
|
||||
@action uploadApnKey(file) {
|
||||
try {
|
||||
this.fetch.uploadFile.perform(
|
||||
file,
|
||||
{
|
||||
disk: 'local',
|
||||
path: `apn`,
|
||||
path: 'apn',
|
||||
subject_uuid: this.currentUser.companyId,
|
||||
subject_type: `company`,
|
||||
type: `apn_key`,
|
||||
subject_type: 'company',
|
||||
type: 'apn_key',
|
||||
},
|
||||
(uploadedFile) => {
|
||||
const apnConfig = this.apn;
|
||||
apnConfig.private_key_file = uploadedFile;
|
||||
apnConfig.private_key_file_id = uploadedFile.id;
|
||||
apnConfig.private_key_path = uploadedFile.path;
|
||||
apnConfig._private_key_path = uploadedFile.path;
|
||||
|
||||
this.apn = apnConfig;
|
||||
}
|
||||
@@ -67,6 +76,30 @@ export default class ConfigureNotificationChannelsComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
@action uploadFirebaseCredentials(file) {
|
||||
try {
|
||||
this.fetch.uploadFile.perform(
|
||||
file,
|
||||
{
|
||||
path: 'firebase',
|
||||
subject_uuid: this.currentUser.companyId,
|
||||
subject_type: 'company',
|
||||
type: 'firebase_credentials',
|
||||
},
|
||||
(uploadedFile) => {
|
||||
const firebaseConfig = this.firebase;
|
||||
firebaseConfig.credentials_file = uploadedFile;
|
||||
firebaseConfig.credentials_file_id = uploadedFile.id;
|
||||
firebaseConfig.credentials_file_path = uploadedFile.path;
|
||||
|
||||
this.firebase = firebaseConfig;
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.notifications.serverError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@action setConfigValues(config) {
|
||||
for (const key in config) {
|
||||
if (this[key] !== undefined) {
|
||||
@@ -94,9 +127,13 @@ export default class ConfigureNotificationChannelsComponent extends Component {
|
||||
const apnConfig = this.apn;
|
||||
delete apnConfig.private_key_file;
|
||||
|
||||
const firebaseConfig = this.firebase;
|
||||
delete firebaseConfig.credentials_file;
|
||||
|
||||
this.fetch
|
||||
.post('settings/notification-channels-config', {
|
||||
apn: apnConfig,
|
||||
firebase: firebaseConfig,
|
||||
})
|
||||
.then(() => {
|
||||
this.notifications.success("Notification channel's configuration saved.");
|
||||
@@ -112,6 +149,7 @@ export default class ConfigureNotificationChannelsComponent extends Component {
|
||||
this.fetch
|
||||
.post('settings/test-notification-channels-config', {
|
||||
apn: this.apn,
|
||||
firebase: this.firebase,
|
||||
title: this.testTitle,
|
||||
message: this.testMessage,
|
||||
apnToken: this.apnToken,
|
||||
|
||||
@@ -1,7 +1,99 @@
|
||||
<div class="fleetbase-dashboard-grid">
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-4">
|
||||
{{#each this.dashboards as |dashboard index|}}
|
||||
<Dashboard::Create @index={{index}} @dashboard={{dashboard}} />
|
||||
{{/each}}
|
||||
<div class="fleetbase-dashboard-grid flex items-center justify-between mb-4 mt-6 px-14">
|
||||
<div class="left-section">
|
||||
<h1 class="text-lg font-bold">{{this.dashboard.currentDashboard.name}}</h1>
|
||||
</div>
|
||||
<div class="fleetbase-dashboard-actions right-section ml-4 flex items-center">
|
||||
<div class="fleetbase-model-select fleetbase-power-select ember-model-select h-10">
|
||||
|
||||
<DropdownButton
|
||||
class="h-10"
|
||||
@text={{if this.dashboard.currentDashboard.name this.dashboard.currentDashboard.name (t "component.dashboard.select-dashboard")}}
|
||||
@textClass="text-sm mr-2"
|
||||
@buttonClass="flex-row-reverse w-44 justify-between"
|
||||
@icon="caret-down"
|
||||
@iconClass="mr-0i"
|
||||
@size="sm"
|
||||
@iconPrefix="fas"
|
||||
@triggerClass="hidden md:flex"
|
||||
as |dd|
|
||||
>
|
||||
<div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
|
||||
<div class="p-1">
|
||||
{{#each this.dashboard.dashboards as |dashboard|}}
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.selectDashboard dashboard)}}>
|
||||
<div class="flex-1 flex flex-row items-center">
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="desktop" />
|
||||
</div>
|
||||
<span>{{dashboard.name}}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{#if (eq this.dashboard.currentDashboard.id dashboard.id)}}
|
||||
<FaIcon @icon="check" class="text-green-500" />
|
||||
{{/if}}
|
||||
</div>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
|
||||
<div class="ml-2 relative h-10">
|
||||
<DropdownButton class="h-10" @icon="ellipsis-h" @size="sm" @iconPrefix="fas" @triggerClass="hidden md:flex" as |dd|>
|
||||
<div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
|
||||
<div class="p-1">
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.createDashboard)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="add" />
|
||||
</div>
|
||||
<span>{{t "component.dashboard.create-new-dashboard"}}</span>
|
||||
</a>
|
||||
|
||||
{{#unless (eq this.dashboard.currentDashboard.user_uuid "system")}}
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.onChangeEdit true)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="edit" />
|
||||
</div>
|
||||
<span>{{t "component.dashboard.edit-layout"}}</span>
|
||||
</a>
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.onAddingWidget true)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="add" />
|
||||
</div>
|
||||
<span>{{t "component.dashboard.add-widgets"}}</span>
|
||||
</a>
|
||||
|
||||
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.deleteDashboard this.dashboard.currentDashboard)}}>
|
||||
<div class="w-6">
|
||||
<FaIcon @icon="trash" />
|
||||
</div>
|
||||
<span>{{t "component.dashboard.delete-dashboard"}}</span>
|
||||
</a>
|
||||
{{/unless}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
{{#if this.dashboard.isEditingDashboard}}
|
||||
<div class="ml-2 h-10">
|
||||
<Button @type="magic" @icon="save" @helpText={{t "component.dashboard.save-dashboard"}} @onClick={{fn this.onChangeEdit false}} class="h-10" />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-10">
|
||||
<Dashboard::Create @isEdit={{this.dashboard.isEditingDashboard}} @isAddingWidget={{this.dashboard.isAddingWidget}} @dashboard={{this.dashboard.currentDashboard}} />
|
||||
{{#if this.dashboard.isAddingWidget}}
|
||||
<EmberWormhole @to="console-home-wormhole">
|
||||
<Dashboard::WidgetPanel
|
||||
@isOpen={{this.dashboard.isAddingWidget}}
|
||||
@onLoad={{this.setWidgetSelectorPanelContext}}
|
||||
@dashboard={{this.dashboard.currentDashboard}}
|
||||
@onClose={{fn this.onAddingWidget false}}
|
||||
/>
|
||||
</EmberWormhole>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -1,48 +1,140 @@
|
||||
import Component from '@glimmer/component';
|
||||
import loadExtensions from '@fleetbase/ember-core/utils/load-extensions';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* DashboardComponent for managing dashboards in an Ember application.
|
||||
* This component handles actions such as selecting, creating, deleting dashboards,
|
||||
* and managing widget selectors and dashboard editing states.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class DashboardComponent extends Component {
|
||||
@service fetch;
|
||||
@tracked extensions;
|
||||
@tracked dashboards = [];
|
||||
@tracked isLoading;
|
||||
/**
|
||||
* Ember Data store service.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service store;
|
||||
|
||||
/**
|
||||
* Internationalization service for managing translations.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service intl;
|
||||
|
||||
/**
|
||||
* Notifications service for displaying alerts or confirmations.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Modals manager service for handling modal dialogs.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service modalsManager;
|
||||
|
||||
/**
|
||||
* Fetch service for handling HTTP requests.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* Dashboard service for business logic related to dashboards.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service dashboard;
|
||||
|
||||
/**
|
||||
* Creates an instance of DashboardComponent.
|
||||
* @memberof DashboardComponent
|
||||
*/
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.loadExtensions();
|
||||
this.dashboard.loadDashboards.perform();
|
||||
}
|
||||
|
||||
@action async loadExtensions() {
|
||||
this.extensions = await loadExtensions();
|
||||
this.loadDashboardBuilds.perform();
|
||||
/**
|
||||
* Action to select a dashboard.
|
||||
* @param {Object} dashboard - The dashboard to be selected.
|
||||
*/
|
||||
@action selectDashboard(dashboard) {
|
||||
this.dashboard.selectDashboard.perform(dashboard);
|
||||
}
|
||||
|
||||
@task *loadDashboard(extension) {
|
||||
this.isLoading = extension.extension;
|
||||
let dashboardBuild;
|
||||
|
||||
try {
|
||||
dashboardBuild = yield this.fetch.get(extension.fleetbase.dashboard, {}, { namespace: '' });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isArray(dashboardBuild)) {
|
||||
this.dashboards = [...this.dashboards, ...dashboardBuild.map((build) => ({ ...build, extension }))];
|
||||
}
|
||||
/**
|
||||
* Sets the context for the widget selector panel.
|
||||
* @param {Object} widgetSelectorContext - The context object for the widget selector.
|
||||
*/
|
||||
@action setWidgetSelectorPanelContext(widgetSelectorContext) {
|
||||
this.widgetSelectorContext = widgetSelectorContext;
|
||||
}
|
||||
|
||||
@task({ enqueue: true, maxConcurrency: 1 }) *loadDashboardBuilds() {
|
||||
const extensionsWithDashboards = this.extensions.filter((extension) => typeof extension.fleetbase?.dashboard === 'string');
|
||||
/**
|
||||
* Creates a new dashboard.
|
||||
* @param {Object} dashboard - The dashboard to be created.
|
||||
* @param {Object} [options={}] - Optional parameters for dashboard creation.
|
||||
*/
|
||||
@action createDashboard(dashboard, options = {}) {
|
||||
this.modalsManager.show('modals/create-dashboard', {
|
||||
title: this.intl.t('component.dashboard.create-a-new-dashboard'),
|
||||
acceptButtonText: this.intl.t('component.dashboard.confirm-create-dashboard'),
|
||||
confirm: async (modal, done) => {
|
||||
modal.startLoading();
|
||||
|
||||
for (let i = 0; i < extensionsWithDashboards.length; i++) {
|
||||
const extension = extensionsWithDashboards[i];
|
||||
yield this.loadDashboard.perform(extension);
|
||||
// Get the name from the modal options
|
||||
const { name } = modal.getOptions();
|
||||
|
||||
await this.dashboard.createDashboard.perform(name);
|
||||
done();
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a dashboard.
|
||||
* @param {Object} dashboard - The dashboard to be deleted.
|
||||
* @param {Object} [options={}] - Optional parameters for dashboard deletion.
|
||||
*/
|
||||
@action deleteDashboard(dashboard, options = {}) {
|
||||
if (this.dashboard.dashboards?.length === 1) {
|
||||
return this.notifications.error(this.intl.t('component.dashboard.you-cannot-delete-this-dashboard'));
|
||||
}
|
||||
|
||||
this.modalsManager.confirm({
|
||||
title: this.intl.t('component.dashboard.are-you-sure-you-want-delete-dashboard', { dashboardName: dashboard.name }),
|
||||
confirm: async (modal, done) => {
|
||||
modal.startLoading();
|
||||
await this.dashboard.deleteDashboard.perform(dashboard);
|
||||
done();
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to handle the addition of a widget.
|
||||
* @param {boolean} [state=true] - The state to set for adding a widget.
|
||||
*/
|
||||
@action onAddingWidget(state = true) {
|
||||
this.dashboard.onAddingWidget(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current dashboard.
|
||||
* @param {Object} dashboard - The dashboard to be set as current.
|
||||
*/
|
||||
@action setCurrentDashboard(dashboard) {
|
||||
this.dashboard.setCurrentDashboard.perform(dashboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the editing state of the dashboard.
|
||||
* @param {boolean} [state=true] - The state to set for editing the dashboard.
|
||||
*/
|
||||
@action onChangeEdit(state = true) {
|
||||
this.dashboard.onChangeEdit(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="dashboard-component-count lg:col-span-2">
|
||||
<h3 class="text-sm dark:text-gray-100 text-black mb-4">{{@options.title}}</h3>
|
||||
<h1 class="text-3xl font-bold dark:text-gray-100 text-black mb-4">
|
||||
{{this.displayValue}}
|
||||
<div class="dashboard-component-count lg:col-span-2 h-full {{@options.wrapperClass}}">
|
||||
<h3 class="text-sm dark:text-gray-100 text-black mb-4 {{@options.titleClass}}">{{this.title}}</h3>
|
||||
<h1 class="text-3xl font-bold dark:text-gray-100 text-black {{@options.valueClass}}">
|
||||
{{this.value}}
|
||||
</h1>
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { computed } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import formatCurrency from '@fleetbase/ember-ui/utils/format-currency';
|
||||
import formatMeters from '@fleetbase/ember-ui/utils/format-meters';
|
||||
import formatBytes from '@fleetbase/ember-ui/utils/format-bytes';
|
||||
@@ -7,11 +7,40 @@ import formatDuration from '@fleetbase/ember-ui/utils/format-duration';
|
||||
import formatDate from '@fleetbase/ember-ui/utils/format-date';
|
||||
|
||||
export default class DashboardCountComponent extends Component {
|
||||
@computed('args.options.{currency,dateFormat,format,value}') get displayValue() {
|
||||
let format = this.args.options?.format;
|
||||
let currency = this.args.options?.currency;
|
||||
let dateFormat = this.args.options?.dateFormat;
|
||||
let value = this.args.options?.value;
|
||||
/**
|
||||
* The title of the metric count.
|
||||
*
|
||||
* @memberof WidgetKeyMetricsCountComponent
|
||||
*/
|
||||
@tracked title;
|
||||
|
||||
/**
|
||||
* The value to render
|
||||
*
|
||||
* @memberof WidgetKeyMetricsCountComponent
|
||||
*/
|
||||
@tracked value;
|
||||
|
||||
/**
|
||||
* Creates an instance of WidgetKeyMetricsCountComponent.
|
||||
* @param {EngineInstance} owner
|
||||
* @param {Object} { options }
|
||||
* @memberof WidgetKeyMetricsCountComponent
|
||||
*/
|
||||
constructor(owner, { options, title }) {
|
||||
super(...arguments);
|
||||
this.title = title;
|
||||
this.createRenderValueFromOptions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the value to render using the options provided.
|
||||
*
|
||||
* @param {Object} [options={}]
|
||||
* @memberof WidgetKeyMetricsCountComponent
|
||||
*/
|
||||
createRenderValueFromOptions(options = {}) {
|
||||
let { format, currency, dateFormat, value } = options;
|
||||
|
||||
switch (format) {
|
||||
case 'money':
|
||||
@@ -38,6 +67,6 @@ export default class DashboardCountComponent extends Component {
|
||||
break;
|
||||
}
|
||||
|
||||
return value;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<div class="col-span-{{or @dashboard.size 12}}">
|
||||
<div class="dashboard-title flex flex-col lg:flex-row lg:items-center">
|
||||
<div class="flex flex-row items-center mb-2 lg:mb-0">
|
||||
{{#if this.isLoading}}
|
||||
<Spinner class="mr-2i" />
|
||||
{{/if}}
|
||||
<h2 class="text-sm font-bold dark:text-gray-100 text-black">{{@dashboard.title}}</h2>
|
||||
</div>
|
||||
<div>
|
||||
<Dashboard::QueryParams @params={{@dashboard.queryParams}} @onChange={{this.onQueryParamsChanged}} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 lg:grid-cols-12 gap-4">
|
||||
{{#each this.dashboard.widgets as |widget|}}
|
||||
{{component (concat "dashboard/" widget.component) options=widget.options}}
|
||||
<div class="fleetbase-dashboard-grid" ...attributes>
|
||||
<GridStack @options={{this.gridOptions}} @onChange={{this.onChangeGrid}}>
|
||||
{{#each @dashboard.widgets as |widget|}}
|
||||
<GridStackItem id={{widget.id}} @options={{spread-widget-options (hash id=widget.id options=widget.grid_options)}} class="relative">
|
||||
{{component widget.component options=widget.options}}
|
||||
{{#if @isEdit}}
|
||||
<div class="absolute top-2 right-2">
|
||||
<Button @type="default" @icon="trash" @helpText={{"Remove widget from the dashboard"}} @onClick={{fn this.removeWidget widget}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</GridStackItem>
|
||||
{{/each}}
|
||||
</div>
|
||||
</GridStack>
|
||||
</div>
|
||||
@@ -1,41 +1,99 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action, computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
|
||||
/**
|
||||
* Component responsible for creating and managing the dashboard layout.
|
||||
* Provides functionalities such as toggling widget float, changing grid layout, and removing widgets.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class DashboardCreateComponent extends Component {
|
||||
@service fetch;
|
||||
@tracked isLoading = false;
|
||||
@tracked dashboard;
|
||||
/**
|
||||
* Notifications service for displaying alerts or errors.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.dashboard = this.args.dashboard;
|
||||
/**
|
||||
* Tracked array to keep track of widgets that have been updated.
|
||||
* @type {Array}
|
||||
*/
|
||||
@tracked updatedWidgets = [];
|
||||
|
||||
/**
|
||||
* Action to toggle the floating state of widgets on the grid.
|
||||
*/
|
||||
@action toggleFloat() {
|
||||
this.shouldFloat = !this.shouldFloat;
|
||||
}
|
||||
|
||||
@action onQueryParamsChanged(changedParams) {
|
||||
this.reloadDashboard.perform(changedParams);
|
||||
/**
|
||||
* Handles changes to the grid layout, such as repositioning or resizing widgets.
|
||||
* Iterates over each widget event detail and updates the corresponding widget's properties if necessary.
|
||||
*
|
||||
* @param {Event} event - Event containing details about the grid change.
|
||||
* @action
|
||||
*/
|
||||
@action onChangeGrid(event) {
|
||||
const { dashboard } = this.args;
|
||||
|
||||
event.detail.forEach((currentWidgetEvent) => {
|
||||
const alreadyUpdated = this.updatedWidgets.find((item) => item.id === currentWidgetEvent.id);
|
||||
if (alreadyUpdated || !this.dashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
const changedWidget = dashboard.widgets.find((widget) => widget.id === currentWidgetEvent.id);
|
||||
if (!changedWidget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { x, y, w, h } = currentWidgetEvent;
|
||||
const response = changedWidget.updateProperties({
|
||||
grid_options: { x, y, w, h },
|
||||
});
|
||||
if (response) {
|
||||
this.updatedWidgets.push(changedWidget);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@task *reloadDashboard(params) {
|
||||
const { extension } = this.args.dashboard;
|
||||
const index = this.args.index;
|
||||
let dashboards = [];
|
||||
/**
|
||||
* Removes a specified widget from the dashboard.
|
||||
* Performs a removal operation on the dashboard and handles any errors that occur during the process.
|
||||
*
|
||||
* @param {Object} widget - The widget object to be removed.
|
||||
* @action
|
||||
*/
|
||||
@action removeWidget(widget) {
|
||||
const { dashboard } = this.args;
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
dashboards = yield this.fetch.get(extension.fleetbase.dashboard, params, { namespace: '' });
|
||||
} catch {
|
||||
return;
|
||||
if (dashboard) {
|
||||
dashboard.removeWidget(widget.id).catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
if (isArray(dashboards)) {
|
||||
this.dashboard = dashboards.objectAt(index);
|
||||
}
|
||||
/**
|
||||
* Computed property that returns grid options based on the current edit state.
|
||||
* Configures grid behavior such as floating, animation, and drag and resize capabilities.
|
||||
*
|
||||
* @computed
|
||||
* @returns {Object} An object containing grid configuration options.
|
||||
*/
|
||||
@computed('args.isEdit') get gridOptions() {
|
||||
return {
|
||||
float: true,
|
||||
animate: true,
|
||||
acceptWidgets: true,
|
||||
alwaysShowResizeHandle: this.args.isEdit,
|
||||
disableDrag: !this.args.isEdit,
|
||||
disableResize: !this.args.isEdit,
|
||||
resizable: { handles: 'all' },
|
||||
cellHeight: 30,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
18
console/app/components/dashboard/metric.hbs
Normal file
18
console/app/components/dashboard/metric.hbs
Normal file
@@ -0,0 +1,18 @@
|
||||
<div class="col-span-{{or @dashboard.size 12}}">
|
||||
<div class="dashboard-title flex flex-col lg:flex-row lg:items-center">
|
||||
<div class="flex flex-row items-center mb-2 lg:mb-0">
|
||||
{{#if this.isLoading}}
|
||||
<Spinner class="mr-2i" />
|
||||
{{/if}}
|
||||
<h2 class="text-sm font-bold dark:text-gray-100 text-black">{{this.dashboard.title}}</h2>
|
||||
</div>
|
||||
<div>
|
||||
<Dashboard::QueryParams @params={{this.dashboard.queryParams}} @onChange={{this.onQueryParamsChanged}} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 lg:grid-cols-12 gap-4">
|
||||
{{#each this.dashboard.widgets as |widget|}}
|
||||
{{component (concat "dashboard/" widget.component) options=widget.options}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
39
console/app/components/dashboard/metric.js
Normal file
39
console/app/components/dashboard/metric.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
|
||||
export default class MetricComponent extends Component {
|
||||
@service fetch;
|
||||
@tracked isLoading = false;
|
||||
@tracked dashboard;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.loadDashboard.perform();
|
||||
}
|
||||
|
||||
@action onQueryParamsChanged(changedParams) {
|
||||
this.loadDashboard.perform(changedParams);
|
||||
}
|
||||
|
||||
@task *loadDashboard(params) {
|
||||
let dashboards = [];
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
dashboards = yield this.fetch.get(this.args.options.endpoint, params, { namespace: '' });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
|
||||
if (isArray(dashboards)) {
|
||||
this.dashboard = dashboards.objectAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
31
console/app/components/dashboard/widget-panel.hbs
Normal file
31
console/app/components/dashboard/widget-panel.hbs
Normal file
@@ -0,0 +1,31 @@
|
||||
<Overlay @isOpen={{@isOpen}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width={{or this.width @width "400px"}}>
|
||||
<Overlay::Header @title={{t "component.dashboard-widget-panel.select-widgets"}} @hideStatusDot={{true}} @titleWrapperClass="leading-5">
|
||||
<div class="flex flex-1 justify-end">
|
||||
<Button @type="default" @icon="times" @helpText={{t "component.dashboard-widget-panel.close-and-save"}} @onClick={{this.onPressClose}} />
|
||||
</div>
|
||||
</Overlay::Header>
|
||||
|
||||
<Overlay::Body @wrapperClass="new-service-rate-overlay-body px-4 space-y-4 pt-4">
|
||||
<div class="grid grid-cols-1 gap-4 text-xs dark:text-gray-100">
|
||||
{{#each this.availableWidgets as |widget|}}
|
||||
<div
|
||||
class="rounded-lg border border-gray-300 bg-white dark:border-gray-700 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-300 ease-in-out shadow-md px-4 py-2 cursor-pointer"
|
||||
{{on "click" (fn this.addWidgetToDashboard widget)}}
|
||||
>
|
||||
<div class="flex flex-row items-center leading-6 mb-2.5">
|
||||
<div class="w-8 flex items-center justify-start">
|
||||
<FaIcon @icon={{widget.icon}} class="text-lg text-gray-600 dark:text-gray-300" />
|
||||
</div>
|
||||
<p class="text-sm truncate font-semibold dark:text-gray-100 text-gray-800">
|
||||
{{t "component.dashboard-widget-panel.widget-name" widgetName=widget.name}}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs dark:text-gray-100 text-gray-800">{{widget.description}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Overlay::Body>
|
||||
</Overlay>
|
||||
60
console/app/components/dashboard/widget-panel.js
Normal file
60
console/app/components/dashboard/widget-panel.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class DashboardWidgetPanelComponent extends Component {
|
||||
@service universe;
|
||||
@tracked availableWidgets = [];
|
||||
@tracked dashboard;
|
||||
@tracked isOpen = true;
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Constructs the component and applies initial state.
|
||||
*/
|
||||
constructor(owner, { dashboard }) {
|
||||
super(...arguments);
|
||||
|
||||
this.availableWidgets = this.universe.getDashboardWidgets();
|
||||
this.dashboard = dashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the overlay context.
|
||||
*
|
||||
* @action
|
||||
* @param {OverlayContextObject} overlayContext
|
||||
*/
|
||||
@action setOverlayContext(overlayContext) {
|
||||
this.context = overlayContext;
|
||||
|
||||
if (typeof this.args.onLoad === 'function') {
|
||||
this.args.onLoad(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
@action addWidgetToDashboard(widget) {
|
||||
// If widget is a component definition/class
|
||||
if (typeof widget.component === 'function') {
|
||||
widget.component = widget.component.name;
|
||||
}
|
||||
|
||||
this.args.dashboard.addWidget(widget).catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles cancel button press.
|
||||
*
|
||||
* @action
|
||||
*/
|
||||
@action onPressClose() {
|
||||
this.isOpen = false;
|
||||
|
||||
if (typeof this.args.onClose === 'function') {
|
||||
this.args.onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
5
console/app/components/modals/create-dashboard.hbs
Normal file
5
console/app/components/modals/create-dashboard.hbs
Normal file
@@ -0,0 +1,5 @@
|
||||
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
|
||||
<div class="modal-body-container">
|
||||
<InputGroup @name="Dashboard name" @value={{@options.name}} @helpText="Enter the name of your dashboard" />
|
||||
</div>
|
||||
</Modal::Default>
|
||||
@@ -2,6 +2,7 @@ import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* Controller responsible for handling two-factor authentication.
|
||||
* @class AuthTwoFaController
|
||||
@@ -53,6 +54,8 @@ export default class AuthTwoFaController extends Controller {
|
||||
|
||||
/**
|
||||
* The current 2FA identity in memory
|
||||
* @property {string} identity
|
||||
* @tracked
|
||||
*/
|
||||
@tracked identity;
|
||||
|
||||
|
||||
@@ -51,6 +51,13 @@ export default class ConsoleController extends Controller {
|
||||
*/
|
||||
@service router;
|
||||
|
||||
/**
|
||||
* Inject the `intl` service.
|
||||
*
|
||||
* @var {Service}
|
||||
*/
|
||||
@service intl;
|
||||
|
||||
/**
|
||||
* Inject the `universe` service.
|
||||
*
|
||||
@@ -205,8 +212,8 @@ export default class ConsoleController extends Controller {
|
||||
const country = this.currentUser.country;
|
||||
|
||||
this.modalsManager.show('modals/create-or-join-org', {
|
||||
title: 'Create or join a organization',
|
||||
acceptButtonText: 'Confirm',
|
||||
title: this.intl.t('console.create-or-join-organization.modal-title'),
|
||||
acceptButtonText: this.intl.t('common.confirm'),
|
||||
acceptButtonIcon: 'check',
|
||||
acceptButtonIconPrefix: 'fas',
|
||||
action: 'join',
|
||||
@@ -226,13 +233,22 @@ export default class ConsoleController extends Controller {
|
||||
const { action, next, name, description, phone, currency, country, timezone } = modal.getOptions();
|
||||
|
||||
if (action === 'join') {
|
||||
return this.fetch.post('auth/join-organization', { next }).then(() => {
|
||||
this.fetch.flushRequestCache('auth/organizations');
|
||||
this.notifications.success('You have joined a new organization!');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 900);
|
||||
});
|
||||
return this.fetch
|
||||
.post('auth/join-organization', { next })
|
||||
.then(() => {
|
||||
this.fetch.flushRequestCache('auth/organizations');
|
||||
this.notifications.success(this.intl.t('console.create-or-join-organization.join-success-notification'));
|
||||
later(
|
||||
this,
|
||||
() => {
|
||||
window.location.reload();
|
||||
},
|
||||
900
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
}
|
||||
|
||||
return this.fetch
|
||||
@@ -246,7 +262,7 @@ export default class ConsoleController extends Controller {
|
||||
})
|
||||
.then(() => {
|
||||
this.fetch.flushRequestCache('auth/organizations');
|
||||
this.notifications.success('You have created a new organization!');
|
||||
this.notifications.success(this.intl.t('console.create-or-join-organization.create-success-notification'));
|
||||
later(
|
||||
this,
|
||||
() => {
|
||||
@@ -254,6 +270,9 @@ export default class ConsoleController extends Controller {
|
||||
},
|
||||
900
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -270,9 +289,9 @@ export default class ConsoleController extends Controller {
|
||||
}
|
||||
|
||||
this.modalsManager.confirm({
|
||||
title: `Are you sure you want to switch organization to ${organization.name}?`,
|
||||
body: `By confirming your account will remain logged in, but your primary organization will be switched.`,
|
||||
acceptButtonText: `Yes, I want to switch organization`,
|
||||
title: this.intl.t('console.switch-organization.modal-title', { organizationName: organization.name }),
|
||||
body: this.intl.t('console.switch-organization.modal-body'),
|
||||
acceptButtonText: this.intl.t('console.switch-organization.modal-accept-button-text'),
|
||||
acceptButtonScheme: 'primary',
|
||||
confirm: (modal) => {
|
||||
modal.startLoading();
|
||||
@@ -281,10 +300,14 @@ export default class ConsoleController extends Controller {
|
||||
.post('auth/switch-organization', { next: organization.uuid })
|
||||
.then(() => {
|
||||
this.fetch.flushRequestCache('auth/organizations');
|
||||
this.notifications.success('You have switched organizations');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 900);
|
||||
this.notifications.success(this.intl.t('console.switch-organization.success-notification'));
|
||||
later(
|
||||
this,
|
||||
() => {
|
||||
window.location.reload();
|
||||
},
|
||||
900
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
@@ -295,8 +318,8 @@ export default class ConsoleController extends Controller {
|
||||
|
||||
@action viewChangelog() {
|
||||
this.modalsManager.show('modals/changelog', {
|
||||
title: 'Changelog',
|
||||
acceptButtonText: 'OK',
|
||||
title: this.intl.t('common.changelog'),
|
||||
acceptButtonText: this.intl.t('common.ok'),
|
||||
hideDeclineButton: true,
|
||||
});
|
||||
}
|
||||
|
||||
169
console/app/controllers/console/admin/organizations/index.js
Normal file
169
console/app/controllers/console/admin/organizations/index.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
/**
|
||||
* Controller for managing organizations in the admin console.
|
||||
*
|
||||
* @class ConsoleAdminOrganizationsController
|
||||
* @extends Controller
|
||||
*/
|
||||
export default class ConsoleAdminOrganizationsController extends Controller {
|
||||
/**
|
||||
* The Ember Data service for interacting with the store.
|
||||
*
|
||||
* @property {Service} store
|
||||
* @type {Object}
|
||||
*/
|
||||
@service store;
|
||||
|
||||
/**
|
||||
* Inject the `intl` service
|
||||
*
|
||||
* @var {Service}
|
||||
*/
|
||||
@service intl;
|
||||
|
||||
/**
|
||||
* The Ember Router service for handling transitions between routes.
|
||||
*
|
||||
* @property {Service} router
|
||||
* @type {Object}
|
||||
*/
|
||||
@service router;
|
||||
|
||||
/**
|
||||
* Inject the `filters` service
|
||||
*
|
||||
* @var {Service}
|
||||
*/
|
||||
@service filters;
|
||||
|
||||
/**
|
||||
* The search query param value.
|
||||
*
|
||||
* @var {String|null}
|
||||
*/
|
||||
@tracked query;
|
||||
|
||||
/**
|
||||
* The current page of data being viewed
|
||||
*
|
||||
* @var {Integer}
|
||||
*/
|
||||
@tracked page = 1;
|
||||
|
||||
/**
|
||||
* The maximum number of items to show per page
|
||||
*
|
||||
* @var {Integer}
|
||||
*/
|
||||
@tracked limit = 20;
|
||||
|
||||
/**
|
||||
* The filterable param `sort`
|
||||
*
|
||||
* @var {String|Array}
|
||||
*/
|
||||
@tracked sort = '-created_at';
|
||||
|
||||
/**
|
||||
* The filterable param `name`
|
||||
*
|
||||
* @var {String}
|
||||
*/
|
||||
@tracked name;
|
||||
|
||||
/**
|
||||
* The filterable param `country`
|
||||
*
|
||||
* @var {String}
|
||||
*/
|
||||
@tracked country;
|
||||
|
||||
/**
|
||||
* Array to store the fetched companies.
|
||||
*
|
||||
* @var {Array}
|
||||
*/
|
||||
@tracked companies = [];
|
||||
|
||||
/**
|
||||
* Queryable parameters for this controller's model
|
||||
*
|
||||
* @var {Array}
|
||||
*/
|
||||
queryParams = ['name', 'page', 'limit', 'sort'];
|
||||
|
||||
/**
|
||||
* Columns for organization
|
||||
*
|
||||
* @memberof ConsoleAdminOrganizationsController
|
||||
*/
|
||||
columns = [
|
||||
{
|
||||
label: this.intl.t('common.name'),
|
||||
valuePath: 'name',
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterComponent: 'filter/string',
|
||||
},
|
||||
{
|
||||
label: this.intl.t('console.admin.organizations.index.owner-name-column'),
|
||||
valuePath: 'owner.name',
|
||||
width: '200px',
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
label: this.intl.t('console.admin.organizations.index.owner-email-column'),
|
||||
valuePath: 'owner.email',
|
||||
width: '200px',
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
},
|
||||
{
|
||||
label: this.intl.t('console.admin.organizations.index.phone-column'),
|
||||
valuePath: 'owner.phone',
|
||||
width: '200px',
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterComponent: 'filter/string',
|
||||
},
|
||||
{
|
||||
label: this.intl.t('console.admin.organizations.index.users-count-column'),
|
||||
valuePath: 'users_count',
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
label: this.intl.t('common.created-at'),
|
||||
valuePath: 'createdAt',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Update search query param and reset page to 1
|
||||
*
|
||||
* @param {Event} event
|
||||
* @memberof ConsoleAdminOrganizationsController
|
||||
*/
|
||||
@action search(event) {
|
||||
this.query = event.target.value ?? '';
|
||||
this.page = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the organization-users route for the selected company.
|
||||
*
|
||||
* @method goToCompany
|
||||
* @param {Object} company - The selected company.
|
||||
*/
|
||||
@action goToCompany(company) {
|
||||
this.router.transitionTo('console.admin.organizations.index.users', company.public_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class ConsoleAdminOrganizationsIndexUsersController extends Controller {
|
||||
/**
|
||||
* Inject the `filters` service
|
||||
*
|
||||
* @var {Service}
|
||||
*/
|
||||
@service filters;
|
||||
|
||||
/**
|
||||
* Inject the `intl` service
|
||||
*
|
||||
* @var {Service}
|
||||
*/
|
||||
@service intl;
|
||||
|
||||
/**
|
||||
* Inject the `router` service
|
||||
*
|
||||
* @var {Service}
|
||||
*/
|
||||
@service router;
|
||||
|
||||
/**
|
||||
* The current page of data being viewed
|
||||
*
|
||||
* @var {Integer}
|
||||
*/
|
||||
@tracked nestedPage = 1;
|
||||
|
||||
/**
|
||||
* The maximum number of items to show per page
|
||||
*
|
||||
* @var {Integer}
|
||||
*/
|
||||
@tracked nestedLimit = 20;
|
||||
|
||||
/**
|
||||
* The filterable param `sort`
|
||||
*
|
||||
* @var {Array|String}
|
||||
*/
|
||||
@tracked nestedSort = '-created_at';
|
||||
|
||||
/**
|
||||
* The filterable param `sort`
|
||||
*
|
||||
* @var {String}
|
||||
*/
|
||||
@tracked nestedQuery = '';
|
||||
|
||||
/**
|
||||
* The company loaded.
|
||||
*
|
||||
* @memberof ConsoleAdminOrganizationsIndexUsersController
|
||||
*/
|
||||
@tracked company;
|
||||
|
||||
/**
|
||||
* Queryable parameters for this controller's model
|
||||
*
|
||||
* @var {Array}
|
||||
*/
|
||||
queryParams = ['nestedPage', 'nestedLimit', 'nestedSort', 'nestedQuery'];
|
||||
|
||||
/**
|
||||
* Columns to render to the table.
|
||||
*
|
||||
* @memberof ConsoleAdminOrganizationsIndexUsersController
|
||||
*/
|
||||
columns = [
|
||||
{
|
||||
label: this.intl.t('common.name'),
|
||||
valuePath: 'name',
|
||||
},
|
||||
{
|
||||
label: this.intl.t('common.phone-number'),
|
||||
valuePath: 'phone',
|
||||
},
|
||||
{
|
||||
label: this.intl.t('common.email'),
|
||||
valuePath: 'email',
|
||||
},
|
||||
{
|
||||
label: this.intl.t('common.status'),
|
||||
valuePath: 'status',
|
||||
cellComponent: 'table/cell/status',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Update search query param and reset page to 1
|
||||
*
|
||||
* @param {Event} event
|
||||
* @memberof ConsoleAdminOrganizationsController
|
||||
*/
|
||||
@action search(event) {
|
||||
this.nestedQuery = event.target.value ?? '';
|
||||
this.nestedPage = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the overlay component context object.
|
||||
*
|
||||
* @param {Object} contextApi
|
||||
* @memberof ConsoleAdminOrganizationsIndexUsersController
|
||||
*/
|
||||
@action setOverlayContext(contextApi) {
|
||||
this.contextApi = contextApi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle closing the overlay.
|
||||
*
|
||||
* @return {Promise<Transition>}
|
||||
* @memberof ConsoleAdminOrganizationsIndexUsersController
|
||||
*/
|
||||
@action onPressClose() {
|
||||
if (this.contextApi && typeof this.contextApi.close === 'function') {
|
||||
this.contextApi.close();
|
||||
}
|
||||
|
||||
return this.router.transitionTo('console.admin.organizations.index');
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,9 @@ export default class OnboardIndexController extends Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set user timezone
|
||||
input.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
return this.fetch
|
||||
|
||||
7
console/app/helpers/spread-widget-options.js
Normal file
7
console/app/helpers/spread-widget-options.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export default helper(function spreadWidgetOptions([params]) {
|
||||
const { id, options } = params;
|
||||
const gridOptions = { id, ...options };
|
||||
return gridOptions;
|
||||
});
|
||||
@@ -1,7 +1,12 @@
|
||||
export function initialize() {
|
||||
const socketClusterClientScript = document.createElement('script');
|
||||
socketClusterClientScript.src = '/assets/socketcluster-client.min.js';
|
||||
document.body.appendChild(socketClusterClientScript);
|
||||
// Check if the script already exists
|
||||
// Only insert the script tag if it doesn't already exist
|
||||
if (!document.querySelector('script[data-socketcluster-client]')) {
|
||||
const socketClusterClientScript = document.createElement('script');
|
||||
socketClusterClientScript.setAttribute('data-socketcluster-client', '1');
|
||||
socketClusterClientScript.src = '/assets/socketcluster-client.min.js';
|
||||
document.body.appendChild(socketClusterClientScript);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
36
console/app/instance-initializers/initialize-widgets.js
Normal file
36
console/app/instance-initializers/initialize-widgets.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
export function initialize(application) {
|
||||
const universe = application.lookup('service:universe');
|
||||
const defaultWidgets = [
|
||||
{
|
||||
widgetId: 'fleetbase-blog',
|
||||
name: 'Fleetbase Blog',
|
||||
description: 'Lists latest news and events from the Fleetbase official team.',
|
||||
icon: 'newspaper',
|
||||
component: 'fleetbase-blog',
|
||||
grid_options: { w: 8, h: 9, minW: 8, minH: 9 },
|
||||
options: {
|
||||
title: 'Fleetbase Blog',
|
||||
},
|
||||
},
|
||||
{
|
||||
widgetId: 'fleetbase-github-card',
|
||||
name: 'Github Card',
|
||||
description: 'Displays current Github stats from the official Fleetbase repo.',
|
||||
icon: faGithub,
|
||||
component: 'github-card',
|
||||
grid_options: { w: 4, h: 8, minW: 4, minH: 8 },
|
||||
options: {
|
||||
title: 'Github Card',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
universe.registerDefaultDashboardWidgets(defaultWidgets);
|
||||
universe.registerDashboardWidgets(defaultWidgets);
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize,
|
||||
};
|
||||
@@ -29,6 +29,7 @@ export default class CategoryModel extends Model {
|
||||
@attr('string') slug;
|
||||
@attr('string') order;
|
||||
@attr('raw') translations;
|
||||
@attr('raw') meta;
|
||||
|
||||
/** @dates */
|
||||
@attr('date') deleted_at;
|
||||
|
||||
@@ -13,6 +13,7 @@ export default class Company extends Model {
|
||||
@attr('string') place_uuid;
|
||||
|
||||
/** @relationships */
|
||||
@belongsTo('user') owner;
|
||||
@belongsTo('file') logo;
|
||||
@belongsTo('file') backdrop;
|
||||
|
||||
@@ -23,6 +24,7 @@ export default class Company extends Model {
|
||||
@attr('string') backdrop_url;
|
||||
@attr('string') description;
|
||||
@attr('raw') options;
|
||||
@attr('number') users_count;
|
||||
@attr('string') type;
|
||||
@attr('string') currency;
|
||||
@attr('string') country;
|
||||
|
||||
54
console/app/models/custom-field-value.js
Normal file
54
console/app/models/custom-field-value.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { getOwner } from '@ember/application';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
|
||||
function isValidFileObjectJson(str) {
|
||||
return typeof str === 'string' && str.startsWith('{') && str.endsWith('}');
|
||||
}
|
||||
|
||||
export default class CustomFieldValueModel extends Model {
|
||||
/** @ids */
|
||||
@attr('string') company_uuid;
|
||||
@attr('string') custom_field_uuid;
|
||||
@attr('string') subject_uuid;
|
||||
@attr('string') subject_type;
|
||||
|
||||
/** @attributes */
|
||||
@attr('string') value;
|
||||
@attr('string') value_type;
|
||||
|
||||
/** @dates */
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@attr('date') deleted_at;
|
||||
|
||||
/** @computed */
|
||||
@computed('value') get asFile() {
|
||||
const owner = getOwner(this);
|
||||
const fetch = owner.lookup(`service:fetch`);
|
||||
const value = this.value;
|
||||
if (!isValidFileObjectJson(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileModel = fetch.jsonToModel(value, 'file');
|
||||
return fileModel;
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAgo() {
|
||||
return formatDistanceToNow(this.created_at);
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAgo() {
|
||||
return formatDistanceToNow(this.updated_at);
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
}
|
||||
}
|
||||
48
console/app/models/custom-field.js
Normal file
48
console/app/models/custom-field.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
|
||||
export default class CustomFieldModel extends Model {
|
||||
/** @ids */
|
||||
@attr('string') company_uuid;
|
||||
@attr('string') category_uuid;
|
||||
@attr('string') subject_uuid;
|
||||
@attr('string') subject_type;
|
||||
|
||||
/** @attributes */
|
||||
@attr('string') name;
|
||||
@attr('string') description;
|
||||
@attr('string') help_text;
|
||||
@attr('string') label;
|
||||
@attr('string') type;
|
||||
@attr('string') component;
|
||||
@attr('string') default_value;
|
||||
@attr('number') order;
|
||||
@attr('boolean') required;
|
||||
@attr('boolean', { defaultValue: true }) editable;
|
||||
@attr('raw') options;
|
||||
@attr('raw') validation_rules;
|
||||
@attr('raw') meta;
|
||||
|
||||
/** @dates */
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
@attr('date') deleted_at;
|
||||
|
||||
/** @computed */
|
||||
@computed('created_at') get createdAgo() {
|
||||
return formatDistanceToNow(this.created_at);
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAgo() {
|
||||
return formatDistanceToNow(this.updated_at);
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
}
|
||||
}
|
||||
58
console/app/models/dashboard-widget.js
Normal file
58
console/app/models/dashboard-widget.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import Model, { attr, belongsTo } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
|
||||
export default class DashboardWidgetModel extends Model {
|
||||
/** @ids */
|
||||
@attr('string') dashboard_uuid;
|
||||
|
||||
/** @relationships */
|
||||
@belongsTo('dashboard') dashboard;
|
||||
|
||||
/** @attributes */
|
||||
@attr('string') name;
|
||||
@attr('string') component;
|
||||
@attr('object') grid_options;
|
||||
@attr('object') options;
|
||||
|
||||
/** @dates */
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
|
||||
/** @computed */
|
||||
@computed('updated_at') get updatedAgo() {
|
||||
return formatDistanceToNow(this.updated_at);
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAtShort() {
|
||||
return format(this.updated_at, 'PP');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAgo() {
|
||||
return formatDistanceToNow(this.created_at);
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAtShort() {
|
||||
return format(this.created_at, 'PP');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the dashboard widget properties on the server
|
||||
*
|
||||
* @param {Object} [properties={}]
|
||||
* @return {Promise<DashboardWidgetModel>}
|
||||
* @memberof DashboardWidgetModel
|
||||
*/
|
||||
updateProperties(properties = {}) {
|
||||
this.setProperties(properties);
|
||||
return this.save();
|
||||
}
|
||||
}
|
||||
86
console/app/models/dashboard.js
Normal file
86
console/app/models/dashboard.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import Model, { attr, hasMany } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
import { getOwner } from '@ember/application';
|
||||
|
||||
export default class DashboardModel extends Model {
|
||||
/** @ids */
|
||||
@attr('string') company_uuid;
|
||||
@attr('string') user_uuid;
|
||||
|
||||
/** @relationships */
|
||||
@hasMany('dashboard-widget', { async: false }) widgets;
|
||||
|
||||
/** @attributes */
|
||||
@attr('string') name;
|
||||
@attr('boolean') is_default;
|
||||
|
||||
/** @dates */
|
||||
@attr('date') created_at;
|
||||
@attr('date') updated_at;
|
||||
|
||||
/** @computed */
|
||||
@computed('updated_at') get updatedAgo() {
|
||||
return formatDistanceToNow(this.updated_at);
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAt() {
|
||||
return format(this.updated_at, 'PPP p');
|
||||
}
|
||||
|
||||
@computed('updated_at') get updatedAtShort() {
|
||||
return format(this.updated_at, 'PP');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAgo() {
|
||||
return formatDistanceToNow(this.created_at);
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAt() {
|
||||
return format(this.created_at, 'PPP p');
|
||||
}
|
||||
|
||||
@computed('created_at') get createdAtShort() {
|
||||
return format(this.created_at, 'PP');
|
||||
}
|
||||
|
||||
/** @methods */
|
||||
addWidget(widget) {
|
||||
const owner = getOwner(this);
|
||||
const store = owner.lookup('service:store');
|
||||
const widgetRecord = store.createRecord('dashboard-widget', { ...widget, dashboard: this });
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
widgetRecord
|
||||
.save()
|
||||
.then((widgetRecord) => {
|
||||
this.widgets.pushObject(widgetRecord);
|
||||
resolve(widgetRecord);
|
||||
})
|
||||
.catch((error) => {
|
||||
store.unloadRecord(widgetRecord);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeWidget(widget) {
|
||||
const owner = getOwner(this);
|
||||
const store = owner.lookup('service:store');
|
||||
const widgetRecord = store.peekRecord('dashboard-widget', widget);
|
||||
|
||||
if (widgetRecord) {
|
||||
return new Promise((resolve, reject) => {
|
||||
widgetRecord
|
||||
.destroyRecord()
|
||||
.then(() => {
|
||||
this.widgets.removeObject(widgetRecord);
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
|
||||
export default class NotificationModel extends Model {
|
||||
|
||||
@@ -49,6 +49,10 @@ Router.map(function () {
|
||||
this.route('notifications');
|
||||
this.route('two-fa-settings');
|
||||
this.route('virtual', { path: '/:slug/:view' });
|
||||
this.route('organizations', function () {
|
||||
this.route('index', { path: '/' });
|
||||
this.route('users', { path: '/:company_id' });
|
||||
});
|
||||
});
|
||||
});
|
||||
this.route('install');
|
||||
|
||||
@@ -12,6 +12,6 @@ export default class AuthResetPasswordRoute extends Route {
|
||||
super.setupController(...arguments);
|
||||
|
||||
// set brand to controller
|
||||
this.brand = await this.store.findRecord('brand', 1);
|
||||
controller.brand = await this.store.findRecord('brand', 1);
|
||||
}
|
||||
}
|
||||
|
||||
19
console/app/routes/console/admin/organizations/index.js
Normal file
19
console/app/routes/console/admin/organizations/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleAdminOrganizationsRoute extends Route {
|
||||
@service store;
|
||||
|
||||
queryParams = {
|
||||
page: { refreshModel: true },
|
||||
query: { refreshModel: true },
|
||||
sort: { refreshModel: true },
|
||||
limit: { refreshModel: true },
|
||||
name: { refreshModel: true },
|
||||
country: { refreshModel: true },
|
||||
};
|
||||
|
||||
model(params) {
|
||||
return this.store.query('company', { view: 'admin', ...params });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import ArrayProxy from '@ember/array/proxy';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { isArray } from '@ember/array';
|
||||
|
||||
export default class ConsoleAdminOrganizationsIndexUsersRoute extends Route {
|
||||
@service fetch;
|
||||
@service store;
|
||||
|
||||
queryParams = {
|
||||
nestedPage: { refreshModel: true },
|
||||
nestedLimit: { refreshModel: true },
|
||||
nestedSort: { refreshModel: true },
|
||||
nestedQuery: { refreshModel: true },
|
||||
};
|
||||
|
||||
model(params) {
|
||||
this.companyId = params.public_id;
|
||||
|
||||
return this.fetch
|
||||
.get(`companies/${this.companyId}/users`, {
|
||||
page: params.nestedPage,
|
||||
limit: params.nestedLimit,
|
||||
sort: params.nestedSort,
|
||||
query: params.nestedQuery,
|
||||
paginate: 1,
|
||||
})
|
||||
.then(this.transformResults.bind(this));
|
||||
}
|
||||
|
||||
transformResults({ users, meta }) {
|
||||
if (isArray(users)) {
|
||||
users = users.map((user) => this.fetch.jsonToModel(user, 'user'));
|
||||
}
|
||||
|
||||
return ArrayProxy.create({ content: users, meta });
|
||||
}
|
||||
|
||||
setupController(controller) {
|
||||
super.setupController(...arguments);
|
||||
controller.company = this.getCompany();
|
||||
}
|
||||
|
||||
getCompany() {
|
||||
const companies = this.store.peekAll('company');
|
||||
return companies.find((company) => {
|
||||
return this.companyId === company.public_id || this.companyId === company.id;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,10 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class InviteForUserRoute extends Route {}
|
||||
export default class InviteForUserRoute extends Route {
|
||||
@service store;
|
||||
|
||||
model() {
|
||||
return this.store.findRecord('brand', 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@ export default class CommentSerializer extends ApplicationSerializer.extend(Embe
|
||||
};
|
||||
}
|
||||
|
||||
serializeAttribute(snapshot, json, key, attributes) {
|
||||
serializeAttribute(snapshot, json, key) {
|
||||
if (key === 'editable') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
super.serializeAttribute(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
serializeHasMany(snapshot, json, relationship) {
|
||||
let key = relationship.key;
|
||||
|
||||
15
console/app/serializers/company.js
Normal file
15
console/app/serializers/company.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
|
||||
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
|
||||
|
||||
export default class CompanySerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
|
||||
/**
|
||||
* Embedded relationship attributes
|
||||
*
|
||||
* @var {Object}
|
||||
*/
|
||||
get attrs() {
|
||||
return {
|
||||
owner: { embedded: 'always' },
|
||||
};
|
||||
}
|
||||
}
|
||||
3
console/app/serializers/dashboard-widget.js
Normal file
3
console/app/serializers/dashboard-widget.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
|
||||
|
||||
export default class DashboardWidgetSerializer extends ApplicationSerializer {}
|
||||
18
console/app/serializers/dashboard.js
Normal file
18
console/app/serializers/dashboard.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
|
||||
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
|
||||
|
||||
export default class DashboardSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
|
||||
attrs = {
|
||||
widgets: { embedded: 'always' },
|
||||
};
|
||||
|
||||
serializeHasMany(snapshot, json, relationship) {
|
||||
let key = relationship.key;
|
||||
|
||||
if (key === 'widgets') {
|
||||
return;
|
||||
}
|
||||
|
||||
return super.serializeHasMany(...arguments);
|
||||
}
|
||||
}
|
||||
255
console/app/services/dashboard.js
Normal file
255
console/app/services/dashboard.js
Normal file
@@ -0,0 +1,255 @@
|
||||
import Service from '@ember/service';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency-decorators';
|
||||
import { action } from '@ember/object';
|
||||
import { isArray } from '@ember/array';
|
||||
|
||||
/**
|
||||
* Service for managing dashboards, including loading, creating, and deleting dashboards, as well as managing the current dashboard and widget states.
|
||||
* Utilizes Ember services such as `store`, `fetch`, `notifications`, and `universe` for data management and user interaction.
|
||||
*
|
||||
* @extends Service
|
||||
*/
|
||||
export default class DashboardService extends Service {
|
||||
/**
|
||||
* Ember Data store service for managing model data.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service store;
|
||||
|
||||
/**
|
||||
* Fetch service for making network requests.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* Notifications service for displaying user notifications.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service notifications;
|
||||
|
||||
/**
|
||||
* Universe service for accessing global application state or utility methods.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service universe;
|
||||
|
||||
/**
|
||||
* Internationalization service.
|
||||
* @type {Service}
|
||||
*/
|
||||
@service intl;
|
||||
|
||||
/**
|
||||
* Tracked array of available dashboards.
|
||||
* @type {Array}
|
||||
*/
|
||||
@tracked dashboards = [];
|
||||
|
||||
/**
|
||||
* Tracked property representing the currently selected dashboard.
|
||||
* @type {Object}
|
||||
*/
|
||||
@tracked currentDashboard;
|
||||
|
||||
/**
|
||||
* Tracked boolean indicating if the dashboard is in editing mode.
|
||||
* @type {boolean}
|
||||
*/
|
||||
@tracked isEditingDashboard = false;
|
||||
|
||||
/**
|
||||
* Tracked boolean indicating if a widget is being added.
|
||||
* @type {boolean}
|
||||
*/
|
||||
@tracked isAddingWidget = false;
|
||||
|
||||
/**
|
||||
* Task for loading dashboards from the store. It sets the current dashboard and checks if adding widget is necessary.
|
||||
*/
|
||||
@task *loadDashboards() {
|
||||
const dashboards = yield this.store.findAll('dashboard');
|
||||
|
||||
if (isArray(dashboards)) {
|
||||
this.dashboards = dashboards.toArray();
|
||||
|
||||
// insert default dashboard if it's not loaded
|
||||
const defaultDashboard = this._createDefaultDashboard();
|
||||
if (this._isDefaultDashboardNotLoaded()) {
|
||||
this.dashboards.unshiftObject(defaultDashboard);
|
||||
}
|
||||
|
||||
// Set the current dashboard
|
||||
this.currentDashboard = this._getNextDashboard();
|
||||
if (this.currentDashboard && this.currentDashboard.widgets.length === 0) {
|
||||
this.onAddingWidget(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for selecting a dashboard. Handles dashboard switching and updates the current dashboard.
|
||||
* @param {Object} dashboard - The dashboard object to select.
|
||||
*/
|
||||
@task *selectDashboard(dashboard) {
|
||||
if (dashboard.user_uuid === 'system') {
|
||||
this.currentDashboard = dashboard;
|
||||
yield this.fetch.post('dashboards/reset-default');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDashboard = yield this.fetch.post('dashboards/switch', { dashboard_uuid: dashboard.id }, { normalizeToEmberData: true }).catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
|
||||
if (currentDashboard) {
|
||||
this.currentDashboard = currentDashboard;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for creating a new dashboard. It handles dashboard creation, success notification, and dashboard selection.
|
||||
* @param {string} name - Name of the new dashboard.
|
||||
*/
|
||||
@task *createDashboard(name) {
|
||||
const dashboardRecord = this.store.createRecord('dashboard', { name, is_default: true });
|
||||
const dashboard = yield dashboardRecord.save().catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
|
||||
if (dashboard) {
|
||||
this.notifications.success(this.intl.t('services.dashboard-service.create-dashboard-success-notification', { dashboardName: dashboard.name }));
|
||||
this.selectDashboard.perform(dashboard);
|
||||
this.dashboards.pushObject(dashboard);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for deleting a dashboard. Handles dashboard deletion and success notification.
|
||||
* @param {Object} dashboard - The dashboard object to delete.
|
||||
* @param {Object} [options={}] - Optional configuration options.
|
||||
*/
|
||||
@task *deleteDashboard(dashboard, options = {}) {
|
||||
yield dashboard.destroyRecord().catch((error) => {
|
||||
this.notification.serverError(error);
|
||||
|
||||
if (typeof options.onError === 'function') {
|
||||
options.onError(error, dashboard);
|
||||
}
|
||||
});
|
||||
|
||||
this.notifications.success(this.intl.t('services.dashboard-service.delete-dashboard-success-notification', { dashboardName: dashboard.name }));
|
||||
yield this.loadDashboards.perform();
|
||||
yield this.selectDashboard.perform(this._getNextDashboard());
|
||||
|
||||
if (typeof options.callback === 'function') {
|
||||
options.callback(this.currentDashboard);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task for setting the current dashboard.
|
||||
* @param {Object} dashboard - The dashboard object to set as current.
|
||||
*/
|
||||
@task *setCurrentDashboard(dashboard) {
|
||||
const currentDashboard = yield this.fetch.post('dashboards/switch', { dashboard_uuid: dashboard.id }, { normalizeToEmberData: true }).catch((error) => {
|
||||
this.notifications.serverError(error);
|
||||
});
|
||||
|
||||
if (currentDashboard) {
|
||||
this.currentDashboard = currentDashboard;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to toggle dashboard editing state.
|
||||
* @param {boolean} [state=true] - State to set for editing.
|
||||
*/
|
||||
@action onChangeEdit(state = true) {
|
||||
this.isEditingDashboard = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to toggle the state of adding a widget.
|
||||
* @param {boolean} [state=true] - State to set for adding a widget.
|
||||
*/
|
||||
@action onAddingWidget(state = true) {
|
||||
this.isAddingWidget = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default dashboard with predefined widgets.
|
||||
* @private
|
||||
* @returns {Object} The default dashboard object.
|
||||
*/
|
||||
_createDefaultDashboard() {
|
||||
let defaultDashboard;
|
||||
|
||||
// check store for default dashboard
|
||||
const loadedDashboars = this.store.peekAll('dashboard');
|
||||
|
||||
// check for default dashboard loaded in store
|
||||
defaultDashboard = loadedDashboars.find((dashboard) => dashboard.id === 'system');
|
||||
if (defaultDashboard) {
|
||||
return defaultDashboard;
|
||||
}
|
||||
|
||||
// create new default dashboard
|
||||
defaultDashboard = this.store.createRecord('dashboard', {
|
||||
id: 'system',
|
||||
uuid: 'system',
|
||||
name: 'Default Dashboard',
|
||||
is_default: false,
|
||||
user_uuid: 'system',
|
||||
widgets: this._createDefaultDashboardWidgets(),
|
||||
});
|
||||
|
||||
return defaultDashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates default widgets for the default dashboard.
|
||||
* @private
|
||||
* @returns {Array} An array of default dashboard widgets.
|
||||
*/
|
||||
_createDefaultDashboardWidgets() {
|
||||
const widgets = this.universe.getDefaultDashboardWidgets().map((defaultWidget) => {
|
||||
return this.store.createRecord('dashboard-widget', defaultWidget);
|
||||
});
|
||||
|
||||
return widgets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if default dashboard is already loaded.
|
||||
* @private
|
||||
* @return {Boolean}
|
||||
* @memberof DashboardService
|
||||
*/
|
||||
_isDefaultDashboardLoaded() {
|
||||
const defaultDashboard = this._createDefaultDashboard();
|
||||
return this.dashboards.some((dashboard) => dashboard.id === defaultDashboard.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if default dashboard is not already loaded.
|
||||
* @private
|
||||
* @return {Boolean}
|
||||
* @memberof DashboardService
|
||||
*/
|
||||
_isDefaultDashboardNotLoaded() {
|
||||
return !this._isDefaultDashboardLoaded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current dasbhoard or next available dashboard.
|
||||
*
|
||||
* @return {DashboardModel}
|
||||
* @memberof DashboardService
|
||||
*/
|
||||
_getNextDashboard() {
|
||||
return this.dashboards.find((dashboard) => dashboard.is_default) || this.dashboards[0];
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,8 @@
|
||||
color: rgb(202 138 4);
|
||||
}
|
||||
|
||||
body[data-theme='dark'] .btn.btn-warning-alert.btn-warning,
|
||||
.btn.btn-warning-alert.btn-warning,
|
||||
.two-fa-enforcement-alert button#two-fa-setup-button.btn.btn-warning,
|
||||
.two-fa-enforcement-alert button#two-fa-setup-button.btn.btn-warning body[data-theme='dark'] .btn.btn-warning-alert.btn-warning,
|
||||
body[data-theme='dark'] .two-fa-enforcement-alert button#two-fa-setup-button.btn.btn-warning {
|
||||
background-color: rgb(202 138 4);
|
||||
border-color: rgb(161 98 7);
|
||||
@@ -23,3 +22,29 @@ body[data-theme='dark'] .two-fa-enforcement-alert button#two-fa-setup-button.btn
|
||||
padding-left: 1rem;
|
||||
padding-top: 0.2rem;
|
||||
}
|
||||
|
||||
.fleetbase-pagination-meta-info-wrapper.within-layout-section-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.without-padding {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.without-padding-y {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.without-padding-x {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
body.console-admin-organizations-index-index .next-table-wrapper > table {
|
||||
table-layout: auto;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
<FaIcon @icon="check-circle" @size="lg" class="text-green-900 mr-4" />
|
||||
</div>
|
||||
<p class="flex-1 text-sm text-green-900 dark:text-green-900">
|
||||
<strong>Check your {{this.selectedMethod}}</strong><br />
|
||||
We've sent you a verification code. Enter the code below to complete the login process.
|
||||
<strong>{{t "auth.two-fa.verify-code.check-title"}}</strong><br />
|
||||
{{t "auth.two-fa.verify-code.check-subtitle"}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -26,22 +26,22 @@
|
||||
{{/if}}
|
||||
{{#if this.isCodeExpired}}
|
||||
<InfoBlock>
|
||||
<div>Your 2FA authentication code has expired. You can request another code if you need more time.</div>
|
||||
<Button @type="primary" @wrapperClass="mt-2" @text="Resend Code" @icon="arrow-rotate-right" @onClick={{this.resendCode}} />
|
||||
<div>{{t "auth.two-fa.verify-code.expired-help-text"}}</div>
|
||||
<Button @type="primary" @wrapperClass="mt-2" @text={{t "auth.two-fa.verify-code.resend-code"}} @icon="arrow-rotate-right" @onClick={{this.resendCode}} />
|
||||
</InfoBlock>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<Button @buttonType="submit" @type="primary" @text="Verify Code" @icon="check-circle" @wrapperClass="btn-block" @isLoading={{this.isLoading}} />
|
||||
<Button @buttonType="submit" @type="primary" @text={{t "auth.two-fa.verify-code.verify-code"}} @icon="check-circle" @wrapperClass="btn-block" @isLoading={{this.isLoading}} />
|
||||
</div>
|
||||
|
||||
<div class="text-center flex flex-row items-center justify-center space-x-4 mt-3.5">
|
||||
<a href="#" class="text-sm text-blue-500 hover:underline inline-block" {{on "click" this.resendCode}}>
|
||||
Resend Code
|
||||
{{t "auth.two-fa.verify-code.resend-code"}}
|
||||
</a>
|
||||
<a href="#" class="text-sm text-danger hover:underline inline-block" {{on "click" this.cancelTwoFactor}}>
|
||||
Cancel Two-Factor
|
||||
{{t "auth.two-fa.verify-code.cancel-two-factor"}}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -18,4 +18,5 @@
|
||||
{{!-- Add Locale Selector to Header --}}
|
||||
<EmberWormhole @to="view-header-actions">
|
||||
<LocaleSelector class="mr-0.5" />
|
||||
</EmberWormhole>
|
||||
</EmberWormhole>
|
||||
<div id="console-wormhole" />
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<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>
|
||||
|
||||
21
console/app/templates/console/admin/organizations/index.hbs
Normal file
21
console/app/templates/console/admin/organizations/index.hbs
Normal file
@@ -0,0 +1,21 @@
|
||||
{{page-title (t "console.admin.organizations.index.title")}}
|
||||
{{!-- template-lint-disable no-unbound --}}
|
||||
<Layout::Section::Header @title={{t "console.admin.organizations.index.title"}} @searchQuery={{unbound this.query}} @onSearch={{this.search}} />
|
||||
|
||||
<Layout::Section::Body>
|
||||
<Table
|
||||
@rows={{@model}}
|
||||
@columns={{this.columns}}
|
||||
@selectable={{false}}
|
||||
@canSelectAll={{false}}
|
||||
@onSetup={{fn (mut this.table)}}
|
||||
@pagination={{true}}
|
||||
@paginationMeta={{@model.meta}}
|
||||
@page={{this.page}}
|
||||
@onPageChange={{fn (mut this.page)}}
|
||||
@tfootVerticalOffset="53"
|
||||
@tfootVerticalOffsetElements=".next-view-section-subheader"
|
||||
@onRowClick={{this.goToCompany}}
|
||||
/>
|
||||
</Layout::Section::Body>
|
||||
{{outlet}}
|
||||
@@ -0,0 +1,19 @@
|
||||
{{page-title (t "common.users")}}
|
||||
<Overlay @isOpen={{@isOpen}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width="600px" @isResizable={{true}}>
|
||||
<Overlay::Header @title={{concat this.company.name " - " (t "common.users")}} @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>
|
||||
</Overlay::Header>
|
||||
|
||||
<Overlay::Body class="without-padding">
|
||||
{{!-- template-lint-disable no-unbound --}}
|
||||
<Layout::Section::Header @title={{t "console.admin.organizations.users.title"}} @searchQuery={{unbound this.nestedQuery}} @onSearch={{this.search}}>
|
||||
<Pagination @meta={{@model.meta}} @page={{this.nestedPage}} @onPageChange={{fn (mut this.nestedPage)}} @metaInfoClass="hidden" @metaInfoWrapperClass="within-layout-section-header" />
|
||||
</Layout::Section::Header>
|
||||
|
||||
<Layout::Section::Body>
|
||||
<Table @rows={{@model}} @columns={{this.columns}} @selectable={{false}} @canSelectAll={{false}} @onSetup={{fn (mut this.table)}} />
|
||||
</Layout::Section::Body>
|
||||
</Overlay::Body>
|
||||
</Overlay>
|
||||
@@ -1,11 +1,7 @@
|
||||
{{page-title "Dashboard"}}
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full pt-6">
|
||||
<div class="console-home-container mx-auto h-screen space-y-4" {{increase-height-by 300}}>
|
||||
<TwoFaEnforcementAlert />
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-4">
|
||||
<GithubCard class="lg:col-span-4" />
|
||||
<FleetbaseBlog class="lg:col-span-8" />
|
||||
</div>
|
||||
<Dashboard @sidebar={{this.sidebarContext}} />
|
||||
</div>
|
||||
</Layout::Section::Body>
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<TwoFaEnforcementAlert />
|
||||
<Dashboard @sidebar={{this.sidebarContext}} />
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
<div id="console-home-wormhole" />
|
||||
@@ -2,8 +2,8 @@
|
||||
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
<div class="mb-8">
|
||||
<img class="mx-auto h-12 w-auto" src={{@brand.icon_url}} alt={{t "app.name"}}>
|
||||
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
|
||||
<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 "invite.for-users.invitation-message" companyName=@model.name}}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -46,9 +46,12 @@ module.exports = function (environment) {
|
||||
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'),
|
||||
entityImage: getenv('DEFAULT_ENTITY_IMAGE', 'https://flb-assets.s3-ap-southeast-1.amazonaws.com/static/parcels/medium.png'),
|
||||
vendorImage: getenv('DEFAULT_VENDOR_IMAGE', 'https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/no-avatar.png'),
|
||||
vehicleImage: getenv('DEFAULT_VEHICLE_IMAGE', 'https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/vehicle-placeholder.png'),
|
||||
vehicleAvatar: getenv('DEFAUL_VEHICLE_AVATAR', 'https://flb-assets.s3-ap-southeast-1.amazonaws.com/static/vehicle-icons/mini_bus.svg'),
|
||||
driverAvatar: getenv('DEFAUL_DRIVER_AVATAR', 'https://flb-assets.s3-ap-southeast-1.amazonaws.com/static/driver-icons/moto-driver.png'),
|
||||
placeAvatar: getenv('DEFAUL_PLACE_AVATAR', 'https://flb-assets.s3-ap-southeast-1.amazonaws.com/static/place-icons/basic-building.png'),
|
||||
},
|
||||
|
||||
'ember-simple-auth': {
|
||||
@@ -60,6 +63,11 @@ module.exports = function (environment) {
|
||||
keyDelimiter: '/',
|
||||
includeEmberDataSupport: true,
|
||||
},
|
||||
|
||||
'ember-cli-notifications': {
|
||||
autoClear: true,
|
||||
clearDuration: 1000 * 3.5,
|
||||
},
|
||||
};
|
||||
|
||||
if (environment === 'development') {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fleetbase/console",
|
||||
"version": "0.3.10",
|
||||
"version": "0.4.11",
|
||||
"private": true,
|
||||
"description": "Fleetbase Console",
|
||||
"repository": "https://github.com/fleetbase/fleetbase",
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "node prebuild.js",
|
||||
"build": "pnpm run prebuild && ember build --environment=production",
|
||||
"build": "pnpm run prebuild && ember build",
|
||||
"lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"",
|
||||
"lint:css": "stylelint \"**/*.css\"",
|
||||
"lint:css:fix": "concurrently \"npm:lint:css -- --fix\"",
|
||||
@@ -21,17 +21,19 @@
|
||||
"lint:hbs:fix": "ember-template-lint . --fix",
|
||||
"lint:js": "eslint . --cache",
|
||||
"lint:js:fix": "eslint . --fix",
|
||||
"postinstall": "patch-package",
|
||||
"lint:intl": "fleetbase-intl-lint",
|
||||
"start": "pnpm run prebuild && ember serve",
|
||||
"start:dev": "pnpm run prebuild && ember serve --environment development",
|
||||
"test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"",
|
||||
"test:ember": "ember test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fleetbase/ember-core": "^0.2.1",
|
||||
"@fleetbase/ember-ui": "^0.2.10",
|
||||
"@fleetbase/storefront-engine": "^0.2.9",
|
||||
"@fleetbase/fleetops-engine": "^0.4.4",
|
||||
"@fleetbase/fleetops-data": "^0.1.8",
|
||||
"@fleetbase/ember-core": "^0.2.6",
|
||||
"@fleetbase/ember-ui": "^0.2.11",
|
||||
"@fleetbase/fleetops-engine": "^0.4.18",
|
||||
"@fleetbase/fleetops-data": "^0.1.13",
|
||||
"@fleetbase/storefront-engine": "^0.3.5",
|
||||
"@fleetbase/dev-engine": "^0.2.1",
|
||||
"@fleetbase/iam-engine": "^0.0.9",
|
||||
"@fleetbase/leaflet-routing-machine": "^3.2.16",
|
||||
@@ -42,6 +44,7 @@
|
||||
"ember-composable-helpers": "^5.0.0",
|
||||
"ember-concurrency": "^3.0.0",
|
||||
"ember-concurrency-decorators": "^2.0.3",
|
||||
"ember-gridstack": "^4.0.0",
|
||||
"ember-intl": "6.3.2",
|
||||
"ember-math-helpers": "^2.18.2",
|
||||
"ember-power-select": "^6.0.1",
|
||||
@@ -49,6 +52,8 @@
|
||||
"ember-radio-button": "3.0.0-beta.1",
|
||||
"ember-tag-input": "^3.1.0",
|
||||
"fleetbase-extensions-indexer": "^0.0.4",
|
||||
"gridstack": "^7.2.2",
|
||||
"patch-package": "^8.0.0",
|
||||
"postcss-at-rules-variables": "^0.3.0",
|
||||
"postcss-custom-properties": "^12.1.9",
|
||||
"postcss-nth-list": "^1.0.2"
|
||||
@@ -137,9 +142,9 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@fleetbase/fleetops-data": "^0.1.8",
|
||||
"@fleetbase/ember-core": "^0.2.1",
|
||||
"@fleetbase/ember-ui": "^0.2.10"
|
||||
"@fleetbase/ember-core": "^0.2.6",
|
||||
"@fleetbase/ember-ui": "^0.2.11",
|
||||
"@fleetbase/fleetops-data": "^0.1.13"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
|
||||
11
console/patches/ember-gridstack+4.0.0.patch
Normal file
11
console/patches/ember-gridstack+4.0.0.patch
Normal file
@@ -0,0 +1,11 @@
|
||||
diff --git a/node_modules/ember-gridstack/addon/components/grid-stack.js b/node_modules/ember-gridstack/addon/components/grid-stack.js
|
||||
index fa51392..fdabb2a 100644
|
||||
--- a/node_modules/ember-gridstack/addon/components/grid-stack.js
|
||||
+++ b/node_modules/ember-gridstack/addon/components/grid-stack.js
|
||||
@@ -133,5 +133,6 @@ export default class GridStackComponent extends Component {
|
||||
removeWidget(element, removeDOM = false, triggerEvent = true) {
|
||||
triggerEvent = triggerEvent && !this.isDestroying && !this.isDestroyed;
|
||||
this.gridStack?.removeWidget(element, removeDOM, triggerEvent);
|
||||
+ this.gridStack?.compact();
|
||||
}
|
||||
}
|
||||
1537
console/pnpm-lock.yaml
generated
1537
console/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -49,6 +49,11 @@ Router.map(function () {
|
||||
this.route('notifications');
|
||||
this.route('two-fa-settings');
|
||||
this.route('virtual', { path: '/:slug/:view' });
|
||||
this.route('organizations', function () {
|
||||
this.route('index', { path: '/' }, function () {
|
||||
this.route('users', { path: '/:public_id/users' });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
this.route('install');
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | dashboard/widget-panel', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.set('myAction', function(val) { ... });
|
||||
|
||||
await render(hbs`<Dashboard::WidgetPanel />`);
|
||||
|
||||
assert.dom().hasText('');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
<Dashboard::WidgetPanel>
|
||||
template block text
|
||||
</Dashboard::WidgetPanel>
|
||||
`);
|
||||
|
||||
assert.dom().hasText('template block text');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Helper | spread-widget-options', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it renders', async function (assert) {
|
||||
this.set('inputValue', '1234');
|
||||
|
||||
await render(hbs`{{spread-widget-options this.inputValue}}`);
|
||||
|
||||
assert.dom().hasText('1234');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Controller | console/admin/organization-users', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:console/admin/organization-users');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Controller | console/admin/organizations', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:console/admin/organizations');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Controller | console/admin/organizations/index/users', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:console/admin/organizations/index/users');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
14
console/tests/unit/models/company-user-test.js
Normal file
14
console/tests/unit/models/company-user-test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Model | company user', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('company-user', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
});
|
||||
14
console/tests/unit/models/custom-field-test.js
Normal file
14
console/tests/unit/models/custom-field-test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Model | custom field', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('custom-field', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
});
|
||||
14
console/tests/unit/models/custom-field-value-test.js
Normal file
14
console/tests/unit/models/custom-field-value-test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Model | custom field value', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('custom-field-value', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
});
|
||||
14
console/tests/unit/models/dashboard-test.js
Normal file
14
console/tests/unit/models/dashboard-test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Model | dashboard', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('dashboard', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
});
|
||||
14
console/tests/unit/models/dashboard-widget-test.js
Normal file
14
console/tests/unit/models/dashboard-widget-test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Model | dashboard widget', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let model = store.createRecord('dashboard-widget', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Route | console/admin/organization-users', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:console/admin/organization-users');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Route | console/admin/organizations', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:console/admin/organizations');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Route | console/admin/organizations/index/users', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let route = this.owner.lookup('route:console/admin/organizations/index/users');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
24
console/tests/unit/serializers/company-test.js
Normal file
24
console/tests/unit/serializers/company-test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Serializer | company', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('company');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('company', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
});
|
||||
24
console/tests/unit/serializers/dashboard-test.js
Normal file
24
console/tests/unit/serializers/dashboard-test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Serializer | dashboard', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('dashboard');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('dashboard', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
});
|
||||
24
console/tests/unit/serializers/dashboard-widget-test.js
Normal file
24
console/tests/unit/serializers/dashboard-widget-test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Serializer | dashboard widget', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('dashboard-widget');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('dashboard-widget', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
});
|
||||
12
console/tests/unit/services/dashboard-test.js
Normal file
12
console/tests/unit/services/dashboard-test.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Service | dashboard', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let service = this.owner.lookup('service:dashboard');
|
||||
assert.ok(service);
|
||||
});
|
||||
});
|
||||
@@ -13,8 +13,10 @@ terms:
|
||||
search: Search
|
||||
search-input: Search Input
|
||||
common:
|
||||
confirm: Confirm
|
||||
edit: Edit
|
||||
save: Save
|
||||
save-changes: Save Changes
|
||||
cancel: Cancel
|
||||
2fa-config: 2FA Config
|
||||
account: Account
|
||||
@@ -35,6 +37,7 @@ common:
|
||||
notification-channels: Notification Channels
|
||||
notifications: Notifications
|
||||
organization: Organization
|
||||
organizations: Organizations
|
||||
overview: Overview
|
||||
phone: Your phone number
|
||||
profile: Profile
|
||||
@@ -49,6 +52,15 @@ common:
|
||||
two-factor: Two Factor
|
||||
uploading: Uploading...
|
||||
your-profile: Your Profile
|
||||
created-at: Created At
|
||||
country: Country
|
||||
phone-number: Phone
|
||||
status: Status
|
||||
close-and-save: Close and Save
|
||||
users: Users
|
||||
changelog: Changelog
|
||||
ok: OK
|
||||
select-file: Select File
|
||||
component:
|
||||
file:
|
||||
dropdown-label: File actions
|
||||
@@ -61,7 +73,9 @@ component:
|
||||
upload-images-videos: Upload Images & Videos
|
||||
upload-documents: Upload Documents
|
||||
upload-documents-files: Upload Documents & Files
|
||||
upload-avatar-files: Upload Custom Avatars
|
||||
dropzone-supported-images-videos: Drag and drop image and video files onto this dropzone
|
||||
dropzone-supported-avatars: Drag and drop SVG or PNG files
|
||||
dropzone-supported-files: Drag and drop files onto this dropzone
|
||||
or-select-button-text: or select files to upload.
|
||||
upload-queue: Upload Queue
|
||||
@@ -81,6 +95,26 @@ component:
|
||||
comment-reply-placeholder: Input your reply...
|
||||
comment-input-empty-notification: You cannot publish empty comments...
|
||||
comment-min-length-notification: Comment must be atleast 2 characters
|
||||
dashboard:
|
||||
select-dashboard: Select Dashboard
|
||||
create-new-dashboard: Create new Dashboard
|
||||
create-a-new-dashboard: Create a new Dashboard
|
||||
confirm-create-dashboard: Create Dashboard!
|
||||
edit-layout: Edit layout
|
||||
add-widgets: Add widgets
|
||||
delete-dashboard: Delete dashboard
|
||||
save-dashboard: Save Dashboard
|
||||
you-cannot-delete-this-dashboard: You cannot delete this dashboard.
|
||||
are-you-sure-you-want-delete-dashboard: Are you sure to delete this {dashboardName}?
|
||||
dashboard-widget-panel:
|
||||
widget-name: >-
|
||||
{widgetName} Widget
|
||||
select-widgets: Select Widgets
|
||||
close-and-save: Close and Save
|
||||
services:
|
||||
dashboard-service:
|
||||
create-dashboard-success-notification: New dashboard `{dashboardName}` created succesfully.
|
||||
delete-dashboard-success-notification: Dashboard `{dashboardName}` was deleted.
|
||||
auth:
|
||||
verification:
|
||||
header-title: Account Verification
|
||||
@@ -97,6 +131,13 @@ auth:
|
||||
send-by-sms: Send by SMS
|
||||
two-fa:
|
||||
verify-code:
|
||||
verification-code: Verification Code
|
||||
check-title: Check your email or phone
|
||||
check-subtitle: We've sent you a verification code. Enter the code below to complete the login process.
|
||||
expired-help-text: Your 2FA authentication code has expired. You can request another code if you need more time.
|
||||
resend-code: Resend Code
|
||||
verify-code: Verify Code
|
||||
cancel-two-factor: Cancel Two-Factor
|
||||
invalid-session-error-notification: Invalid session. Please try again.
|
||||
verification-successful-notification: Verification successful!
|
||||
verification-code-expired-notification: Verification code has expired. Please request a new one.
|
||||
@@ -148,6 +189,15 @@ auth:
|
||||
submit-button: Reset Password
|
||||
back-button: Back
|
||||
console:
|
||||
create-or-join-organization:
|
||||
modal-title: Create or join a organization
|
||||
join-success-notification: You have joined a new organization!
|
||||
create-success-notification: You have created a new organization!
|
||||
switch-organization:
|
||||
modal-title: Are you sure you want to switch organization to {organizationName}?
|
||||
modal-body: By confirming your account will remain logged in, but your primary organization will be switched.
|
||||
modal-accept-button-text: Yes, I want to switch organization
|
||||
success-notification: You have switched organizations
|
||||
account:
|
||||
index:
|
||||
upload-new: Upload new
|
||||
@@ -183,6 +233,17 @@ console:
|
||||
notifications:
|
||||
title: Notifications
|
||||
notification-settings: Notification Settings
|
||||
organizations:
|
||||
index:
|
||||
title: Organizations
|
||||
owner-name-column: Owner
|
||||
owner-phone-column: Owner Phone
|
||||
owner-email-column: Owner Phone
|
||||
users-count-column: Users
|
||||
phone-column: Phone
|
||||
email-column: Email
|
||||
users:
|
||||
title: Users
|
||||
settings:
|
||||
index:
|
||||
title: Organization Settings
|
||||
|
||||
@@ -3,6 +3,7 @@ variable "REGISTRY" { default = "" }
|
||||
variable "VERSION" { default = "latest" }
|
||||
variable "CACHE" { default = "" }
|
||||
variable "GCP" { default = false }
|
||||
variable "GITHUB_AUTH_KEY" { default = "" }
|
||||
|
||||
group "default" {
|
||||
targets = ["app", "app-httpd"]
|
||||
@@ -28,9 +29,9 @@ target "app" {
|
||||
compact(["latest", VERSION])
|
||||
) : []
|
||||
|
||||
secret = [
|
||||
"type=file,id=composer_auth,src=./composer-auth.json"
|
||||
]
|
||||
args = {
|
||||
GITHUB_AUTH_KEY = "${GITHUB_AUTH_KEY}"
|
||||
}
|
||||
|
||||
cache-from = notequal("", CACHE) ? ["${CACHE}"] : []
|
||||
cache-to = notequal("", CACHE) ? ["${CACHE},mode=max,ignore-error=true"] : []
|
||||
@@ -47,4 +48,4 @@ target "app-httpd" {
|
||||
GCP ? "${REGISTRY}/app-httpd:%s" : "${REGISTRY}:app-httpd-%s",
|
||||
compact(["latest", VERSION])
|
||||
) : []
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
- "./docker/database/:/docker-entrypoint-initdb.d/"
|
||||
- "./docker/database/mysql:/var/lib/mysql"
|
||||
environment:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
|
||||
MYSQL_DATABASE: "fleetbase"
|
||||
|
||||
socket:
|
||||
@@ -23,6 +23,21 @@ services:
|
||||
SOCKETCLUSTER_WORKERS: 10
|
||||
SOCKETCLUSTER_BROKERS: 10
|
||||
|
||||
queue:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
target: events-dev
|
||||
args:
|
||||
ENVIRONMENT: development
|
||||
environment:
|
||||
DATABASE_URL: "mysql://root@database/fleetbase"
|
||||
QUEUE_CONNECTION: redis
|
||||
CACHE_DRIVER: redis
|
||||
CACHE_PATH: /fleetbase/api/storage/framework/cache
|
||||
CACHE_URL: tcp://cache
|
||||
REDIS_URL: tcp://cache
|
||||
|
||||
console:
|
||||
ports:
|
||||
- "4200:4200"
|
||||
@@ -41,10 +56,12 @@ services:
|
||||
target: app-dev
|
||||
args:
|
||||
ENVIRONMENT: development
|
||||
GITHUB_AUTH_KEY: ${GITHUB_AUTH_KEY}
|
||||
environment:
|
||||
DATABASE_URL: "mysql://root@database/fleetbase"
|
||||
QUEUE_CONNECTION: redis
|
||||
CACHE_DRIVER: redis
|
||||
CACHE_PATH: /var/www/html/api/storage/framework/cache
|
||||
CACHE_PATH: /fleetbase/api/storage/framework/cache
|
||||
CACHE_URL: tcp://cache
|
||||
REDIS_URL: tcp://cache
|
||||
SESSION_DOMAIN: localhost
|
||||
@@ -55,14 +72,13 @@ services:
|
||||
depends_on:
|
||||
- database
|
||||
- cache
|
||||
- queue
|
||||
|
||||
httpd:
|
||||
volumes:
|
||||
- ./api/storage:/var/www/html/api/storage
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/httpd/Dockerfile
|
||||
dockerfile: docker/httpd/Dockerfile
|
||||
ports:
|
||||
- "8000:80"
|
||||
depends_on:
|
||||
- application
|
||||
- application
|
||||
|
||||
@@ -1,117 +1,127 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
# Base stage
|
||||
FROM php:7.4-fpm-bullseye as base
|
||||
FROM dunglas/frankenphp:1.1.0-php8.2-bookworm as base
|
||||
|
||||
# Download and install GEOS PHP bindings
|
||||
RUN mkdir -p /usr/src/php/ext \
|
||||
&& curl -L https://git.osgeo.org/gitea/geos/php-geos/archive/1.0.0.tar.gz > /tmp/php-geos.tar.gz \
|
||||
&& tar -C /usr/src/php/ext -xzvf /tmp/php-geos.tar.gz
|
||||
# Install packages
|
||||
RUN apt-get update && apt-get install -y git bind9-utils mycli nodejs npm \
|
||||
&& mkdir -p /root/.ssh \
|
||||
&& ssh-keyscan github.com >> /root/.ssh/known_hosts
|
||||
|
||||
# Install required packages and PHP extensions
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y nano git unzip libzip-dev libgd-dev libfreetype6-dev libjpeg62-turbo-dev libpng-dev \
|
||||
imagemagick libmagickwand-dev --no-install-recommends libmemcached-dev libgeos-dev libgmp-dev \
|
||||
default-mysql-client libicu-dev tmux nginx dnsutils mycli redis-tools \
|
||||
&& pecl install imagick \
|
||||
&& docker-php-ext-enable imagick \
|
||||
&& docker-php-ext-configure gd --with-external-gd \
|
||||
&& docker-php-ext-configure php-geos \
|
||||
&& docker-php-ext-install -j$(nproc) gmp php-geos gd zip pdo_mysql sockets intl bcmath \
|
||||
&& pecl install redis-4.3.0 memcached-3.1.3 \
|
||||
&& docker-php-ext-enable redis memcached opcache bcmath
|
||||
|
||||
# Clear cache
|
||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
# Install PHP Extensions
|
||||
RUN install-php-extensions \
|
||||
pdo_mysql \
|
||||
gd \
|
||||
bcmath \
|
||||
redis \
|
||||
intl \
|
||||
zip \
|
||||
gmp \
|
||||
apcu \
|
||||
opcache \
|
||||
memcached \
|
||||
imagick \
|
||||
geos \
|
||||
sockets \
|
||||
pcntl \
|
||||
@composer
|
||||
|
||||
# Update PHP configurations
|
||||
RUN sed -e 's/^expose_php.*/expose_php = Off/' "$PHP_INI_DIR/php.ini-production" > "$PHP_INI_DIR/php.ini" \
|
||||
&& sed -i -e 's/^upload_max_filesize.*/upload_max_filesize = 600M/' -e 's/^post_max_size.*/post_max_size = 0/' \
|
||||
-e 's/^memory_limit.*/memory_limit = 600M/' "$PHP_INI_DIR/php.ini"
|
||||
&& sed -i -e 's/^upload_max_filesize.*/upload_max_filesize = 600M/' -e 's/^post_max_size.*/post_max_size = 0/' \
|
||||
-e 's/^memory_limit.*/memory_limit = 600M/' "$PHP_INI_DIR/php.ini"
|
||||
|
||||
# Install global node modules
|
||||
RUN npm install -g chokidar
|
||||
|
||||
# Install ssm-parent
|
||||
COPY --from=ghcr.io/springload/ssm-parent:1.8 /usr/bin/ssm-parent /sbin/ssm-parent
|
||||
|
||||
# Install Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Set some build ENV variables
|
||||
ENV LOG_CHANNEL=stdout
|
||||
ENV CACHE_DRIVER=null
|
||||
ENV BROADCAST_DRIVER=socketcluster
|
||||
ENV QUEUE_CONNECTION=redis
|
||||
|
||||
# For development only
|
||||
WORKDIR /var/www/html
|
||||
COPY --chown=www-data:nogroup ./packages ./packages
|
||||
|
||||
# Setup application
|
||||
WORKDIR /var/www/html/api
|
||||
COPY --chown=www-data:nogroup ./api ./
|
||||
RUN chown -R www-data:www-data /var/www/html/api
|
||||
|
||||
# Setup composer root directory
|
||||
RUN mkdir -p /root/.composer
|
||||
RUN mkdir -p /var/www/.composer && chown www-data:www-data /var/www/.composer
|
||||
|
||||
# Setup logging
|
||||
RUN mkdir -p ./storage/logs/ && touch ./storage/logs/laravel-$(date +'%Y-%m-%d').log
|
||||
RUN chown -R www-data:www-data ./storage
|
||||
RUN chmod -R 755 ./storage
|
||||
|
||||
# Load the secret into the auth.json file if exists, and install dependencies
|
||||
COPY composer-auth.jso[n] /root/.composer/auth.json
|
||||
RUN --mount=type=secret,id=composer_auth,target=/root/.composer/auth.json \
|
||||
if [ -f "/root/.composer/auth.json" ]; then \
|
||||
mkdir -p /var/www/.composer/ && \
|
||||
cp /root/.composer/auth.json /var/www/.composer/auth.json && \
|
||||
chown www-data:www-data /var/www/.composer/auth.json && \
|
||||
su www-data -s /bin/sh -c "composer install && composer dumpautoload"; \
|
||||
fi; \
|
||||
su www-data -s /bin/sh -c "composer install && composer dumpautoload"
|
||||
|
||||
# Continue
|
||||
USER root
|
||||
WORKDIR /
|
||||
|
||||
# Create unique instance ID
|
||||
RUN echo $(cat /proc/sys/kernel/random/uuid) > /.fleetbase
|
||||
ENV CADDYFILE_PATH=/fleetbase/Caddyfile
|
||||
ENV OCTANE_SERVER=frankenphp
|
||||
|
||||
# Set environment
|
||||
ARG ENVIRONMENT=production
|
||||
ENV APP_ENV=$ENVIRONMENT
|
||||
|
||||
# Set workdir to application
|
||||
WORKDIR /var/www/html/api
|
||||
# Setup github auth
|
||||
ARG GITHUB_AUTH_KEY
|
||||
|
||||
# Copy Caddyfile
|
||||
COPY --chown=www-data:www-data ./Caddyfile $CADDYFILE_PATH
|
||||
|
||||
# Create /fleetbase directory and set correct permissions
|
||||
RUN mkdir -p /fleetbase/api && chown -R www-data:www-data /fleetbase
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /fleetbase/api
|
||||
|
||||
# If GITHUB_AUTH_KEY is provided, create auth.json with it
|
||||
RUN if [ -n "$GITHUB_AUTH_KEY" ]; then echo "{\"github-oauth\": {\"github.com\": \"$GITHUB_AUTH_KEY\"}}" > auth.json; fi
|
||||
|
||||
# Optimize Composer Dependency Installation
|
||||
COPY --chown=www-data:www-data ./api/composer.json ./api/composer.lock /fleetbase/api/
|
||||
|
||||
# Pre-install Composer dependencies
|
||||
RUN su www-data -s /bin/sh -c "composer install --no-scripts --optimize-autoloader --no-dev"
|
||||
|
||||
# Setup application
|
||||
COPY --chown=www-data:www-data ./api /fleetbase/api
|
||||
|
||||
# Dump autoload
|
||||
RUN su www-data -s /bin/sh -c "composer dumpautoload"
|
||||
|
||||
# Setup composer root directory
|
||||
RUN mkdir -p /root/.composer
|
||||
RUN mkdir -p /fleetbase/api/.composer && chown www-data:www-data /fleetbase/api/.composer
|
||||
|
||||
# Setup logging
|
||||
RUN mkdir -p /fleetbase/api/storage/logs/ && touch /fleetbase/api/storage/logs/laravel-$(date +'%Y-%m-%d').log
|
||||
RUN chown -R www-data:www-data /fleetbase/api/storage
|
||||
RUN chmod -R 755 /fleetbase/api/storage
|
||||
|
||||
# Set permissions for deploy script
|
||||
RUN chmod +x /fleetbase/api/deploy.sh
|
||||
|
||||
# Scheduler base stage
|
||||
FROM base as scheduler-base
|
||||
|
||||
# Install go-crond
|
||||
RUN curl -L https://github.com/webdevops/go-crond/releases/download/0.6.1/go-crond-64-linux-dynamic > /usr/local/bin/go-crond && chmod +x /usr/local/bin/go-crond
|
||||
RUN curl -L https://github.com/webdevops/go-crond/releases/download/23.12.0/go-crond.linux.amd64 > /usr/local/bin/go-crond && chmod +x /usr/local/bin/go-crond
|
||||
COPY docker/crontab ./crontab
|
||||
RUN chmod 0600 ./crontab
|
||||
|
||||
# Scheduler dev stage
|
||||
FROM scheduler-base as scheduler-dev
|
||||
ENTRYPOINT []
|
||||
CMD ["go-crond", "--verbose", "--no-auto", "root:./crontab"]
|
||||
CMD ["go-crond", "--verbose", "root:./crontab"]
|
||||
|
||||
# Scheduler stage
|
||||
FROM scheduler-base as scheduler
|
||||
ENTRYPOINT ["/sbin/ssm-parent", "-c", ".ssm-parent.yaml", "run", "--"]
|
||||
CMD ["go-crond", "--verbose", "--no-auto", "root:./crontab"]
|
||||
|
||||
# Application dev stage
|
||||
FROM base as app-dev
|
||||
ENTRYPOINT ["docker-php-entrypoint"]
|
||||
CMD ["php-fpm"]
|
||||
CMD ["go-crond", "--verbose", "root:./crontab"]
|
||||
|
||||
# Events stage
|
||||
FROM base as events
|
||||
ENTRYPOINT ["/sbin/ssm-parent", "-c", ".ssm-parent.yaml", "run", "--", "docker-php-entrypoint"]
|
||||
CMD ["php", "artisan", "queue:work", "sqs"]
|
||||
CMD ["php", "artisan", "queue:work"]
|
||||
|
||||
# Events stage
|
||||
FROM base as events-dev
|
||||
ENTRYPOINT []
|
||||
CMD ["php", "artisan", "queue:work"]
|
||||
|
||||
# Application dev stage
|
||||
FROM base as app-dev
|
||||
ENTRYPOINT ["docker-php-entrypoint"]
|
||||
# Add --watch flag later
|
||||
CMD ["sh", "-c", "php artisan octane:frankenphp --port=8000 --host=0.0.0.0 --caddyfile $CADDYFILE_PATH"]
|
||||
|
||||
# Application stage
|
||||
FROM base as app
|
||||
ENTRYPOINT ["/sbin/ssm-parent", "-c", ".ssm-parent.yaml", "run", "--", "docker-php-entrypoint"]
|
||||
CMD ["php-fpm"]
|
||||
CMD ["sh", "-c", "php artisan octane:frankenphp --port=8000 --host=0.0.0.0 --https --http-redirect --caddyfile $CADDYFILE_PATH"]
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
# Build stage
|
||||
FROM php:8.2.4-fpm-alpine3.17 as build
|
||||
|
||||
# download and install geos php bindings
|
||||
# need to run docker-php-ext-configure to create /usr/src/php/ext dir
|
||||
RUN mkdir -p /usr/src/php/ext && curl -L https://git.osgeo.org/gitea/geos/php-geos/archive/1.0.0.tar.gz > /tmp/php-geos.tar.gz && tar -C /usr/src/php/ext -xzvf /tmp/php-geos.tar.gz
|
||||
|
||||
# Update package repositories
|
||||
RUN apk update
|
||||
|
||||
# Install dependencies
|
||||
RUN apk add --no-cache autoconf g++ gcc make build-base linux-headers git unzip geos-dev libzip-dev libgd gd-dev libpng-dev imagemagick imagemagick-libs imagemagick-dev libmemcached-dev libmemcached-libs gmp-dev icu-dev icu icu-libs tmux
|
||||
|
||||
# Install PHP extensions
|
||||
RUN docker-php-ext-install -j$(nproc) gmp gd zip pdo_mysql sockets intl
|
||||
RUN pecl install imagick redis-5.3.7 memcached-3.2.0
|
||||
RUN docker-php-ext-enable imagick redis memcached opcache
|
||||
RUN docker-php-ext-configure gd --enable-gd --with-external-gd
|
||||
RUN docker-php-ext-configure php-geos
|
||||
|
||||
# Configure PHP ini settings
|
||||
RUN sed -e 's/^expose_php.*/expose_php = Off/' "$PHP_INI_DIR/php.ini-production" > "$PHP_INI_DIR/php.ini"
|
||||
RUN sed -i -e 's/^upload_max_filesize.*/upload_max_filesize = 600M/' -e 's/^post_max_size.*/post_max_size = 0/' -e 's/^memory_limit.*/memory_limit = 600M/' "$PHP_INI_DIR/php.ini"
|
||||
|
||||
# set recommended PHP.ini settings
|
||||
# see https://secure.php.net/manual/en/opcache.installation.php
|
||||
RUN { \
|
||||
echo 'opcache.memory_consumption=128'; \
|
||||
echo 'opcache.interned_strings_buffer=8'; \
|
||||
echo 'opcache.max_accelerated_files=4000'; \
|
||||
echo 'opcache.revalidate_freq=2'; \
|
||||
echo 'opcache.fast_shutdown=1'; \
|
||||
echo 'opcache.enable_cli=1'; \
|
||||
} > /usr/local/etc/php/conf.d/opcache-recommended.ini
|
||||
|
||||
# Get springload ssm-parent
|
||||
RUN curl -L https://github.com/springload/ssm-parent/releases/download/v1.4.3/ssm-parent_1.4.3_linux_amd64.tar.gz > /tmp/ssm-parent.tar.gz && tar -C /sbin -xvf /tmp/ssm-parent.tar.gz ssm-parent && rm /tmp/ssm-parent.tar.gz
|
||||
|
||||
# Install composer
|
||||
WORKDIR /var/www
|
||||
COPY docker/composer-install.sh ./
|
||||
RUN chmod +x ./composer-install.sh && ./composer-install.sh
|
||||
|
||||
# Copy application files
|
||||
WORKDIR /var/www/html
|
||||
USER www-data
|
||||
COPY . .
|
||||
|
||||
# Install dependencies
|
||||
RUN composer install --no-dev --no-scripts --no-autoloader --working-dir=/var/www/html/api
|
||||
COPY --chown=www-data:nogroup . ./
|
||||
RUN composer dumpautoload /var/www/html/api
|
||||
|
||||
# Use root user
|
||||
USER root
|
||||
|
||||
# Set environment variables
|
||||
ARG ENVIRONMENT=production
|
||||
ENV APP_ENV=$ENVIRONMENT
|
||||
|
||||
# Final stage
|
||||
FROM php:8.2.4-fpm-alpine as final
|
||||
|
||||
# Install dependencies
|
||||
RUN apk add --no-cache autoconf g++ gcc make build-base linux-headers git unzip geos-dev libzip-dev libgd gd-dev libpng-dev imagemagick imagemagick-libs imagemagick-dev libmemcached-dev libmemcached-libs gmp-dev icu-dev icu icu-libs tmux
|
||||
|
||||
# Copy application files
|
||||
WORKDIR /var/www/html
|
||||
COPY --from=build /var/www/html .
|
||||
|
||||
# Set ownership and permissions
|
||||
RUN chown -R www-data:www-data /var/www/html \
|
||||
&& chmod -R 775 /var/www/html/storage /var/www/html/bootstrap/cache
|
||||
|
||||
# Set entrypoint and command
|
||||
ENTRYPOINT ["/sbin/ssm-parent", "-c", ".ssm-parent.yaml", "run", "--", "docker-php-entrypoint"]
|
||||
CMD ["php-fpm"]
|
||||
|
||||
# Scheduler stages
|
||||
FROM alpine:3.14 as scheduler
|
||||
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
# Install go-crond
|
||||
RUN curl -L https://github.com/webdevops/go-crond/releases/download/0.6.1/go-crond-64-linux-dynamic > /usr/local/bin/go-crond \
|
||||
&& chmod +x /usr/local/bin/go-crond
|
||||
|
||||
# Copy application files
|
||||
WORKDIR /var/www/html
|
||||
COPY --chown=www-data:nogroup . ./
|
||||
|
||||
# Copy crontab file and set permissions
|
||||
COPY --chown=www-data:nogroup docker/crontab ./crontab
|
||||
RUN chmod 0600 ./crontab
|
||||
|
||||
# Set ownership and permissions
|
||||
RUN chown -R www-data:www-data /var/www/html
|
||||
|
||||
# Set entrypoint and command
|
||||
ENTRYPOINT []
|
||||
CMD ["go-crond", "--verbose", "--no-auto", "root:./crontab"]
|
||||
|
||||
# Application stages
|
||||
FROM final as app-dev
|
||||
|
||||
# Set entrypoint and command
|
||||
ENTRYPOINT ["docker-php-entrypoint"]
|
||||
CMD ["php-fpm"]
|
||||
|
||||
# Application stage
|
||||
FROM final as app
|
||||
ENTRYPOINT ["/sbin/ssm-parent", "-c", ".ssm-parent.yaml", "run", "--", "docker-php-entrypoint"]
|
||||
CMD ["php-fpm"]
|
||||
|
||||
FROM final as events
|
||||
|
||||
# Set entrypoint and command
|
||||
ENTRYPOINT ["/sbin/ssm-parent", "-c", ".ssm-parent.yaml", "run", "--", "docker-php-entrypoint"]
|
||||
CMD ["php", "api/artisan", "queue:work", "events"]
|
||||
|
||||
FROM final as jobs
|
||||
|
||||
# Set entrypoint and command
|
||||
ENTRYPOINT ["/sbin/ssm-parent", "-c", ".ssm-parent.yaml", "run", "--", "docker-php-entrypoint"]
|
||||
CMD ["php", "api/artisan", "queue:work", "sqs"]
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
EXPECTED_SIGNATURE="$(curl https://composer.github.io/installer.sig)"
|
||||
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
|
||||
ACTUAL_SIGNATURE="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"
|
||||
|
||||
if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]
|
||||
then
|
||||
>&2 echo 'ERROR: Invalid installer signature'
|
||||
rm composer-setup.php
|
||||
exit 1
|
||||
fi
|
||||
|
||||
php composer-setup.php --install-dir=/usr/bin/ --filename=composer
|
||||
RESULT=$?
|
||||
rm composer-setup.php
|
||||
exit $RESULT
|
||||
@@ -1 +1 @@
|
||||
* * * * * php /var/www/html/api/artisan schedule:run
|
||||
* * * * * php /fleetbase/api/artisan schedule:run
|
||||
|
||||
@@ -4,4 +4,4 @@ FROM nginx:stable-alpine
|
||||
ENV NGINX_APPLICATION_HOSTNAME application
|
||||
|
||||
COPY docker/httpd/vhost.conf /etc/nginx/templates/default.conf.template
|
||||
COPY api/public/ /var/www/html/api/public/
|
||||
COPY api/public/ /fleetbase/api/public/
|
||||
@@ -1,7 +1,7 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /var/www/html/api/public;
|
||||
root /fleetbase/api/public;
|
||||
# hide nginx version for security purposes
|
||||
server_tokens off;
|
||||
access_log /var/log/nginx/access.log;
|
||||
@@ -12,34 +12,32 @@ server {
|
||||
send_timeout 600;
|
||||
index index.php;
|
||||
|
||||
# tweaks
|
||||
client_max_body_size 600M;
|
||||
client_body_buffer_size 1m;
|
||||
client_header_buffer_size 1k;
|
||||
large_client_header_buffers 4 16k;
|
||||
keepalive_timeout 2 2;
|
||||
|
||||
#gzip tweaks
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_min_length 1100;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain application/javascript application/json application/x-javascript text/xml text/css;
|
||||
|
||||
#open file cache tweaks
|
||||
open_file_cache max=2000 inactive=20s;
|
||||
open_file_cache_valid 60s;
|
||||
open_file_cache_min_uses 5;
|
||||
open_file_cache_errors off;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$args;
|
||||
}
|
||||
|
||||
location /uploads {
|
||||
alias /var/www/html/api/storage/app/uploads;
|
||||
autoindex on;
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location /storage {
|
||||
alias /var/www/html/api/storage/app/public;
|
||||
autoindex on;
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ [^/]\.php(/|$) {
|
||||
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
|
||||
if (!-f $document_root$fastcgi_script_name) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
|
||||
|
||||
fastcgi_pass ${NGINX_APPLICATION_HOSTNAME}:9000;
|
||||
fastcgi_index index.php;
|
||||
proxy_set_header Host $host;
|
||||
proxy_pass http://${NGINX_APPLICATION_HOSTNAME}:8000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<VirtualHost *:80>
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/html/api/public
|
||||
|
||||
<Directory /var/www/html/api/public>
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
|
||||
</VirtualHost>
|
||||
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
||||
@@ -52,13 +52,13 @@ app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
|
||||
{{- define "helm.commonVariables" -}}
|
||||
- name: CACHE_URL
|
||||
value: $(REDIS_SERVICE_PORT)/1
|
||||
value: tcp://redis-service.{{ .Release.Namespace }}.svc.cluster.local/1
|
||||
- name: CACHE_DRIVER
|
||||
value: redis
|
||||
- name: SOCKETCLUSTER_PORT
|
||||
value: "80"
|
||||
- name: SOCKETCLUSTER_HOST
|
||||
value: $(SOCKETCLUSTER_SERVICE_HOST)
|
||||
value: socketcluster.{{ .Release.Namespace }}.svc.cluster.local
|
||||
{{- end }}
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
|
||||
@@ -7,7 +7,7 @@ metadata:
|
||||
{{- include "helm.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
replicas: 1
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
@@ -66,7 +66,7 @@ metadata:
|
||||
{{- include "helm.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
replicas: 1
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
|
||||
@@ -10,7 +10,7 @@ metadata:
|
||||
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||
annotations:
|
||||
"helm.sh/hook": pre-install,pre-upgrade
|
||||
"helm.sh/hook-weight": "0"
|
||||
"helm.sh/hook-weight": "20"
|
||||
"helm.sh/hook-delete-policy": before-hook-creation
|
||||
spec:
|
||||
template:
|
||||
|
||||
@@ -30,4 +30,3 @@ spec:
|
||||
port: 6379
|
||||
targetPort: 6379
|
||||
type: ClusterIP
|
||||
|
||||
|
||||
@@ -33,4 +33,3 @@ spec:
|
||||
port: 80
|
||||
targetPort: 8000
|
||||
type: ClusterIP
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user