Compare commits

..

61 Commits

Author SHA1 Message Date
Ronald A. Richardson
ff59941947 wip: binary build 2024-04-03 15:15:27 +08:00
Ronald A. Richardson
013e19f39e working on single executable binary for linux and macos of fleetbase 2024-04-02 21:21:24 +08:00
Ronald A. Richardson
dba2b7afc5 hotfix: bump console version 2024-04-02 17:56:06 +08:00
Ron
a6e6625ae1 Merge pull request #239 from fleetbase/dev-v0.4.16
v0.4.16
2024-04-02 16:55:13 +07:00
Ronald A. Richardson
3777569f22 added upgraded storefront 2024-04-02 17:50:07 +08:00
Ronald A. Richardson
fa5b6700a3 ready for release, full core dependency upgrades, security improvements and new feature for driver onboard 2024-04-02 17:45:01 +08:00
Ronald A. Richardson
6cce6a9db2 added cache to github card component, add password verification for changing email or password, ensured reset password link is is invalidated when expired or deleted 2024-04-02 17:09:10 +08:00
Ronald A. Richardson
39d405cb57 refactored github card to use cache for 6 hours 2024-03-28 15:12:17 +08:00
Ron
9f88c7bc79 Merge pull request #238 from fleetbase/dev-v0.4.15
v0.4.15
2024-03-27 20:04:55 +07:00
Ron
b3816c394a Merge pull request #236 from fleetbase/feature/helm_eks
feature/helm eks
2024-03-27 20:00:08 +07:00
Ronald A. Richardson
34462c61c4 Prepeared next release with latest upgrades/ patches 2024-03-27 20:58:40 +08:00
Eugene Dementyev
080302eb86 Fix socketcluster port for eks 2024-03-20 18:19:06 +13:00
Eugene Dementyev
b063cf6338 Set api host and socketcluster host from github 2024-03-20 18:03:15 +13:00
Eugene Dementyev
b6dca79251 Fix registry 2024-03-19 23:26:38 +13:00
Eugene Dementyev
c35af4d47a Add EKS deployments 2024-03-19 21:23:21 +13:00
Eugene Dementyev
2a89659cc3 Adapt the Helm chart to be used with AWS EKS 2024-03-19 21:05:47 +13:00
Ron
f8ae75d767 Merge pull request #235 from fleetbase/dev-v0.4.14
v0.4.14
2024-03-17 12:27:18 +08:00
Ronald A. Richardson
924448a4d0 Upgraded fleetops for distance matrix calculation optimization and provider configuration 2024-03-17 12:20:40 +08:00
Ron
5426ac374d Merge pull request #234 from fleetbase/dev-v0.4.13
v0.4.13
2024-03-15 17:27:12 +08:00
Ronald A. Richardson
73f56b7958 added translations for schedule monitor ui 2024-03-15 17:23:06 +08:00
Ronald A. Richardson
095eb87e14 preparing for new release 2024-03-15 17:00:31 +08:00
Ronald A. Richardson
fabc16612b core-api upgrade includes improved upload error reporting 2024-03-13 18:02:22 +08:00
Ronald A. Richardson
3389cba935 added the visibility_handler to the gcs filesystem driver config 2024-03-13 17:13:23 +08:00
Ron
099ea57c39 Merge pull request #231 from fleetbase/dev-v0.4.12
v0.4.12
2024-03-13 15:26:36 +08:00
Ronald A. Richardson
5492bc7299 - Revamped service rates to integrate with the new order configuration system.
- Enhanced activities to now support proof of delivery requirements.
- Patched and improved the stringent validation for creating new orders.
- Added a new column `order_config_uuid` to the `service_rates` table for better data management.
- Various other minor improvements and optimizations for enhanced performance and stability.
- Critical patch and improvement to location service
2024-03-13 15:15:42 +08:00
Ron
00b037a385 Merge pull request #229 from fleetbase/dev-v0.4.11
v0.4.11
2024-03-12 18:18:42 +08:00
Ronald A. Richardson
151fea2505 ready for release 2024-03-12 18:13:35 +08:00
Ronald A. Richardson
08f901d865 patches following major release 2024-03-12 17:55:51 +08:00
Ron
116873a1ce Merge pull request #225 from fleetbase/dev-v0.4.10
v0.4.10
2024-03-11 21:12:01 +08:00
Ronald A. Richardson
20a1793447 remove test url from cors allowed_origins 2024-03-11 21:07:23 +08:00
Ronald A. Richardson
72ab83dc7a release is ready 2024-03-11 21:06:25 +08:00
Ronald A. Richardson
171e365ba6 added commercial license 2024-03-11 20:33:39 +08:00
Ronald A. Richardson
009f2d6e53 Merge branch 'dev-v0.4.10' of github.com:fleetbase/fleetbase into dev-v0.4.10 2024-03-11 16:25:17 +08:00
Ronald A. Richardson
4a59318feb upgraded fleetops-data; fixed custom fields and custom field value models; patch notification model 2024-03-11 16:24:46 +08:00
Ron
8920039b40 Merge pull request #227 from fleetbase/fix/helm_install_order 2024-03-11 12:44:53 +08:00
Eugene Dementyev
cd9be05714 Remove helm service hooks. Use DNS names for services 2024-03-08 20:40:31 +13:00
Ron
fbe35545e9 Merge pull request #223 from fleetbase/feature/nginx_host_header
Make nginx pass the Host header
2024-03-06 13:14:35 +08:00
Ron
56ab967d7a Merge pull request #224 from fleetbase/fix/helm_install_order
Fix/helm install order
2024-03-06 13:14:03 +08:00
Ronald A. Richardson
79604c7981 preparing for v0.4.10 release 2024-03-06 13:10:31 +08:00
Eugene Dementyev
312eb1aa6f Adds pre-install for redis service 2024-03-02 20:55:17 +13:00
Eugene Dementyev
6572a59120 Fixes helm install order 2024-03-02 20:21:18 +13:00
Eugene Dementyev
fa536c6183 Make nginx pass the Host header 2024-02-27 19:54:50 +13:00
Ron
d4626be332 Merge pull request #221 from fleetbase/dev-v0.4.9
v0.4.9
2024-02-24 16:02:05 +07:00
Ronald A. Richardson
ffc54ecdbb upgrade dependencies which has expansion hotfixes 2024-02-24 16:59:12 +08:00
Ron
9bf097b88b Merge pull request #220 from fleetbase/dev-v0.4.8
v0.4.8
2024-02-23 12:48:50 +07:00
Ronald A. Richardson
52c8df8b66 prepared for next release 2024-02-23 13:47:15 +08:00
Ron
908e0eb9ee Merge pull request #219 from fleetbase/dev-v0.4.7
v0.4.7
2024-02-22 17:11:51 +07:00
Ronald A. Richardson
6438138913 add upgraded core-api submodule 2024-02-22 17:57:22 +08:00
Ronald A. Richardson
aee06a2146 update composer lockfile 2024-02-22 17:54:01 +08:00
Ronald A. Richardson
ccacc6c597 adds fleetops migrations hotfix 2024-02-22 12:43:33 +08:00
Ron
62c396b789 Merge pull request #218 from fleetbase/dev-v0.4.6
v0.4.6
2024-02-21 17:48:54 +07:00
Ronald A. Richardson
295da5f331 fix composer files 2024-02-21 18:44:27 +08:00
Ronald A. Richardson
e775ccc2c8 upgraded dependencies and prepared for release 2024-02-21 18:40:36 +08:00
Ronald A. Richardson
bd0759881f updated notification channel config for firebase and apn 2024-02-21 18:16:15 +08:00
Ronald A. Richardson
830ae69b1d hotfix: composer.lock with core-api upgrade 2024-02-17 20:04:35 +08:00
Ron
3b9a80866f Merge pull request #213 from fleetbase/dev-v0.4.5
v0.4.5
2024-02-17 18:26:25 +07:00
Ronald A. Richardson
76badbf949 patch admin organization browser; upgrade core-api; upgrade fleetops 2024-02-17 19:21:07 +08:00
Ronald A. Richardson
f968556585 hotfix organizations browsers in admin w/ more details, better querying and admin based view, upgrade of dependencies with major patches 2024-02-16 17:32:44 +07:00
Ronald A. Richardson
6d01eab305 use fixed fleetbase/laravel-mysql-spatial v1.0.2 2024-02-16 01:37:20 +07:00
Ronald A. Richardson
9406446db1 explicitly set production environment on gitlab cd workflow 2024-02-16 00:16:22 +07:00
Ronald A. Richardson
565db7bbce explicitly set production environment for cd workflows 2024-02-16 00:15:15 +07:00
81 changed files with 5956 additions and 3063 deletions

View File

@@ -147,7 +147,7 @@ jobs:
run: |
set -eu
pnpm build
pnpm build --environment production
working-directory: ./console
- name: Deploy Console 🚀

170
.github/workflows/eks-cd.yml vendored Normal file
View 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}

View File

@@ -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
View 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.

21
LICENSE
View File

@@ -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
View 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.

View File

@@ -9,9 +9,9 @@
"license": "MIT",
"require": {
"php": "^8.0",
"fleetbase/core-api": "^1.4.4",
"fleetbase/fleetops-api": "^0.4.10",
"fleetbase/storefront-api": "^0.3.0",
"fleetbase/core-api": "^1.4.16",
"fleetbase/fleetops-api": "^0.4.24",
"fleetbase/storefront-api": "^0.3.7",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^10.0",
"laravel/octane": "^2.3",
@@ -84,6 +84,7 @@
}
},
"config": {
"secure-http": false,
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,

3073
api/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@ return [
'allowed_headers' => ['*'],
'exposed_headers' => [],
'exposed_headers' => ['x-compressed-json', 'access-console-sandbox', 'access-console-sandbox-key'],
'max_age' => 0,

View File

@@ -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,
],
],

View File

@@ -17,3 +17,6 @@ php artisan fleetbase:seed
# Restart queue
php artisan queue:restart
# Sync scheduler
php artisan schedule-monitor:sync

View File

@@ -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'}}" />

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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,

View File

@@ -0,0 +1 @@
<div id="console-wormhole" />

View File

@@ -1,11 +1,15 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action, computed } from '@ember/object';
import { computed } from '@ember/object';
import { isArray } from '@ember/array';
import { isBlank } from '@ember/utils';
import { task } from 'ember-concurrency';
import { storageFor } from 'ember-local-storage';
import { add, isPast } from 'date-fns';
import fetch from 'fetch';
export default class GithubCardComponent extends Component {
@storageFor('local-cache') localCache;
@tracked data;
@tracked tags;
@tracked isLoading = false;
@@ -30,29 +34,47 @@ export default class GithubCardComponent extends Component {
constructor() {
super(...arguments);
this.loadRepositoryData();
this.loadRepositoryTags();
this.getRepositoryData.perform();
this.getRepositoryTags.perform();
}
@action loadRepositoryData() {
this.isLoading = true;
@task *getRepositoryData() {
// Check if cached data and expiration are available
const cachedData = this.localCache.get('fleetbase-github-data');
const expiration = this.localCache.get('fleetbase-github-data-expiration');
return fetch('https://api.github.com/repos/fleetbase/fleetbase')
.then((response) => {
return response.json().then((data) => {
this.data = data;
});
})
.finally(() => {
this.isLoading = false;
});
// Check if the cached data is still valid
if (cachedData && expiration && !isPast(new Date(expiration))) {
// Use cached data
this.data = cachedData;
} else {
// Fetch new data
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase');
if (response.ok) {
this.data = yield response.json();
this.localCache.set('fleetbase-github-data', this.data);
this.localCache.set('fleetbase-github-data-expiration', add(new Date(), { hours: 6 }));
}
}
}
@action loadRepositoryTags() {
return fetch('https://api.github.com/repos/fleetbase/fleetbase/tags').then((response) => {
return response.json().then((data) => {
this.tags = data;
});
});
@task *getRepositoryTags() {
// Check if cached tags and expiration are available
const cachedTags = this.localCache.get('fleetbase-github-tags');
const expiration = this.localCache.get('fleetbase-github-tags-expiration');
// Check if the cached tags are still valid
if (cachedTags && expiration && !isPast(new Date(expiration))) {
// Use cached tags
this.tags = cachedTags;
} else {
// Fetch new tags
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase/tags');
if (response.ok) {
this.tags = yield response.json();
this.localCache.set('fleetbase-github-tags', this.tags);
this.localCache.set('fleetbase-github-tags-expiration', add(new Date(), { hours: 6 }));
}
}
}
}

View File

@@ -0,0 +1,13 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="px-5">
{{#if @options.body}}
<p class="dark:text-gray-400 text-gray-700 mb-4">{{@options.body}}</p>
{{/if}}
<InputGroup>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<InputGroup @name="Password" @type="password" @value={{this.password}} @wrapperClass="mb-0i" />
<InputGroup @name="Confirm Password" @type="password" @value={{this.confirmPassword}} @wrapperClass="mb-0i" />
</div>
</InputGroup>
</div>
</Modal::Default>

View File

@@ -0,0 +1,49 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
export default class ModalsValidatePasswordComponent extends Component {
@service fetch;
@service notifications;
@tracked options = {};
@tracked password;
@tracked confirmPassword;
constructor(owner, { options }) {
super(...arguments);
this.options = options;
this.setupOptions();
}
setupOptions() {
this.options.title = 'Validate Current Password';
this.options.acceptButtonText = 'Validate Password';
this.options.declineButtonHidden = true;
this.options.confirm = (modal) => {
modal.startLoading();
return this.validatePassword.perform();
};
}
@task *validatePassword() {
let isPasswordValid = false;
try {
yield this.fetch.post('users/validate-password', {
password: this.password,
password_confirmation: this.confirmPassword,
});
isPasswordValid = true;
} catch (error) {
this.notifications.serverError(error, 'Invalid current password.');
}
if (typeof this.options.onValidated === 'function') {
this.options.onValidated(isPasswordValid);
}
return isPasswordValid;
}
}

View File

@@ -1,7 +1,7 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { task } from 'ember-concurrency';
export default class AuthResetPasswordController extends Controller {
/**
@@ -54,38 +54,23 @@ export default class AuthResetPasswordController extends Controller {
@tracked password_confirmation;
/**
* Loading stae of password reset.
* The reset password task.
*
* @memberof AuthResetPasswordController
*/
@tracked isLoading;
/**
* The reset password action.
*
* @memberof AuthResetPasswordController
*/
@action resetPassword(event) {
// firefox patch
@task *resetPassword(event) {
event.preventDefault();
const { code, password, password_confirmation } = this;
const { id } = this.model;
this.isLoading = true;
try {
yield this.fetch.post('auth/reset-password', { link: id, code, password, password_confirmation });
} catch (error) {
return this.notifications.serverError(error);
}
this.fetch
.post('auth/reset-password', { link: id, code, password, password_confirmation })
.then(() => {
this.notifications.success(this.intl.t('auth.reset-password.success-message'));
return this.router.transitionTo('auth.login');
})
.catch((error) => {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
this.notifications.success(this.intl.t('auth.reset-password.success-message'));
yield this.router.transitionTo('auth.login');
}
}

View File

@@ -34,17 +34,11 @@ export default class ConsoleAccountAuthController extends Controller {
@service router;
/**
* The user's current password.
* @type {string}
*/
@tracked password;
/**
* The user's confirmation of the new password.
* Service for managing modals.
*
* @type {string}
* @type {router}
*/
@tracked confirmPassword;
@service modalsManager;
/**
* The new password the user intends to set.
@@ -60,13 +54,6 @@ export default class ConsoleAccountAuthController extends Controller {
*/
@tracked newConfirmPassword;
/**
* Flag indicating whether the current password has been validated.
*
* @type {boolean}
*/
@tracked isPasswordValidated = false;
/**
* System-wide two-factor authentication configuration.
*
@@ -106,28 +93,6 @@ export default class ConsoleAccountAuthController extends Controller {
this.loadUserTwoFaSettings.perform();
}
/**
* Validates the user's current password.
*
* @method validatePassword
* @param {Event} event - The event object triggering the action.
*/
@action validatePassword(event) {
event.preventDefault();
this.validatePasswordTask.perform();
}
/**
* Initiates the task to change the user's password asynchronously.
*
* @method changeUserPasswordTask
* @param {Event} event - The event object triggering the action.
*/
@action changeUserPassword(event) {
event.preventDefault();
this.changeUserPasswordTask.perform();
}
/**
* Handles the event when two-factor authentication is toggled.
*
@@ -163,6 +128,58 @@ export default class ConsoleAccountAuthController extends Controller {
this.saveUserTwoFaSettings.perform(this.twoFaSettings);
}
/**
* Initiates the task to change the user's password asynchronously.
*
* @method changePassword
*/
@task *changePassword(event) {
// If from event fired
if (event instanceof Event) {
event.preventDefault();
}
// Validate current password
const isPasswordValid = yield this.validatePassword.perform();
if (!isPasswordValid) {
this.newPassword = undefined;
this.newConfirmPassword = undefined;
return;
}
try {
yield this.fetch.post('users/change-password', {
password: this.newPassword,
password_confirmation: this.newConfirmPassword,
});
this.notifications.success('Password change successfully.');
} catch (error) {
this.notifications.serverError(error, 'Failed to change password.');
}
this.newPassword = undefined;
this.newConfirmPassword = undefined;
}
/**
* Task to validate current password
*
* @return {boolean}
*/
@task *validatePassword() {
let isPasswordValid = false;
yield this.modalsManager.show('modals/validate-password', {
body: 'You must validate your current password before it can be changed.',
onValidated: (isValid) => {
isPasswordValid = isValid;
},
});
return isPasswordValid;
}
/**
* Initiates the task to save user-specific two-factor authentication settings asynchronously.
*
@@ -209,40 +226,4 @@ export default class ConsoleAccountAuthController extends Controller {
}
return twoFaConfig;
}
/**
* Initiates the task to validate the user's current password asynchronously.
*
* @method validatePasswordTask
*/
@task *validatePasswordTask() {
try {
yield this.fetch.post('users/validate-password', {
password: this.password,
password_confirmation: this.confirmPassword,
});
this.isPasswordValidated = true;
} catch (error) {
this.notifications.serverError(error, 'Invalid current password.');
}
}
/**
* Initiates the task to change the user's password asynchronously.
*
* @method changeUserPasswordTask
*/
@task *changeUserPasswordTask() {
try {
yield this.fetch.post('users/change-password', {
password: this.newPassword,
password_confirmation: this.newConfirmPassword,
});
this.notifications.success('Password change successfully.');
} catch (error) {
this.notifications.error('Failed to change password');
}
}
}

View File

@@ -1,8 +1,8 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { alias } from '@ember/object/computed';
import { task } from 'ember-concurrency';
export default class ConsoleAccountIndexController extends Controller {
/**
@@ -18,6 +18,7 @@ export default class ConsoleAccountIndexController extends Controller {
* @memberof ConsoleAccountIndexController
*/
@service fetch;
/**
* Inject the `notifications` service.
*
@@ -26,11 +27,11 @@ export default class ConsoleAccountIndexController extends Controller {
@service notifications;
/**
* The loading state of request.
* Inject the `modalsManager` service.
*
* @memberof ConsoleAccountIndexController
*/
@tracked isLoading = false;
@service modalsManager;
/**
* Alias to the currentUser service user record.
@@ -50,9 +51,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({
@@ -66,27 +67,64 @@ export default class ConsoleAccountIndexController extends Controller {
}
/**
* Save the Profile settings.
* Starts the task to change password
*
* @return {Promise}
* @param {Event} event
* @memberof ConsoleAccountIndexController
*/
@action saveProfile() {
const user = this.user;
@task *saveProfile(event) {
// If from event fired
if (event instanceof Event) {
event.preventDefault();
}
this.isLoading = true;
let canUpdateProfile = true;
// If email has been changed prompt for password validation
if (this.changedUserAttribute('email')) {
canUpdateProfile = yield this.validatePassword.perform();
}
return user
.save()
.then((user) => {
if (canUpdateProfile === true) {
try {
const user = yield this.user.save();
this.notifications.success('Profile changes saved.');
this.currentUser.set('user', user);
})
.catch((error) => {
} catch (error) {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
}
} else {
this.user.rollbackAttributes();
}
}
/**
* Task to validate current password
*
* @return {boolean}
* @memberof ConsoleAccountIndexController
*/
@task *validatePassword() {
let isPasswordValid = false;
yield this.modalsManager.show('modals/validate-password', {
body: 'You must validate your password to update the account email address.',
onValidated: (isValid) => {
isPasswordValid = isValid;
},
});
return isPasswordValid;
}
/**
* Checks if any user attribute has been changed
*
* @param {string} attributeKey
* @return {boolean}
* @memberof ConsoleAccountIndexController
*/
changedUserAttribute(attributeKey) {
const changedAttributes = this.user.changedAttributes();
return changedAttributes[attributeKey] !== undefined;
}
}

View File

@@ -1,8 +1,7 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
/**
* Controller for managing organizations in the admin console.
@@ -41,6 +40,13 @@ export default class ConsoleAdminOrganizationsController extends Controller {
*/
@service filters;
/**
* The search query param value.
*
* @var {String|null}
*/
@tracked query;
/**
* The current page of data being viewed
*
@@ -104,14 +110,36 @@ export default class ConsoleAdminOrganizationsController extends Controller {
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: 'phone',
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',
@@ -119,15 +147,14 @@ export default class ConsoleAdminOrganizationsController extends Controller {
];
/**
* `search` is a task that performs a search query on the 'company' model in the store.
* Update search query param and reset page to 1
*
* @method search
* @param {string} query - The search query.
* @returns {Promise} A promise that resolves with the search results.
* @public
* @param {Event} event
* @memberof ConsoleAdminOrganizationsController
*/
@task({ restartable: true }) *search(event) {
this.companies = yield this.store.query('company', { query: event.target.value });
@action search(event) {
this.query = event.target.value ?? '';
this.page = 1;
}
/**

View File

@@ -2,9 +2,6 @@ import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { isBlank } from '@ember/utils';
import { task } from 'ember-concurrency-decorators';
import { timeout } from 'ember-concurrency';
export default class ConsoleAdminOrganizationsIndexUsersController extends Controller {
/**
@@ -63,6 +60,13 @@ export default class ConsoleAdminOrganizationsIndexUsersController extends Contr
*/
@tracked company;
/**
* The overlay context API.
*
* @memberof ConsoleAdminOrganizationsIndexUsersController
*/
@tracked contextApi;
/**
* Queryable parameters for this controller's model
*
@@ -95,6 +99,17 @@ export default class ConsoleAdminOrganizationsIndexUsersController extends Contr
},
];
/**
* 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.
*
@@ -118,22 +133,4 @@ export default class ConsoleAdminOrganizationsIndexUsersController extends Contr
return this.router.transitionTo('console.admin.organizations.index');
}
/**
* `search` is a task that performs a search query on the 'company' model in the store.
*
* @method search
* @param {string} query - The search query.
* @returns {Promise} A promise that resolves with the search results.
* @public
*/
@task({ restartable: true }) *search(event) {
const searchQuery = event.target.value ?? '';
if (isBlank(searchQuery)) {
return;
}
yield timeout(600);
this.nestedQuery = searchQuery;
}
}

View File

@@ -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');
}
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View 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');
}
}

View 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');
}
}

View File

@@ -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 {

View File

@@ -3,13 +3,21 @@ import { inject as service } from '@ember/service';
export default class AuthResetPasswordRoute extends Route {
@service store;
@service fetch;
@service router;
@service notifications;
@service intl;
async model(params) {
return params;
async model({ id }) {
return this.fetch.get('auth/validate-verification', { id });
}
async setupController(controller) {
async setupController(controller, model) {
super.setupController(...arguments);
if (model.is_valid === false) {
this.notifications.warning(this.intl.t('auth.reset-password.invalid-verification-code'));
return this.router.transitionTo('auth');
}
// set brand to controller
controller.brand = await this.store.findRecord('brand', 1);

View File

@@ -14,11 +14,6 @@ export default class ConsoleAdminOrganizationsRoute extends Route {
};
model(params) {
return this.store.query('company', params);
}
setupController(controller, model) {
super.setupController(controller, model);
controller.companies = model;
return this.store.query('company', { view: 'admin', ...params });
}
}

View 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');
}
}

View 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`);
}
}

View 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' },
};
}
}

View File

@@ -44,3 +44,7 @@ body[data-theme='dark'] .two-fa-enforcement-alert button#two-fa-setup-button.btn
padding-left: 0 !important;
padding-right: 0 !important;
}
body.console-admin-organizations-index-index .next-table-wrapper > table {
table-layout: auto;
}

View File

@@ -6,14 +6,14 @@
</h2>
</div>
<form class="space-y-6" {{on "submit" this.resetPassword}}>
<form class="space-y-6" {{on "submit" (perform this.resetPassword)}}>
<InputGroup @name={{t "auth.reset-password.form.code.label"}} @value={{this.code}} @inputClass="form-input-lg" @helpText={{t "auth.reset-password.form.code.help-text"}} />
<InputGroup @name={{t "auth.reset-password.form.password.label"}} @value={{this.password}} @type="password" @inputClass="form-input-lg" @helpText={{t "auth.reset-password.form.password.help-text"}} />
<InputGroup @name={{t "auth.reset-password.form.confirm-password.label"}} @value={{this.password_confirmation}} @type="password" @inputClass="form-input-lg" @helpText={{t "auth.reset-password.form.confirm-password.help-text"}} />
<div class="flex space-x-2">
<Button @icon="check" @size="lg" @type="primary" @buttonType="submit" @text={{t "auth.reset-password.form.submit-button"}} @onClick={{this.resetPassword}} @isLoading={{this.isLoading}} />
<Button @size="lg" @buttonType="button" @text={{t "auth.reset-password.form.back-button"}} @onClick={{fn (transition-to "auth.login")}} @disabled={{this.isLoading}} />
<Button @icon="check" @size="lg" @type="primary" @buttonType="submit" @text={{t "auth.reset-password.form.submit-button"}} @onClick={{perform this.resetPassword}} @isLoading={{not this.resetPassword.isIdle}} />
<Button @size="lg" @buttonType="button" @text={{t "auth.reset-password.form.back-button"}} @onClick={{fn (transition-to "auth.login")}} @disabled={{not this.resetPassword.isIdle}} />
</div>
</form>
</div>

View File

@@ -19,4 +19,4 @@
<EmberWormhole @to="view-header-actions">
<LocaleSelector class="mr-0.5" />
</EmberWormhole>
<div id="console-wormhole" />
<ConsoleWormhole />

View File

@@ -5,51 +5,28 @@
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
<div class="max-w-3xl my-10 mx-auto space-y-6">
<ContentPanel @title="Change Password" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
{{#if this.isPasswordValidated}}
<form id="change-password-form" aria-label="change-password" {{on "submit" this.changeUserPassword}}>
<legend class="mb-3">Change Password</legend>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<InputGroup @name="Enter new Password" @type="password" @value={{this.newPassword}} />
<InputGroup @name="Confirm Password" @type="password" @value={{this.newConfirmPassword}} />
</div>
<Button @type="primary" @buttonType="submit" @text="Confirm & Change Password" @icon="save" {{on "click" this.changeUserPassword}} />
</form>
{{else}}
<form id="validate-password-form" aria-label="validate-password" {{on "submit" this.validatePassword}}>
<legend class="mb-3">Validate Current Password</legend>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<InputGroup @name="Password" @type="password" @value={{this.password}} />
<InputGroup @name="Confirm Password" @type="password" @value={{this.confirmPassword}} />
</div>
<Button @type="primary" @buttonType="submit" @text="Continue" @icon="check" {{on "click" this.validatePassword}} />
</form>
{{/if}}
<form id="change-password-form" aria-label="change-password" {{on "submit" (perform this.changePassword)}}>
<legend class="mb-3">Change Password</legend>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<InputGroup @name="Enter new Password" @type="password" @value={{this.newPassword}} />
<InputGroup @name="Confirm Password" @type="password" @value={{this.newConfirmPassword}} />
</div>
<Button @type="primary" @buttonType="submit" @text="Confirm & Change Password" @icon="save" {{on "click" (perform this.changePassword)}} />
</form>
</ContentPanel>
{{#if this.isSystemTwoFaEnabled}}
<ContentPanel @title="2FA Settings" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<div class="mb-3">
{{#if this.loadUserTwoFaSettings.isIdle}}
<TwoFaSettings
@twoFaMethods={{this.methods}}
@twoFaSettings={{this.twoFaSettings}}
@onTwoFaToggled={{this.onTwoFaToggled}}
@onTwoFaMethodSelected={{this.onTwoFaMethodSelected}}
/>
<TwoFaSettings @twoFaMethods={{this.methods}} @twoFaSettings={{this.twoFaSettings}} @onTwoFaToggled={{this.onTwoFaToggled}} @onTwoFaMethodSelected={{this.onTwoFaMethodSelected}} />
{{else}}
<div class="flex items-center justify-center p-4">
<Spinner @loadingMessage="Loading User 2FA Settings..." @wrapperClass="flex flex-row" @iconClass="mr-2" />
</div>
{{/if}}
</div>
<Button
@type="primary"
@buttonType="submit"
@text="Save 2FA Settings"
@icon="save"
@onClick={{this.saveTwoFactorAuthSettings}}
@isLoading={{this.saveUserTwoFaSettings.isRunning}}
/>
<Button @type="primary" @buttonType="submit" @text="Save 2FA Settings" @icon="save" @onClick={{this.saveTwoFactorAuthSettings}} @isLoading={{this.saveUserTwoFaSettings.isRunning}} />
</ContentPanel>
{{/if}}
</div>

View File

@@ -1,10 +1,10 @@
<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}}>
<form class="flex flex-col md:flex-row" {{on "submit" (perform this.saveProfile)}}>
<div class="w-32 flex flex-col justify-center mb-6 mr-6">
<Image src={{this.user.avatar_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{this.user.name}} class="w-32 h-32 rounded-md" />
<FileUpload @name={{t "console.account.index.photos"}} @accept="image/*" @onFileAdded={{this.uploadNewPhoto}} @labelClass="flex flex-row items-center justify-center" as |queue|>
@@ -35,11 +35,12 @@
<InputGroup @name={{t "common.date-of-birth"}} @type="date" @value={{this.user.date_of_birth}} />
</div>
<div class="mt-3 flex items-center justify-end">
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text={{t "common.save-button-text"}} @onClick={{this.saveProfile}} @isLoading={{this.isLoading}} />
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text={{t "common.save-button-text"}} @onClick={{perform this.saveProfile}} @isLoading={{not this.saveProfile.isIdle}} />
</div>
</div>
</form>
</ContentPanel>
</div>
</div>
<Spacer @height="500px" />
</Layout::Section::Body>

View File

@@ -6,6 +6,7 @@
<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}}

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,15 +1,16 @@
{{page-title (t "console.admin.organizations.index.title")}}
<Layout::Section::Header @title={{t "console.admin.organizations.index.title"}} @searchQuery={{this.query}} @onSearch={{perform this.search}} />
{{!-- 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={{this.companies}}
@rows={{@model}}
@columns={{this.columns}}
@selectable={{true}}
@canSelectAll={{true}}
@selectable={{false}}
@canSelectAll={{false}}
@onSetup={{fn (mut this.table)}}
@pagination={{true}}
@paginationMeta={{this.companies}}
@paginationMeta={{@model.meta}}
@page={{this.page}}
@onPageChange={{fn (mut this.page)}}
@tfootVerticalOffset="53"

View File

@@ -1,5 +1,5 @@
{{page-title (t "common.users")}}
<Overlay @isOpen={{@isOpen}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width={{or this.width @width "600px"}}>
<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}} />
@@ -7,12 +7,13 @@
</Overlay::Header>
<Overlay::Body class="without-padding">
<Layout::Section::Header @title={{t "console.admin.organizations.users.title"}} @searchQuery={{this.nestedQuery}} @onSearch={{perform this.search}}>
{{!-- 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={{true}} @canSelectAll={{true}} @onSetup={{fn (mut this.table)}} />
<Table @rows={{@model}} @columns={{this.columns}} @selectable={{false}} @canSelectAll={{false}} @onSetup={{fn (mut this.table)}} />
</Layout::Section::Body>
</Overlay::Body>
</Overlay>

View 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}}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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') {

View File

@@ -1,6 +1,6 @@
{
"name": "@fleetbase/console",
"version": "0.4.4",
"version": "0.4.16",
"private": true,
"description": "Fleetbase Console",
"repository": "https://github.com/fleetbase/fleetbase",
@@ -24,17 +24,18 @@
"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.4",
"@fleetbase/ember-ui": "^0.2.10",
"@fleetbase/storefront-engine": "^0.3.0",
"@fleetbase/fleetops-engine": "^0.4.10",
"@fleetbase/fleetops-data": "^0.1.8",
"@fleetbase/dev-engine": "^0.2.1",
"@fleetbase/iam-engine": "^0.0.9",
"@fleetbase/ember-core": "^0.2.8",
"@fleetbase/ember-ui": "^0.2.12",
"@fleetbase/fleetops-engine": "^0.4.24",
"@fleetbase/fleetops-data": "^0.1.14",
"@fleetbase/storefront-engine": "^0.3.7",
"@fleetbase/dev-engine": "^0.2.2",
"@fleetbase/iam-engine": "^0.0.10",
"@fleetbase/leaflet-routing-machine": "^3.2.16",
"@ember/legacy-built-in-components": "^0.4.1",
"@fortawesome/ember-fontawesome": "^0.4.1",
@@ -141,9 +142,9 @@
},
"pnpm": {
"overrides": {
"@fleetbase/ember-core": "^0.2.4",
"@fleetbase/ember-ui": "^0.2.10",
"@fleetbase/fleetops-data": "^0.1.8"
"@fleetbase/ember-core": "^0.2.8",
"@fleetbase/ember-ui": "^0.2.12",
"@fleetbase/fleetops-data": "^0.1.14"
}
},
"prettier": {

4388
console/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -54,6 +54,9 @@ Router.map(function () {
this.route('users', { path: '/:public_id/users' });
});
});
this.route('schedule-monitor', function () {
this.route('logs', { path: '/:id/logs' });
});
});
});
this.route('install');

View File

@@ -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 | console-wormhole', 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`<ConsoleWormhole />`);
assert.dom().hasText('');
// Template block usage:
await render(hbs`
<ConsoleWormhole>
template block text
</ConsoleWormhole>
`);
assert.dom().hasText('template block text');
});
});

View File

@@ -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 | modals/validate-password', 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`<Modals::ValidatePassword />`);
assert.dom().hasText('');
// Template block usage:
await render(hbs`
<Modals::ValidatePassword>
template block text
</Modals::ValidatePassword>
`);
assert.dom().hasText('template block text');
});
});

View File

@@ -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);
});
});

View 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);
});
});

View 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);
});
});

View File

@@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Route | console/admin/schedule-monitor', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup('route:console/admin/schedule-monitor');
assert.ok(route);
});
});

View File

@@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Route | console/admin/schedule-monitor/logs', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup('route:console/admin/schedule-monitor/logs');
assert.ok(route);
});
});

View File

@@ -0,0 +1,24 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Serializer | company', function (hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function (assert) {
let store = this.owner.lookup('service:store');
let serializer = store.serializerFor('company');
assert.ok(serializer);
});
test('it serializes records', function (assert) {
let store = this.owner.lookup('service:store');
let record = store.createRecord('company', {});
let serializedRecord = record.serialize();
assert.ok(serializedRecord);
});
});

View File

@@ -16,6 +16,7 @@ common:
confirm: Confirm
edit: Edit
save: Save
save-changes: Save Changes
cancel: Cancel
2fa-config: 2FA Config
account: Account
@@ -59,6 +60,11 @@ common:
users: Users
changelog: Changelog
ok: OK
select-file: Select File
back: Back
next: Next
continue: Continue
done: Done
component:
file:
dropdown-label: File actions
@@ -71,7 +77,9 @@ component:
upload-images-videos: Upload Images & Videos
upload-documents: Upload Documents
upload-documents-files: Upload Documents & Files
upload-avatar-files: Upload Custom Avatars
dropzone-supported-images-videos: Drag and drop image and video files onto this dropzone
dropzone-supported-avatars: Drag and drop SVG or PNG files
dropzone-supported-files: Drag and drop files onto this dropzone
or-select-button-text: or select files to upload.
upload-queue: Upload Queue
@@ -171,6 +179,7 @@ auth:
slow-connection-message: Experiencing connectivity issues.
reset-password:
success-message: Your password has been reset! Login to continue.
invalid-verification-code: This reset password link is invalid or expired.
title: Reset your password
form:
code:
@@ -200,6 +209,22 @@ console:
phone: Your phone number.
photos: photos
admin:
schedule-monitor:
schedule-monitor: Schedule Monitor
task-logs-for: >-
Task Logs For:
showing-last-count: Showing last {count} logs
name: Name
type: Type
timezone: Timezone
last-started: Last Started
last-finished: Last Finished
last-failure: Last Failure
date: Date
memory: Memory
runtime: Runtime
output: Output
no-output: No output
config:
database:
title: Database Configuration
@@ -232,6 +257,10 @@ console:
organizations:
index:
title: Organizations
owner-name-column: Owner
owner-phone-column: Owner Phone
owner-email-column: Owner Phone
users-count-column: Users
phone-column: Phone
email-column: Email
users:

View File

@@ -12,7 +12,32 @@ server {
send_timeout 600;
index index.php;
# tweaks
client_max_body_size 600M;
client_body_buffer_size 1m;
client_header_buffer_size 1k;
large_client_header_buffers 4 16k;
keepalive_timeout 2 2;
#gzip tweaks
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1100;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain application/javascript application/json application/x-javascript text/xml text/css;
#open file cache tweaks
open_file_cache max=2000 inactive=20s;
open_file_cache_valid 60s;
open_file_cache_min_uses 5;
open_file_cache_errors off;
location / {
proxy_set_header Host $host;
proxy_pass http://${NGINX_APPLICATION_HOSTNAME}:8000;
}
}

View File

@@ -52,13 +52,13 @@ app.kubernetes.io/instance: {{ .Release.Name }}
{{- define "helm.commonVariables" -}}
- name: CACHE_URL
value: $(REDIS_SERVICE_PORT)/1
value: tcp://redis-service.{{ .Release.Namespace }}.svc.cluster.local/1
- name: CACHE_DRIVER
value: redis
- name: SOCKETCLUSTER_PORT
value: "80"
- name: SOCKETCLUSTER_HOST
value: $(SOCKETCLUSTER_SERVICE_HOST)
value: socketcluster.{{ .Release.Namespace }}.svc.cluster.local
{{- end }}
{{/*
Create the name of the service account to use

View File

@@ -34,7 +34,11 @@ spec:
- name: {{ .Chart.Name }}-httpd
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
{{- if .Values.gcp }}
image: "{{ .Values.image.repository }}/app-httpd:{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- else }}
image: "{{ .Values.image.repository }}:app-httpd-{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- end }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: NGINX_APPLICATION_HOSTNAME
@@ -56,7 +60,11 @@ spec:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
{{- if .Values.gcp }}
image: "{{ .Values.image.repository }}/app:{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- else }}
image: "{{ .Values.image.repository }}:app-{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- end }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
envFrom:
- secretRef:

View File

@@ -35,7 +35,11 @@ spec:
command: ["php", "artisan", "queue:work"]
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
{{- if .Values.gcp }}
image: "{{ .Values.image.repository }}/events:{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- else }}
image: "{{ .Values.image.repository }}:events-{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- end }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
envFrom:
- secretRef:
@@ -93,7 +97,11 @@ spec:
- name: scheduler
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
{{- if .Values.gcp }}
image: "{{ .Values.image.repository }}/scheduler:{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- else }}
image: "{{ .Values.image.repository }}:scheduler-{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- end }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
envFrom:
- secretRef:

View File

@@ -10,7 +10,7 @@ metadata:
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "0"
"helm.sh/hook-weight": "20"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
template:
@@ -28,8 +28,12 @@ spec:
- name: deployment-job
securityContext:
{{- toYaml .Values.securityContext | nindent 10 }}
{{- if .Values.gcp }}
image: "{{ .Values.image.repository }}/app:{{ .Values.image.tag | default .Chart.AppVersion }}"
command: ["./deploy.sh"]
{{- else }}
image: "{{ .Values.image.repository }}:app-{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- end }}
args: ["./deploy.sh"]
env:
{{- include "helm.commonVariables" . | nindent 12 }}
envFrom:

View File

@@ -44,7 +44,7 @@ spec:
pathType: ImplementationSpecific
backend:
service:
name: fleetbase-app
name: {{ include "helm.fullname" . }}
port:
number: {{ $svcPort }}
- host: {{ .Values.socketcluster_host }}

View File

@@ -30,4 +30,3 @@ spec:
port: 6379
targetPort: 6379
type: ClusterIP

View File

@@ -32,5 +32,4 @@ spec:
- protocol: TCP
port: 80
targetPort: 8000
type: ClusterIP
type: {{ .Values.service.type }}

47
static-build.Dockerfile Normal file
View File

@@ -0,0 +1,47 @@
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
# # Install packages
# RUN apt-get update && apt-get install -y git bind9-utils mycli nodejs npm \
# && mkdir -p /root/.ssh \
# && ssh-keyscan github.com >> /root/.ssh/known_hosts
# Set some build ENV variables
ENV LOG_CHANNEL=stdout
ENV CACHE_DRIVER=null
ENV BROADCAST_DRIVER=socketcluster
ENV QUEUE_CONNECTION=redis
ENV CADDYFILE_PATH=/fleetbase/Caddyfile
ENV OCTANE_SERVER=frankenphp
# Set environment
ARG ENVIRONMENT=production
ENV APP_ENV=$ENVIRONMENT
# Copy Caddyfile
COPY ./Caddyfile $CADDYFILE_PATH
# Create /fleetbase directory and set correct permissions
RUN mkdir -p /fleetbase && mkdir -p /fleetbase/api && mkdir -p /fleetbase/console
# Set working directory
WORKDIR /fleetbase/api
# Setup api
COPY ./api /fleetbase/api
# Setup console
COPY ./console /fleetbase/console
# Set permissions for deploy script
RUN chmod +x /fleetbase/api/deploy.sh
# Pre-install Composer dependencies
# RUN /bin/sh -c "composer install --no-scripts --optimize-autoloader --no-dev"
# Dump autoload
# RUN /bin/sh -c "composer dumpautoload"
# Build binary
RUN EMBED=/ \
PHP_EXTENSIONS=pdo_mysql,gd,bcmath,redis,intl,zip,gmp,apcu,opcache,memcached,imagick,geos,sockets,pcntl \
./build-static.sh

View File

@@ -62,7 +62,7 @@ deploy:
# build the builder
- docker buildx build -t frontend-${STACK}-${VERSION} --load --target builder -f console/Dockerfile .
- docker run -i --rm --env-file /tmp/dotenv.file -v "$(pwd)/console_dist:/app/dist" frontend-${STACK}-${VERSION} pnpm build
- docker run -i --rm --env-file /tmp/dotenv.file -v "$(pwd)/console_dist:/app/dist" frontend-${STACK}-${VERSION} pnpm build --environment production
- |-
DEPLOY_BUCKET=${PROJECT}-${STACK}
wget -O- https://github.com/bep/s3deploy/releases/download/v2.11.0/s3deploy_2.11.0_linux-amd64.tar.gz | tar xzv -f - s3deploy