Compare commits
109 Commits
v7.2.0
...
fix/audit-
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c2901b537 | |||
| 2cae7ea638 | |||
| 66e98a96cb | |||
| e7b2ead0e2 | |||
| a89657b0b8 | |||
| 4ee15e1b6e | |||
| ed40df6295 | |||
| 6d590c050d | |||
| 47f9a54f97 | |||
| 17b0d72fbf | |||
| 0269c2421d | |||
| 5b260e4915 | |||
| 5290410a17 | |||
| dc9041aaec | |||
| b0dc441d68 | |||
| b0fc3eb5af | |||
| d9980e866d | |||
|
|
52ae3404ee | ||
| eecb4f4f53 | |||
| 744345af81 | |||
|
|
7694d1b0fb | ||
|
|
d8010d4c0c | ||
| a57b49c936 | |||
| a84ba41ea9 | |||
| dc200646f7 | |||
| e273e870ef | |||
|
|
6fc34aca00 | ||
| f97ac70678 | |||
| 6ceb681463 | |||
| 716ee6eba0 | |||
| f6b0f6b0cd | |||
| 737a652ff0 | |||
| 2995e5c9dc | |||
| 338c7a2e41 | |||
| ad27358deb | |||
| 495754816c | |||
|
|
96f2518af9 | ||
|
|
280bdeeb1b | ||
| 46cdeb0bab | |||
| d41f88f8bf | |||
| 815d6e97a8 | |||
| 4e35aefe41 | |||
| ca84915e43 | |||
|
|
31cc7e9e4d | ||
| 4ec107705e | |||
|
|
7740d2ac86 | ||
|
|
8c2aeacc85 | ||
|
|
8b8e8aec15 | ||
|
|
6ac3f660e9 | ||
| 7ee576a9c1 | |||
| f4c8699aaf | |||
|
|
4273ca6e5c | ||
|
|
4ba043b77e | ||
| 0169415ea2 | |||
| 86791dbaca | |||
| d5b58a3cbd | |||
| 3d8281d27e | |||
| b1a014c7bc | |||
| 505d0af2b3 | |||
| ece6bd1d78 | |||
| 8dc18b155a | |||
| aecd597687 | |||
| c99f106bae | |||
| d2fc7ae6fe | |||
|
|
c1c1d0055a | ||
| f37ec82d39 | |||
| 6e9e30e0f0 | |||
| 990ddb5cd3 | |||
| c6ebbb48bb | |||
| 95cc0b1c91 | |||
| 81b282f1f1 | |||
| 8c5b357dd2 | |||
| a13b2cbfd2 | |||
| 19617c2285 | |||
| f9794a973f | |||
| 65efe62d19 | |||
| 683ddcaf53 | |||
|
|
113e0bbc3c | ||
| 2af97e40bf | |||
|
|
83cbe3aece | ||
| ceac1ba614 | |||
|
|
765fdbdf9d | ||
|
|
43ae73c5f3 | ||
|
|
e57a0de8a9 | ||
|
|
3de491105b | ||
|
|
2fe690e962 | ||
|
|
b826d37086 | ||
|
|
bfbfd55fe7 | ||
|
|
15f38efd52 | ||
|
|
5d25681485 | ||
|
|
d26df376f8 | ||
| cff3fb3bad | |||
|
|
fb3c49aa8b | ||
|
|
af1657e226 | ||
|
|
d7c7302c12 | ||
| 26ce95f7c1 | |||
|
|
4924df2ef3 | ||
| 2e141a5d52 | |||
|
|
cb1978bcaf | ||
|
|
387f5122f1 | ||
|
|
db5887de21 | ||
| fe24d9bcbd | |||
|
|
6c6b1cbf46 | ||
|
|
4d65c9c999 | ||
| 4417279275 | |||
|
|
365f12996d | ||
|
|
ef1015f33b | ||
| b43dfb5cf4 | |||
|
|
225e693d1f |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
@@ -2,22 +2,24 @@ name: Build
|
|||||||
run-name: Running Lint Check and Licence checker on Pull Request
|
run-name: Running Lint Check and Licence checker on Pull Request
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: '24.5.0'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Build-and-ng-test:
|
Build-and-ng-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.15.1
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
- name: Install Google Chrome
|
- name: Install Google Chrome
|
||||||
run: |
|
run: |
|
||||||
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
|
|
||||||
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
|
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y google-chrome-stable xvfb
|
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||||
|
apt install -y ./google-chrome*.deb
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: echo "$NPMRC" > client/.npmrc
|
run: echo "$NPMRC" > client/.npmrc
|
||||||
@@ -25,6 +27,15 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NPMRC: ${{ secrets.NPMRC}}
|
NPMRC: ${{ secrets.NPMRC}}
|
||||||
|
|
||||||
|
- name: Check audit
|
||||||
|
# Audit should fail and stop the CI if critical vulnerability found
|
||||||
|
run: |
|
||||||
|
npm audit --omit=dev
|
||||||
|
cd ./sas
|
||||||
|
npm audit --omit=dev
|
||||||
|
cd ../client
|
||||||
|
npm audit --audit-level=critical --omit=dev
|
||||||
|
|
||||||
- name: Lint check
|
- name: Lint check
|
||||||
run: npm run lint:check
|
run: npm run lint:check
|
||||||
|
|
||||||
@@ -52,26 +63,27 @@ jobs:
|
|||||||
|
|
||||||
Build-and-test-development:
|
Build-and-test-development:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: Build-production-and-ng-test
|
needs: Build-and-ng-test
|
||||||
|
env:
|
||||||
|
CHROME_BIN: /usr/bin/google-chrome
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.15.1
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
touch client/.npmrc
|
touch client/.npmrc
|
||||||
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
||||||
|
|
||||||
- run: apt-get update
|
- name: Install system dependencies
|
||||||
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
run: |
|
||||||
- run: apt install -y ./google-chrome*.deb;
|
apt-get update
|
||||||
- run: export CHROME_BIN=/usr/bin/google-chrome
|
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||||
- run: apt-get update -y
|
apt install -y ./google-chrome*.deb
|
||||||
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb
|
apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb jq zip
|
||||||
- run: apt -y install jq
|
|
||||||
|
|
||||||
- name: Write cypress credentials
|
- name: Write cypress credentials
|
||||||
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
||||||
@@ -86,17 +98,18 @@ jobs:
|
|||||||
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
|
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
|
||||||
npm ci
|
npm ci
|
||||||
|
|
||||||
# Install pm2 and prepare SASJS server
|
- name: Setup and start SASjs server
|
||||||
- run: npm i -g pm2
|
run: |
|
||||||
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
npm i -g pm2
|
||||||
- run: unzip linux.zip
|
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||||
- run: touch .env
|
unzip linux.zip
|
||||||
- run: echo RUN_TIMES=js >> .env
|
touch .env
|
||||||
- run: echo NODE_PATH=node >> .env
|
echo RUN_TIMES=js >> .env
|
||||||
- run: echo CORS=enable >> .env
|
echo NODE_PATH=node >> .env
|
||||||
- run: echo WHITELIST=http://localhost:4200 >> .env
|
echo CORS=enable >> .env
|
||||||
- run: cat .env
|
echo WHITELIST=http://localhost:4200 >> .env
|
||||||
- run: pm2 start api-linux --wait-ready
|
cat .env
|
||||||
|
pm2 start api-linux --wait-ready
|
||||||
|
|
||||||
- name: Deploy mocked services
|
- name: Deploy mocked services
|
||||||
run: |
|
run: |
|
||||||
@@ -106,11 +119,6 @@ jobs:
|
|||||||
sasjs cbd -t server-ci
|
sasjs cbd -t server-ci
|
||||||
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
|
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
|
||||||
|
|
||||||
- name: Install ZIP
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install zip
|
|
||||||
|
|
||||||
- name: Prepare and run frontend and cypress
|
- name: Prepare and run frontend and cypress
|
||||||
run: |
|
run: |
|
||||||
cd ./client
|
cd ./client
|
||||||
@@ -126,7 +134,7 @@ jobs:
|
|||||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
||||||
cat ./cypress.config.ts
|
cat ./cypress.config.ts
|
||||||
# Start frontend and run cypress
|
# Start frontend and run cypress
|
||||||
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
|
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
|
||||||
|
|
||||||
- name: Zip Cypress videos
|
- name: Zip Cypress videos
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
@@ -2,38 +2,31 @@ name: Lighthouse Checks
|
|||||||
run-name: Running Lighthouse Performance and Accessibility Checks on Pull Request
|
run-name: Running Lighthouse Performance and Accessibility Checks on Pull Request
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: '24.5.0'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lighthouse:
|
lighthouse:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [20.15.1]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
- name: Install Google Chrome
|
- name: Install Google Chrome
|
||||||
run: |
|
run: |
|
||||||
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
|
|
||||||
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
|
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y google-chrome-stable xvfb
|
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||||
|
apt install -y ./google-chrome*.deb
|
||||||
|
|
||||||
- name: Install pm2 for process management
|
- name: Install global packages
|
||||||
run: npm i -g pm2
|
run: npm i -g pm2 @sasjs/cli wait-on
|
||||||
|
|
||||||
- name: Install @sasjs/cli
|
- name: Setup and start SASjs server
|
||||||
run: npm i -g @sasjs/cli
|
|
||||||
|
|
||||||
- name: Install wait-on globally
|
|
||||||
run: npm install -g wait-on
|
|
||||||
|
|
||||||
- name: Create .env file for sasjs/server
|
|
||||||
run: |
|
run: |
|
||||||
touch .env
|
touch .env
|
||||||
echo RUN_TIMES=js >> .env
|
echo RUN_TIMES=js >> .env
|
||||||
@@ -41,15 +34,9 @@ jobs:
|
|||||||
echo CORS=enable >> .env
|
echo CORS=enable >> .env
|
||||||
echo WHITELIST=http://localhost:4200 >> .env
|
echo WHITELIST=http://localhost:4200 >> .env
|
||||||
cat .env
|
cat .env
|
||||||
|
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||||
- name: Download sasjs/server package from github using curl
|
unzip linux.zip
|
||||||
run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
pm2 start api-linux --wait-ready
|
||||||
|
|
||||||
- name: Unzip downloaded package
|
|
||||||
run: unzip linux.zip
|
|
||||||
|
|
||||||
- name: Run sasjs server
|
|
||||||
run: pm2 start api-linux --wait-ready
|
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: echo "$NPMRC" > client/.npmrc
|
run: echo "$NPMRC" > client/.npmrc
|
||||||
@@ -98,4 +85,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: Lighthouse results
|
name: Lighthouse results
|
||||||
path: client/lighthouse-reports
|
path: client/lighthouse-reports
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
|
|||||||
@@ -5,27 +5,31 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: '24.5.0'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Build-production-and-ng-test:
|
Build-production-and-ng-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
CHROME_BIN: /usr/bin/google-chrome
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.14.0
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
touch client/.npmrc
|
touch client/.npmrc
|
||||||
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
||||||
|
|
||||||
- name: Install Chrome for Angular tests
|
- name: Install Chrome for Angular tests
|
||||||
run: |
|
run: |
|
||||||
apt-get update
|
apt-get update
|
||||||
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||||
apt install -y ./google-chrome*.deb;
|
apt install -y ./google-chrome*.deb
|
||||||
export CHROME_BIN=/usr/bin/google-chrome
|
|
||||||
|
|
||||||
- name: Write cypress credentials
|
- name: Write cypress credentials
|
||||||
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
||||||
@@ -41,11 +45,11 @@ jobs:
|
|||||||
npm ci
|
npm ci
|
||||||
|
|
||||||
- name: Check audit
|
- name: Check audit
|
||||||
# Audit should fail and stop the CI if critical vulnerability found
|
# Audit should fail and stop the CI if critical vulnerability found
|
||||||
run: |
|
run: |
|
||||||
npm audit --audit-level=critical --omit=dev
|
npm audit --omit=dev
|
||||||
cd ./sas
|
cd ./sas
|
||||||
npm audit --audit-level=critical --omit=dev
|
npm audit --omit=dev
|
||||||
cd ../client
|
cd ../client
|
||||||
npm audit --audit-level=critical --omit=dev
|
npm audit --audit-level=critical --omit=dev
|
||||||
|
|
||||||
@@ -63,25 +67,26 @@ jobs:
|
|||||||
Build-and-test-development:
|
Build-and-test-development:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: Build-production-and-ng-test
|
needs: Build-production-and-ng-test
|
||||||
|
env:
|
||||||
|
CHROME_BIN: /usr/bin/google-chrome
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.14.0
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
touch client/.npmrc
|
touch client/.npmrc
|
||||||
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
||||||
|
|
||||||
- run: apt-get update
|
- name: Install system dependencies
|
||||||
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
run: |
|
||||||
- run: apt install -y ./google-chrome*.deb;
|
apt-get update
|
||||||
- run: export CHROME_BIN=/usr/bin/google-chrome
|
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||||
- run: apt-get update -y
|
apt install -y ./google-chrome*.deb
|
||||||
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb
|
apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb jq zip
|
||||||
- run: apt -y install jq
|
|
||||||
|
|
||||||
- name: Write cypress credentials
|
- name: Write cypress credentials
|
||||||
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
||||||
@@ -96,17 +101,18 @@ jobs:
|
|||||||
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
|
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
|
||||||
npm ci
|
npm ci
|
||||||
|
|
||||||
# Install pm2 and prepare SASJS server
|
- name: Setup and start SASjs server
|
||||||
- run: npm i -g pm2
|
run: |
|
||||||
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
npm i -g pm2
|
||||||
- run: unzip linux.zip
|
curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||||
- run: touch .env
|
unzip linux.zip
|
||||||
- run: echo RUN_TIMES=js >> .env
|
touch .env
|
||||||
- run: echo NODE_PATH=node >> .env
|
echo RUN_TIMES=js >> .env
|
||||||
- run: echo CORS=enable >> .env
|
echo NODE_PATH=node >> .env
|
||||||
- run: echo WHITELIST=http://localhost:4200 >> .env
|
echo CORS=enable >> .env
|
||||||
- run: cat .env
|
echo WHITELIST=http://localhost:4200 >> .env
|
||||||
- run: pm2 start api-linux --wait-ready
|
cat .env
|
||||||
|
pm2 start api-linux --wait-ready
|
||||||
|
|
||||||
- name: Deploy mocked services
|
- name: Deploy mocked services
|
||||||
run: |
|
run: |
|
||||||
@@ -116,11 +122,6 @@ jobs:
|
|||||||
sasjs cbd -t server-ci
|
sasjs cbd -t server-ci
|
||||||
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
|
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
|
||||||
|
|
||||||
- name: Install ZIP
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install zip
|
|
||||||
|
|
||||||
- name: Prepare and run frontend and cypress
|
- name: Prepare and run frontend and cypress
|
||||||
run: |
|
run: |
|
||||||
cd ./client
|
cd ./client
|
||||||
@@ -136,7 +137,7 @@ jobs:
|
|||||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
||||||
cat ./cypress.config.ts
|
cat ./cypress.config.ts
|
||||||
# Start frontend and run cypress
|
# Start frontend and run cypress
|
||||||
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
|
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
|
||||||
|
|
||||||
- name: Zip Cypress videos
|
- name: Zip Cypress videos
|
||||||
if: always()
|
if: always()
|
||||||
@@ -155,10 +156,10 @@ jobs:
|
|||||||
needs: [Build-production-and-ng-test, Build-and-test-development]
|
needs: [Build-production-and-ng-test, Build-and-test-development]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.14.0
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
@@ -168,17 +169,11 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NPMRC: ${{ secrets.NPMRC}}
|
NPMRC: ${{ secrets.NPMRC}}
|
||||||
|
|
||||||
- name: Install packages
|
- name: Install system packages
|
||||||
run: |
|
run: |
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install zip -y
|
apt-get install -y zip jq doxygen
|
||||||
# sasjs cli is used to compile & build the SAS services
|
|
||||||
npm i -g @sasjs/cli
|
npm i -g @sasjs/cli
|
||||||
# jq is used to parse the release JSON
|
|
||||||
apt-get install jq -y
|
|
||||||
# doxygen is used for the SASJS docs
|
|
||||||
apt-get update
|
|
||||||
apt-get install doxygen -y
|
|
||||||
|
|
||||||
- name: Frontend Preliminary Build
|
- name: Frontend Preliminary Build
|
||||||
description: We want to prevent creating empty release if frontend fails
|
description: We want to prevent creating empty release if frontend fails
|
||||||
@@ -228,6 +223,8 @@ jobs:
|
|||||||
cp sasjs/utils/favicon.ico ../client/dist/favicon.ico
|
cp sasjs/utils/favicon.ico ../client/dist/favicon.ico
|
||||||
sasjs c -t server
|
sasjs c -t server
|
||||||
rm -rf sasjsbuild/tests
|
rm -rf sasjsbuild/tests
|
||||||
|
server_apploc="/Public/app/dc"
|
||||||
|
sed -i "s|apploc=\"[^\"]*\"|apploc=\"${server_apploc}\"|g" sasjsbuild/services/web/index.html
|
||||||
sasjs b -t server
|
sasjs b -t server
|
||||||
cp sasjsbuild/server.json.zip ./sasjs_server.json.zip
|
cp sasjsbuild/server.json.zip ./sasjs_server.json.zip
|
||||||
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,6 +14,7 @@ client/documentation
|
|||||||
client/**/sheet-crypto.tgz
|
client/**/sheet-crypto.tgz
|
||||||
client/.nx
|
client/.nx
|
||||||
client/libraries/sheet-crypto.tgz
|
client/libraries/sheet-crypto.tgz
|
||||||
|
client/lighthouse-reports
|
||||||
cypress.env.json
|
cypress.env.json
|
||||||
sasjsbuild
|
sasjsbuild
|
||||||
sasjsresults
|
sasjsresults
|
||||||
|
|||||||
108
CHANGELOG.md
108
CHANGELOG.md
@@ -1,3 +1,111 @@
|
|||||||
|
## [7.4.1](https://git.datacontroller.io/dc/dc/compare/v7.4.0...v7.4.1) (2026-03-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* support for SASIOSNF engine (SNOW alias) plus meta assignment ([7694d1b](https://git.datacontroller.io/dc/dc/commit/7694d1b0fb2bd0407c8598147fbae87a00d889a8))
|
||||||
|
|
||||||
|
# [7.4.0](https://git.datacontroller.io/dc/dc/compare/v7.3.0...v7.4.0) (2026-02-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* cli bump for mf_getscheme support ([a84ba41](https://git.datacontroller.io/dc/dc/commit/a84ba41ea9f0c97ae24f0a572b8cf5ec200f2132))
|
||||||
|
* missing upcase on SNOW section, plus local sasjs target ([dc20064](https://git.datacontroller.io/dc/dc/commit/dc200646f7df2fd1910841f392c314532aae7581))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* SAS code changes for snowflake support ([e273e87](https://git.datacontroller.io/dc/dc/commit/e273e870efbf7875db869b760f2c7b1f39d571ae))
|
||||||
|
|
||||||
|
# [7.3.0](https://git.datacontroller.io/dc/dc/compare/v7.2.8...v7.3.0) (2026-02-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bump xlsx, add crypto-shim ([8dc18b1](https://git.datacontroller.io/dc/dc/commit/8dc18b155abfc20fd0b043e0d70bbbc17e6b6811))
|
||||||
|
* correctly applying deletes on viya, also ([46cdeb0](https://git.datacontroller.io/dc/dc/commit/46cdeb0babee6870553a41877cbe85204e7099c4))
|
||||||
|
* crypto module requirement for sheetjs/crypto package ([505d0af](https://git.datacontroller.io/dc/dc/commit/505d0af2b3b3c1c79c65045dcaffc263e0f8e796))
|
||||||
|
* disable parsing excel in web worker beacuse it breaks in the stream apps ([280bdee](https://git.datacontroller.io/dc/dc/commit/280bdeeb1b82f00689f46c68a3cde3f2d24bc18f))
|
||||||
|
* Display all contexts when installing DC on Viya ([d41f88f](https://git.datacontroller.io/dc/dc/commit/d41f88f8bf5bb2c725ee3edba085e8961ab8c727))
|
||||||
|
* **edit:** use cellValidation keys and hotDataSchema to fill in defaults on add row ([4957548](https://git.datacontroller.io/dc/dc/commit/495754816c0e757b8f8b1c0ad51246dc7b65d957))
|
||||||
|
* enabling closeouts for UPDATE in CAS tables ([8b8e8ae](https://git.datacontroller.io/dc/dc/commit/8b8e8aec159ff2f50cfa4683bcd7a25aabb75bf8))
|
||||||
|
* enabling rollback when the table has formatted values ([815d6e9](https://git.datacontroller.io/dc/dc/commit/815d6e97a8e304d79d48cc949ba126e02a318dc1))
|
||||||
|
* improvements to validations ([6ceb681](https://git.datacontroller.io/dc/dc/commit/6ceb6814633691b6d4ac2cb898cfb75e9d609102))
|
||||||
|
* remove IE checks and conditions ([ece6bd1](https://git.datacontroller.io/dc/dc/commit/ece6bd1d787d722531334fc4f1396a94cf6d92ec))
|
||||||
|
* updates to demodata to enable auto CAS promote ([7740d2a](https://git.datacontroller.io/dc/dc/commit/7740d2ac8694295b33b40a30603d8239818896f5))
|
||||||
|
* upgrade angular core and compiler ([aecd597](https://git.datacontroller.io/dc/dc/commit/aecd5976875a7c01189248c5f5aa3478b28c1ab2))
|
||||||
|
* using fcopy instead of binary copy for file upload, for Viya 2026 compatibility ([716ee6e](https://git.datacontroller.io/dc/dc/commit/716ee6eba0a28f4f0a7a96b0719caf15da9b6e78))
|
||||||
|
* **viewer:** search causing blank Handsontable ([338c7a2](https://git.datacontroller.io/dc/dc/commit/338c7a2e418c47e34331bd04718cd816f978837c)), closes [#206](https://git.datacontroller.io/dc/dc/issues/206)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* adding demo data job ([8c2aeac](https://git.datacontroller.io/dc/dc/commit/8c2aeacc85da5c106c356709cefcb412ed0a71db))
|
||||||
|
* **dq rules:** notnull validation when invalid cell, will auto populate a default value ([96f2518](https://git.datacontroller.io/dc/dc/commit/96f2518af9e547956be5862a1322d9ab8e07369b))
|
||||||
|
|
||||||
|
## [7.2.8](https://git.datacontroller.io/dc/dc/compare/v7.2.7...v7.2.8) (2026-02-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bump adapter version ([f4c8699](https://git.datacontroller.io/dc/dc/commit/f4c8699aaf0b1e01b447296978a4f6dedc8903f9))
|
||||||
|
|
||||||
|
## [7.2.7](https://git.datacontroller.io/dc/dc/compare/v7.2.6...v7.2.7) (2026-02-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* dclib not found error in getchangeinfo job ([86791db](https://git.datacontroller.io/dc/dc/commit/86791dbaca39034a19bf8f34efbddf898c57f2f7))
|
||||||
|
|
||||||
|
## [7.2.6](https://git.datacontroller.io/dc/dc/compare/v7.2.5...v7.2.6) (2026-01-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** update angular and moment ([8c5b357](https://git.datacontroller.io/dc/dc/commit/8c5b357dd286db331a6dcdeb3fd499fe3b634288))
|
||||||
|
|
||||||
|
## [7.2.5](https://git.datacontroller.io/dc/dc/compare/v7.2.4...v7.2.5) (2025-12-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* (build) rebuilt package-lock files ([bfbfd55](https://git.datacontroller.io/dc/dc/commit/bfbfd55fe7e2dff3ce707763a2c7939ff365318b))
|
||||||
|
* (deps) bump @sasjs/cli and @sasjs/core ([d7c7302](https://git.datacontroller.io/dc/dc/commit/d7c7302c12ac60f355ab9b3b1b461fcf7d0719b8))
|
||||||
|
* (deps) bumped @sasjs/core, @sasjs/cli, @sasjs/utils and @sasjs/adapter ([af1657e](https://git.datacontroller.io/dc/dc/commit/af1657e226a4efd22cc87401a3850c4a665c2680))
|
||||||
|
* configurable audit table on restore check ([26ce95f](https://git.datacontroller.io/dc/dc/commit/26ce95f7c1d2260f81c240cd6b058db154d997e4)), closes [#193](https://git.datacontroller.io/dc/dc/issues/193)
|
||||||
|
* improved testing ([fb3c49a](https://git.datacontroller.io/dc/dc/commit/fb3c49aa8bfdc6acf2ae3034b885010dcdce32a6))
|
||||||
|
* output values to intended macro variables ([43ae73c](https://git.datacontroller.io/dc/dc/commit/43ae73c5f3ad919394201f54984b61bb2a52fcfe))
|
||||||
|
|
||||||
|
## [7.2.4](https://git.datacontroller.io/dc/dc/compare/v7.2.3...v7.2.4) (2025-10-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* ensure reload after applying licence key ([cb1978b](https://git.datacontroller.io/dc/dc/commit/cb1978bcaf23b0bf45b5d3b78b9707fd4e48a5f4))
|
||||||
|
* snyk report security patches ([387f512](https://git.datacontroller.io/dc/dc/commit/387f5122f1ea6dff55d23c9223f17737283a94d3))
|
||||||
|
|
||||||
|
## [7.2.3](https://git.datacontroller.io/dc/dc/compare/v7.2.2...v7.2.3) (2025-10-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* opening second table in viewer throws an error ([6c6b1cb](https://git.datacontroller.io/dc/dc/commit/6c6b1cbf460e5291ec746af017e764b894fff8d5))
|
||||||
|
|
||||||
|
## [7.2.2](https://git.datacontroller.io/dc/dc/compare/v7.2.1...v7.2.2) (2025-09-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* jsrsasign, @sasjs/cli bump ([365f129](https://git.datacontroller.io/dc/dc/commit/365f12996db3ef50a4f4f099d5af15696c43bb42))
|
||||||
|
|
||||||
|
## [7.2.1](https://git.datacontroller.io/dc/dc/compare/v7.2.0...v7.2.1) (2025-08-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* removing localhost from index.html ([225e693](https://git.datacontroller.io/dc/dc/commit/225e693d1fd4381f2b8ce42fecb508f0a9e9dad8))
|
||||||
|
|
||||||
# [7.2.0](https://git.datacontroller.io/dc/dc/compare/v7.1.1...v7.2.0) (2025-08-08)
|
# [7.2.0](https://git.datacontroller.io/dc/dc/compare/v7.1.1...v7.2.0) (2025-08-08)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,8 @@
|
|||||||
"zone.js",
|
"zone.js",
|
||||||
"text-encoding",
|
"text-encoding",
|
||||||
"crypto-js/md5",
|
"crypto-js/md5",
|
||||||
|
"crypto-js/sha1",
|
||||||
|
"crypto-js/sha512",
|
||||||
"buffer",
|
"buffer",
|
||||||
"numbro",
|
"numbro",
|
||||||
"@clr/icons",
|
"@clr/icons",
|
||||||
@@ -51,26 +53,22 @@
|
|||||||
"base64-arraybuffer",
|
"base64-arraybuffer",
|
||||||
"@handsontable/formulajs"
|
"@handsontable/formulajs"
|
||||||
],
|
],
|
||||||
"polyfills": [
|
"polyfills": ["src/polyfills.ts", "zone.js"],
|
||||||
"src/polyfills.ts",
|
|
||||||
"zone.js"
|
|
||||||
],
|
|
||||||
"outputPath": "dist",
|
"outputPath": "dist",
|
||||||
"resourcesOutputPath": "images",
|
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"main": "src/main.ts",
|
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/images"
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "src/images",
|
||||||
|
"output": "images"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": ["src/styles.scss"],
|
||||||
"src/styles.scss"
|
"scripts": ["node_modules/marked/marked.min.js"],
|
||||||
],
|
"webWorkerTsConfig": "tsconfig.worker.json",
|
||||||
"scripts": [
|
"main": "src/main.ts"
|
||||||
"node_modules/marked/marked.min.js"
|
|
||||||
],
|
|
||||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -103,9 +101,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"vendorChunk": true,
|
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"buildOptimizer": false,
|
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
"namedChunks": true
|
"namedChunks": true
|
||||||
@@ -134,20 +130,11 @@
|
|||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
"options": {
|
"options": {
|
||||||
"polyfills": [
|
"polyfills": ["src/polyfills.ts", "zone.js", "zone.js/testing"],
|
||||||
"src/polyfills.ts",
|
|
||||||
"zone.js",
|
|
||||||
"zone.js/testing"
|
|
||||||
],
|
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": ["src/favicon.ico", "src/assets"],
|
||||||
"src/favicon.ico",
|
"styles": ["src/styles.scss"],
|
||||||
"src/assets"
|
|
||||||
],
|
|
||||||
"styles": [
|
|
||||||
"src/styles.scss"
|
|
||||||
],
|
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"karmaConfig": "karma.conf.js",
|
"karmaConfig": "karma.conf.js",
|
||||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||||
@@ -156,10 +143,7 @@
|
|||||||
"lint": {
|
"lint": {
|
||||||
"builder": "@angular-eslint/builder:lint",
|
"builder": "@angular-eslint/builder:lint",
|
||||||
"options": {
|
"options": {
|
||||||
"lintFilePatterns": [
|
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
|
||||||
"src/**/*.ts",
|
|
||||||
"src/**/*.html"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
95
client/cypress/e2e/csv-limited.cy.ts
Normal file
95
client/cypress/e2e/csv-limited.cy.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
const username = Cypress.env('username')
|
||||||
|
const password = Cypress.env('password')
|
||||||
|
const hostUrl = Cypress.env('hosturl')
|
||||||
|
const appLocation = Cypress.env('appLocation')
|
||||||
|
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||||
|
const serverType = Cypress.env('serverType')
|
||||||
|
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||||
|
const fixturePath = 'csvs/'
|
||||||
|
|
||||||
|
context('csv file upload restriction (free tier): ', function () {
|
||||||
|
this.beforeEach(() => {
|
||||||
|
cy.visit(hostUrl + appLocation)
|
||||||
|
|
||||||
|
cy.get('body').then(($body) => {
|
||||||
|
const usernameInput = $body.find('input.username')[0]
|
||||||
|
|
||||||
|
if (usernameInput && !Cypress.dom.isHidden(usernameInput)) {
|
||||||
|
cy.get('input.username').type(username)
|
||||||
|
cy.get('input.password').type(password)
|
||||||
|
cy.get('.login-group button').click()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('.app-loading', { timeout: longerCommandTimeout }).should(
|
||||||
|
'not.exist'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Skip licensing page if presented - continue with free tier
|
||||||
|
cy.url().then((url) => {
|
||||||
|
if (url.includes('licensing')) {
|
||||||
|
cy.get('button').contains('Continue with free tier').click()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
visitPage('home')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('1 | File upload is restricted on free tier', () => {
|
||||||
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||||
|
|
||||||
|
// Click upload button - should show feature locked modal
|
||||||
|
cy.get('.buttonBar button:last-child').should('exist').click()
|
||||||
|
|
||||||
|
cy.get('.modal-title').should('contain', 'Locked Feature (File Upload)')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
||||||
|
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||||
|
.should('not.exist')
|
||||||
|
.then(() => {
|
||||||
|
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||||
|
timeout: longerCommandTimeout
|
||||||
|
}).then((treeNodes: any) => {
|
||||||
|
let targetLib
|
||||||
|
|
||||||
|
for (let node of treeNodes) {
|
||||||
|
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
||||||
|
targetLib = node
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.get(targetLib).within(() => {
|
||||||
|
cy.get('.clr-tree-node-content-container > button').click()
|
||||||
|
|
||||||
|
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||||
|
for (let innerNode of innerNodes) {
|
||||||
|
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||||
|
innerNode.click()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachFile = (filename: string, callback?: any) => {
|
||||||
|
cy.get('.buttonBar button:last-child')
|
||||||
|
.should('exist')
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
cy.get('input[type="file"]#file-upload')
|
||||||
|
.attachFile(`/${fixturePath}/${filename}`)
|
||||||
|
.then(() => {
|
||||||
|
if (callback) callback()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const visitPage = (url: string) => {
|
||||||
|
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||||
|
}
|
||||||
@@ -309,6 +309,83 @@ context('excel tests: ', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('22 | Uploads password protected Excel and unlocks with correct password', (done) => {
|
||||||
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||||
|
|
||||||
|
cy.get('.buttonBar button:last-child')
|
||||||
|
.should('exist')
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
cy.get('input[type="file"]#file-upload')
|
||||||
|
.attachFile(`/${fixturePath}/regular_excel_password.xlsx`)
|
||||||
|
.then(() => {
|
||||||
|
// Wait for password modal to appear
|
||||||
|
cy.get('#filePasswordInput', { timeout: 10000 })
|
||||||
|
.should('be.visible')
|
||||||
|
.type('123123')
|
||||||
|
|
||||||
|
// Click Unlock button
|
||||||
|
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
|
||||||
|
|
||||||
|
// Click away the overlay
|
||||||
|
cy.get('.modal-footer .btn.btn-primary', { timeout: 5000 }).click()
|
||||||
|
|
||||||
|
// Verify file loads successfully
|
||||||
|
cy.get('.btn-upload-preview', { timeout: 60000 })
|
||||||
|
.should('be.visible')
|
||||||
|
.then(() => {
|
||||||
|
submitExcel()
|
||||||
|
rejectExcel(done)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('23 | Uploads password protected Excel and handles wrong password', (done) => {
|
||||||
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||||
|
|
||||||
|
cy.get('.buttonBar button:last-child')
|
||||||
|
.should('exist')
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
cy.get('input[type="file"]#file-upload')
|
||||||
|
.attachFile(`/${fixturePath}/regular_excel_password.xlsx`)
|
||||||
|
.then(() => {
|
||||||
|
// First attempt: Enter wrong password
|
||||||
|
cy.get('#filePasswordInput', { timeout: 10000 })
|
||||||
|
.should('be.visible')
|
||||||
|
.type('wrongpassword')
|
||||||
|
|
||||||
|
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
|
||||||
|
|
||||||
|
// Verify error message appears
|
||||||
|
cy.get('.modal-footer .color-red', { timeout: 10000 })
|
||||||
|
.should('be.visible')
|
||||||
|
.should('contain', "Sorry that didn't work, try again.")
|
||||||
|
|
||||||
|
// Modal should still be open for retry
|
||||||
|
cy.get('#filePasswordInput')
|
||||||
|
.should('be.visible')
|
||||||
|
.clear()
|
||||||
|
.type('123123')
|
||||||
|
|
||||||
|
// Second attempt: Enter correct password
|
||||||
|
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
|
||||||
|
|
||||||
|
// Click away the overlay
|
||||||
|
cy.get('.modal-footer .btn.btn-primary', { timeout: 5000 }).click()
|
||||||
|
|
||||||
|
// Verify file loads successfully
|
||||||
|
cy.get('.btn-upload-preview', { timeout: 60000 })
|
||||||
|
.should('be.visible')
|
||||||
|
.then(() => {
|
||||||
|
submitExcel()
|
||||||
|
rejectExcel(done)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Large files break Cypress
|
// Large files break Cypress
|
||||||
|
|
||||||
// it ('? | Uploads Excel with size of 5MB', (done) => {
|
// it ('? | Uploads Excel with size of 5MB', (done) => {
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_
|
|||||||
2,even more dummy data,Option 3,42,12FEB1960,01JAN1960:00:00:42,0:02:22,3,44
|
2,even more dummy data,Option 3,42,12FEB1960,01JAN1960:00:00:42,0:02:22,3,44
|
||||||
3,"It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:",Option 2,1613.001,27FEB1961,01JAN1960:00:07:03,0:00:44,3,44
|
3,"It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told: It was a dark and stormy night. The wind was blowing a gale! The captain said to his mate - mate, tell us a tale. And this, is the tale he told:",Option 2,1613.001,27FEB1961,01JAN1960:00:07:03,0:00:44,3,44
|
||||||
4,if you can fill the unforgiving minute,Option 1,1613.0011235,02AUG1971,29MAY1973:06:12:03,0:06:52,3,44
|
4,if you can fill the unforgiving minute,Option 1,1613.0011235,02AUG1971,29MAY1973:06:12:03,0:06:52,3,44
|
||||||
1010,10 bottles of beer on the wall,Option 1,0.9153696885,04MAR1962,01JAN1960:12:47:55,0:01:40,92,76
|
1010,"10 bottles of beer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
on the wall",Option 1,0.9153696885,04MAR1962,01JAN1960:12:47:55,0:01:40,92,76
|
||||||
1011,11 bottles of beer on the wall,Option 1,0.3531217558,29MAR1960,01JAN1960:03:33:24,0:01:03,80,29
|
1011,11 bottles of beer on the wall,Option 1,0.3531217558,29MAR1960,01JAN1960:03:33:24,0:01:03,80,29
|
||||||
1012,12 bottles of beer on the wall,Option 1,0.6743748717,02AUG1962,01JAN1960:07:25:59,0:00:10,16,98
|
1012,12 bottles of beer on the wall,Option 1,0.6743748717,02AUG1962,01JAN1960:07:25:59,0:00:10,16,98
|
||||||
1013,13 bottles of beer on the wall,Option 1,0.1305445992,11SEP1960,01JAN1960:13:51:32,0:00:35,73,15
|
1013,13 bottles of beer on the wall,Option 1,0.1305445992,11SEP1960,01JAN1960:13:51:32,0:00:35,73,15
|
||||||
|
|||||||
|
BIN
client/cypress/fixtures/excels/regular_excel_password.xlsx
Normal file
BIN
client/cypress/fixtures/excels/regular_excel_password.xlsx
Normal file
Binary file not shown.
BIN
client/libraries/xlsx-0.20.3.tgz
Normal file
BIN
client/libraries/xlsx-0.20.3.tgz
Normal file
Binary file not shown.
@@ -10,7 +10,7 @@ const check = (cwd) => {
|
|||||||
onlyAllow:
|
onlyAllow:
|
||||||
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
|
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
|
||||||
excludePackages:
|
excludePackages:
|
||||||
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;handsontable@16.0.1;hyperformula@2.7.1;hyperformula@3.0.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1'
|
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;handsontable@^16.0.1;handsontable@16.2.0;hyperformula@2.7.1;hyperformula@3.0.0;hyperformula@3.1.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1'
|
||||||
},
|
},
|
||||||
(error, json) => {
|
(error, json) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ module.exports = {
|
|||||||
ci: {
|
ci: {
|
||||||
collect: {
|
collect: {
|
||||||
settings: {
|
settings: {
|
||||||
preset: "desktop",
|
preset: 'desktop',
|
||||||
chromeFlags: "--no-sandbox --disable-dev-shm-usage"
|
chromeFlags: '--no-sandbox --disable-dev-shm-usage'
|
||||||
},
|
},
|
||||||
url: [
|
url: [
|
||||||
'http://localhost:5000/AppStream/clickme/#/home/tables',
|
'http://localhost:5000/AppStream/clickme/#/home/tables',
|
||||||
@@ -37,6 +37,10 @@ module.exports = {
|
|||||||
{ minScore: 0.4, aggregationMethod: 'median' }
|
{ minScore: 0.4, aggregationMethod: 'median' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
upload: {
|
||||||
|
target: 'filesystem',
|
||||||
|
outputDir: './lighthouse-reports'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11707
client/package-lock.json
generated
11707
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -32,26 +32,27 @@
|
|||||||
"compodoc:build": "compodoc -p tsconfig.doc.json --name 'Data Controller Client'",
|
"compodoc:build": "compodoc -p tsconfig.doc.json --name 'Data Controller Client'",
|
||||||
"compodoc:build-and-serve": "compodoc -p tsconfig.doc.json -s --name 'Data Controller Client'",
|
"compodoc:build-and-serve": "compodoc -p tsconfig.doc.json -s --name 'Data Controller Client'",
|
||||||
"compodoc:serve": "compodoc -s --name 'Data Controller Client'",
|
"compodoc:serve": "compodoc -s --name 'Data Controller Client'",
|
||||||
"lighthouse": "lhci autorun"
|
"lighthouse": "lhci autorun",
|
||||||
|
"ng": "ng"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^17.3.3",
|
"@angular/animations": "^19.2.18",
|
||||||
"@angular/cdk": "^17.3.3",
|
"@angular/cdk": "^19.2.19",
|
||||||
"@angular/common": "^17.3.3",
|
"@angular/common": "^19.2.18",
|
||||||
"@angular/compiler": "^17.3.3",
|
"@angular/compiler": "^19.2.18",
|
||||||
"@angular/core": "^17.3.3",
|
"@angular/core": "^19.2.18",
|
||||||
"@angular/forms": "^17.3.3",
|
"@angular/forms": "^19.2.18",
|
||||||
"@angular/platform-browser": "^17.3.3",
|
"@angular/platform-browser": "^19.2.18",
|
||||||
"@angular/platform-browser-dynamic": "^17.3.3",
|
"@angular/platform-browser-dynamic": "^19.2.18",
|
||||||
"@angular/router": "^17.3.3",
|
"@angular/router": "^19.2.18",
|
||||||
"@cds/core": "^6.15.1",
|
"@cds/core": "^6.15.1",
|
||||||
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
|
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
|
||||||
"@clr/icons": "^13.0.2",
|
"@clr/icons": "^13.0.2",
|
||||||
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
|
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
|
||||||
"@handsontable/angular-wrapper": "16.0.1",
|
"@handsontable/angular-wrapper": "16.0.1",
|
||||||
"@sasjs/adapter": "^4.12.2",
|
"@sasjs/adapter": "^4.16.3",
|
||||||
"@sasjs/utils": "^3.4.0",
|
"@sasjs/utils": "^3.5.3",
|
||||||
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
||||||
"@types/d3-graphviz": "^2.6.7",
|
"@types/d3-graphviz": "^2.6.7",
|
||||||
"@types/text-encoding": "0.0.35",
|
"@types/text-encoding": "0.0.35",
|
||||||
@@ -66,9 +67,9 @@
|
|||||||
"hyperformula": "^2.5.0",
|
"hyperformula": "^2.5.0",
|
||||||
"iconv-lite": "^0.5.0",
|
"iconv-lite": "^0.5.0",
|
||||||
"jquery-datetimepicker": "^2.5.21",
|
"jquery-datetimepicker": "^2.5.21",
|
||||||
"jsrsasign": "^10.2.0",
|
"jsrsasign": "11.1.1",
|
||||||
"marked": "^5.0.0",
|
"marked": "^5.0.0",
|
||||||
"moment": "^2.26.0",
|
"moment": "^2.30.1",
|
||||||
"ngx-clipboard": "^16.0.0",
|
"ngx-clipboard": "^16.0.0",
|
||||||
"ngx-json-viewer": "file:libraries/ngx-json-viewer-3.2.1.tgz",
|
"ngx-json-viewer": "file:libraries/ngx-json-viewer-3.2.1.tgz",
|
||||||
"nodejs": "0.0.0",
|
"nodejs": "0.0.0",
|
||||||
@@ -81,18 +82,18 @@
|
|||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"vm": "^0.1.0",
|
"vm": "^0.1.0",
|
||||||
"webpack": "^5.91.0",
|
"webpack": "^5.91.0",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "file:libraries/xlsx-0.20.3.tgz",
|
||||||
"zone.js": "~0.14.4"
|
"zone.js": "~0.15.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^17.3.3",
|
"@angular-devkit/build-angular": "^19.2.19",
|
||||||
"@angular-eslint/builder": "17.3.0",
|
"@angular-eslint/builder": "19.8.1",
|
||||||
"@angular-eslint/eslint-plugin": "17.3.0",
|
"@angular-eslint/eslint-plugin": "19.8.1",
|
||||||
"@angular-eslint/eslint-plugin-template": "17.3.0",
|
"@angular-eslint/eslint-plugin-template": "19.8.1",
|
||||||
"@angular-eslint/schematics": "17.3.0",
|
"@angular-eslint/schematics": "19.8.1",
|
||||||
"@angular-eslint/template-parser": "17.3.0",
|
"@angular-eslint/template-parser": "19.8.1",
|
||||||
"@angular/cli": "^17.3.3",
|
"@angular/cli": "^19.2.19",
|
||||||
"@angular/compiler-cli": "^17.3.3",
|
"@angular/compiler-cli": "^19.2.18",
|
||||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||||
"@compodoc/compodoc": "^1.1.21",
|
"@compodoc/compodoc": "^1.1.21",
|
||||||
"@cypress/webpack-preprocessor": "^5.17.1",
|
"@cypress/webpack-preprocessor": "^5.17.1",
|
||||||
@@ -128,7 +129,7 @@
|
|||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"ts-loader": "^9.2.8",
|
"ts-loader": "^9.2.8",
|
||||||
"ts-node": "^3.3.0",
|
"ts-node": "^3.3.0",
|
||||||
"typescript": "~5.4.4",
|
"typescript": "~5.8.3",
|
||||||
"wait-on": "^6.0.1",
|
"wait-on": "^6.0.1",
|
||||||
"watch": "^1.0.2"
|
"watch": "^1.0.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export interface HandsontableStaticConfig {
|
|||||||
* Cached viyaApi collections, search and selected endpoint
|
* Cached viyaApi collections, search and selected endpoint
|
||||||
*/
|
*/
|
||||||
export const globals: {
|
export const globals: {
|
||||||
|
embed: boolean
|
||||||
rootParam: string
|
rootParam: string
|
||||||
dcLib: string
|
dcLib: string
|
||||||
xlmaps: XLMapListItem[]
|
xlmaps: XLMapListItem[]
|
||||||
@@ -69,6 +70,7 @@ export const globals: {
|
|||||||
handsontable: HandsontableStaticConfig
|
handsontable: HandsontableStaticConfig
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
} = {
|
} = {
|
||||||
|
embed: false,
|
||||||
rootParam: <string>'',
|
rootParam: <string>'',
|
||||||
dcLib: '',
|
dcLib: '',
|
||||||
xlmaps: [],
|
xlmaps: [],
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<header class="app-header">
|
<header class="app-header" *ngIf="!embed">
|
||||||
<!-- <button
|
<!-- <button
|
||||||
*ngIf="
|
*ngIf="
|
||||||
isMainRoute('view') ||
|
isMainRoute('view') ||
|
||||||
@@ -213,9 +213,10 @@
|
|||||||
</header>
|
</header>
|
||||||
<nav
|
<nav
|
||||||
*ngIf="
|
*ngIf="
|
||||||
router.url.includes('submitted') ||
|
!embed &&
|
||||||
router.url.includes('approve') ||
|
(router.url.includes('submitted') ||
|
||||||
router.url.includes('history')
|
router.url.includes('approve') ||
|
||||||
|
router.url.includes('history'))
|
||||||
"
|
"
|
||||||
class="subnav"
|
class="subnav"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Location } from '@angular/common'
|
|||||||
import '@clr/icons'
|
import '@clr/icons'
|
||||||
import '@clr/icons/shapes/all-shapes'
|
import '@clr/icons/shapes/all-shapes'
|
||||||
import { globals } from './_globals'
|
import { globals } from './_globals'
|
||||||
import * as moment from 'moment'
|
import moment from 'moment'
|
||||||
import { EventService } from './services/event.service'
|
import { EventService } from './services/event.service'
|
||||||
import { AppService } from './services/app.service'
|
import { AppService } from './services/app.service'
|
||||||
import { InfoModal } from './models/InfoModal'
|
import { InfoModal } from './models/InfoModal'
|
||||||
@@ -42,7 +42,8 @@ ClarityIcons.addIcons(
|
|||||||
selector: 'my-app',
|
selector: 'my-app',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.scss'],
|
styleUrls: ['./app.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
private dcAdapterSettings: DcAdapterSettings | undefined
|
private dcAdapterSettings: DcAdapterSettings | undefined
|
||||||
@@ -69,6 +70,7 @@ export class AppComponent {
|
|||||||
|
|
||||||
public syssite = this.appService.syssite
|
public syssite = this.appService.syssite
|
||||||
public licenceState = this.licenceService.licenceState
|
public licenceState = this.licenceService.licenceState
|
||||||
|
public embed = globals.embed
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private appService: AppService,
|
private appService: AppService,
|
||||||
@@ -142,6 +144,16 @@ export class AppComponent {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const hashQuery = window.location.hash.split('?')[1]
|
||||||
|
if (hashQuery) {
|
||||||
|
const embedParam = new URLSearchParams(hashQuery).get('embed')
|
||||||
|
if (embedParam !== null) {
|
||||||
|
const isEmbed = embedParam !== 'false'
|
||||||
|
globals.embed = isEmbed
|
||||||
|
this.embed = isEmbed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.subscribeToShowAbortModal()
|
this.subscribeToShowAbortModal()
|
||||||
this.subscribeToRequestsModal()
|
this.subscribeToRequestsModal()
|
||||||
this.subscribeToStartupData()
|
this.subscribeToStartupData()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HttpClientModule } from '@angular/common/http'
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { BrowserModule } from '@angular/platform-browser'
|
import { BrowserModule } from '@angular/platform-browser'
|
||||||
@@ -36,12 +36,12 @@ import { AppSettingsService } from './services/app-settings.service'
|
|||||||
InfoModalComponent,
|
InfoModalComponent,
|
||||||
ViyaApiExplorerComponent
|
ViyaApiExplorerComponent
|
||||||
],
|
],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
HttpClientModule,
|
|
||||||
ROUTING,
|
ROUTING,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
ClarityModule,
|
ClarityModule,
|
||||||
@@ -50,7 +50,12 @@ import { AppSettingsService } from './services/app-settings.service'
|
|||||||
DirectivesModule,
|
DirectivesModule,
|
||||||
NgxJsonViewerModule
|
NgxJsonViewerModule
|
||||||
],
|
],
|
||||||
providers: [AppService, SasStoreService, LicensingGuard, AppSettingsService],
|
providers: [
|
||||||
bootstrap: [AppComponent]
|
AppService,
|
||||||
|
SasStoreService,
|
||||||
|
LicensingGuard,
|
||||||
|
AppSettingsService,
|
||||||
|
provideHttpClient(withInterceptorsFromDi())
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { DcAdapterSettings } from '../models/DcAdapterSettings'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class DeployComponent implements OnInit {
|
export class DeployComponent implements OnInit {
|
||||||
public step: number = 0
|
public step: number = 0
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ import {
|
|||||||
selector: 'app-automatic-deploy',
|
selector: 'app-automatic-deploy',
|
||||||
templateUrl: './automatic.component.html',
|
templateUrl: './automatic.component.html',
|
||||||
styleUrls: ['./automatic.component.scss'],
|
styleUrls: ['./automatic.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class AutomaticComponent implements OnInit {
|
export class AutomaticComponent implements OnInit {
|
||||||
@Input() sasJs!: SASjs
|
@Input() sasJs!: SASjs
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import { SasService } from 'src/app/services/sas.service'
|
|||||||
selector: 'app-manual-deploy',
|
selector: 'app-manual-deploy',
|
||||||
templateUrl: './manual.component.html',
|
templateUrl: './manual.component.html',
|
||||||
styleUrls: ['./manual.component.scss'],
|
styleUrls: ['./manual.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ManualComponent implements OnInit {
|
export class ManualComponent implements OnInit {
|
||||||
@Input() sasJs!: SASjs
|
@Input() sasJs!: SASjs
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import { SasjsService } from 'src/app/services/sasjs.service'
|
|||||||
selector: 'app-sasjs-configurator',
|
selector: 'app-sasjs-configurator',
|
||||||
templateUrl: './sasjs-configurator.component.html',
|
templateUrl: './sasjs-configurator.component.html',
|
||||||
styleUrls: ['./sasjs-configurator.component.scss'],
|
styleUrls: ['./sasjs-configurator.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class SasjsConfiguratorComponent implements OnInit {
|
export class SasjsConfiguratorComponent implements OnInit {
|
||||||
@Input() sasJs!: SASjs
|
@Input() sasJs!: SASjs
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[appDragNdrop]'
|
selector: '[appDragNdrop]',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class DragNdropDirective {
|
export class DragNdropDirective {
|
||||||
@HostBinding('class.fileover') fileOver: boolean = false
|
@HostBinding('class.fileover') fileOver: boolean = false
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
import { FileUploader } from '../models/FileUploader.class'
|
import { FileUploader } from '../models/FileUploader.class'
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[appFileDrop]'
|
selector: '[appFileDrop]',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class FileDropDirective {
|
export class FileDropDirective {
|
||||||
@Input() uploader?: FileUploader
|
@Input() uploader?: FileUploader
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
import { FileUploader } from '../models/FileUploader.class'
|
import { FileUploader } from '../models/FileUploader.class'
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[appFileSelect]'
|
selector: '[appFileSelect]',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class FileSelectDirective {
|
export class FileSelectDirective {
|
||||||
@Input() uploader?: FileUploader
|
@Input() uploader?: FileUploader
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'
|
|||||||
* Calling functions in html is bad for performance
|
* Calling functions in html is bad for performance
|
||||||
*/
|
*/
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[ngVar]'
|
selector: '[ngVar]',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class NgVarDirective {
|
export class NgVarDirective {
|
||||||
@Input()
|
@Input()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Directive, HostListener } from '@angular/core'
|
import { Directive, HostListener } from '@angular/core'
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[appStealFocus]'
|
selector: '[appStealFocus]',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class StealFocusDirective {
|
export class StealFocusDirective {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { HelperService } from 'src/app/services/helper.service'
|
|||||||
import { SasStoreService } from 'src/app/services/sas-store.service'
|
import { SasStoreService } from 'src/app/services/sas-store.service'
|
||||||
import { DcValidator } from 'src/app/shared/dc-validator/dc-validator'
|
import { DcValidator } from 'src/app/shared/dc-validator/dc-validator'
|
||||||
import { DcValidation } from 'src/app/shared/dc-validator/models/dc-validation.model'
|
import { DcValidation } from 'src/app/shared/dc-validator/models/dc-validation.model'
|
||||||
|
import { isEmpty } from 'src/app/shared/dc-validator/utils/isEmpty'
|
||||||
import {
|
import {
|
||||||
EditRecordDropdownChangeEvent,
|
EditRecordDropdownChangeEvent,
|
||||||
EditRecordInputFocusedEvent
|
EditRecordInputFocusedEvent
|
||||||
@@ -24,7 +25,8 @@ import { EditRecordModal } from '../../models/EditRecordModal'
|
|||||||
selector: 'app-edit-record',
|
selector: 'app-edit-record',
|
||||||
templateUrl: './edit-record.component.html',
|
templateUrl: './edit-record.component.html',
|
||||||
styleUrls: ['./edit-record.component.scss'],
|
styleUrls: ['./edit-record.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class EditRecordComponent implements OnInit {
|
export class EditRecordComponent implements OnInit {
|
||||||
@Input() currentRecord!: EditRecordModal
|
@Input() currentRecord!: EditRecordModal
|
||||||
@@ -145,23 +147,63 @@ export class EditRecordComponent implements OnInit {
|
|||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
async recordInputChange(event: any, colName: string) {
|
async recordInputChange(event: any, colName: string): Promise<void> {
|
||||||
const colRules = this.currentRecordValidator?.getRule(colName)
|
const colRules = this.currentRecordValidator?.getRule(colName)
|
||||||
const value = event.target.value
|
const value = event.target.value
|
||||||
|
|
||||||
this.helperService.debounceCall(300, () => {
|
this.helperService.debounceCall(300, () => {
|
||||||
this.validateRecordCol(colRules, value).then((valid: boolean) => {
|
this.validateRecordCol(colRules, value).then((valid: boolean) => {
|
||||||
const index = this.currentRecordInvalidCols.indexOf(colName)
|
this.updateValidationState(colName, valid)
|
||||||
|
|
||||||
if (valid) {
|
if (!valid) {
|
||||||
if (index > -1) this.currentRecordInvalidCols.splice(index, 1)
|
this.tryAutoPopulateNotNull(event, colName, colRules, value)
|
||||||
} else {
|
|
||||||
if (index < 0) this.currentRecordInvalidCols.push(colName)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the invalid columns list based on validation result
|
||||||
|
*/
|
||||||
|
private updateValidationState(colName: string, valid: boolean): void {
|
||||||
|
const index = this.currentRecordInvalidCols.indexOf(colName)
|
||||||
|
|
||||||
|
if (valid && index > -1) {
|
||||||
|
this.currentRecordInvalidCols.splice(index, 1)
|
||||||
|
} else if (!valid && index < 0) {
|
||||||
|
this.currentRecordInvalidCols.push(colName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-populates NOTNULL default value when the field is empty and has a default
|
||||||
|
*/
|
||||||
|
private tryAutoPopulateNotNull(
|
||||||
|
event: any,
|
||||||
|
colName: string,
|
||||||
|
colRules: DcValidation | undefined,
|
||||||
|
value: any
|
||||||
|
): void {
|
||||||
|
if (
|
||||||
|
!isEmpty(value) ||
|
||||||
|
!this.currentRecordValidator ||
|
||||||
|
!this.currentRecord
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValue =
|
||||||
|
this.currentRecordValidator.getNotNullDefaultValue(colName)
|
||||||
|
if (defaultValue === undefined) return
|
||||||
|
|
||||||
|
this.currentRecord[colName] = defaultValue
|
||||||
|
event.target.value = defaultValue
|
||||||
|
|
||||||
|
this.validateRecordCol(colRules, defaultValue).then((isValid: boolean) => {
|
||||||
|
this.updateValidationState(colName, isValid)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onNextRecordClick() {
|
onNextRecordClick() {
|
||||||
this.onNextRecord.emit()
|
this.onNextRecord.emit()
|
||||||
}
|
}
|
||||||
@@ -171,23 +213,8 @@ export class EditRecordComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public copyToClip(text: string) {
|
public copyToClip(text: string) {
|
||||||
const modalElement = document.querySelector('#recordModalRef .modal-title')
|
navigator.clipboard.writeText(text)
|
||||||
|
this.generatedRecordUrl = text
|
||||||
if (modalElement) {
|
|
||||||
const selBox = document.createElement('textarea')
|
|
||||||
selBox.style.position = 'fixed'
|
|
||||||
selBox.style.left = '0'
|
|
||||||
selBox.style.top = '0'
|
|
||||||
selBox.style.opacity = '0'
|
|
||||||
selBox.style.zIndex = '5000'
|
|
||||||
selBox.value = text
|
|
||||||
modalElement.appendChild(selBox)
|
|
||||||
selBox.focus()
|
|
||||||
selBox.select()
|
|
||||||
document.execCommand('copy')
|
|
||||||
modalElement.removeChild(selBox)
|
|
||||||
this.generatedRecordUrl = text
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateEditRecordUrl() {
|
async generateEditRecordUrl() {
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
|
|||||||
selector: 'app-upload-stater',
|
selector: 'app-upload-stater',
|
||||||
templateUrl: './upload-stater.component.html',
|
templateUrl: './upload-stater.component.html',
|
||||||
styleUrls: ['./upload-stater.component.scss'],
|
styleUrls: ['./upload-stater.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class UploadStaterComponent implements OnInit {
|
export class UploadStaterComponent implements OnInit {
|
||||||
public statesList: string[] = [] //States appended to be displayed
|
public statesList: string[] = [] //States appended to be displayed
|
||||||
|
|||||||
@@ -165,7 +165,7 @@
|
|||||||
class="card-header clr-row buttonBar headerBar clr-flex-md-row clr-justify-content-center clr-justify-content-lg-end"
|
class="card-header clr-row buttonBar headerBar clr-flex-md-row clr-justify-content-center clr-justify-content-lg-end"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
*ngIf="tableTrue"
|
*ngIf="tableTrue && !embed"
|
||||||
class="clr-col-12 clr-col-md-3 clr-col-lg-4 backBtn"
|
class="clr-col-12 clr-col-md-3 clr-col-lg-4 backBtn"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { Col } from '../shared/dc-validator/models/col.model'
|
|||||||
import { DcValidation } from '../shared/dc-validator/models/dc-validation.model'
|
import { DcValidation } from '../shared/dc-validator/models/dc-validation.model'
|
||||||
import { DQRule } from '../shared/dc-validator/models/dq-rules.model'
|
import { DQRule } from '../shared/dc-validator/models/dq-rules.model'
|
||||||
import { getHotDataSchema } from '../shared/dc-validator/utils/getHotDataSchema'
|
import { getHotDataSchema } from '../shared/dc-validator/utils/getHotDataSchema'
|
||||||
|
import { isEmpty } from '../shared/dc-validator/utils/isEmpty'
|
||||||
import { globals } from '../_globals'
|
import { globals } from '../_globals'
|
||||||
import { UploadStaterComponent } from './components/upload-stater/upload-stater.component'
|
import { UploadStaterComponent } from './components/upload-stater/upload-stater.component'
|
||||||
import { DynamicExtendedCellValidation } from './models/dynamicExtendedCellValidation'
|
import { DynamicExtendedCellValidation } from './models/dynamicExtendedCellValidation'
|
||||||
@@ -70,7 +71,8 @@ import { ParseResult } from '../models/ParseResult.interface'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
@ViewChildren('uploadStater')
|
@ViewChildren('uploadStater')
|
||||||
@@ -262,6 +264,9 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
public badEdit = false
|
public badEdit = false
|
||||||
public badEditCause: string | undefined
|
public badEditCause: string | undefined
|
||||||
public badEditTitle: string | undefined
|
public badEditTitle: string | undefined
|
||||||
|
get embed() {
|
||||||
|
return globals.embed
|
||||||
|
}
|
||||||
public tableTrue: boolean | undefined
|
public tableTrue: boolean | undefined
|
||||||
public saveLoading = false
|
public saveLoading = false
|
||||||
public approvers: string[] = []
|
public approvers: string[] = []
|
||||||
@@ -1044,12 +1049,16 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new empty row object with proper structure
|
* Creates a new empty row object with proper structure.
|
||||||
|
* Columns with NOTNULL DQ rules are pre-populated with their RULE_VALUE.
|
||||||
*/
|
*/
|
||||||
private createEmptyRow(): any {
|
private createEmptyRow(): any {
|
||||||
const newRow: any = {}
|
const newRow: any = {}
|
||||||
this.headerColumns.forEach((col: string) => {
|
this.cellValidation.forEach((rule: any) => {
|
||||||
newRow[col] = ''
|
const dataKey = rule.data
|
||||||
|
newRow[dataKey] = this.hotDataSchema.hasOwnProperty(dataKey)
|
||||||
|
? this.hotDataSchema[dataKey]
|
||||||
|
: ''
|
||||||
})
|
})
|
||||||
newRow['noLinkOption'] = true
|
newRow['noLinkOption'] = true
|
||||||
return newRow
|
return newRow
|
||||||
@@ -2675,13 +2684,14 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
// Note: this.headerColumns and this.columnHeader contains same data
|
// Note: this.headerColumns and this.columnHeader contains same data
|
||||||
// need to resolve redundancy
|
// need to resolve redundancy
|
||||||
|
|
||||||
// default schema
|
// default schema - includes NOTNULL defaults from DQ rules
|
||||||
for (let i = 0; i < this.headerColumns.length; i++) {
|
for (let i = 0; i < this.headerColumns.length; i++) {
|
||||||
const colType = this.cellValidation[i].type
|
const colType = this.cellValidation[i].type
|
||||||
|
|
||||||
this.hotDataSchema[this.cellValidation[i].data] = getHotDataSchema(
|
this.hotDataSchema[this.cellValidation[i].data] = getHotDataSchema(
|
||||||
colType,
|
colType,
|
||||||
this.cellValidation[i]
|
this.cellValidation[i],
|
||||||
|
this.dcValidator?.getDqDetails()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2986,6 +2996,29 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Auto-populate NOTNULL default when validation fails due to empty value
|
||||||
|
hot.addHook(
|
||||||
|
'afterValidate',
|
||||||
|
(isValid: boolean, value: any, row: number, prop: string | number) => {
|
||||||
|
if (isValid || !isEmpty(value)) return
|
||||||
|
|
||||||
|
const colName =
|
||||||
|
typeof prop === 'string'
|
||||||
|
? prop
|
||||||
|
: (hot.colToProp(prop as number) as string)
|
||||||
|
|
||||||
|
const defaultValue = this.dcValidator?.getNotNullDefaultValue(colName)
|
||||||
|
if (defaultValue === undefined) return
|
||||||
|
|
||||||
|
// Auto-populate using setTimeout to avoid modifying during validation
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isEmpty(hot.getDataAtRowProp(row, colName))) {
|
||||||
|
hot.setDataAtRowProp(row, colName, defaultValue, 'autoPopulate')
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
hot.addHook('beforePaste', (data: any, cords: any) => {
|
hot.addHook('beforePaste', (data: any, cords: any) => {
|
||||||
const startCol = cords[0].startCol
|
const startCol = cords[0].startCol
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const freeTierConfig: LicenceState = {
|
|||||||
lineage_daily_limit: 3,
|
lineage_daily_limit: 3,
|
||||||
tables_in_library_limit: 35,
|
tables_in_library_limit: 35,
|
||||||
viewbox: true,
|
viewbox: true,
|
||||||
fileUpload: true,
|
fileUpload: false,
|
||||||
editRecord: true,
|
editRecord: true,
|
||||||
addRecord: true
|
addRecord: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class GroupComponent implements OnInit {
|
export class GroupComponent implements OnInit {
|
||||||
public groups: Array<any> | undefined
|
public groups: Array<any> | undefined
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import { LicenceService } from '../services/licence.service'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class HomeComponent implements AfterContentInit {
|
export class HomeComponent implements AfterContentInit {
|
||||||
public treeNodeLibraries: Array<any> | null = null
|
public treeNodeLibraries: Array<any> | null = null
|
||||||
|
|||||||
@@ -1,178 +1,182 @@
|
|||||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core'
|
import { Component, OnInit, ViewEncapsulation } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AppService, LicenceService, SasService } from '../services'
|
import { AppService, LicenceService, SasService } from '../services'
|
||||||
import { LicenseKeyData } from '../models/LicenseKeyData'
|
import { LicenseKeyData } from '../models/LicenseKeyData'
|
||||||
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
|
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
|
||||||
|
|
||||||
enum LicenseActions {
|
enum LicenseActions {
|
||||||
key = 'key',
|
key = 'key',
|
||||||
register = 'register',
|
register = 'register',
|
||||||
limit = 'limit',
|
limit = 'limit',
|
||||||
update = 'update'
|
update = 'update'
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-licensing',
|
selector: 'app-licensing',
|
||||||
templateUrl: './licensing.component.html',
|
templateUrl: './licensing.component.html',
|
||||||
styleUrls: ['./licensing.component.scss'],
|
styleUrls: ['./licensing.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
})
|
standalone: false
|
||||||
export class LicensingComponent implements OnInit {
|
})
|
||||||
public action: LicenseActions | null = null
|
export class LicensingComponent implements OnInit {
|
||||||
|
public action: LicenseActions | null = null
|
||||||
public licenseErrors: { [key: string]: string } = {
|
|
||||||
missing: `Licence key is missing - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
|
public licenseErrors: { [key: string]: string } = {
|
||||||
expired: `Licence key is expired - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
|
missing: `Licence key is missing - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
|
||||||
invalid: `Licence key is invalid - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
|
expired: `Licence key is expired - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
|
||||||
missmatch: `Your SYSSITE (below) is not found in the licence key - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`
|
invalid: `Licence key is invalid - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
|
||||||
}
|
missmatch: `Your SYSSITE (below) is not found in the licence key - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`
|
||||||
|
}
|
||||||
public keyError: string | undefined
|
|
||||||
public errorDetails: string | undefined
|
public keyError: string | undefined
|
||||||
public missmatchedKey: string | undefined
|
public errorDetails: string | undefined
|
||||||
public licenceKeyValue: string = ''
|
public missmatchedKey: string | undefined
|
||||||
public activationKeyValue: string = ''
|
public licenceKeyValue: string = ''
|
||||||
|
public activationKeyValue: string = ''
|
||||||
public applyingKeys: boolean = false
|
|
||||||
|
public applyingKeys: boolean = false
|
||||||
public syssite = this.appService.syssite
|
|
||||||
public currentLicenceKey = this.licenceService.licenceKey
|
public syssite = this.appService.syssite
|
||||||
public currentActivationKey = this.licenceService.activationKey
|
public currentLicenceKey = this.licenceService.licenceKey
|
||||||
public isAppFreeTier = this.licenceService.isAppFreeTier
|
public currentActivationKey = this.licenceService.activationKey
|
||||||
public userCountLimitation = this.licenceService.userCountLimitation
|
public isAppFreeTier = this.licenceService.isAppFreeTier
|
||||||
|
public userCountLimitation = this.licenceService.userCountLimitation
|
||||||
public licenseKeyData: LicenseKeyData | null = null
|
|
||||||
|
public licenseKeyData: LicenseKeyData | null = null
|
||||||
public inputType: 'file' | 'paste' = 'file'
|
|
||||||
public licenceFileError: string | undefined
|
public inputType: 'file' | 'paste' = 'file'
|
||||||
public licenceFileLoading: boolean = false
|
public licenceFileError: string | undefined
|
||||||
public licencefile: { filename: string } = {
|
public licenceFileLoading: boolean = false
|
||||||
filename: ''
|
public licencefile: { filename: string } = {
|
||||||
}
|
filename: ''
|
||||||
|
}
|
||||||
constructor(
|
|
||||||
private route: ActivatedRoute,
|
constructor(
|
||||||
private licenceService: LicenceService,
|
private route: ActivatedRoute,
|
||||||
private sasService: SasService,
|
private router: Router,
|
||||||
private appService: AppService
|
private licenceService: LicenceService,
|
||||||
) {}
|
private sasService: SasService,
|
||||||
|
private appService: AppService
|
||||||
ngOnInit(): void {
|
) {}
|
||||||
this.licenceKeyValue = this.currentLicenceKey || ''
|
|
||||||
this.activationKeyValue = this.currentActivationKey || ''
|
ngOnInit(): void {
|
||||||
|
this.licenceKeyValue = this.currentLicenceKey || ''
|
||||||
this.route.queryParams.subscribe((queryParams: any) => {
|
this.activationKeyValue = this.currentActivationKey || ''
|
||||||
this.keyError = queryParams.error
|
|
||||||
this.missmatchedKey = queryParams.missmatchId
|
this.route.queryParams.subscribe((queryParams: any) => {
|
||||||
|
this.keyError = queryParams.error
|
||||||
if (queryParams.details) {
|
this.missmatchedKey = queryParams.missmatchId
|
||||||
this.errorDetails = atob(queryParams.details)
|
|
||||||
}
|
if (queryParams.details) {
|
||||||
})
|
this.errorDetails = atob(queryParams.details)
|
||||||
|
}
|
||||||
this.route.params.subscribe((params: any) => {
|
})
|
||||||
let actionInUrl = params.action
|
|
||||||
|
this.route.params.subscribe((params: any) => {
|
||||||
if (actionInUrl) {
|
let actionInUrl = params.action
|
||||||
if (Object.values(LicenseActions).includes(actionInUrl)) {
|
|
||||||
this.action = actionInUrl
|
if (actionInUrl) {
|
||||||
}
|
if (Object.values(LicenseActions).includes(actionInUrl)) {
|
||||||
}
|
this.action = actionInUrl
|
||||||
})
|
}
|
||||||
|
}
|
||||||
this.licenseKeyData = this.licenceService.getLicenseKeyData()
|
})
|
||||||
}
|
|
||||||
|
this.licenseKeyData = this.licenceService.getLicenseKeyData()
|
||||||
public trimKeys() {
|
}
|
||||||
this.licenceKeyValue = this.licenceKeyValue.trim()
|
|
||||||
this.activationKeyValue = this.activationKeyValue.trim()
|
public trimKeys() {
|
||||||
}
|
this.licenceKeyValue = this.licenceKeyValue.trim()
|
||||||
|
this.activationKeyValue = this.activationKeyValue.trim()
|
||||||
public copySyssite(copyIconRef: any, copyTooltip: any, syssite: string[]) {
|
}
|
||||||
const syssiteString = syssite.join('\n')
|
|
||||||
|
public copySyssite(copyIconRef: any, copyTooltip: any, syssite: string[]) {
|
||||||
navigator.clipboard.writeText(syssiteString).then(() => {
|
const syssiteString = syssite.join('\n')
|
||||||
copyIconRef.setAttribute('shape', 'check')
|
|
||||||
copyIconRef.setAttribute('class', 'is-success')
|
navigator.clipboard.writeText(syssiteString).then(() => {
|
||||||
copyTooltip.innerText = 'Copied!'
|
copyIconRef.setAttribute('shape', 'check')
|
||||||
|
copyIconRef.setAttribute('class', 'is-success')
|
||||||
setTimeout(() => {
|
copyTooltip.innerText = 'Copied!'
|
||||||
copyIconRef.setAttribute('shape', 'copy')
|
|
||||||
copyIconRef.removeAttribute('class')
|
setTimeout(() => {
|
||||||
copyTooltip.innerText = 'Copy to clipboard'
|
copyIconRef.setAttribute('shape', 'copy')
|
||||||
}, 1000)
|
copyIconRef.removeAttribute('class')
|
||||||
})
|
copyTooltip.innerText = 'Copy to clipboard'
|
||||||
}
|
}, 1000)
|
||||||
|
})
|
||||||
public applyKeys() {
|
}
|
||||||
this.applyingKeys = true
|
|
||||||
|
public applyKeys() {
|
||||||
let table = {
|
this.applyingKeys = true
|
||||||
keyupload: [
|
|
||||||
{
|
let table = {
|
||||||
ACTIVATION_KEY: this.activationKeyValue,
|
keyupload: [
|
||||||
LICENCE_KEY: this.licenceKeyValue
|
{
|
||||||
}
|
ACTIVATION_KEY: this.activationKeyValue,
|
||||||
]
|
LICENCE_KEY: this.licenceKeyValue
|
||||||
}
|
}
|
||||||
|
]
|
||||||
this.sasService
|
}
|
||||||
.request('admin/registerkey', table)
|
|
||||||
.then((res: RequestWrapperResponse) => {
|
this.sasService
|
||||||
if (
|
.request('admin/registerkey', table)
|
||||||
res.adapterResponse.return &&
|
.then((res: RequestWrapperResponse) => {
|
||||||
res.adapterResponse.return[0] &&
|
if (
|
||||||
res.adapterResponse.return[0].MSG === 'SUCCESS'
|
res.adapterResponse.return &&
|
||||||
) {
|
res.adapterResponse.return[0] &&
|
||||||
location.replace(location.href.split('#')[0])
|
res.adapterResponse.return[0].MSG === 'SUCCESS'
|
||||||
}
|
) {
|
||||||
})
|
this.router.navigateByUrl('/').then(() => {
|
||||||
.finally(() => {
|
window.location.reload()
|
||||||
this.applyingKeys = false
|
})
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
.finally(() => {
|
||||||
public onFileCapture(event: any, dropped = false) {
|
this.applyingKeys = false
|
||||||
let file = dropped ? event[0] : event.target.files[0]
|
})
|
||||||
this.licencefile.filename = file.name
|
}
|
||||||
|
|
||||||
if (!file) return
|
public onFileCapture(event: any, dropped = false) {
|
||||||
|
let file = dropped ? event[0] : event.target.files[0]
|
||||||
this.licenceFileLoading = true
|
this.licencefile.filename = file.name
|
||||||
|
|
||||||
const reader = new FileReader()
|
if (!file) return
|
||||||
|
|
||||||
reader.onload = (evt) => {
|
this.licenceFileLoading = true
|
||||||
this.licenceFileError = 'Error reading file.'
|
|
||||||
|
const reader = new FileReader()
|
||||||
if (!evt || !evt.target) return
|
|
||||||
if (evt.target.readyState != 2) return
|
reader.onload = (evt) => {
|
||||||
if (evt.target.error) return
|
this.licenceFileError = 'Error reading file.'
|
||||||
if (!evt.target.result) return
|
|
||||||
|
if (!evt || !evt.target) return
|
||||||
this.licenceFileLoading = false
|
if (evt.target.readyState != 2) return
|
||||||
this.licenceFileError = undefined
|
if (evt.target.error) return
|
||||||
const fileArr = evt.target.result.toString().split('\n')
|
if (!evt.target.result) return
|
||||||
this.activationKeyValue = fileArr[1]
|
|
||||||
this.licenceKeyValue = fileArr[0]
|
this.licenceFileLoading = false
|
||||||
}
|
this.licenceFileError = undefined
|
||||||
|
const fileArr = evt.target.result.toString().split('\n')
|
||||||
reader.readAsText(file)
|
this.activationKeyValue = fileArr[1]
|
||||||
}
|
this.licenceKeyValue = fileArr[0]
|
||||||
|
}
|
||||||
public switchType(type: 'paste' | 'file') {
|
|
||||||
this.inputType = type
|
reader.readAsText(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
get disableApplyButton(): boolean {
|
public switchType(type: 'paste' | 'file') {
|
||||||
if (this.licenceKeyValue.length < 1 || this.activationKeyValue.length < 1)
|
this.inputType = type
|
||||||
return true
|
}
|
||||||
if (
|
|
||||||
this.licenceKeyValue === this.currentLicenceKey &&
|
get disableApplyButton(): boolean {
|
||||||
this.activationKeyValue === this.currentActivationKey
|
if (this.licenceKeyValue.length < 1 || this.activationKeyValue.length < 1)
|
||||||
)
|
return true
|
||||||
return true
|
if (
|
||||||
|
this.licenceKeyValue === this.currentLicenceKey &&
|
||||||
return false
|
this.activationKeyValue === this.currentActivationKey
|
||||||
}
|
)
|
||||||
}
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -239,13 +239,7 @@
|
|||||||
|
|
||||||
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
||||||
<div (click)="downloadSVG()" clrDropdownItem>SVG</div>
|
<div (click)="downloadSVG()" clrDropdownItem>SVG</div>
|
||||||
<div
|
<div (click)="downloadPNG()" clrDropdownItem>PNG</div>
|
||||||
*ngIf="!helperService.isMicrosoft"
|
|
||||||
(click)="downloadPNG()"
|
|
||||||
clrDropdownItem
|
|
||||||
>
|
|
||||||
PNG
|
|
||||||
</div>
|
|
||||||
<div (click)="downloadDot()" clrDropdownItem>Dot</div>
|
<div (click)="downloadDot()" clrDropdownItem>Dot</div>
|
||||||
<div *ngIf="flatdata" (click)="downloadCSV()" clrDropdownItem>
|
<div *ngIf="flatdata" (click)="downloadCSV()" clrDropdownItem>
|
||||||
CSV
|
CSV
|
||||||
@@ -366,13 +360,7 @@
|
|||||||
|
|
||||||
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
||||||
<div (click)="renderToDownload('SVG')" clrDropdownItem>SVG</div>
|
<div (click)="renderToDownload('SVG')" clrDropdownItem>SVG</div>
|
||||||
<div
|
<div (click)="renderToDownload('PNG')" clrDropdownItem>PNG</div>
|
||||||
*ngIf="!helperService.isMicrosoft"
|
|
||||||
(click)="renderToDownload('PNG')"
|
|
||||||
clrDropdownItem
|
|
||||||
>
|
|
||||||
PNG
|
|
||||||
</div>
|
|
||||||
<div (click)="downloadDot(); cancelRenderingGraph()" clrDropdownItem>
|
<div (click)="downloadDot(); cancelRenderingGraph()" clrDropdownItem>
|
||||||
Dot
|
Dot
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ const moment = require('moment')
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class LineageComponent {
|
export class LineageComponent {
|
||||||
public switchFlag: boolean = false
|
public switchFlag: boolean = false
|
||||||
@@ -746,28 +747,13 @@ export class LineageComponent {
|
|||||||
return URL.createObjectURL(svg_blob)
|
return URL.createObjectURL(svg_blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSVGBlob() {
|
|
||||||
let svg: any = document.getElementById('graph')
|
|
||||||
let serializer = new XMLSerializer()
|
|
||||||
let svg_blob = new Blob([serializer.serializeToString(svg)], {
|
|
||||||
type: 'image/svg+xml'
|
|
||||||
})
|
|
||||||
return svg_blob
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadSVG() {
|
downloadSVG() {
|
||||||
d3Viz.graphviz('#graph').resetZoom()
|
d3Viz.graphviz('#graph').resetZoom()
|
||||||
|
|
||||||
if (navigator.appVersion.toString().indexOf('.NET') > 0) {
|
let downloadLink = document.createElement('a')
|
||||||
window.navigator.msSaveBlob(this.getSVGBlob(), this.constructName('svg'))
|
downloadLink.href = this.getSVGURL()
|
||||||
} else {
|
downloadLink.download = this.constructName('svg')
|
||||||
let downloadLink = document.createElement('a')
|
downloadLink.click()
|
||||||
downloadLink.href = this.getSVGURL()
|
|
||||||
downloadLink.download = this.constructName('svg')
|
|
||||||
document.body.appendChild(downloadLink)
|
|
||||||
downloadLink.click()
|
|
||||||
document.body.removeChild(downloadLink)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadPNG() {
|
async downloadPNG() {
|
||||||
@@ -795,16 +781,11 @@ export class LineageComponent {
|
|||||||
var a = document.createElement('a')
|
var a = document.createElement('a')
|
||||||
var blob = new Blob([csvArray], { type: 'text/csv' })
|
var blob = new Blob([csvArray], { type: 'text/csv' })
|
||||||
|
|
||||||
if (navigator.appVersion.toString().indexOf('.NET') > 0) {
|
var url = window.URL.createObjectURL(blob)
|
||||||
window.navigator.msSaveBlob(blob, this.constructName('csv'))
|
a.href = url
|
||||||
} else {
|
a.download = this.constructName('csv')
|
||||||
var url = window.URL.createObjectURL(blob)
|
a.click()
|
||||||
a.href = url
|
window.URL.revokeObjectURL(url)
|
||||||
a.download = this.constructName('csv')
|
|
||||||
a.click()
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
a.remove()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDotUrl() {
|
private getDotUrl() {
|
||||||
@@ -813,23 +794,11 @@ export class LineageComponent {
|
|||||||
return window.URL.createObjectURL(dot_blob)
|
return window.URL.createObjectURL(dot_blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDotBlob() {
|
|
||||||
let data = this.vizInput
|
|
||||||
let dot_blob = new Blob([data], { type: 'text/plain' })
|
|
||||||
return dot_blob
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadDot() {
|
downloadDot() {
|
||||||
if (navigator.appVersion.toString().indexOf('.NET') > 0) {
|
let downloadLink = document.createElement('a')
|
||||||
window.navigator.msSaveBlob(this.getDotBlob(), this.constructName('txt'))
|
downloadLink.href = this.getDotUrl()
|
||||||
} else {
|
downloadLink.download = this.constructName('txt')
|
||||||
let downloadLink = document.createElement('a')
|
downloadLink.click()
|
||||||
downloadLink.href = this.getDotUrl()
|
|
||||||
downloadLink.download = this.constructName('txt')
|
|
||||||
document.body.appendChild(downloadLink)
|
|
||||||
downloadLink.click()
|
|
||||||
document.body.removeChild(downloadLink)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public showSvg() {
|
public showSvg() {
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ class ValueFilter implements ClrDatagridStringFilterInterface<MetaData> {
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class MetadataComponent implements OnInit {
|
export class MetadataComponent implements OnInit {
|
||||||
metaDataList: Array<any> | undefined
|
metaDataList: Array<any> | undefined
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ enum FileLoadingState {
|
|||||||
selector: 'app-multi-dataset',
|
selector: 'app-multi-dataset',
|
||||||
templateUrl: './multi-dataset.component.html',
|
templateUrl: './multi-dataset.component.html',
|
||||||
styleUrls: ['./multi-dataset.component.scss'],
|
styleUrls: ['./multi-dataset.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class MultiDatasetComponent implements OnInit, AfterViewInit {
|
export class MultiDatasetComponent implements OnInit, AfterViewInit {
|
||||||
@HostBinding('class.content-container') contentContainerClass = true
|
@HostBinding('class.content-container') contentContainerClass = true
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class NotFoundComponent implements OnInit {
|
export class NotFoundComponent implements OnInit {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { Pipe, PipeTransform } from '@angular/core'
|
|||||||
import { bytesToSize } from '@sasjs/utils/utils/bytesToSize'
|
import { bytesToSize } from '@sasjs/utils/utils/bytesToSize'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'convertSize'
|
name: 'convertSize',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ConvertSizePipe implements PipeTransform {
|
export class ConvertSizePipe implements PipeTransform {
|
||||||
transform(bytes: string | number, ...args: string[]): string {
|
transform(bytes: string | number, ...args: string[]): string {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
import * as moment from 'moment'
|
import moment from 'moment'
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'dateTimeFormatter'
|
name: 'dateTimeFormatter',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class DateTimeFormatterPipe implements PipeTransform {
|
export class DateTimeFormatterPipe implements PipeTransform {
|
||||||
transform(value: Date | string, type: string): string {
|
transform(value: Date | string, type: string): string {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'linkinze'
|
name: 'linkinze',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class LinkinzePipe implements PipeTransform {
|
export class LinkinzePipe implements PipeTransform {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { Pipe, PipeTransform } from '@angular/core'
|
|||||||
import { HelperService } from '../services/helper.service'
|
import { HelperService } from '../services/helper.service'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'sasToJsDate'
|
name: 'sasToJsDate',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class sasToJsDatePipe implements PipeTransform {
|
export class sasToJsDatePipe implements PipeTransform {
|
||||||
constructor(private helperService: HelperService) {}
|
constructor(private helperService: HelperService) {}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'pkSpaceSeparate'
|
name: 'pkSpaceSeparate',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class PkSpaceSeparatePipe implements PipeTransform {
|
export class PkSpaceSeparatePipe implements PipeTransform {
|
||||||
transform(value: string): string {
|
transform(value: string): string {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'prettyjson'
|
name: 'prettyjson',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class PrettyjsonPipe implements PipeTransform {
|
export class PrettyjsonPipe implements PipeTransform {
|
||||||
transform(rawJson: any): string {
|
transform(rawJson: any): string {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { Pipe, PipeTransform } from '@angular/core'
|
|||||||
import { HelperService } from '../services/helper.service'
|
import { HelperService } from '../services/helper.service'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'secondsParser'
|
name: 'secondsParser',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class SecondsParserPipe implements PipeTransform {
|
export class SecondsParserPipe implements PipeTransform {
|
||||||
constructor(private helperService: HelperService) {}
|
constructor(private helperService: HelperService) {}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'thousandSeparator'
|
name: 'thousandSeparator',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ThousandSeparatorPipe implements PipeTransform {
|
export class ThousandSeparatorPipe implements PipeTransform {
|
||||||
transform(value: string | number, separator?: string): string {
|
transform(value: string | number, separator?: string): string {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'toNumber'
|
name: 'toNumber',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ToNumberPipe implements PipeTransform {
|
export class ToNumberPipe implements PipeTransform {
|
||||||
transform(value: string | number): number {
|
transform(value: string | number): number {
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ registerLocaleData(localeEnGB)
|
|||||||
templateUrl: './query.component.html',
|
templateUrl: './query.component.html',
|
||||||
styleUrls: ['./query.component.scss'],
|
styleUrls: ['./query.component.scss'],
|
||||||
providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }],
|
providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class QueryComponent
|
export class QueryComponent
|
||||||
implements AfterViewInit, AfterContentInit, OnDestroy
|
implements AfterViewInit, AfterContentInit, OnDestroy
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ interface ChangesObj {
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
|
export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
|
||||||
private _detailsSub: Subscription | undefined
|
private _detailsSub: Subscription | undefined
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ class SubmittedFilter implements ClrDatagridStringFilterInterface<ApproveData> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubmitReasonFilter
|
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<ApproveData> {
|
||||||
implements ClrDatagridStringFilterInterface<ApproveData>
|
|
||||||
{
|
|
||||||
accepts(data: ApproveData, search: string): boolean {
|
accepts(data: ApproveData, search: string): boolean {
|
||||||
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
}
|
}
|
||||||
@@ -53,7 +51,8 @@ class SubmitReasonFilter
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ApproveComponent implements OnInit {
|
export class ApproveComponent implements OnInit {
|
||||||
public approveList: Array<ApproveData> | undefined
|
public approveList: Array<ApproveData> | undefined
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ class SubmitterFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubmitReasonFilter
|
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||||
implements ClrDatagridStringFilterInterface<HistoryData>
|
|
||||||
{
|
|
||||||
accepts(data: HistoryData, search: string): boolean {
|
accepts(data: HistoryData, search: string): boolean {
|
||||||
return data.submittedReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
return data.submittedReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
}
|
}
|
||||||
@@ -65,7 +63,8 @@ class ReviewedFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class HistoryComponent implements OnInit {
|
export class HistoryComponent implements OnInit {
|
||||||
public history: Array<any> = []
|
public history: Array<any> = []
|
||||||
|
|||||||
@@ -17,17 +17,13 @@ interface SubmitterData {
|
|||||||
approver: string
|
approver: string
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubmittedFilter
|
class SubmittedFilter implements ClrDatagridStringFilterInterface<SubmitterData> {
|
||||||
implements ClrDatagridStringFilterInterface<SubmitterData>
|
|
||||||
{
|
|
||||||
accepts(data: SubmitterData, search: string): boolean {
|
accepts(data: SubmitterData, search: string): boolean {
|
||||||
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubmitReasonFilter
|
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<SubmitterData> {
|
||||||
implements ClrDatagridStringFilterInterface<SubmitterData>
|
|
||||||
{
|
|
||||||
accepts(data: SubmitterData, search: string): boolean {
|
accepts(data: SubmitterData, search: string): boolean {
|
||||||
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
}
|
}
|
||||||
@@ -40,7 +36,8 @@ class SubmitReasonFilter
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class SubmitterComponent implements OnInit, AfterViewInit {
|
export class SubmitterComponent implements OnInit, AfterViewInit {
|
||||||
public remained: number = 0
|
public remained: number = 0
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RoleComponent implements OnInit {
|
export class RoleComponent implements OnInit {
|
||||||
public roles: Array<any> | undefined
|
public roles: Array<any> | undefined
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class HomeRouteComponent implements OnInit, OnDestroy {
|
export class HomeRouteComponent implements OnInit, OnDestroy {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { Component, OnInit, OnDestroy } from '@angular/core'
|
|||||||
templateUrl: './multi-dataset-route.component.html',
|
templateUrl: './multi-dataset-route.component.html',
|
||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
}
|
},
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class MultiDatasetRouteComponent implements OnInit, OnDestroy {
|
export class MultiDatasetRouteComponent implements OnInit, OnDestroy {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ReviewRouteComponent implements OnInit {
|
export class ReviewRouteComponent implements OnInit {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class UsernavRouteComponent implements OnInit {
|
export class UsernavRouteComponent implements OnInit {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ViewRouteComponent implements OnInit, OnDestroy {
|
export class ViewRouteComponent implements OnInit, OnDestroy {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class XLMapRouteComponent implements OnInit, OnDestroy {
|
export class XLMapRouteComponent implements OnInit, OnDestroy {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import cloneDeep from 'lodash-es/cloneDeep'
|
import cloneDeep from 'lodash-es/cloneDeep'
|
||||||
import * as CryptoMD5 from 'crypto-js/md5'
|
import CryptoMD5 from 'crypto-js/md5'
|
||||||
import { SasService } from './sas.service'
|
import { SasService } from './sas.service'
|
||||||
|
|
||||||
const librariesToShow = 50
|
const librariesToShow = 50
|
||||||
@@ -11,12 +11,8 @@ const librariesToShow = 50
|
|||||||
export class HelperService {
|
export class HelperService {
|
||||||
public shownLibraries: number = librariesToShow
|
public shownLibraries: number = librariesToShow
|
||||||
public loadMoreCount: number = librariesToShow
|
public loadMoreCount: number = librariesToShow
|
||||||
public isMicrosoft: boolean = false
|
|
||||||
|
|
||||||
constructor(private sasService: SasService) {
|
constructor(private sasService: SasService) {}
|
||||||
this.isMicrosoft = this.isIEorEDGE()
|
|
||||||
console.log('Is IE or Edge?', this.isMicrosoft)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a JavaScript date object to a SAS Date or Datetime, given the logic below:
|
* Converts a JavaScript date object to a SAS Date or Datetime, given the logic below:
|
||||||
@@ -215,32 +211,6 @@ export class HelperService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public isIEorEDGE() {
|
|
||||||
var ua = window.navigator.userAgent
|
|
||||||
|
|
||||||
var msie = ua.indexOf('MSIE ')
|
|
||||||
if (msie > 0) {
|
|
||||||
// IE 10 or older => return version number
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var trident = ua.indexOf('Trident/')
|
|
||||||
if (trident > 0) {
|
|
||||||
// IE 11 => return version number
|
|
||||||
var rv = ua.indexOf('rv:')
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var edge = ua.indexOf('Edge/')
|
|
||||||
if (edge > 0) {
|
|
||||||
// Edge (IE 12+) => return version number
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// other browser
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
public convertObjectsToArray(
|
public convertObjectsToArray(
|
||||||
objectArray: Array<object>,
|
objectArray: Array<object>,
|
||||||
deepClone: boolean = false
|
deepClone: boolean = false
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'
|
|||||||
import { BehaviorSubject } from 'rxjs'
|
import { BehaviorSubject } from 'rxjs'
|
||||||
import { LicenseKeyData } from '../models/LicenseKeyData'
|
import { LicenseKeyData } from '../models/LicenseKeyData'
|
||||||
import { SasService } from './sas.service'
|
import { SasService } from './sas.service'
|
||||||
import * as moment from 'moment'
|
import moment from 'moment'
|
||||||
import * as base64Converter from 'base64-arraybuffer'
|
import * as base64Converter from 'base64-arraybuffer'
|
||||||
import * as encoding from 'text-encoding'
|
import * as encoding from 'text-encoding'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
|
|||||||
@@ -120,9 +120,12 @@ export class SasViyaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getComputeContexts(): Observable<ViyaComputeContexts> {
|
getComputeContexts(): Observable<ViyaComputeContexts> {
|
||||||
return this.get<ViyaComputeContexts>(`${this.serverUrl}/compute/contexts`, {
|
return this.get<ViyaComputeContexts>(
|
||||||
withCredentials: true
|
`${this.serverUrl}/compute/contexts?limit=1000`,
|
||||||
})
|
{
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getComputeContextById(id: string): Observable<ComputeContextDetails> {
|
getComputeContextById(id: string): Observable<ComputeContextDetails> {
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import { AbortDetails, InfoModal } from '../../models/InfoModal'
|
|||||||
selector: 'app-info-modal',
|
selector: 'app-info-modal',
|
||||||
templateUrl: './info-modal.component.html',
|
templateUrl: './info-modal.component.html',
|
||||||
styleUrls: ['./info-modal.component.scss'],
|
styleUrls: ['./info-modal.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class InfoModalComponent implements OnInit {
|
export class InfoModalComponent implements OnInit {
|
||||||
@Output() onConfirmModalClick: EventEmitter<any> = new EventEmitter()
|
@Output() onConfirmModalClick: EventEmitter<any> = new EventEmitter()
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { AlertsService } from './alerts.service'
|
|||||||
selector: 'app-alerts',
|
selector: 'app-alerts',
|
||||||
templateUrl: './alerts.component.html',
|
templateUrl: './alerts.component.html',
|
||||||
styleUrls: ['./alerts.component.scss'],
|
styleUrls: ['./alerts.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class AlertsComponent implements OnInit {
|
export class AlertsComponent implements OnInit {
|
||||||
public alerts: Array<Alert> = []
|
public alerts: Array<Alert> = []
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export type OnLoadingMoreEvent = {
|
|||||||
selector: 'app-autocomplete',
|
selector: 'app-autocomplete',
|
||||||
templateUrl: './autocomplete.component.html',
|
templateUrl: './autocomplete.component.html',
|
||||||
styleUrls: ['./autocomplete.component.scss'],
|
styleUrls: ['./autocomplete.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class AutocompleteComponent implements OnInit, AfterViewInit {
|
export class AutocompleteComponent implements OnInit, AfterViewInit {
|
||||||
@ViewChild('input') inputElement: any
|
@ViewChild('input') inputElement: any
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'
|
|||||||
selector: 'contact-link',
|
selector: 'contact-link',
|
||||||
templateUrl: './contact-link.component.html',
|
templateUrl: './contact-link.component.html',
|
||||||
styleUrls: ['./contact-link.component.scss'],
|
styleUrls: ['./contact-link.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ContactLinkComponent implements OnInit {
|
export class ContactLinkComponent implements OnInit {
|
||||||
@Input() classes: string = ''
|
@Input() classes: string = ''
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { Tab } from './models/dsmeta-groupped.model'
|
|||||||
selector: 'app-dataset-info',
|
selector: 'app-dataset-info',
|
||||||
templateUrl: './dataset-info.component.html',
|
templateUrl: './dataset-info.component.html',
|
||||||
styleUrls: ['./dataset-info.component.scss'],
|
styleUrls: ['./dataset-info.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class DatasetInfoComponent implements OnInit, OnChanges {
|
export class DatasetInfoComponent implements OnInit, OnChanges {
|
||||||
@Input() open: boolean = false
|
@Input() open: boolean = false
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import { TableClickEmitter } from './models/TableClickEmitter'
|
|||||||
selector: 'dc-tree',
|
selector: 'dc-tree',
|
||||||
templateUrl: './dc-tree.component.html',
|
templateUrl: './dc-tree.component.html',
|
||||||
styleUrls: ['./dc-tree.component.scss'],
|
styleUrls: ['./dc-tree.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class DcTreeComponent implements OnInit, AfterViewInit, OnChanges {
|
export class DcTreeComponent implements OnInit, AfterViewInit, OnChanges {
|
||||||
// REFACTOR NOTICE
|
// REFACTOR NOTICE
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from './models/dc-validation.model'
|
} from './models/dc-validation.model'
|
||||||
import { DQRule, DQRuleTypes } from './models/dq-rules.model'
|
import { DQRule, DQRuleTypes } from './models/dq-rules.model'
|
||||||
import { getDqDataCols } from './utils/getDqDataCols'
|
import { getDqDataCols } from './utils/getDqDataCols'
|
||||||
|
import { getNotNullDefault } from './utils/getNotNullDefault'
|
||||||
import { mergeColsRules } from './utils/mergeColsRules'
|
import { mergeColsRules } from './utils/mergeColsRules'
|
||||||
import { parseColType } from './utils/parseColType'
|
import { parseColType } from './utils/parseColType'
|
||||||
import { dqValidate } from './validations/dq-validation'
|
import { dqValidate } from './validations/dq-validation'
|
||||||
@@ -133,6 +134,19 @@ export class DcValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the RULE_VALUE for a NOTNULL rule on the given column.
|
||||||
|
* Used for auto-populating default values when cells are empty.
|
||||||
|
* Converts to number for numeric columns.
|
||||||
|
*
|
||||||
|
* @param col column name
|
||||||
|
* @returns RULE_VALUE (string or number) if NOTNULL rule exists, otherwise undefined
|
||||||
|
*/
|
||||||
|
getNotNullDefaultValue(col: string): string | number | undefined {
|
||||||
|
const colRule = this.getRule(col)
|
||||||
|
return getNotNullDefault(col, this.dqrules, colRule?.type)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves dropdown source for given dc validation rule
|
* Retrieves dropdown source for given dc validation rule
|
||||||
* The values comes from MPE_SELECTBOX table
|
* The values comes from MPE_SELECTBOX table
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import Handsontable from 'handsontable'
|
import Handsontable from 'handsontable'
|
||||||
import Core from 'handsontable/core'
|
import Core from 'handsontable/core'
|
||||||
|
|
||||||
export class CustomAutocompleteEditor extends Handsontable.editors
|
export class CustomAutocompleteEditor
|
||||||
.AutocompleteEditor {
|
extends Handsontable.editors.AutocompleteEditor
|
||||||
|
{
|
||||||
constructor(instance: Core) {
|
constructor(instance: Core) {
|
||||||
super(instance)
|
super(instance)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export interface DcColumnSettings {
|
|||||||
export interface DcValidation extends HotColumnSettings, DcColumnSettings {}
|
export interface DcValidation extends HotColumnSettings, DcColumnSettings {}
|
||||||
|
|
||||||
export interface DcValidationRuleUpdate
|
export interface DcValidationRuleUpdate
|
||||||
extends Handsontable.ColumnSettings,
|
extends Handsontable.ColumnSettings, DcColumnSettings {
|
||||||
DcColumnSettings {
|
|
||||||
data?: string
|
data?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DQRule } from '../models/dq-rules.model'
|
||||||
import { getHotDataSchema } from '../utils/getHotDataSchema'
|
import { getHotDataSchema } from '../utils/getHotDataSchema'
|
||||||
|
|
||||||
describe('DC Validator - hot data schema', () => {
|
describe('DC Validator - hot data schema', () => {
|
||||||
@@ -8,4 +9,58 @@ describe('DC Validator - hot data schema', () => {
|
|||||||
).toEqual(1)
|
).toEqual(1)
|
||||||
expect(getHotDataSchema('missing')).toEqual('')
|
expect(getHotDataSchema('missing')).toEqual('')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('NOTNULL defaults', () => {
|
||||||
|
const dqRules: DQRule[] = [
|
||||||
|
{
|
||||||
|
BASE_COL: 'TEXT_COL',
|
||||||
|
RULE_TYPE: 'NOTNULL',
|
||||||
|
RULE_VALUE: 'default_text',
|
||||||
|
X: 1
|
||||||
|
},
|
||||||
|
{ BASE_COL: 'NUM_COL', RULE_TYPE: 'NOTNULL', RULE_VALUE: '42', X: 1 }
|
||||||
|
]
|
||||||
|
|
||||||
|
it('should return NOTNULL default for text column', () => {
|
||||||
|
expect(getHotDataSchema('text', { data: 'TEXT_COL' }, dqRules)).toEqual(
|
||||||
|
'default_text'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return NOTNULL default as number for numeric column', () => {
|
||||||
|
expect(getHotDataSchema('numeric', { data: 'NUM_COL' }, dqRules)).toEqual(
|
||||||
|
42
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fall back to type default when no NOTNULL rule exists', () => {
|
||||||
|
expect(
|
||||||
|
getHotDataSchema('numeric', { data: 'OTHER_COL' }, dqRules)
|
||||||
|
).toEqual('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prioritize NOTNULL over autocomplete first option', () => {
|
||||||
|
const rulesWithAutocomplete: DQRule[] = [
|
||||||
|
{
|
||||||
|
BASE_COL: 'SELECT_COL',
|
||||||
|
RULE_TYPE: 'NOTNULL',
|
||||||
|
RULE_VALUE: 'priority_value',
|
||||||
|
X: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BASE_COL: 'SELECT_COL',
|
||||||
|
RULE_TYPE: 'HARDSELECT',
|
||||||
|
RULE_VALUE: 'ignored',
|
||||||
|
X: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
expect(
|
||||||
|
getHotDataSchema(
|
||||||
|
'autocomplete',
|
||||||
|
{ data: 'SELECT_COL', source: ['first', 'second'] },
|
||||||
|
rulesWithAutocomplete
|
||||||
|
)
|
||||||
|
).toEqual('priority_value')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { DQRule } from '../models/dq-rules.model'
|
||||||
|
import { getNotNullDefault } from '../utils/getNotNullDefault'
|
||||||
|
|
||||||
|
describe('DC Validator - getNotNullDefault', () => {
|
||||||
|
const dqRules: DQRule[] = [
|
||||||
|
{
|
||||||
|
BASE_COL: 'TEXT_COL',
|
||||||
|
RULE_TYPE: 'NOTNULL',
|
||||||
|
RULE_VALUE: 'default_text',
|
||||||
|
X: 1
|
||||||
|
},
|
||||||
|
{ BASE_COL: 'NUM_COL', RULE_TYPE: 'NOTNULL', RULE_VALUE: '42', X: 1 },
|
||||||
|
{ BASE_COL: 'EMPTY_RULE', RULE_TYPE: 'NOTNULL', RULE_VALUE: ' ', X: 1 },
|
||||||
|
{
|
||||||
|
BASE_COL: 'OTHER_COL',
|
||||||
|
RULE_TYPE: 'HARDSELECT',
|
||||||
|
RULE_VALUE: 'some_value',
|
||||||
|
X: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
it('should return string value for text columns', () => {
|
||||||
|
expect(getNotNullDefault('TEXT_COL', dqRules, 'text')).toEqual(
|
||||||
|
'default_text'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return number for numeric columns when RULE_VALUE is numeric', () => {
|
||||||
|
expect(getNotNullDefault('NUM_COL', dqRules, 'numeric')).toEqual(42)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return string for numeric columns when RULE_VALUE is not numeric', () => {
|
||||||
|
const rulesWithNonNumeric: DQRule[] = [
|
||||||
|
{
|
||||||
|
BASE_COL: 'NUM_COL',
|
||||||
|
RULE_TYPE: 'NOTNULL',
|
||||||
|
RULE_VALUE: 'not_a_number',
|
||||||
|
X: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
expect(
|
||||||
|
getNotNullDefault('NUM_COL', rulesWithNonNumeric, 'numeric')
|
||||||
|
).toEqual('not_a_number')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined for empty RULE_VALUE', () => {
|
||||||
|
expect(getNotNullDefault('EMPTY_RULE', dqRules, 'text')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined for columns without NOTNULL rule', () => {
|
||||||
|
expect(getNotNullDefault('OTHER_COL', dqRules, 'text')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined for non-existent columns', () => {
|
||||||
|
expect(getNotNullDefault('MISSING_COL', dqRules, 'text')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined for empty dqRules array', () => {
|
||||||
|
expect(getNotNullDefault('TEXT_COL', [], 'text')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return string when colType is undefined', () => {
|
||||||
|
expect(getNotNullDefault('NUM_COL', dqRules, undefined)).toEqual('42')
|
||||||
|
})
|
||||||
|
})
|
||||||
39
client/src/app/shared/dc-validator/tests/isEmpty.spec.ts
Normal file
39
client/src/app/shared/dc-validator/tests/isEmpty.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { isEmpty } from '../utils/isEmpty'
|
||||||
|
|
||||||
|
describe('DC Validator - isEmpty', () => {
|
||||||
|
it('should return true for null', () => {
|
||||||
|
expect(isEmpty(null)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for undefined', () => {
|
||||||
|
expect(isEmpty(undefined)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for empty string', () => {
|
||||||
|
expect(isEmpty('')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for whitespace-only string', () => {
|
||||||
|
expect(isEmpty(' ')).toBe(true)
|
||||||
|
expect(isEmpty('\t\n')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for non-empty string', () => {
|
||||||
|
expect(isEmpty('hello')).toBe(false)
|
||||||
|
expect(isEmpty(' hello ')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for number zero', () => {
|
||||||
|
expect(isEmpty(0)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for non-zero numbers', () => {
|
||||||
|
expect(isEmpty(42)).toBe(false)
|
||||||
|
expect(isEmpty(-1)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for boolean values', () => {
|
||||||
|
expect(isEmpty(true)).toBe(false)
|
||||||
|
expect(isEmpty(false)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { DcValidation } from '../models/dc-validation.model'
|
import { DcValidation } from '../models/dc-validation.model'
|
||||||
|
import { DQRule } from '../models/dq-rules.model'
|
||||||
|
import { getNotNullDefault } from './getNotNullDefault'
|
||||||
|
|
||||||
const schemaTypeMap: { [key: string]: any } = {
|
const schemaTypeMap: { [key: string]: any } = {
|
||||||
numeric: '',
|
numeric: '',
|
||||||
@@ -7,14 +9,25 @@ const schemaTypeMap: { [key: string]: any } = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema defines the default values for given types. For example when new row is added.
|
* Schema defines the default values for given types. For example when new row is added.
|
||||||
|
* Priority: NOTNULL RULE_VALUE > autocomplete first option > type default
|
||||||
*/
|
*/
|
||||||
export const getHotDataSchema = (
|
export function getHotDataSchema(
|
||||||
type: string | undefined,
|
type: string | undefined,
|
||||||
cellValidation?: DcValidation
|
cellValidation?: DcValidation,
|
||||||
): any => {
|
dqRules?: DQRule[]
|
||||||
|
): any {
|
||||||
|
// Check for NOTNULL default first
|
||||||
|
if (dqRules && cellValidation?.data) {
|
||||||
|
const defaultValue = getNotNullDefault(cellValidation.data, dqRules, type)
|
||||||
|
if (defaultValue !== undefined) {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!type) return schemaTypeMap.default
|
if (!type) return schemaTypeMap.default
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case 'dropdown':
|
||||||
case 'autocomplete': {
|
case 'autocomplete': {
|
||||||
return cellValidation && cellValidation.source
|
return cellValidation && cellValidation.source
|
||||||
? (cellValidation.source as string[] | number[])[0]
|
? (cellValidation.source as string[] | number[])[0]
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { DQRule } from '../models/dq-rules.model'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the NOTNULL default value for a column from DQ rules.
|
||||||
|
* Converts to number for numeric columns based on colType parameter.
|
||||||
|
*
|
||||||
|
* @param colName column name to look up
|
||||||
|
* @param dqRules array of DQ rules
|
||||||
|
* @param colType column type (e.g., 'numeric', 'text')
|
||||||
|
* @returns default value (string or number) if NOTNULL rule exists with non-empty value, otherwise undefined
|
||||||
|
*/
|
||||||
|
export function getNotNullDefault(
|
||||||
|
colName: string,
|
||||||
|
dqRules: DQRule[],
|
||||||
|
colType?: string
|
||||||
|
): string | number | undefined {
|
||||||
|
const notNullRule = dqRules.find(
|
||||||
|
(rule: DQRule) => rule.BASE_COL === colName && rule.RULE_TYPE === 'NOTNULL'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!notNullRule?.RULE_VALUE || notNullRule.RULE_VALUE.trim().length === 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colType === 'numeric' && !isNaN(Number(notNullRule.RULE_VALUE))) {
|
||||||
|
return Number(notNullRule.RULE_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
return notNullRule.RULE_VALUE
|
||||||
|
}
|
||||||
8
client/src/app/shared/dc-validator/utils/isEmpty.ts
Normal file
8
client/src/app/shared/dc-validator/utils/isEmpty.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Checks if a value is considered empty for NOTNULL validation purposes.
|
||||||
|
* A value is empty if it's null, undefined, or a string that is blank after trimming.
|
||||||
|
*/
|
||||||
|
export function isEmpty(value: unknown): boolean {
|
||||||
|
if (value === null || value === undefined) return true
|
||||||
|
return value.toString().trim().length === 0
|
||||||
|
}
|
||||||
@@ -7,7 +7,8 @@ import { Options } from './models/options.interface'
|
|||||||
selector: 'app-excel-password-modal',
|
selector: 'app-excel-password-modal',
|
||||||
styleUrls: ['./excel-password-modal.component.scss'],
|
styleUrls: ['./excel-password-modal.component.scss'],
|
||||||
templateUrl: './excel-password-modal.component.html',
|
templateUrl: './excel-password-modal.component.html',
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ExcelPasswordModalComponent {
|
export class ExcelPasswordModalComponent {
|
||||||
options$: Observable<Options> = this.excelPasswordModalService.optionsSubject$
|
options$: Observable<Options> = this.excelPasswordModalService.optionsSubject$
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Service } from '../service.interface'
|
|||||||
selector: 'app-loading-indicator',
|
selector: 'app-loading-indicator',
|
||||||
templateUrl: './loading-indicator.component.html',
|
templateUrl: './loading-indicator.component.html',
|
||||||
styleUrls: ['./loading-indicator.component.scss'],
|
styleUrls: ['./loading-indicator.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class LoadingIndicatorComponent implements OnInit, OnDestroy {
|
export class LoadingIndicatorComponent implements OnInit, OnDestroy {
|
||||||
public loading: boolean = false
|
public loading: boolean = false
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ interface User {
|
|||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
styleUrls: ['./login.component.scss'],
|
styleUrls: ['./login.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit, OnDestroy {
|
export class LoginComponent implements OnInit, OnDestroy {
|
||||||
private _subscription: Subscription = new Subscription()
|
private _subscription: Subscription = new Subscription()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
ViewEncapsulation
|
ViewEncapsulation
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { SASjsRequest } from '@sasjs/adapter'
|
import { SASjsRequest } from '@sasjs/adapter'
|
||||||
import * as moment from 'moment'
|
import moment from 'moment'
|
||||||
import { HelperService } from 'src/app/services/helper.service'
|
import { HelperService } from 'src/app/services/helper.service'
|
||||||
import { LoggerService } from '../../services/logger.service'
|
import { LoggerService } from '../../services/logger.service'
|
||||||
import { SasService } from '../../services/sas.service'
|
import { SasService } from '../../services/sas.service'
|
||||||
@@ -23,7 +23,8 @@ interface SASjsRequestExtended extends SASjsRequest {
|
|||||||
selector: 'app-requests-modal',
|
selector: 'app-requests-modal',
|
||||||
templateUrl: './requests-modal.component.html',
|
templateUrl: './requests-modal.component.html',
|
||||||
styleUrls: ['./requests-modal.component.scss'],
|
styleUrls: ['./requests-modal.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RequestsModalComponent implements OnInit {
|
export class RequestsModalComponent implements OnInit {
|
||||||
private _opened: boolean = false
|
private _opened: boolean = false
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import { globals } from '../../_globals'
|
|||||||
selector: 'app-sidebar',
|
selector: 'app-sidebar',
|
||||||
templateUrl: './sidebar.component.html',
|
templateUrl: './sidebar.component.html',
|
||||||
styleUrls: ['./sidebar.component.scss'],
|
styleUrls: ['./sidebar.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class SidebarComponent implements OnInit {
|
export class SidebarComponent implements OnInit {
|
||||||
@ViewChild('sidebarNav') sidebarNav!: ElementRef
|
@ViewChild('sidebarNav') sidebarNav!: ElementRef
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { OnLoadingMoreEvent } from '../autocomplete/autocomplete.component'
|
|||||||
selector: 'app-soft-select',
|
selector: 'app-soft-select',
|
||||||
templateUrl: './soft-select.component.html',
|
templateUrl: './soft-select.component.html',
|
||||||
styleUrls: ['./soft-select.component.scss'],
|
styleUrls: ['./soft-select.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class SoftSelectComponent implements OnInit, OnChanges {
|
export class SoftSelectComponent implements OnInit, OnChanges {
|
||||||
@Input() inputId: string = ''
|
@Input() inputId: string = ''
|
||||||
|
|||||||
@@ -375,38 +375,30 @@ export class SpreadsheetUtil {
|
|||||||
fileType: string
|
fileType: string
|
||||||
): Promise<ParseResult> {
|
): Promise<ParseResult> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (this.licenceState.value.submit_rows_limit !== Infinity) {
|
if (!this.licenceState.value.fileUpload) {
|
||||||
uploader.queue.pop()
|
uploader.queue.pop()
|
||||||
return reject(
|
return reject(
|
||||||
'Excel files only. To unlock CSV uploads, please contact support@datacontroller.io'
|
'File uploads are not enabled for this licence. Please contact support@datacontroller.io'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseParams.encoding === 'WLATIN1') {
|
if (parseParams.encoding !== 'WLATIN1') return resolve({ uploader })
|
||||||
let reader = new FileReader()
|
|
||||||
const self = this
|
|
||||||
// Closure to capture the file information.
|
|
||||||
reader.onload = (theFile: any) => {
|
|
||||||
let encoded = iconv.decode(
|
|
||||||
Buffer.from(theFile.target.result),
|
|
||||||
'CP-1252'
|
|
||||||
)
|
|
||||||
let blob = new Blob([encoded], { type: fileType })
|
|
||||||
let encodedFile: File = blobToFile(blob, parseParams.file.name)
|
|
||||||
uploader.queue.pop()
|
|
||||||
uploader.addToQueue([encodedFile])
|
|
||||||
|
|
||||||
return resolve({
|
const reader = new FileReader()
|
||||||
uploader
|
reader.onload = (theFile) => {
|
||||||
})
|
if (!theFile.target?.result) return resolve({ uploader })
|
||||||
}
|
|
||||||
|
|
||||||
reader.readAsArrayBuffer(parseParams.file)
|
const text = theFile.target.result as string
|
||||||
} else {
|
const encoded = iconv.encode(text, 'CP-1252')
|
||||||
return resolve({
|
const blob = new Blob([encoded], { type: fileType })
|
||||||
uploader
|
const encodedFile: File = blobToFile(blob, parseParams.file.name)
|
||||||
})
|
uploader.queue.pop()
|
||||||
|
uploader.addToQueue([encodedFile])
|
||||||
|
|
||||||
|
return resolve({ uploader })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reader.readAsText(parseParams.file)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,6 +503,21 @@ export class SpreadsheetUtil {
|
|||||||
return resolve(XLSX.read(data, opts))
|
return resolve(XLSX.read(data, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TEMPORARILY DISABLED: Web Worker for XLSX parsing
|
||||||
|
// Worker is disabled because Angular/webpack bundles it as a separate chunk
|
||||||
|
// with a numeric filename (e.g., 411.hash.js). In SAS9/Viya streaming
|
||||||
|
// environments, all JS files need to be served through SASJobExecution
|
||||||
|
// with _program= parameter, but our post-build processor can't reliably
|
||||||
|
// find and replace the worker chunk reference in the minified output.
|
||||||
|
// FIX: Add "namedChunks": true to production config in angular.json
|
||||||
|
// (under projects.datacontroller.architect.build.configurations.production)
|
||||||
|
// This will output worker as "spreadsheet-worker.hash.js" instead of
|
||||||
|
// numeric ID, making it findable by post-processor.
|
||||||
|
// Trade-off: UI may briefly freeze when parsing large Excel files.
|
||||||
|
|
||||||
|
return resolve(XLSX.read(data, opts))
|
||||||
|
|
||||||
|
/*
|
||||||
if (typeof Worker === 'undefined') {
|
if (typeof Worker === 'undefined') {
|
||||||
console.info(
|
console.info(
|
||||||
'Not using worker to parse the XLSX - no Worker available in this environment'
|
'Not using worker to parse the XLSX - no Worker available in this environment'
|
||||||
@@ -551,6 +558,7 @@ export class SpreadsheetUtil {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
return resolve(XLSX.read(data, opts))
|
return resolve(XLSX.read(data, opts))
|
||||||
}, 600 * 1000) // 10 minutes
|
}, 600 * 1000) // 10 minutes
|
||||||
|
*/
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { RequestWrapperResponse } from 'src/app/models/request-wrapper/RequestWr
|
|||||||
selector: 'app-terms',
|
selector: 'app-terms',
|
||||||
templateUrl: './terms.component.html',
|
templateUrl: './terms.component.html',
|
||||||
styleUrls: ['./terms.component.scss'],
|
styleUrls: ['./terms.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class TermsComponent implements OnInit, AfterViewInit {
|
export class TermsComponent implements OnInit, AfterViewInit {
|
||||||
@ViewChild('markdownCard') markdownCard!: ElementRef
|
@ViewChild('markdownCard') markdownCard!: ElementRef
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import { globals } from 'src/app/_globals'
|
|||||||
selector: 'app-header-actions',
|
selector: 'app-header-actions',
|
||||||
templateUrl: './header-actions.component.html',
|
templateUrl: './header-actions.component.html',
|
||||||
styleUrls: ['./header-actions.component.scss'],
|
styleUrls: ['./header-actions.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class HeaderActions implements OnInit, OnDestroy {
|
export class HeaderActions implements OnInit, OnDestroy {
|
||||||
public userName: string = 'Not logged in'
|
public userName: string = 'Not logged in'
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ import { Viewbox } from './models/viewbox.model'
|
|||||||
selector: 'app-viewboxes',
|
selector: 'app-viewboxes',
|
||||||
templateUrl: './viewboxes.component.html',
|
templateUrl: './viewboxes.component.html',
|
||||||
styleUrls: ['./viewboxes.component.scss'],
|
styleUrls: ['./viewboxes.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
@ViewChildren('resizeBox') resizeBoxQuery!: QueryList<ElementRef> //make query list, handle multiple
|
@ViewChildren('resizeBox') resizeBoxQuery!: QueryList<ElementRef> //make query list, handle multiple
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
* We use normal version of the XLSX (SheetJS)
|
* We use normal version of the XLSX (SheetJS)
|
||||||
* Because at the moment "@sheet/crypto" can't work in the Web Worker environment
|
* Because at the moment "@sheet/crypto" can't work in the Web Worker environment
|
||||||
* Because of the missing "global" variable.
|
* Because of the missing "global" variable.
|
||||||
|
*
|
||||||
|
* Version bumped to v0.20.3 (`libraries/xlsx-0.20.3.tgz`)
|
||||||
|
* @see https://cdn.sheetjs.com/
|
||||||
*/
|
*/
|
||||||
import * as XLSX from 'xlsx'
|
import * as XLSX from 'xlsx'
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class StageComponent implements OnInit, AfterViewInit {
|
export class StageComponent implements OnInit, AfterViewInit {
|
||||||
public table_id: any
|
public table_id: any
|
||||||
|
|||||||
@@ -236,7 +236,19 @@
|
|||||||
<div class="admin-action">
|
<div class="admin-action">
|
||||||
Download Configuration
|
Download Configuration
|
||||||
|
|
||||||
<button (click)="downloadConfiguration()" class="btn btn-info btn-sm">
|
<input
|
||||||
|
type="text"
|
||||||
|
class="clr-input libref-input"
|
||||||
|
maxlength="8"
|
||||||
|
[ngModel]="dcLib"
|
||||||
|
(ngModelChange)="targetLibref = $event.toUpperCase()"
|
||||||
|
placeholder="Target Libref"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
(click)="downloadConfiguration()"
|
||||||
|
[disabled]="targetLibref !== dcLib && !isValidLibref(targetLibref)"
|
||||||
|
class="btn btn-info btn-sm"
|
||||||
|
>
|
||||||
DOWNLOAD
|
DOWNLOAD
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.libref-input {
|
||||||
|
width: 100px;
|
||||||
|
margin: 0 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { EnvironmentInfo } from './models/environment-info.model'
|
|||||||
import { AppSettingsService } from '../services/app-settings.service'
|
import { AppSettingsService } from '../services/app-settings.service'
|
||||||
import { AppSettings } from '../models/AppSettings'
|
import { AppSettings } from '../models/AppSettings'
|
||||||
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
|
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
|
||||||
|
import { globals } from '../_globals'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-system',
|
selector: 'app-system',
|
||||||
@@ -18,7 +19,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class SystemComponent implements OnInit {
|
export class SystemComponent implements OnInit {
|
||||||
appInfo: AppInfo = {
|
appInfo: AppInfo = {
|
||||||
@@ -38,6 +40,8 @@ export class SystemComponent implements OnInit {
|
|||||||
responseModal: boolean = false
|
responseModal: boolean = false
|
||||||
|
|
||||||
Infinity = Infinity
|
Infinity = Infinity
|
||||||
|
dcLib: string = globals.dcLib
|
||||||
|
targetLibref: string = globals.dcLib
|
||||||
|
|
||||||
licenceState = this.licenceService.licenceState
|
licenceState = this.licenceService.licenceState
|
||||||
settings: AppSettings
|
settings: AppSettings
|
||||||
@@ -70,13 +74,21 @@ export class SystemComponent implements OnInit {
|
|||||||
this.appSettingsService.setAppSettings(this.settings)
|
this.appSettingsService.setAppSettings(this.settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isValidLibref(value: string): boolean {
|
||||||
|
return /^[A-Za-z_]\w{0,7}$/.test(value.trim())
|
||||||
|
}
|
||||||
|
|
||||||
downloadConfiguration() {
|
downloadConfiguration() {
|
||||||
let sasjsConfig = this.sasService.getSasjsConfig()
|
let sasjsConfig = this.sasService.getSasjsConfig()
|
||||||
let storage = sasjsConfig.serverUrl
|
let storage = sasjsConfig.serverUrl
|
||||||
let metaData = sasjsConfig.appLoc
|
let metaData = sasjsConfig.appLoc
|
||||||
let path = this.sasService.getExecutionPath()
|
let path = this.sasService.getExecutionPath()
|
||||||
|
let lib = this.targetLibref.toUpperCase().trim()
|
||||||
let downUrl =
|
let downUrl =
|
||||||
storage + path + '/?_program=' + metaData + '/services/admin/exportconfig'
|
storage + path + '/?_program=' + metaData + '/services/admin/exportconfig'
|
||||||
|
if (lib && lib !== this.dcLib && this.isValidLibref(lib)) {
|
||||||
|
downUrl += '&dclib=' + encodeURIComponent(lib)
|
||||||
|
}
|
||||||
window.open(downUrl)
|
window.open(downUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class UserComponent implements OnInit {
|
export class UserComponent implements OnInit {
|
||||||
public users: Array<any> | undefined
|
public users: Array<any> | undefined
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ViewerComponent
|
export class ViewerComponent
|
||||||
implements AfterContentInit, AfterViewInit, OnDestroy
|
implements AfterContentInit, AfterViewInit, OnDestroy
|
||||||
@@ -157,15 +158,26 @@ export class ViewerComponent
|
|||||||
return ' '
|
return ' '
|
||||||
},
|
},
|
||||||
afterGetColHeader: (col: number, th: any, headerLevel: number) => {
|
afterGetColHeader: (col: number, th: any, headerLevel: number) => {
|
||||||
const column = this.hotInstance?.colToProp(col) as string
|
// CRITICAL: Prevent "colToProp method cannot be called because this Handsontable instance has been destroyed" error
|
||||||
|
// This callback can be triggered even after the instance is destroyed during rapid table switching
|
||||||
|
if (!this.hotInstance || this.hotInstance.isDestroyed) {
|
||||||
|
// Graceful fallback: apply only dark mode styling when instance is unavailable
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// header columns styling - primary keys
|
try {
|
||||||
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
const column = this.hotInstance.colToProp(col) as string
|
||||||
|
|
||||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
||||||
|
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||||
|
|
||||||
// Dark mode
|
// Apply dark mode styling to all headers
|
||||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
} catch (error) {
|
||||||
|
// Safety net: if colToProp() fails, still apply basic styling
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
rowHeaderWidth: 15,
|
rowHeaderWidth: 15,
|
||||||
rowHeights: 20,
|
rowHeights: 20,
|
||||||
@@ -199,12 +211,21 @@ export class ViewerComponent
|
|||||||
let colInfo: DataFormat | undefined
|
let colInfo: DataFormat | undefined
|
||||||
let textInfo = 'No info found'
|
let textInfo = 'No info found'
|
||||||
|
|
||||||
if (this.hotInstance) {
|
if (
|
||||||
const hotSelected: [number, number, number, number][] =
|
this.hotInstance &&
|
||||||
this.hotInstance.getSelected() || []
|
!this.hotInstance.isDestroyed &&
|
||||||
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
|
!this.isTableSwitching
|
||||||
const colName = this.hotInstance?.colToProp(selectedCol)
|
) {
|
||||||
colInfo = this.$dataFormats?.vars[colName]
|
try {
|
||||||
|
const hotSelected: [number, number, number, number][] =
|
||||||
|
this.hotInstance.getSelected() || []
|
||||||
|
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
|
||||||
|
const colName = this.hotInstance.colToProp(selectedCol)
|
||||||
|
colInfo = this.$dataFormats?.vars[colName]
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors during table switching
|
||||||
|
colInfo = undefined
|
||||||
|
}
|
||||||
|
|
||||||
if (colInfo)
|
if (colInfo)
|
||||||
textInfo = `LABEL: ${colInfo?.label}<br>TYPE: ${colInfo?.type}<br>LENGTH: ${colInfo?.length}<br>FORMAT: ${colInfo?.format}`
|
textInfo = `LABEL: ${colInfo?.label}<br>TYPE: ${colInfo?.type}<br>LENGTH: ${colInfo?.length}<br>FORMAT: ${colInfo?.format}`
|
||||||
@@ -224,6 +245,13 @@ export class ViewerComponent
|
|||||||
private hotInstance: Handsontable | null = null
|
private hotInstance: Handsontable | null = null
|
||||||
public hotInstanceClickListener: boolean = false
|
public hotInstanceClickListener: boolean = false
|
||||||
|
|
||||||
|
// Race condition prevention for rapid table switching
|
||||||
|
private isTableSwitching: boolean = false
|
||||||
|
private switchingTimeout: any = null
|
||||||
|
|
||||||
|
// Prevents duplicate setupHot() calls within short time windows
|
||||||
|
private lastSetupTime: number = 0
|
||||||
|
|
||||||
public viewboxOpen: boolean = false
|
public viewboxOpen: boolean = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -505,17 +533,7 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public copyToClip() {
|
public copyToClip() {
|
||||||
let selBox = document.createElement('textarea')
|
navigator.clipboard.writeText(this.webQueryText)
|
||||||
selBox.style.position = 'fixed'
|
|
||||||
selBox.style.left = '0'
|
|
||||||
selBox.style.top = '0'
|
|
||||||
selBox.style.opacity = '0'
|
|
||||||
selBox.value = this.webQueryText
|
|
||||||
document.body.appendChild(selBox)
|
|
||||||
selBox.focus()
|
|
||||||
selBox.select()
|
|
||||||
document.execCommand('copy')
|
|
||||||
document.body.removeChild(selBox)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public goToViewer() {
|
public goToViewer() {
|
||||||
@@ -599,10 +617,24 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onTableClick(libTable: any, library: any) {
|
public onTableClick(libTable: any, library: any) {
|
||||||
this.lib = library.LIBRARYREF
|
// OPTIMIZATION: Prevent race conditions and destroyed instance errors during rapid table switching
|
||||||
this.table = libTable
|
if (this.isTableSwitching) {
|
||||||
this.selectLibTable(libTable)
|
return
|
||||||
this.viewData(0)
|
}
|
||||||
|
|
||||||
|
// Clear any existing timeout to prevent stale operations
|
||||||
|
if (this.switchingTimeout) {
|
||||||
|
clearTimeout(this.switchingTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PERFORMANCE: Debounce table switches to prevent rapid successive calls
|
||||||
|
// This ensures only the final table selection is processed
|
||||||
|
this.switchingTimeout = setTimeout(() => {
|
||||||
|
this.lib = library.LIBRARYREF
|
||||||
|
this.table = libTable
|
||||||
|
this.selectLibTable(libTable)
|
||||||
|
this.viewData(0)
|
||||||
|
}, 50) // 50ms debounce - fast enough for good UX, slow enough to prevent issues
|
||||||
}
|
}
|
||||||
|
|
||||||
public async selectTable(lib: string, initial?: boolean, library?: any) {
|
public async selectTable(lib: string, initial?: boolean, library?: any) {
|
||||||
@@ -721,6 +753,10 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async viewData(filter_pk: number) {
|
public async viewData(filter_pk: number) {
|
||||||
|
// CRITICAL: Set switching flag to prevent concurrent operations and race conditions
|
||||||
|
// This prevents callbacks from accessing destroyed instances during table switching
|
||||||
|
this.isTableSwitching = true
|
||||||
|
|
||||||
this.loadingTableView = true
|
this.loadingTableView = true
|
||||||
|
|
||||||
let libDataset: any
|
let libDataset: any
|
||||||
@@ -961,14 +997,25 @@ export class ViewerComponent
|
|||||||
|
|
||||||
this.loadingTableView = false
|
this.loadingTableView = false
|
||||||
|
|
||||||
//If we try to setup hot when no data is returned it errors `isDestoryed`.
|
// Setup Handsontable after async operations complete
|
||||||
//That is intorduced by HOT update
|
// Original issue: setupHot() called before API responses populated headerPks array
|
||||||
if (!this.noData && !this.noDataReqErr && libDataset) this.setupHot()
|
// Solution: Delay ensures both API paths (lines 328 & 886) have chance to set headerPks
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.noData && !this.noDataReqErr && libDataset) {
|
||||||
|
this.setupHot()
|
||||||
|
}
|
||||||
|
}, 50) // Optimized from 100ms - fast enough for API completion, slow enough to prevent race conditions
|
||||||
|
|
||||||
|
// RACE CONDITION PREVENTION: Reset switching flag after setup completion
|
||||||
|
// This allows new table switches after current operation finishes
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isTableSwitching = false
|
||||||
|
}, 300) // Optimized from 700ms to match reduced setup times
|
||||||
|
|
||||||
// Fix ARIA accessibility issues after data loading
|
// Fix ARIA accessibility issues after data loading
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.fixAriaAccessibility()
|
this.fixAriaAccessibility()
|
||||||
}, 1500)
|
}, 500)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area.
|
* This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area.
|
||||||
@@ -988,7 +1035,7 @@ export class ViewerComponent
|
|||||||
})
|
})
|
||||||
this.hotInstanceClickListener = true
|
this.hotInstanceClickListener = true
|
||||||
}
|
}
|
||||||
}, 2000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1116,51 +1163,135 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CRITICAL CLEANUP (workaround needed for HOT version 16 and above): Safely destroys Handsontable instances
|
||||||
|
*
|
||||||
|
* Purpose: Prevents "instance destroyed" errors and memory leaks during table switching
|
||||||
|
*
|
||||||
|
* Called from:
|
||||||
|
* - ngOnDestroy() - component cleanup
|
||||||
|
*
|
||||||
|
* Safety features:
|
||||||
|
* - Checks if instance exists and is not already destroyed
|
||||||
|
* - Try-catch prevents errors during destruction
|
||||||
|
* - Sets instance to null to prevent stale references
|
||||||
|
*/
|
||||||
|
private cleanupHotInstance() {
|
||||||
|
if (this.hotInstance && !this.hotInstance.isDestroyed) {
|
||||||
|
try {
|
||||||
|
this.hotInstance.destroy()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error destroying Handsontable instance:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.hotInstance = null
|
||||||
|
this.hooksAttached = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PERFORMANCE: Configures Handsontable with enhanced error handling (workaround needed for HOT version 16 and above)
|
||||||
|
*
|
||||||
|
* 1. Duplicate call prevention (500ms window)
|
||||||
|
* 2. Multiple validation checks to prevent race conditions
|
||||||
|
* 3. Forced render for immediate primary key styling
|
||||||
|
*
|
||||||
|
* Instance lifecycle is managed by Angular's hot-table component via [data] and [settings] bindings.
|
||||||
|
* This method only applies additional config that can't go through bindings (hooks, PK styling).
|
||||||
|
*/
|
||||||
private setupHot() {
|
private setupHot() {
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - this.lastSetupTime < 500) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.lastSetupTime = now
|
||||||
|
|
||||||
|
// VALIDATION: Don't setup if we're currently switching tables or still loading
|
||||||
|
if (this.loadingTableView || !this.libDataset) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.hotInstance || this.hotInstance.isDestroyed) {
|
||||||
|
this.hotInstance = this.hotTableComponent?.hotInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hotInstance && !this.hotInstance.isDestroyed) {
|
||||||
|
this.configureHotInstance()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance not ready yet — Angular may still be creating the component
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.loadingTableView && this.libDataset) {
|
if (this.isTableSwitching || this.loadingTableView || !this.libDataset) {
|
||||||
this.hotInstance = this.hotTableComponent?.hotInstance
|
return
|
||||||
|
|
||||||
if (this.hotInstance) {
|
|
||||||
this.hotInstance.updateSettings({
|
|
||||||
height: this.hotTable.height,
|
|
||||||
modifyColWidth: (width: any, col: any) => {
|
|
||||||
if (width > 500) return 500
|
|
||||||
else return width
|
|
||||||
},
|
|
||||||
afterGetColHeader: (col: number, th: any) => {
|
|
||||||
const column = this.hotInstance?.colToProp(col) as string
|
|
||||||
|
|
||||||
// header columns styling - primary keys
|
|
||||||
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
|
||||||
|
|
||||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
|
||||||
|
|
||||||
// Dark mode
|
|
||||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add hooks for accessibility fixes
|
|
||||||
this.hotInstance.addHook('afterRender', () => {
|
|
||||||
// Fix ARIA accessibility issues after each render
|
|
||||||
this.fixAriaAccessibility()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.hotInstance.addHook('afterChange', () => {
|
|
||||||
// Fix ARIA accessibility issues after any data change
|
|
||||||
setTimeout(() => {
|
|
||||||
this.fixAriaAccessibility()
|
|
||||||
}, 50)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix ARIA accessibility issues after table setup
|
this.hotInstance = this.hotTableComponent?.hotInstance
|
||||||
setTimeout(() => {
|
this.configureHotInstance()
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
private hooksAttached = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies settings that can't go through Angular [settings] binding:
|
||||||
|
* - Primary key column header styling
|
||||||
|
* - Column width cap
|
||||||
|
* - ARIA accessibility hooks (attached once per instance)
|
||||||
|
*/
|
||||||
|
private configureHotInstance() {
|
||||||
|
if (!this.hotInstance || this.hotInstance.isDestroyed) return
|
||||||
|
|
||||||
|
this.hotInstance.updateSettings({
|
||||||
|
height: this.hotTable.height,
|
||||||
|
modifyColWidth: (width: any, col: any) => {
|
||||||
|
if (width > 500) return 500
|
||||||
|
else return width
|
||||||
|
},
|
||||||
|
afterGetColHeader: (col: number, th: any) => {
|
||||||
|
// CRITICAL: Same safety checks as initial callback to prevent destroyed instance errors
|
||||||
|
if (!this.hotInstance || this.hotInstance.isDestroyed) {
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const column = this.hotInstance.colToProp(col) as string
|
||||||
|
|
||||||
|
// PRIMARY KEY STYLING: Apply special styling to PK columns (populated from API response)
|
||||||
|
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
||||||
|
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||||
|
|
||||||
|
// DARK MODE: Apply to all headers
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
} catch (error) {
|
||||||
|
// SAFETY NET: Ensure basic styling is always applied
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add hooks for accessibility fixes
|
||||||
|
// Hooks are attached once per instance to avoid accumulating duplicate listeners
|
||||||
|
if (!this.hooksAttached) {
|
||||||
|
this.hotInstance.addHook('afterRender', () => {
|
||||||
|
// Fix ARIA accessibility issues after each render
|
||||||
this.fixAriaAccessibility()
|
this.fixAriaAccessibility()
|
||||||
}, 500)
|
})
|
||||||
}, 1000)
|
|
||||||
|
this.hotInstance.addHook('afterChange', () => {
|
||||||
|
// Fix ARIA accessibility issues after any data change
|
||||||
|
setTimeout(() => {
|
||||||
|
this.fixAriaAccessibility()
|
||||||
|
}, 50)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.hooksAttached = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force immediate render to apply primary key styling
|
||||||
|
// Without this, styling would wait for ~2 seconds to be applied
|
||||||
|
// (workaround needed for HOT version 16 and above)
|
||||||
|
this.hotInstance.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadWithParameters() {
|
async loadWithParameters() {
|
||||||
@@ -1233,13 +1364,27 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
// Clean up the MutationObserver
|
// Proper component destruction to prevent memory leaks and errors
|
||||||
|
|
||||||
|
// Prevent any new operations during cleanup
|
||||||
|
this.isTableSwitching = true
|
||||||
|
|
||||||
|
// Clear any pending debounced table switches
|
||||||
|
if (this.switchingTimeout) {
|
||||||
|
clearTimeout(this.switchingTimeout)
|
||||||
|
this.switchingTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safely destroy Handsontable instance
|
||||||
|
this.cleanupHotInstance()
|
||||||
|
|
||||||
|
// Clean up ARIA accessibility observers
|
||||||
if (this.ariaObserver) {
|
if (this.ariaObserver) {
|
||||||
this.ariaObserver.disconnect()
|
this.ariaObserver.disconnect()
|
||||||
this.ariaObserver = undefined
|
this.ariaObserver = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up the interval
|
// Clear ARIA check intervals
|
||||||
if (this.ariaCheckInterval) {
|
if (this.ariaCheckInterval) {
|
||||||
clearInterval(this.ariaCheckInterval)
|
clearInterval(this.ariaCheckInterval)
|
||||||
this.ariaCheckInterval = undefined
|
this.ariaCheckInterval = undefined
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { ViyaApis } from './models/viya-apis.models'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ViyaApiExplorerComponent implements OnInit {
|
export class ViyaApiExplorerComponent implements OnInit {
|
||||||
collections: ViyaApis = {}
|
collections: ViyaApis = {}
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ enum Tabs {
|
|||||||
selector: 'app-xlmap',
|
selector: 'app-xlmap',
|
||||||
templateUrl: './xlmap.component.html',
|
templateUrl: './xlmap.component.html',
|
||||||
styleUrls: ['./xlmap.component.scss'],
|
styleUrls: ['./xlmap.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
|
export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
|
||||||
@HostBinding('class.content-container') contentContainerClass = true
|
@HostBinding('class.content-container') contentContainerClass = true
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user