mirror of
https://github.com/fleetbase/fleetbase.git
synced 2026-01-07 23:04:14 +00:00
Compare commits
173 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f88c7bc79 | ||
|
|
b3816c394a | ||
|
|
34462c61c4 | ||
|
|
080302eb86 | ||
|
|
b063cf6338 | ||
|
|
b6dca79251 | ||
|
|
c35af4d47a | ||
|
|
2a89659cc3 | ||
|
|
f8ae75d767 | ||
|
|
924448a4d0 | ||
|
|
5426ac374d | ||
|
|
73f56b7958 | ||
|
|
095eb87e14 | ||
|
|
fabc16612b | ||
|
|
3389cba935 | ||
|
|
099ea57c39 | ||
|
|
5492bc7299 | ||
|
|
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 | ||
|
|
e8f5638cad | ||
|
|
e216f11f7c | ||
|
|
41064be1ff | ||
|
|
ae17ccb199 | ||
|
|
0fbf901c9d | ||
|
|
00806d4f0c | ||
|
|
683353517d | ||
|
|
27b1af6c40 | ||
|
|
ab8cbf9ea6 | ||
|
|
e8249adddd | ||
|
|
b2c44842ce | ||
|
|
f9e6c8d50d | ||
|
|
6cd0625753 | ||
|
|
21ea3f5644 | ||
|
|
727b2e399f | ||
|
|
ec913f82d7 | ||
|
|
9bb2ea15e0 | ||
|
|
575e4e0952 | ||
|
|
f2058e0d58 | ||
|
|
9669e917ab | ||
|
|
2b6a200811 | ||
|
|
1d92764958 | ||
|
|
13db00c39f | ||
|
|
eedeb8f209 | ||
|
|
bac95cca00 | ||
|
|
520e9771b4 | ||
|
|
75742de7ac | ||
|
|
8628695b0b | ||
|
|
74fd369854 | ||
|
|
5d43df0e1b | ||
|
|
c96a3eb2bf | ||
|
|
4424ed1754 | ||
|
|
6c8b09db61 | ||
|
|
8d5e649646 | ||
|
|
6844e61dd4 | ||
|
|
7acf97a5e0 | ||
|
|
6a68c85d94 | ||
|
|
914a5abacb | ||
|
|
fdcd3bb805 | ||
|
|
5a4d6846b5 | ||
|
|
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 }}}
|
||||
|
||||
170
.github/workflows/eks-cd.yml
vendored
Normal file
170
.github/workflows/eks-cd.yml
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
name: Fleetbase EKS CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["eksdeploy/*"]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
PROJECT: ${{ secrets.PROJECT }}
|
||||
GITHUB_AUTH_KEY: ${{ secrets._GITHUB_AUTH_TOKEN }}
|
||||
|
||||
jobs:
|
||||
build_service:
|
||||
name: Build and Deploy the Service
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # This is required for requesting the JWT
|
||||
contents: read # This is required for actions/checkout
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- 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: ${{ secrets.EKS_DEPLOYER_ROLE }}
|
||||
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: 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: Update kube config
|
||||
run: aws eks update-kubeconfig --name ${{ secrets.EKS_CLUSTER_NAME }} --region ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Deploy the images 🚀
|
||||
env:
|
||||
REGISTRY: ${{ steps.login-ecr.outputs.registry }}/${{ env.PROJECT }}-${{ env.STACK }}
|
||||
run: |-
|
||||
set -eu
|
||||
# run deploy.sh script before deployments
|
||||
helm upgrade -i ${{ env.PROJECT }} infra/helm -n ${{ env.PROJECT}}-${{ env.STACK }} --set image.repository=${{ env.REGISTRY }} \
|
||||
--set image.tag=${{ env.VERSION }} --set 'api_host=${{ secrets.API_HOST }}' --set 'socketcluster_host=${{ secrets.SOCKETCLUSTER_HOST }}' \
|
||||
--set gcp=false --set 'ingress.annotations.kubernetes\.io/ingress\.class=null' --set 'ingress.annotations.alb\.ingress\.kubernetes\.io/scheme=internet-facing' \
|
||||
--set serviceAccount.name=default --set serviceAccount.create=false --set ingress.className=alb \
|
||||
--set 'ingress.annotations.alb\.ingress\.kubernetes\.io/listen-ports=[{"HTTPS":443}]' \
|
||||
--set service.type=NodePort
|
||||
|
||||
build_frontend:
|
||||
name: Build and Deploy the Console
|
||||
needs: [build_service]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # This is required for requesting the JWT
|
||||
contents: read # This is required for actions/checkout
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- 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: ${{ secrets.EKS_DEPLOYER_ROLE }}
|
||||
role-session-name: github
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- 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
|
||||
|
||||
./ssm-parent -n /actions/${{ env.PROJECT }}/${{ env.STACK }}/configuration dotenv /tmp/dotenv.file
|
||||
# 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
|
||||
|
||||
- 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
|
||||
|
||||
- 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: Install dependencies
|
||||
run: pnpm install
|
||||
working-directory: ./console
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
API_HOST: ${{ secrets.API_HOST }}
|
||||
SOCKETCLUSTER_HOST: ${{ secrets.SOCKETCLUSTER_HOST }}
|
||||
SOCKETCLUSTER_PORT: "443" # it uses common ingress so port 443
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
pnpm build --environment production
|
||||
working-directory: ./console
|
||||
|
||||
- name: Deploy Console 🚀
|
||||
run: |
|
||||
set -u
|
||||
|
||||
DEPLOY_BUCKET=${STATIC_DEPLOY_BUCKET:-${{ env.PROJECT }}-${{ env.STACK }}}
|
||||
# this value will come from the dotenv above
|
||||
echo "Deploying to $DEPLOY_BUCKET"
|
||||
wget -O- https://github.com/bep/s3deploy/releases/download/v2.11.0/s3deploy_2.11.0_linux-amd64.tar.gz | tar xzv -f - s3deploy
|
||||
./s3deploy -region ${AWS_REGION} -source console/dist -bucket ${DEPLOY_BUCKET}
|
||||
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.12",
|
||||
"fleetbase/fleetops-api": "^0.4.3",
|
||||
"fleetbase/storefront-api": "^0.2.8",
|
||||
"fruitcake/laravel-cors": "^2.0",
|
||||
"php": "^8.0",
|
||||
"fleetbase/core-api": "^1.4.15",
|
||||
"fleetbase/fleetops-api": "^0.4.23",
|
||||
"fleetbase/storefront-api": "^0.3.6",
|
||||
"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
|
||||
}
|
||||
|
||||
8237
api/composer.lock
generated
8237
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,
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DRIVER', 'local'),
|
||||
'default' => env('FILESYSTEM_DRIVER', 'public'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -58,6 +58,7 @@ return [
|
||||
'bucket' => env('GOOGLE_CLOUD_STORAGE_BUCKET', env('AWS_BUCKET')),
|
||||
'path_prefix' => env('GOOGLE_CLOUD_STORAGE_PATH_PREFIX', null),
|
||||
'storage_api_uri' => env('GOOGLE_CLOUD_STORAGE_API_URI', env('AWS_URL')),
|
||||
'visibility_handler' => \League\Flysystem\GoogleCloudStorage\UniformBucketLevelAccessVisibility::class,
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
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,7 @@ php artisan sandbox:migrate --force
|
||||
php artisan fleetbase:seed
|
||||
|
||||
# Restart queue
|
||||
php artisan queue:restart
|
||||
php artisan queue:restart
|
||||
|
||||
# Sync scheduler
|
||||
php artisan schedule-monitor:sync
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -127,6 +127,15 @@ export default class AuthVerificationController extends Controller {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow user to manually trigger no code received prompt.
|
||||
*
|
||||
* @memberof AuthVerificationController
|
||||
*/
|
||||
@action onDidntReceiveCode() {
|
||||
this.stillWaiting = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the input
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,9 +50,9 @@ export default class ConsoleAccountIndexController extends Controller {
|
||||
file,
|
||||
{
|
||||
path: `uploads/${this.user.company_uuid}/users/${this.user.slug}`,
|
||||
key_uuid: this.user.id,
|
||||
key_type: `user`,
|
||||
type: `user_avatar`,
|
||||
subject_uuid: this.user.id,
|
||||
subject_type: 'user',
|
||||
type: 'user_avatar',
|
||||
},
|
||||
(uploadedFile) => {
|
||||
this.user.setProperties({
|
||||
|
||||
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,136 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* The overlay context API.
|
||||
*
|
||||
* @memberof ConsoleAdminOrganizationsIndexUsersController
|
||||
*/
|
||||
@tracked contextApi;
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class ConsoleAdminScheduleMonitorLogsController extends Controller {
|
||||
/**
|
||||
* The router service.
|
||||
*
|
||||
* @memberof ConsoleAdminScheduleMonitorLogsController
|
||||
*/
|
||||
@service router;
|
||||
/**
|
||||
* The fetch service.
|
||||
*
|
||||
* @memberof ConsoleAdminScheduleMonitorLogsController
|
||||
*/
|
||||
@service fetch;
|
||||
|
||||
/**
|
||||
* Tracked property for logs.
|
||||
* @type {Array}
|
||||
*/
|
||||
@tracked logs = [];
|
||||
|
||||
/**
|
||||
* Tracked property for the context API.
|
||||
* @type {Object}
|
||||
*/
|
||||
@tracked contextApi;
|
||||
|
||||
/**
|
||||
* Periodically reloads logs every 3 seconds.
|
||||
*
|
||||
* @memberof ConsoleAdminScheduleMonitorLogsController
|
||||
*/
|
||||
@task *reload(task) {
|
||||
this.logs = yield this.fetch.get(`schedule-monitor/${task.id}/logs`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.schedule-monitor');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
44
console/app/models/comment.js
Normal file
44
console/app/models/comment.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
|
||||
export default class CommentModel extends Model {
|
||||
/** @ids */
|
||||
@attr('string') company_uuid;
|
||||
@attr('string') parent_comment_uuid;
|
||||
@attr('string') subject_uuid;
|
||||
@attr('string') subject_type;
|
||||
|
||||
/** @relationships */
|
||||
@belongsTo('user') author;
|
||||
@belongsTo('comment', { inverse: 'replies' }) parent;
|
||||
@hasMany('comment', { inverse: 'parent' }) replies;
|
||||
|
||||
/** @attributes */
|
||||
@attr('string') content;
|
||||
@attr('boolean') editable;
|
||||
@attr('raw') tags;
|
||||
@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');
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -10,7 +10,7 @@ Router.map(function () {
|
||||
this.route('auth', function () {
|
||||
this.route('login', { path: '/' });
|
||||
this.route('forgot-password');
|
||||
this.route('reset-password');
|
||||
this.route('reset-password', { path: '/reset-password/:id' });
|
||||
this.route('two-fa');
|
||||
this.route('verification');
|
||||
});
|
||||
@@ -49,26 +49,10 @@ Router.map(function () {
|
||||
this.route('notifications');
|
||||
this.route('two-fa-settings');
|
||||
this.route('virtual', { path: '/:slug/:view' });
|
||||
});
|
||||
|
||||
this.mount('@fleetbase/dev-engine', {
|
||||
as: 'developers',
|
||||
path: 'developers'
|
||||
});
|
||||
|
||||
this.mount('@fleetbase/fleetops-engine', {
|
||||
as: 'fleet-ops',
|
||||
path: 'fleet-ops'
|
||||
});
|
||||
|
||||
this.mount('@fleetbase/iam-engine', {
|
||||
as: 'iam',
|
||||
path: 'iam'
|
||||
});
|
||||
|
||||
this.mount('@fleetbase/storefront-engine', {
|
||||
as: 'storefront',
|
||||
path: 'storefront'
|
||||
this.route('organizations', function () {
|
||||
this.route('index', { path: '/' });
|
||||
this.route('users', { path: '/:company_id' });
|
||||
});
|
||||
});
|
||||
});
|
||||
this.route('install');
|
||||
|
||||
@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
|
||||
export default class AuthResetPasswordRoute extends Route {
|
||||
@service store;
|
||||
|
||||
model() {
|
||||
return this.store.findRecord('brand', 1);
|
||||
async model(params) {
|
||||
return params;
|
||||
}
|
||||
|
||||
async setupController(controller) {
|
||||
super.setupController(...arguments);
|
||||
|
||||
// set brand to controller
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
10
console/app/routes/console/admin/schedule-monitor.js
Normal file
10
console/app/routes/console/admin/schedule-monitor.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleAdminScheduleMonitorRoute extends Route {
|
||||
@service fetch;
|
||||
|
||||
model() {
|
||||
return this.fetch.get('schedule-monitor/tasks');
|
||||
}
|
||||
}
|
||||
14
console/app/routes/console/admin/schedule-monitor/logs.js
Normal file
14
console/app/routes/console/admin/schedule-monitor/logs.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ConsoleAdminScheduleMonitorLogsRoute extends Route {
|
||||
@service fetch;
|
||||
|
||||
model({ id }) {
|
||||
return this.fetch.get(`schedule-monitor/${id}`);
|
||||
}
|
||||
|
||||
async setupController(controller, model) {
|
||||
controller.logs = await this.fetch.get(`schedule-monitor/${model.id}/logs`);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
38
console/app/serializers/comment.js
Normal file
38
console/app/serializers/comment.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
|
||||
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
|
||||
|
||||
export default class CommentSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
|
||||
/**
|
||||
* Embedded relationship attributes
|
||||
*
|
||||
* @var {Object}
|
||||
*/
|
||||
get attrs() {
|
||||
return {
|
||||
author: { embedded: 'always' },
|
||||
parent: { embedded: 'always' },
|
||||
replies: { embedded: 'always' },
|
||||
};
|
||||
}
|
||||
|
||||
serializeAttribute(snapshot, json, key) {
|
||||
if (key === 'editable') {
|
||||
return;
|
||||
}
|
||||
|
||||
super.serializeAttribute(...arguments);
|
||||
}
|
||||
|
||||
serializeHasMany(snapshot, json, relationship) {
|
||||
let key = relationship.key;
|
||||
if (key === 'replies') {
|
||||
return;
|
||||
} else {
|
||||
super.serializeHasMany(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
serializeBelongsTo() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
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,7 +4,8 @@
|
||||
color: rgb(202 138 4);
|
||||
}
|
||||
|
||||
.two-fa-enforcement-alert button#two-fa-setup-button.btn.btn-warning,
|
||||
.btn.btn-warning-alert.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);
|
||||
@@ -21,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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg">
|
||||
<div class="mb-4">
|
||||
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
|
||||
<Image src={{this.brand.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
|
||||
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
|
||||
{{t "auth.reset-password.title"}}
|
||||
</h2>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,9 +1,10 @@
|
||||
{{page-title "Account Verification"}}
|
||||
{{page-title (t "auth.verification.header-title")}}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
|
||||
<div class="mb-8">
|
||||
<img class="mx-auto h-12 w-auto " src="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}}>
|
||||
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
|
||||
Verify your email address
|
||||
{{t "auth.verification.title"}}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -12,15 +13,16 @@
|
||||
<FaIcon @icon="shield-check" @size="lg" class="text-blue-900 mr-4" />
|
||||
</div>
|
||||
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
|
||||
<strong>Almost done!</strong><br> Check your email for a verification code.
|
||||
{{t "auth.verification.message-text" htmlSafe=true}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="mt-8 space-y-6" {{on "submit" this.verifyCode}}>
|
||||
<InputGroup @type="tel" @name="Verification Code" @value={{this.code}} @helpText="Enter the verification code you received via email." @inputClass="input-lg" {{on "input" this.validateInput}} />
|
||||
<InputGroup @type="tel" @name={{t "auth.verification.verification-input-label"}} @value={{this.code}} @helpText={{t "auth.verification.verification-code-text"}} @inputClass="input-lg" {{on "input" this.validateInput}} />
|
||||
|
||||
<div>
|
||||
<div class="flex flex-row items-center space-x-4">
|
||||
<Button @icon="check" @iconPrefix="fas" @buttonType="submit" @type="primary" @size="lg" @text="Verify & Continue" @isLoading={{this.isLoading}} @disabled={{this.isNotReadyToSubmit}} @onClick={{this.verifyCode}} />
|
||||
<a href="#" {{on "click" this.onDidntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "auth.verification.didnt-receive-a-code"}}</a>
|
||||
</div>
|
||||
|
||||
{{#if this.stillWaiting}}
|
||||
@@ -30,14 +32,14 @@
|
||||
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-400" />
|
||||
</div>
|
||||
<div class="ml-3 flex items-center">
|
||||
<span class="text-lg font-extrabold text-yellow-800">Didn't receive an email yet?</span>
|
||||
<span class="text-lg font-extrabold text-yellow-800">{{t "auth.verification.didnt-receive-a-code"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-3">
|
||||
<p class="text-yellow-700">Use alternaitve options below to verify your account.</p>
|
||||
<div class="py-1">
|
||||
<p class="text-yellow-700 text-sm">{{t "auth.verification.not-sent.alternative-choice"}}</p>
|
||||
<div class="flex items-center mt-3">
|
||||
<Button @type="default" class="mr-2" @onClick={{this.resendEmail}}>Resend Email</Button>
|
||||
<Button @type="default" @onClick={{this.resendBySms}}>Send by SMS</Button>
|
||||
<Button @buttonType="button" @type="warning" @wrapperClass="mr-2" @onClick={{this.resendEmail}} class="btn-warning-alert">{{t "auth.verification.not-sent.resend-email"}}</Button>
|
||||
<Button @buttonType="button" @type="warning" @onClick={{this.resendBySms}} class="btn-warning-alert">{{t "auth.verification.not-sent.send-by-sms"}}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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" />
|
||||
@@ -1,7 +1,7 @@
|
||||
<Layout::Section::Header @title={{t "common.profile"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto">
|
||||
<ContentPanel @title={{t "common.your-profile"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<form class="flex flex-col md:flex-row" {{on "submit" this.saveProfile}}>
|
||||
@@ -42,4 +42,5 @@
|
||||
</ContentPanel>
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="500px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -2,16 +2,26 @@
|
||||
|
||||
<EmberWormhole @to="sidebar-menu-items">
|
||||
<Layout::Sidebar::Item @route="console.admin.index" @icon="rectangle-list">{{t "common.overview"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.organizations" @icon="building">{{t "common.organizations"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.branding" @icon="palette">{{t "common.branding"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.notifications" @icon="bell">{{t "common.notifications"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.two-fa-settings" @icon="shield-halved">{{t "common.2fa-config"}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item @route="console.admin.schedule-monitor" @icon="calendar-check">{{t "console.admin.schedule-monitor.schedule-monitor"}}</Layout::Sidebar::Item>
|
||||
{{#each this.universe.adminMenuItems as |menuItem|}}
|
||||
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.admin.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item
|
||||
@onClick={{fn this.universe.transitionMenuItem "console.admin.virtual" menuItem}}
|
||||
@item={{menuItem}}
|
||||
@icon={{menuItem.icon}}
|
||||
>{{menuItem.title}}</Layout::Sidebar::Item>
|
||||
{{/each}}
|
||||
{{#each this.universe.adminMenuPanels as |menuPanel|}}
|
||||
<Layout::Sidebar::Panel @open={{menuPanel.open}} @title={{menuPanel.title}}>
|
||||
{{#each menuPanel.items as |menuItem|}}
|
||||
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.admin.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
|
||||
<Layout::Sidebar::Item
|
||||
@onClick={{fn this.universe.transitionMenuItem "console.admin.virtual" menuItem}}
|
||||
@item={{menuItem}}
|
||||
@icon={{menuItem.icon}}
|
||||
>{{menuItem.title}}</Layout::Sidebar::Item>
|
||||
{{/each}}
|
||||
</Layout::Sidebar::Panel>
|
||||
{{/each}}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</Layout::Section::Header>
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-6">
|
||||
<ContentPanel @title={{t "console.admin.branding.title"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
<form class="flex flex-col" {{on "submit" this.save}}>
|
||||
@@ -63,4 +63,5 @@
|
||||
</ContentPanel>
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -4,7 +4,7 @@
|
||||
</Layout::Section::Header>
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 1200}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-4">
|
||||
{{#each-in this.groupedNotifications as |groupName notifications|}}
|
||||
<ContentPanel @title={{concat (smart-humanize groupName) (t "console.admin.notifications.notification-settings") }} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
@@ -29,4 +29,5 @@
|
||||
{{/each-in}}
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
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>
|
||||
34
console/app/templates/console/admin/schedule-monitor.hbs
Normal file
34
console/app/templates/console/admin/schedule-monitor.hbs
Normal file
@@ -0,0 +1,34 @@
|
||||
{{page-title (t "console.admin.schedule-monitor.schedule-monitor")}}
|
||||
<Layout::Section::Header @title={{t "console.admin.schedule-monitor.schedule-monitor"}} />
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="next-table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th {{set-width "300px"}}>{{t "console.admin.schedule-monitor.name"}}</th>
|
||||
<th {{set-width "100px"}}>{{t "console.admin.schedule-monitor.type"}}</th>
|
||||
<th {{set-width "90px"}}>{{t "console.admin.schedule-monitor.timezone"}}</th>
|
||||
<th>{{t "console.admin.schedule-monitor.last-started"}}</th>
|
||||
<th>{{t "console.admin.schedule-monitor.last-finished"}}</th>
|
||||
<th>{{t "console.admin.schedule-monitor.last-failure"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @model as |task|}}
|
||||
<tr>
|
||||
<td>
|
||||
<LinkTo @route="console.admin.schedule-monitor.logs" @model={{task}}>{{task.name}}</LinkTo>
|
||||
</td>
|
||||
<td>{{task.type}}</td>
|
||||
<td>{{task.timezone}}</td>
|
||||
<td>{{n-a task.last_started_at_fmt}}</td>
|
||||
<td>{{n-a task.last_finished_at_fmt}}</td>
|
||||
<td>{{n-a task.last_failed_at_fmt}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Layout::Section::Body>
|
||||
{{outlet}}
|
||||
@@ -0,0 +1,34 @@
|
||||
{{page-title (concat (t "console.admin.schedule-monitor.schedule-monitor") " - " @model.name)}}
|
||||
<Overlay @isOpen={{true}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width="600px" @isResizable={{true}}>
|
||||
<Overlay::Header @title={{concat (t "console.admin.schedule-monitor.task-logs-for") @model.name}} @titleClass="max-w-400px truncate" @hideStatusDot={{true}} @titleWrapperClass="leading-5">
|
||||
<div class="flex flex-1 justify-end">
|
||||
<Button @type="default" @icon="times" @helpText={{t "common.close-and-save"}} @onClick={{this.onPressClose}} />
|
||||
</div>
|
||||
</Overlay::Header>
|
||||
|
||||
<Overlay::Body>
|
||||
<div class="p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="text-sm">{{t "console.admin.schedule-monitor.showing-last-count" count=20}}</div>
|
||||
<Button @size="xs" @icon="arrows-rotate" @onClick={{perform this.reload @model}} @isLoading={{not this.reload.isIdle}} />
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
{{#each this.logs as |log|}}
|
||||
<div class="bg-gray-100 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 rounded-lg p-2 font-mono text-xs">
|
||||
<div class="font-mono"><span class="font-semibold font-mono">{{t "console.admin.schedule-monitor.date"}}:</span> {{log.created_at_fmt}}</div>
|
||||
<div class="font-mono"><span class="font-semibold font-mono">{{t "console.admin.schedule-monitor.memory"}}:</span> {{format-bytes log.meta.memory}}</div>
|
||||
<div class="font-mono"><span class="font-semibold font-mono">{{t "console.admin.schedule-monitor.runtime"}}:</span> {{format-milliseconds log.meta.runtime}}</div>
|
||||
<div class="font-semibold font-mono mb-2">{{t "console.admin.schedule-monitor.output"}}:</div>
|
||||
<div class="whitespace-pre-line overflow-hidden bg-black text-green-400 rounded-lg p-2 font-mono text-xs border border-gray-900">
|
||||
{{#if log.meta.output}}
|
||||
{{log.meta.output}}
|
||||
{{else}}
|
||||
{{t "console.admin.schedule-monitor.no-output"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</Overlay::Body>
|
||||
</Overlay>
|
||||
@@ -4,7 +4,7 @@
|
||||
</Layout::Section::Header>
|
||||
|
||||
<Layout::Section::Body class="overflow-y-scroll h-full">
|
||||
<div class="container mx-auto h-screen" {{increase-height-by 1200}}>
|
||||
<div class="container mx-auto h-screen">
|
||||
<div class="max-w-3xl my-10 mx-auto space-y-4">
|
||||
<ContentPanel @title="2FA Config" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
|
||||
{{#if this.loadSystemTwoFaConfig.isIdle}}
|
||||
@@ -25,4 +25,5 @@
|
||||
</ContentPanel>
|
||||
</div>
|
||||
</div>
|
||||
<Spacer @height="300px" />
|
||||
</Layout::Section::Body>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{{page-title "Account Verification"}}
|
||||
{{page-title (t "onboard.verify-email.header-title")}}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
|
||||
<div class="mb-8">
|
||||
<img class="mx-auto h-12 w-auto" src="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}}>
|
||||
<img class="mx-auto h-12 w-auto " src="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}}>
|
||||
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
|
||||
{{t "onboard.verify-email.title"}}
|
||||
</h2>
|
||||
@@ -12,15 +13,16 @@
|
||||
<FaIcon @icon="shield-check" @size="lg" class="text-blue-900 mr-4" />
|
||||
</div>
|
||||
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
|
||||
{{t "onboard.verify-email.message-text" htmlSafe=true}}
|
||||
{{t "onboard.verify-email.message-text" htmlSafe=true}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="mt-8 space-y-6" {{on "submit" this.verifyCode}}>
|
||||
<InputGroup @type="tel" @name="Verification Code" @value={{this.code}} @helpText={{t "onboard.verify-email.verification-code-text"}} @inputClass="input-lg" {{on "input" this.validateInput}} />
|
||||
<InputGroup @type="tel" @name={{t "onboard.verify-email.verification-input-label"}} @value={{this.code}} @helpText={{t "onboard.verify-email.verification-code-text"}} @inputClass="input-lg" {{on "input" this.validateInput}} />
|
||||
|
||||
<div>
|
||||
<Button @icon="check" @iconPrefix="fas" @buttonType="submit" @type="primary" @size="lg" @text={{t "onboard.verify-email.verify-button-text"}} @isLoading={{this.isLoading}} @disabled={{this.isNotReadyToSubmit}} @onClick={{this.verifyCode}} />
|
||||
<div class="flex flex-row items-center space-x-4">
|
||||
<Button @icon="check" @iconPrefix="fas" @buttonType="submit" @type="primary" @size="lg" @text="Verify & Continue" @isLoading={{this.isLoading}} @disabled={{this.isNotReadyToSubmit}} @onClick={{this.verifyCode}} />
|
||||
<a href="#" {{on "click" this.onDidntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "onboard.verify-email.didnt-receive-a-code"}}</a>
|
||||
</div>
|
||||
|
||||
{{#if this.stillWaiting}}
|
||||
@@ -30,14 +32,14 @@
|
||||
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-400" />
|
||||
</div>
|
||||
<div class="ml-3 flex items-center">
|
||||
<span class="text-lg font-extrabold text-yellow-800">{{t "onboard.verify-email.not-sent.message"}}</span>
|
||||
<span class="text-lg font-extrabold text-yellow-800">{{t "onboard.verify-email.didnt-receive-a-code"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-3">
|
||||
<p class="text-yellow-700">{{t "onboard.verify-email.not-sent.alternative-choice"}}</p>
|
||||
<div class="py-1">
|
||||
<p class="text-yellow-700 text-sm">{{t "onboard.verify-email.not-sent.alternative-choice"}}</p>
|
||||
<div class="flex items-center mt-3">
|
||||
<Button @type="default" class="mr-2" @onClick={{this.resendEmail}}>{{t "onboard.verify-email.not-sent.resent-button-text"}}</Button>
|
||||
<Button @type="default" @onClick={{this.resendBySms}}>{{t "onboard.verify-email.not-sent.sms-button-text"}}</Button>
|
||||
<Button @buttonType="button" @type="warning" @wrapperClass="mr-2" @onClick={{this.resendEmail}} class="btn-warning-alert">{{t "onboard.verify-email.not-sent.resend-email"}}</Button>
|
||||
<Button @buttonType="button" @type="warning" @onClick={{this.resendBySms}} class="btn-warning-alert">{{t "onboard.verify-email.not-sent.send-by-sms"}}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,9 +46,13 @@ 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'),
|
||||
vehicleAvatar: getenv('DEFAULT_VEHICLE_AVATAR', 'https://flb-assets.s3-ap-southeast-1.amazonaws.com/static/vehicle-icons/mini_bus.svg'),
|
||||
driverAvatar: getenv('DEFAULT_DRIVER_AVATAR', 'https://flb-assets.s3-ap-southeast-1.amazonaws.com/static/driver-icons/moto-driver.png'),
|
||||
placeAvatar: getenv('DEFAULT_PLACE_AVATAR', 'https://flb-assets.s3-ap-southeast-1.amazonaws.com/static/place-icons/basic-building.png'),
|
||||
extensionIcon: getenv('DEFAULT_EXTENSION_ICON', 'https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/default-extension-icon.svg'),
|
||||
},
|
||||
|
||||
'ember-simple-auth': {
|
||||
@@ -60,6 +64,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.9",
|
||||
"version": "0.4.15",
|
||||
"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.9",
|
||||
"@fleetbase/storefront-engine": "^0.2.8",
|
||||
"@fleetbase/fleetops-engine": "^0.4.3",
|
||||
"@fleetbase/fleetops-data": "^0.1.7",
|
||||
"@fleetbase/ember-core": "^0.2.8",
|
||||
"@fleetbase/ember-ui": "^0.2.11",
|
||||
"@fleetbase/fleetops-engine": "^0.4.23",
|
||||
"@fleetbase/fleetops-data": "^0.1.14",
|
||||
"@fleetbase/storefront-engine": "^0.3.6",
|
||||
"@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.7",
|
||||
"@fleetbase/ember-core": "^0.2.1",
|
||||
"@fleetbase/ember-ui": "^0.2.9"
|
||||
"@fleetbase/ember-core": "^0.2.8",
|
||||
"@fleetbase/ember-ui": "^0.2.11",
|
||||
"@fleetbase/fleetops-data": "^0.1.14"
|
||||
}
|
||||
},
|
||||
"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();
|
||||
}
|
||||
}
|
||||
1968
console/pnpm-lock.yaml
generated
1968
console/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ Router.map(function () {
|
||||
this.route('auth', function () {
|
||||
this.route('login', { path: '/' });
|
||||
this.route('forgot-password');
|
||||
this.route('reset-password');
|
||||
this.route('reset-password', { path: '/reset-password/:id' });
|
||||
this.route('two-fa');
|
||||
this.route('verification');
|
||||
});
|
||||
@@ -49,6 +49,14 @@ 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('schedule-monitor', function () {
|
||||
this.route('logs', { path: '/:id/logs' });
|
||||
});
|
||||
});
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Controller | console/admin/schedule-monitor/logs', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup('controller:console/admin/schedule-monitor/logs');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
14
console/tests/unit/models/comment-test.js
Normal file
14
console/tests/unit/models/comment-test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
import { setupTest } from '@fleetbase/console/tests/helpers';
|
||||
|
||||
module('Unit | Model | comment', 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('comment', {});
|
||||
assert.ok(model);
|
||||
});
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user