Compare commits

..

1 Commits

Author SHA1 Message Date
henrik f246e75eab fix: handle national language datetime formats NLDATM and NLDATE
Build / Build-and-ng-test (pull_request) Successful in 4m56s
Build / Build-and-test-development (pull_request) Successful in 9m40s
2025-03-05 15:59:13 +01:00
248 changed files with 16485 additions and 23929 deletions
+7 -19
View File
@@ -1,23 +1,11 @@
#!/bin/sh #!/bin/sh
# Using `--silent` helps for showing any errs in the first line of the response # Avoid commits to the master branch
# The first line is picked up by the VS Code GIT UI popup when rc is not 0 BRANCH=`git rev-parse --abbrev-ref HEAD`
REGEX="^(master|development)$"
if npm run --silent lint:check:silent ; then if [[ "$BRANCH" =~ $REGEX ]]; then
exit 0 echo "You are on branch $BRANCH. Are you sure you want to commit to this branch?"
else echo "If so, commit with -n to bypass the pre-commit hook."
npm run --silent lint:fix:silent
echo "❌ Prettier check failed! We ran lint:fix for you. Please add & commit again."
exit 1
fi
## Avoid large commits
# https://www.backblaze.com/blog/how-many-bytes-are-in-a-megabyte-really/
size_limit=$((2 * 2**20)) # 2mbs
# https://git-scm.com/docs/git-rev-list#Documentation/git-rev-list.txt---disk-usage
commit_size=$(git rev-list --disk-usage HEAD^..HEAD)
test "$commit_size" -lt "$size_limit" || (
echo "Commit size is too large: $commit_size > $size_limit"
echo "Force commit using --no-verify"
exit 1 exit 1
) fi
-1
View File
@@ -1 +0,0 @@
* text=auto eol=lf
+4 -4
View File
@@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 24.5.0 node-version: 20.14.0
- name: Install Google Chrome - name: Install Google Chrome
run: | run: |
@@ -58,7 +58,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 24.5.0 node-version: 20.14.0
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
@@ -70,7 +70,7 @@ jobs:
- run: apt install -y ./google-chrome*.deb; - run: apt install -y ./google-chrome*.deb;
- run: export CHROME_BIN=/usr/bin/google-chrome - run: export CHROME_BIN=/usr/bin/google-chrome
- run: apt-get update -y - run: apt-get update -y
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb - run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
- run: apt -y install jq - run: apt -y install jq
- name: Write cypress credentials - name: Write cypress credentials
@@ -126,7 +126,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" npm start & 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"
- name: Zip Cypress videos - name: Zip Cypress videos
if: always() if: always()
-101
View File
@@ -1,101 +0,0 @@
name: Lighthouse Checks
run-name: Running Lighthouse Performance and Accessibility Checks on Pull Request
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [24.5.0]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install Google Chrome
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 install -y google-chrome-stable xvfb
- name: Install pm2 for process management
run: npm i -g pm2
- name: Install @sasjs/cli
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: |
touch .env
echo RUN_TIMES=js >> .env
echo NODE_PATH=node >> .env
echo CORS=enable >> .env
echo WHITELIST=http://localhost:4200 >> .env
cat .env
- name: Download sasjs/server package from github using curl
run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
- name: Unzip downloaded package
run: unzip linux.zip
- name: Run sasjs server
run: pm2 start api-linux --wait-ready
- name: Write .npmrc file
run: echo "$NPMRC" > client/.npmrc
shell: bash
env:
NPMRC: ${{ secrets.NPMRC}}
- name: Install npm dependencies
run: |
cd client
# Decrypt and Install sheet
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
npm ci
npm install -g replace-in-files-cli
- name: Update appLoc in index.html
run: |
cd client
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/proj/sasjs/genesis-mocks"' ./src/index.html
- name: Build Frontend
run: |
cd client
npm run build
- name: Deploy JS mocked services and frontend to the local SASjs Server instance
run: |
cd sas/mocks
npm ci
sasjs cbd -t server-ci
- name: Start frontend server
run: |
cd client
npx ng serve --host 0.0.0.0 --port 4200 &
wait-on http://localhost:4200
- name: Run Lighthouse CI
run: |
cd client
npx lhci autorun
- name: Lighthouse Result Artifacts
if: always()
uses: actions/upload-artifact@v3
with:
name: Lighthouse results
path: client/lighthouse-reports
include-hidden-files: true
+19 -21
View File
@@ -13,12 +13,12 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 24.5.0 node-version: 20.14.0
- 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: |
@@ -41,7 +41,7 @@ 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 --audit-level=critical --omit=dev
cd ./sas cd ./sas
@@ -68,19 +68,19 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 24.5.0 node-version: 20.14.0
- 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 - run: apt-get update
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- run: apt install -y ./google-chrome*.deb; - run: apt install -y ./google-chrome*.deb;
- run: export CHROME_BIN=/usr/bin/google-chrome - run: export CHROME_BIN=/usr/bin/google-chrome
- run: apt-get update -y - run: apt-get update -y
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb - run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
- run: apt -y install jq - run: apt -y install jq
- name: Write cypress credentials - name: Write cypress credentials
@@ -136,7 +136,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" npm start & 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"
- name: Zip Cypress videos - name: Zip Cypress videos
if: always() if: always()
@@ -158,7 +158,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 24.5.0 node-version: 20.14.0
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
@@ -228,8 +228,6 @@ 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
@@ -239,20 +237,20 @@ jobs:
cd sas cd sas
sasjs c -t viya sasjs c -t viya
rm -rf sasjsbuild/tests rm -rf sasjsbuild/tests
sed -i -e 's/servertype="SASJS"/servertype="SASVIYA"/g' sasjsbuild/services/DC.html sed -i -e 's/servertype="SASJS"/servertype="SASVIYA"/g' sasjsbuild/services/clickme.html
sasjs b -t viya sasjs b -t viya
cp sasjsbuild/viya.sas ./viya.sas cp sasjsbuild/viya.sas ./demostream_viya.sas
# compile Viya Full deploy (without web) # compile Viya Full deploy (without web)
rm -rf sasjsbuild/services/web rm -rf sasjsbuild/services/web
rm sasjsbuild/services/DC.html rm sasjsbuild/services/clickme.html
sasjs b -t viya sasjs b -t viya
cp sasjsbuild/viya.sas ./viya_noweb.sas cp sasjsbuild/viya.sas ./viya.sas
cp sasjsbuild/viya.json ./viya_noweb.json cp sasjsbuild/viya.json ./viya.json
- name: Zip Frontend (including viya.json for full viya deploy) - name: Zip Frontend (including viya.json for full viya deploy)
run: | run: |
cd sas cd sas
cp sasjsbuild/viya.json ../client/dist/viya.json cp sasjsbuild/viya.json ../client/dist
cd .. cd ..
zip -r frontend.zip ./client/dist zip -r frontend.zip ./client/dist
@@ -279,8 +277,8 @@ jobs:
URL="https://git.datacontroller.io/api/v1/repos/dc/dc/releases/$RELEASE_ID/assets?access_token=${{ secrets.RELEASE_TOKEN }}" URL="https://git.datacontroller.io/api/v1/repos/dc/dc/releases/$RELEASE_ID/assets?access_token=${{ secrets.RELEASE_TOKEN }}"
curl -k $URL -F attachment=@frontend.zip curl -k $URL -F attachment=@frontend.zip
curl -k $URL -F attachment=@sas/demostream_sas9.sas curl -k $URL -F attachment=@sas/demostream_sas9.sas
curl -k $URL -F attachment=@sas/viya.sas curl -k $URL -F attachment=@sas/demostream_viya.sas
curl -k $URL -F attachment=@sas/sasjs_server.json.zip curl -k $URL -F attachment=@sas/sasjs_server.json.zip
curl -k $URL -F attachment=@sas/sas9.sas curl -k $URL -F attachment=@sas/sas9.sas
curl -k $URL -F attachment=@sas/viya_noweb.sas curl -k $URL -F attachment=@sas/viya.sas
curl -k $URL -F attachment=@sas/viya_noweb.json curl -k $URL -F attachment=@sas/viya.json
-2
View File
@@ -14,7 +14,6 @@ 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
@@ -22,4 +21,3 @@ sasjsresults
.sasjsrc .sasjsrc
client/.npmrc client/.npmrc
*~ *~
.lighthouseci
-319
View File
@@ -1,322 +1,3 @@
# [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)
### Bug Fixes
* **ci:** cypress dependency package not available anymore ([26cdd73](https://git.datacontroller.io/dc/dc/commit/26cdd733315ef8babe9498ce93f6eb29c587dabd))
* **hot v16 migration:** multi dataset fixed issues, and cypress tests adapted ([712b384](https://git.datacontroller.io/dc/dc/commit/712b3848480a8769d149e00b0d2de91396022b66))
* obsolete cypress deps ([2ba4b53](https://git.datacontroller.io/dc/dc/commit/2ba4b5383e23bff8dfeb82b0ef473e5871c94709))
* remaining hot migrations - handsontable/angular-wrapper ([b419cd5](https://git.datacontroller.io/dc/dc/commit/b419cd507837e846e9dfcc6b729254d56cc196e6))
### Features
* lighthouse accessibility check pipeline ([670ec2c](https://git.datacontroller.io/dc/dc/commit/670ec2c71cb2d24e9d79e297a8cbc6136aa315c8))
## [7.1.1](https://git.datacontroller.io/dc/dc/compare/v7.1.0...v7.1.1) (2025-07-24)
### Bug Fixes
* **viewboxes:** hot v16 fails to load because of relative height `100%` ([672dd6d](https://git.datacontroller.io/dc/dc/commit/672dd6d4f1fda27e3706dd7caa42b45922319497))
# [7.1.0](https://git.datacontroller.io/dc/dc/compare/v7.0.3...v7.1.0) (2025-07-23)
### Bug Fixes
* adapter bump ([b495c41](https://git.datacontroller.io/dc/dc/commit/b495c41626c85b7c4141d9361e4d3a826efd6c05))
* bumping CLI to 4.12.10 ([a08a717](https://git.datacontroller.io/dc/dc/commit/a08a717ca8d49e8a7d63f3fd91c6a7d42a1d6d8b))
* bumping sasjs/core and sasjs/cli ([63e9af4](https://git.datacontroller.io/dc/dc/commit/63e9af402ed65f6be4426e76ee1376a40e6ed097))
### Features
* improving accessibility score up to 100, hot update to v16.0.1 ([71c308d](https://git.datacontroller.io/dc/dc/commit/71c308d052400ecedc03f8020a5a69471ac6b116))
## [7.0.3](https://git.datacontroller.io/dc/dc/compare/v7.0.2...v7.0.3) (2025-06-26)
### Bug Fixes
* makedata vars ([e7cb471](https://git.datacontroller.io/dc/dc/commit/e7cb471c0b60058b03fe8cbed5e3e2e70dd72e26))
* viya deploy makedata missing params ([7a82316](https://git.datacontroller.io/dc/dc/commit/7a8231615cb56710351fae5868e8fdeed54d180c))
## [7.0.2](https://git.datacontroller.io/dc/dc/compare/v7.0.1...v7.0.2) (2025-06-21)
### Bug Fixes
* **viya deploy:** run makedata in new window to ensure logs are available for the user ([0b4042a](https://git.datacontroller.io/dc/dc/commit/0b4042af6011fdc65cfaaa5d4b1d8f48cd67f3b3))
## [7.0.1](https://git.datacontroller.io/dc/dc/compare/v7.0.0...v7.0.1) (2025-06-11)
### Bug Fixes
* refresh process ([4ecd186](https://git.datacontroller.io/dc/dc/commit/4ecd186e5cb22dd436f2d7f1200956f4e3f27425))
# [7.0.0](https://git.datacontroller.io/dc/dc/compare/v6.16.2...v7.0.0) (2025-06-11)
### Bug Fixes
* bumping adapter to re-enable JES API method ([e874143](https://git.datacontroller.io/dc/dc/commit/e874143a95d0ac2e56c0793e04b979c27f96d74b))
* commit git hooks checking lint ([69f687a](https://git.datacontroller.io/dc/dc/commit/69f687a85f1cc562346b6167813d617cb9bd3404))
* ensuring apploc is not case sensitive. Closes [#171](https://git.datacontroller.io/dc/dc/issues/171) ([24545f2](https://git.datacontroller.io/dc/dc/commit/24545f2acdd5bd73cbe062526f2bd043269cc6a3))
* export unregistered formats ([f6d7d6f](https://git.datacontroller.io/dc/dc/commit/f6d7d6f90c978ac8c071471dfb67a60834424de5)), closes [#158](https://git.datacontroller.io/dc/dc/issues/158)
* reload startupservice after user approves the MPE_TABLES page ([e5f8e50](https://git.datacontroller.io/dc/dc/commit/e5f8e500c125ee233c6f7af5ad0077c0ed6abfcb))
* showing catalog_cnt in libinfo ([e44a25d](https://git.datacontroller.io/dc/dc/commit/e44a25dcc39ba4b9714257c60da84c2dfa613a85)), closes [#160](https://git.datacontroller.io/dc/dc/issues/160)
### Features
* adding 4 new tables for catalogs ([e4dbab8](https://git.datacontroller.io/dc/dc/commit/e4dbab8b1654b24e610e4b0603d1cf2b02a451e2))
* capturing catalog specific information, closes [#159](https://git.datacontroller.io/dc/dc/issues/159) ([b4c586a](https://git.datacontroller.io/dc/dc/commit/b4c586a859929e0122cd46449e43d4ca597b8b2b))
* viewer added catalog_cnt ([2aa19d1](https://git.datacontroller.io/dc/dc/commit/2aa19d1dca747f41274a032cde78d8ba73d66224))
### BREAKING CHANGES
* Introduction of 4 new tables for capturing information related to catalogs and their objects. Migration script prepared and available in the DB folder (usual place)
## [6.16.2](https://git.datacontroller.io/dc/dc/compare/v6.16.1...v6.16.2) (2025-06-06)
### Bug Fixes
* streaming viya deploy `isStreaming` function stability fix ([4830c6d](https://git.datacontroller.io/dc/dc/commit/4830c6d2191cb47abcc7919bc1d49e55595e6121))
## [6.16.1](https://git.datacontroller.io/dc/dc/compare/v6.16.0...v6.16.1) (2025-06-06)
### Bug Fixes
* viya deploy updating index html based on URL ([86134f4](https://git.datacontroller.io/dc/dc/commit/86134f478ae0b9426e01bfcc9ca4ee597ca733f7))
* viya streamed app deploy page flow fix ([89ab296](https://git.datacontroller.io/dc/dc/commit/89ab2961513b245eeea48d1867c6496d3261761e))
# [6.16.0](https://git.datacontroller.io/dc/dc/compare/v6.15.2...v6.16.0) (2025-06-05)
### Bug Fixes
* adapter bump ([ca7caa2](https://git.datacontroller.io/dc/dc/commit/ca7caa25b6eea1bd4579fb8b67ec9b211a893079))
* automatic viya deploy timing issue ([037a97b](https://git.datacontroller.io/dc/dc/commit/037a97b6ffa27b40891531ae6812ebe5b5e71e34))
* bump core to ensure ff works on viya streaming deploy ([cbd69df](https://git.datacontroller.io/dc/dc/commit/cbd69df708edf3a8446115ca7315fac3557dcf97)), closes [#156](https://git.datacontroller.io/dc/dc/issues/156)
* viya deploy load data timing ([abdbb67](https://git.datacontroller.io/dc/dc/commit/abdbb674713796e5308eb4272197a5c253868a85))
### Features
* viya deploy, update the index.html contextname ([7223955](https://git.datacontroller.io/dc/dc/commit/72239558af2ee50cdfc71b7e185e6661ab568ba1))
## [6.15.2](https://git.datacontroller.io/dc/dc/compare/v6.15.1...v6.15.2) (2025-06-04)
### Bug Fixes
* pipeline updates for DC.html ([624a7a8](https://git.datacontroller.io/dc/dc/commit/624a7a8f37f0265cf576da310ac330c75aa417cf))
## [6.15.1](https://git.datacontroller.io/dc/dc/compare/v6.15.0...v6.15.1) (2025-06-04)
### Bug Fixes
* updating pipeline to default to streaming on viya ([4b55894](https://git.datacontroller.io/dc/dc/commit/4b558948d997f456ff25a12a58827fe0d2075493))
# [6.15.0](https://git.datacontroller.io/dc/dc/compare/v6.14.10...v6.15.0) (2025-06-04)
### Bug Fixes
* makedata with context name ([da4d0b2](https://git.datacontroller.io/dc/dc/commit/da4d0b28c7109afd6f96455e1e0e80a40d25a942))
### Features
* viya deploy context ([6c96ef7](https://git.datacontroller.io/dc/dc/commit/6c96ef7fb0a55754a84ff0a8bbab838b78c1acaf))
## [6.14.10](https://git.datacontroller.io/dc/dc/compare/v6.14.9...v6.14.10) (2025-06-02)
### Bug Fixes
* bump core ([0e8503e](https://git.datacontroller.io/dc/dc/commit/0e8503ed2bb22a0fc3924ac929e7f19626772e0a))
* default to home directory for SAS Drive in Viya ([9682b54](https://git.datacontroller.io/dc/dc/commit/9682b548e6106d99d97dcc023a35d93addfd5170))
## [6.14.9](https://git.datacontroller.io/dc/dc/compare/v6.14.8...v6.14.9) (2025-06-02)
### Bug Fixes
* default DC path for viya ([f3125ff](https://git.datacontroller.io/dc/dc/commit/f3125ff4641e47e33cb203228f5b1014ea3343bc))
## [6.14.8](https://git.datacontroller.io/dc/dc/compare/v6.14.7...v6.14.8) (2025-05-28)
### Bug Fixes
* CSP issues, clarity local library build, fixed some style issues ([841201a](https://git.datacontroller.io/dc/dc/commit/841201adab582149b1cca3a42e75f7cac75167f9))
* deploy page, makedata error handling, added local build of clarity, to address clr-stack-view CSP issues (inline styles) ([7b5e7ae](https://git.datacontroller.io/dc/dc/commit/7b5e7ae18414152f9b9d8f2d94fc94de43152003))
* improved deploy flow for Viya ([9604661](https://git.datacontroller.io/dc/dc/commit/9604661f3b76111387bc9474cc26348d73ab112e))
* requests modal causing VIYA CSP errors ([1dc6934](https://git.datacontroller.io/dc/dc/commit/1dc69341cadb837e1f11624d5cf35788bbb98d96))
* sas viya service init timing issue ([9de04e9](https://git.datacontroller.io/dc/dc/commit/9de04e9a0ce016e1a9fb8b19c656077079ddcf2f))
* scss of components transferred to the global styles.scss so we do not cause CSP (inline styles) issues when streaming to Viya ([6c171a6](https://git.datacontroller.io/dc/dc/commit/6c171a6394aba8104fe0f50aa8a4e6b9fa8023a2))
* viya deploy page improved flow ([4bd2154](https://git.datacontroller.io/dc/dc/commit/4bd215491f8cdc68f78bade68e7cb98e07edc81e))
## [6.14.7](https://git.datacontroller.io/dc/dc/compare/v6.14.6...v6.14.7) (2025-05-08)
### Bug Fixes
* updated hot, clarity and improved accessibility score. ([2844c70](https://git.datacontroller.io/dc/dc/commit/2844c70f9507036216b8b621900c2bb9010c1d34))
## [6.14.6](https://git.datacontroller.io/dc/dc/compare/v6.14.5...v6.14.6) (2025-04-03)
### Bug Fixes
* history table modal links styling ([c63fcdd](https://git.datacontroller.io/dc/dc/commit/c63fcdd465950ada439d7d69622a3886e8f3a783))
## [6.14.5](https://git.datacontroller.io/dc/dc/compare/v6.14.4...v6.14.5) (2025-03-24)
### Bug Fixes
* improving accessibility lighthouse score ([7f3577c](https://git.datacontroller.io/dc/dc/commit/7f3577c3ef9f44e55a58bc64fbf89a3a64006dd4))
* prevent errors when using sqlrc in a DI job in a HOOK ([d1f0879](https://git.datacontroller.io/dc/dc/commit/d1f0879f0acf7e816c80f7635fd02f4f284214ed))
* user profile style fix, new select library and table icons ([69f8830](https://git.datacontroller.io/dc/dc/commit/69f883034fabbed31aa5d832e20561c4ae3042db))
## [6.14.4](https://git.datacontroller.io/dc/dc/compare/v6.14.3...v6.14.4) (2025-03-18)
### Bug Fixes
* removing cli dependency warnings2 ([43c0f73](https://git.datacontroller.io/dc/dc/commit/43c0f73c2189ff762986a964caae6b0b108164fc))
## [6.14.3](https://git.datacontroller.io/dc/dc/compare/v6.14.2...v6.14.3) (2025-03-15)
### Bug Fixes
* NLDAT & NLDATM formats are now being staged ([3f5cb1e](https://git.datacontroller.io/dc/dc/commit/3f5cb1e2defe390220e904e4bf04a165cb31fec4))
## [6.14.2](https://git.datacontroller.io/dc/dc/compare/v6.14.1...v6.14.2) (2025-03-10)
### Bug Fixes
* improving instructions for setup ([83b3d77](https://git.datacontroller.io/dc/dc/commit/83b3d775b6e33653b087ca9f4eb3ad5b0dbbd479))
## [6.14.1](https://git.datacontroller.io/dc/dc/compare/v6.14.0...v6.14.1) (2025-03-05)
### Bug Fixes
* handle national language datetime formats ([149e318](https://git.datacontroller.io/dc/dc/commit/149e318a8787be0109f25aeec3a1270ea75a97b2))
* updating logic to use NLDAT formats ([95289aa](https://git.datacontroller.io/dc/dc/commit/95289aa9524d3cb2b1c248cfb84f6b0d0a490c32))
# [6.14.0](https://git.datacontroller.io/dc/dc/compare/v6.13.2...v6.14.0) (2025-02-26) # [6.14.0](https://git.datacontroller.io/dc/dc/compare/v6.13.2...v6.14.0) (2025-02-26)
+1 -33
View File
@@ -23,42 +23,10 @@ _Problems with the above include:_
Data Controller for SAS® solves all these issues in a simple-to-install, user-friendly, secure, documented, battle-tested web application. Available on Viya, SAS 9 EBI, and [SASjs Server](https://server.sasjs.io). Data Controller for SAS® solves all these issues in a simple-to-install, user-friendly, secure, documented, battle-tested web application. Available on Viya, SAS 9 EBI, and [SASjs Server](https://server.sasjs.io).
An individual Viya deploy can be done in just 2 lines of #SAS code! For more information:
```sas
filename dc url "https://git.datacontroller.io/dc/dc/releases/download/latest/viya.sas";
%inc dc;
```
For a multi-user deploy, using a shared system account, please see [deploy docs](https://docs.datacontroller.io/deploy-viya/).
For further information:
* Main site: https://datacontroller.io * Main site: https://datacontroller.io
* Docs: https://docs.datacontroller.io * Docs: https://docs.datacontroller.io
* Code: https://code.datacontroller.io * Code: https://code.datacontroller.io
For support, contact support@4gl.io or reach out on [Matrix](https://matrix.to/#/#dc:4gl.io)! For support, contact support@4gl.io or reach out on [Matrix](https://matrix.to/#/#dc:4gl.io)!
## Development
### Lighthouse CI
This project includes automated Lighthouse performance and accessibility checks that run on pull requests. The checks ensure:
- **Accessibility Score**: Minimum 1.0 (100%) median score across all tested pages
The Lighthouse CI workflow:
1. Sets up the development environment with SASjs server and mocked services
2. Builds and serves the Angular frontend
3. Runs Lighthouse CI against key application pages
4. Uploads results as artifacts for review
To run Lighthouse checks locally:
```bash
cd client
npm install
npm run lighthouse
```
Configuration is in `client/lighthouserc.js`.
+32 -16
View File
@@ -41,8 +41,6 @@
"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",
@@ -53,22 +51,26 @@
"base64-arraybuffer", "base64-arraybuffer",
"@handsontable/formulajs" "@handsontable/formulajs"
], ],
"polyfills": ["src/polyfills.ts", "zone.js"], "polyfills": [
"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": ["src/styles.scss"], "styles": [
"scripts": ["node_modules/marked/marked.min.js"], "src/styles.scss"
"webWorkerTsConfig": "tsconfig.worker.json", ],
"main": "src/main.ts" "scripts": [
"node_modules/marked/marked.min.js"
],
"webWorkerTsConfig": "tsconfig.worker.json"
}, },
"configurations": { "configurations": {
"production": { "production": {
@@ -101,7 +103,9 @@
} }
}, },
"development": { "development": {
"vendorChunk": true,
"extractLicenses": false, "extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true, "sourceMap": true,
"optimization": false, "optimization": false,
"namedChunks": true "namedChunks": true
@@ -130,11 +134,20 @@
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"polyfills": ["src/polyfills.ts", "zone.js", "zone.js/testing"], "polyfills": [
"src/polyfills.ts",
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"], "assets": [
"styles": ["src/styles.scss"], "src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": [], "scripts": [],
"karmaConfig": "karma.conf.js", "karmaConfig": "karma.conf.js",
"webWorkerTsConfig": "tsconfig.worker.json" "webWorkerTsConfig": "tsconfig.worker.json"
@@ -143,7 +156,10 @@
"lint": { "lint": {
"builder": "@angular-eslint/builder:lint", "builder": "@angular-eslint/builder:lint",
"options": { "options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] "lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
} }
} }
} }
+21 -20
View File
@@ -32,26 +32,27 @@ context('excel tests: ', function () {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('regular.csv', () => { attachExcelFile('regular.csv', () => {
cy.get('#approval-btn', { timeout: 60000 }).should('be.visible') cy.get('#approval-btn', { timeout: 60000 })
// .then(() => { .should('be.visible')
// cy.get('#hotInstance', { timeout: 30000 }) // .then(() => {
// .find('div.ht_master.handsontable') // cy.get('#hotInstance', { timeout: 30000 })
// .find('div.wtHolder') // .find('div.ht_master.handsontable')
// .find('div.wtHider') // .find('div.wtHolder')
// .find('div.wtSpreader') // .find('div.wtHider')
// .find('table.htCore') // .find('div.wtSpreader')
// .find('tbody') // .find('table.htCore')
// .then((data) => { // .find('tbody')
// let cell: any = data[0].children[0].children[1] // .then((data) => {
// expect(cell.innerText).to.equal('0') // let cell: any = data[0].children[0].children[1]
// cell = data[0].children[0].children[2] // expect(cell.innerText).to.equal('0')
// expect(cell.innerText).to.equal('44') // cell = data[0].children[0].children[2]
// cell = data[0].children[0].children[3] // expect(cell.innerText).to.equal('44')
// expect(cell.innerText).to.equal('abc') // cell = data[0].children[0].children[3]
// cell = data[0].children[0].children[6] // expect(cell.innerText).to.equal('abc')
// expect(cell.innerText).to.equal('Option abc') // cell = data[0].children[0].children[6]
// }) // expect(cell.innerText).to.equal('Option abc')
// }) // })
// })
}) })
}) })
+5 -1
View File
@@ -217,7 +217,11 @@ const rejectExcel = (callback?: any) => {
.should('contain', 'Approve') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if (approvalButton.innerText.toLowerCase().includes('approve')) { if (
approvalButton.innerText
.toLowerCase()
.includes('approve')
) {
approvalButton.click() approvalButton.click()
break break
} }
+98 -187
View File
@@ -34,162 +34,93 @@ context('excel multi load tests: ', function () {
it('1 | Uploads Excel file with multiple sheets, 3 sheets including data, 2 sheets matched with dataset', (done) => { it('1 | Uploads Excel file with multiple sheets, 3 sheets including data, 2 sheets matched with dataset', (done) => {
attachExcelFile('multi_load_test_2.xlsx', () => { attachExcelFile('multi_load_test_2.xlsx', () => {
checkHotUserDatasetTable( checkHotUserDatasetTable('hotTableUserDataset', [
'hotTableUserDataset', [library, mpeXTestTable],
[ [library, mpeTablesTable]
[library, mpeXTestTable], ], () => {
[library, mpeTablesTable] cy.get('#continue-btn').trigger('click').then(() => {
], checkIfTreeHasTables([`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], undefined, (includes: boolean) => {
() => { if (includes) {
cy.get('#continue-btn') // MPE_TABLES sheet does not have data so 1 error image must be shown
.trigger('click') hasErrorTables(1, (valid: boolean) => {
.then(() => { if (valid) done()
checkIfTreeHasTables( })
[`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], }
undefined, })
(includes: boolean) => { })
if (includes) { })
// MPE_TABLES sheet does not have data so 1 error image must be shown
hasErrorTables(1, (valid: boolean) => {
if (valid) done()
})
}
}
)
})
}
)
}) })
}) })
it('2 | Uploads Excel file with multiple sheets, 2 sheets matched with dataset, 1 matched sheet does not have data', (done) => { it('2 | Uploads Excel file with multiple sheets, 2 sheets matched with dataset, 1 matched sheet does not have data', (done) => {
attachExcelFile('multi_load_test_1.xlsx', () => { attachExcelFile('multi_load_test_1.xlsx', () => {
checkHotUserDatasetTable( checkHotUserDatasetTable('hotTableUserDataset', [
'hotTableUserDataset', [library, mpeXTestTable],
[ [library, mpeTablesTable]
[library, mpeXTestTable], ], () => {
[library, mpeTablesTable] cy.get('#continue-btn').trigger('click').then(() => {
], checkIfTreeHasTables([`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], `${library}.${mpeXTestTable}`, (includes: boolean) => {
() => { if (includes) {
cy.get('#continue-btn') cy.get('#hotTable').should('be.visible').then(() => {
.trigger('click') checkHotUserDatasetTable('hotTable', [
.then(() => { ['No', '1', 'more dummy data'],
checkIfTreeHasTables( ['No', '1', '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:'],
[`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], ['No', '1', 'if you can fill the unforgiving minute']
`${library}.${mpeXTestTable}`, ], () => {
(includes: boolean) => { submitTables()
if (includes) {
cy.get('#hotTable')
.should('be.visible')
.then(() => {
checkHotUserDatasetTable(
'hotTable',
[
['No', '1', 'more dummy data'],
[
'No',
'1',
'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:'
],
[
'No',
'1',
'if you can fill the unforgiving minute'
]
],
() => {
submitTables()
hasSuccessSubmits(2, (valid: boolean) => { hasSuccessSubmits(2, (valid: boolean) => {
if (valid) done() if (valid) done()
}) })
}
) })
}) })
} }
} })
) })
}) })
}
)
}) })
}) })
it('3 | Uploads Excel file with multiple sheets, 1 sheets has 2 tables', (done) => { it('3 | Uploads Excel file with multiple sheets, 1 sheets has 2 tables', (done) => {
attachExcelFile('multi_load_test_1.xlsx', () => { attachExcelFile('multi_load_test_1.xlsx', () => {
checkHotUserDatasetTable( checkHotUserDatasetTable('hotTableUserDataset', [
'hotTableUserDataset', [library, mpeXTestTable],
[ [library, mpeTablesTable]
[library, mpeXTestTable], ], () => {
[library, mpeTablesTable] cy.get('#continue-btn').trigger('click').then(() => {
], checkIfTreeHasTables([`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], `${library}.${mpeXTestTable}`, (includes: boolean) => {
() => { if (includes) {
cy.get('#continue-btn') cy.get('#hotTable').should('be.visible').then(() => {
.trigger('click') checkHotUserDatasetTable('hotTable', [
.then(() => { ['No', '1', 'more dummy data'],
checkIfTreeHasTables( ['No', '1', '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:'],
[`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], ['No', '1', 'if you can fill the unforgiving minute']
`${library}.${mpeXTestTable}`, ], () => {
(includes: boolean) => { clickOnTreeNode('DC996664.MPE_TABLES', () => {
if (includes) { cy.wait(1000).then(() => {
cy.get('#hotTable') cy.get('#hotTable').should('be.visible').then(() => {
.should('be.visible') checkHotUserDatasetTable('hotTable', [
.then(() => { ['No', 'DC914286', 'MPE_COLUMN_LEVEL_SECURITY'],
checkHotUserDatasetTable( ['No', 'DC914286', 'MPE_XLMAP_INFO'],
'hotTable', ['No', 'DC914286', 'MPE_XLMAP_RULES']
[ ], () => {
['No', '1', 'more dummy data'], submitTables()
[
'No',
'1',
'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:'
],
[
'No',
'1',
'if you can fill the unforgiving minute'
]
],
() => {
clickOnTreeNode('DC996664.MPE_TABLES', () => {
cy.wait(1000).then(() => {
cy.get('#hotTable')
.should('be.visible')
.then(() => {
checkHotUserDatasetTable(
'hotTable',
[
[
'No',
'DC914286',
'MPE_COLUMN_LEVEL_SECURITY'
],
['No', 'DC914286', 'MPE_XLMAP_INFO'],
['No', 'DC914286', 'MPE_XLMAP_RULES']
],
() => {
submitTables()
hasSuccessSubmits( hasSuccessSubmits(2, (valid: boolean) => {
2, if (valid) done()
(valid: boolean) => { })
if (valid) done()
} })
)
}
)
})
})
})
}
)
}) })
} })
} })
) })
}) })
} }
) })
})
})
}) })
}) })
@@ -211,31 +142,25 @@ const attachExcelFile = (excelFilename: string, callback?: any) => {
}) })
} }
const checkHotUserDatasetTable = ( const checkHotUserDatasetTable = (hotId: string, dataToContain: any[][], callback?: () => void) => {
hotId: string,
dataToContain: any[][],
callback?: () => void
) => {
cy.get(`#${hotId}`, { timeout: longerCommandTimeout }) cy.get(`#${hotId}`, { timeout: longerCommandTimeout })
.find('div.ht_master.handsontable') .find('div.ht_master.handsontable')
.find('div.wtHolder') .find('div.wtHolder')
.find('div.wtHider') .find('div.wtHider')
.find('div.wtSpreader') .find('div.wtSpreader')
.find('table.htCore') .find('table.htCore')
.find('tbody') .find('tbody')
.then((data) => { .then((data) => {
cy.wait(2000).then(() => { cy.wait(2000).then(() => {
for (let rowI = 0; rowI < dataToContain.length; rowI++) { for (let rowI = 0; rowI < dataToContain.length; rowI++) {
for (let colI = 0; colI < dataToContain[rowI].length; colI++) { for (let colI = 0; colI < dataToContain[rowI].length; colI++) {
expect(data[0].children[rowI].children[colI]).to.contain( expect(data[0].children[rowI].children[colI]).to.contain(dataToContain[rowI][colI])
dataToContain[rowI][colI]
)
}
} }
}
if (callback) callback() if (callback) callback()
})
}) })
})
} }
const clickOnTreeNode = (clickOnNode: string, callback?: () => void) => { const clickOnTreeNode = (clickOnNode: string, callback?: () => void) => {
@@ -249,11 +174,7 @@ const clickOnTreeNode = (clickOnNode: string, callback?: () => void) => {
}) })
} }
const checkIfTreeHasTables = ( const checkIfTreeHasTables = (tables: string[], clickOnNode?: string, callback?: (includes: boolean) => void) => {
tables: string[],
clickOnNode?: string,
callback?: (includes: boolean) => void
) => {
cy.get('.nav-tree clr-tree > clr-tree-node').then((treeNodes: any) => { cy.get('.nav-tree clr-tree > clr-tree-node').then((treeNodes: any) => {
let datasets = tables let datasets = tables
let nodesCorrect = true let nodesCorrect = true
@@ -286,26 +207,16 @@ const submitTables = () => {
cy.wait(1000) cy.wait(1000)
} }
const hasSuccessSubmits = ( const hasSuccessSubmits = (expectedNoOfSubmits: number, callback: (valid: boolean) => void) => {
expectedNoOfSubmits: number, cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="success"]').should('be.visible').then(($nodes) => {
callback: (valid: boolean) => void callback(expectedNoOfSubmits === $nodes.length)
) => { })
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="success"]')
.should('be.visible')
.then(($nodes) => {
callback(expectedNoOfSubmits === $nodes.length)
})
} }
const hasErrorTables = ( const hasErrorTables = (expectedNoOfErrors: number, callback: (valid: boolean) => void) => {
expectedNoOfErrors: number, cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="danger"]').should('be.visible').then(($nodes) => {
callback: (valid: boolean) => void callback(expectedNoOfErrors === $nodes.length)
) => { })
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="danger"]')
.should('be.visible')
.then(($nodes) => {
callback(expectedNoOfErrors === $nodes.length)
})
} }
const visitPage = (url: string) => { const visitPage = (url: string) => {
+15 -84
View File
@@ -234,7 +234,7 @@ context('excel tests: ', function () {
cy.get('.btn-upload-preview', { timeout: 60000 }) cy.get('.btn-upload-preview', { timeout: 60000 })
.should('be.visible') .should('be.visible')
.then(() => { .then(() => {
cy.get('#hotTable', { timeout: 30000 }) cy.get('#hotInstance', { timeout: 30000 })
.find('div.ht_master.handsontable') .find('div.ht_master.handsontable')
.find('div.wtHolder') .find('div.wtHolder')
.find('div.wtHider') .find('div.wtHider')
@@ -283,7 +283,7 @@ context('excel tests: ', function () {
cy.get('.btn-upload-preview', { timeout: 60000 }) cy.get('.btn-upload-preview', { timeout: 60000 })
.should('be.visible') .should('be.visible')
.then(() => { .then(() => {
cy.get('#hotTable', { timeout: 30000 }) cy.get('#hotInstance', { timeout: 30000 })
.find('div.ht_master.handsontable') .find('div.ht_master.handsontable')
.find('div.wtHolder') .find('div.wtHolder')
.find('div.wtHider') .find('div.wtHider')
@@ -309,83 +309,6 @@ 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) => {
@@ -476,7 +399,11 @@ const rejectExcel = (callback?: any) => {
.should('contain', 'Approve') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if (approvalButton.innerText.toLowerCase().includes('approve')) { if (
approvalButton.innerText
.toLowerCase()
.includes('approve')
) {
approvalButton.click() approvalButton.click()
break break
} }
@@ -505,7 +432,11 @@ const acceptExcel = (callback?: any) => {
.should('contain', 'Approve') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if (approvalButton.innerText.toLowerCase().includes('approve')) { if (
approvalButton.innerText
.toLowerCase()
.includes('approve')
) {
approvalButton.click() approvalButton.click()
break break
} }
@@ -524,7 +455,7 @@ const acceptExcel = (callback?: any) => {
} }
const checkResultOfFormulaUpload = (callback?: any) => { const checkResultOfFormulaUpload = (callback?: any) => {
cy.get('#hotTable', { timeout: longerCommandTimeout }) cy.get('#hotInstance', { timeout: longerCommandTimeout })
.find('div.ht_master.handsontable') .find('div.ht_master.handsontable')
.find('div.wtHolder') .find('div.wtHolder')
.find('div.wtHider') .find('div.wtHider')
@@ -540,7 +471,7 @@ const checkResultOfFormulaUpload = (callback?: any) => {
const checkResultOfXLSUpload = (callback?: any) => { const checkResultOfXLSUpload = (callback?: any) => {
cy.viewport(1280, 720) cy.viewport(1280, 720)
cy.get('#hotTable', { timeout: 30000 }) cy.get('#hotInstance', { timeout: 30000 })
.find('div.ht_master.handsontable') .find('div.ht_master.handsontable')
.find('div.wtHolder') .find('div.wtHolder')
.find('div.wtHider') .find('div.wtHider')
@@ -569,7 +500,7 @@ const checkResultOfXLSUpload = (callback?: any) => {
if (callback) callback() if (callback) callback()
}) })
cy.get('#hotTable', { timeout: 30000 }) cy.get('#hotInstance', { timeout: 30000 })
.find('div.ht_master.handsontable') .find('div.ht_master.handsontable')
.find('div.wtHolder') .find('div.wtHolder')
.scrollTo('right') .scrollTo('right')
+7 -8
View File
@@ -16,6 +16,7 @@ context('filtering tests: ', function () {
this.beforeEach(() => { this.beforeEach(() => {
cy.visit(hostUrl + appLocation, { timeout: longerCommandTimeout }) cy.visit(hostUrl + appLocation, { timeout: longerCommandTimeout })
visitPage('home') visitPage('home')
}) })
@@ -298,16 +299,14 @@ const setFilterWithValue = (
cy.get('.no-values') cy.get('.no-values')
.should('not.exist') .should('not.exist')
.then(() => { .then(() => {
cy.get('.in-values-modal clr-checkbox-wrapper input').then( cy.get('.in-values-modal clr-checkbox-wrapper input').then((inputs: any) => {
(inputs: any) => { inputs[0].click()
inputs[0].click() cy.get('.in-values-modal .modal-footer button').click()
cy.get('.in-values-modal .modal-footer button').click()
cy.get('.modal-footer .btn-success-outline').click() cy.get('.modal-footer .btn-success-outline').click()
if (callback) callback() if (callback) callback()
} })
)
}) })
}) })
+8 -1
View File
@@ -23,6 +23,7 @@ interface EditConfigTableCells {
context('licensing tests: ', function () { context('licensing tests: ', function () {
this.beforeAll(() => { this.beforeAll(() => {
cy.loginAndUpdateValidKey() cy.loginAndUpdateValidKey()
}) })
@@ -370,6 +371,8 @@ context('licensing tests: ', function () {
}) })
}) })
} }
}) })
const logout = (callback?: any) => { const logout = (callback?: any) => {
@@ -694,7 +697,11 @@ const approveTable = (callback?: any) => {
.should('contain', 'Approve') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if (approvalButton.innerText.toLowerCase().includes('approve')) { if (
approvalButton.innerText
.toLowerCase()
.includes('approve')
) {
approvalButton.click() approvalButton.click()
break break
} }
+6 -1
View File
@@ -18,6 +18,7 @@ context('liveness tests: ', function () {
this.beforeEach(() => { this.beforeEach(() => {
cy.visit(hostUrl + appLocation) cy.visit(hostUrl + appLocation)
visitPage('home') visitPage('home')
}) })
@@ -124,7 +125,11 @@ const rejectExcel = (callback?: any) => {
.should('contain', 'Approve') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if (approvalButton.innerText.toLowerCase().includes('approve')) { if (
approvalButton.innerText
.toLowerCase()
.includes('approve')
) {
approvalButton.click() approvalButton.click()
break break
} }
+27 -32
View File
@@ -76,8 +76,7 @@ context('editor tests: ', function () {
cy.get('.viewbox-open').click() cy.get('.viewbox-open').click()
openTableFromViewboxTree( openTableFromViewboxTree(
libraryToOpenIncludes, libraryToOpenIncludes,
viewboxes.map((viewbox) => viewbox.viewbox_table) viewboxes.map((viewbox) => viewbox.viewbox_table))
)
cy.get('.open-viewbox').then((viewboxNodes: any) => { cy.get('.open-viewbox').then((viewboxNodes: any) => {
let found = 0 let found = 0
@@ -92,34 +91,32 @@ context('editor tests: ', function () {
if (found < viewboxes.length) return if (found < viewboxes.length) return
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then( cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then((viewboxNodes: any) => {
(viewboxNodes: any) => { for (let viewboxNode of viewboxNodes) {
for (let viewboxNode of viewboxNodes) { cy.get(viewboxNode).within(() => {
cy.get(viewboxNode).within(() => { cy.get('.table-title').then((tableTitle) => {
cy.get('.table-title').then((tableTitle) => { const title = tableTitle[0].innerText
const title = tableTitle[0].innerText const viewbox = viewboxes.find((vb) =>
const viewbox = viewboxes.find((vb) => title.toLowerCase().includes(vb.viewbox_table)
title.toLowerCase().includes(vb.viewbox_table) )
)
if (viewbox) { if (viewbox) {
cy.get('.ht_master.handsontable .htCore thead tr').then( cy.get('.ht_master.handsontable .htCore thead tr').then(
(viewboxColNodes: any) => { (viewboxColNodes: any) => {
let allColsHtml = viewboxColNodes[0].innerHTML let allColsHtml = viewboxColNodes[0].innerHTML
for (let col of viewbox?.columns) { for (let col of viewbox?.columns) {
if (!allColsHtml.includes(col)) return if (!allColsHtml.includes(col)) return
}
done()
} }
)
} done()
}) }
)
}
}) })
} })
} }
) })
}) })
}) })
@@ -398,13 +395,11 @@ context('editor tests: ', function () {
}) })
const removeAllColumns = () => { const removeAllColumns = () => {
cy.get('.configuration-wrapper clr-icon[shape="trash"]').then( cy.get('.configuration-wrapper clr-icon[shape="trash"]').then(removeNodes => {
(removeNodes) => { for (let removeNode of removeNodes) {
for (let removeNode of removeNodes) { removeNode.click()
removeNode.click()
}
} }
) })
} }
const checkColumns = (columns: string[], callback: () => void) => { const checkColumns = (columns: string[], callback: () => void) => {
@@ -417,7 +412,7 @@ const checkColumns = (columns: string[], callback: () => void) => {
console.log('viewboxColNode', viewboxColNodes) console.log('viewboxColNode', viewboxColNodes)
console.log('columns', columns) console.log('columns', columns)
for (let i = 0; i < viewboxColNodes.length; i++) { for (let i = 0; i < viewboxColNodes.length; i++) {
const col = columns[i] || '' const col = columns[i]|| ''
const colNode = viewboxColNodes[i] const colNode = viewboxColNodes[i]
if ( if (
@@ -0,0 +1,255 @@
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 = 'excels_general/'
const downloadsFolder = Cypress.config('downloadsFolder')
import { deleteDownloadsFolder } from '../util/deleteDownloadFolder'
context('download files test: ', function () {
this.beforeAll(() => {
cy.visit(`${hostUrl}/SASLogon/logout`)
cy.loginAndUpdateValidKey()
})
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
cy.get('input.username').type(username)
cy.get('input.password').type(password)
cy.get('.login-group button').click()
visitPage('home')
})
this.afterEach(() => {
deleteDownloadsFolder()
})
it('1 | downloads audit file', (done) => {
visitPage('approve/toapprove')
cy.get('.app-loading', { timeout: longerCommandTimeout })
.should('not.exist')
.then(() => {
cy.get('.btn.btn-success')
.should('be.visible')
.then((buttons) => {
buttons[0].click()
const id = buttons[0].id
checkForFileDownloaded(id, 'zip', () => done())
})
})
})
it('2 | downloads viewer csv', (done) => {
visitPage('view/data')
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openDownloadModal(() => {
cy.get('select')
.select('CSV')
.then(() => {
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
button.trigger('click')
const id = button[0].id
checkForFileDownloaded(id, 'csv', () => done())
})
})
})
})
it('3 | downloads viewer excel', (done) => {
visitPage('view/data')
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openDownloadModal(() => {
cy.get('select')
.select('Excel')
.then(() => {
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
button.trigger('click')
const id = button[0].id
checkForFileDownloaded(id, 'xlsx', () => done())
})
})
})
})
it('4 | downloads viewer SAS Datalines', (done) => {
visitPage('view/data')
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openDownloadModal(() => {
cy.get('select')
.select('SAS Datalines')
.then(() => {
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
button.trigger('click')
const id = button[0].id
checkForFileDownloaded(id, 'sas', () => done())
})
})
})
})
it('5 | downloads viewer SAS DDL', (done) => {
visitPage('view/data')
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openDownloadModal(() => {
cy.get('select')
.select('SAS DDL')
.then(() => {
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
button.trigger('click')
const id = button[0].id
checkForFileDownloaded(id, 'ddl', () => done(), '_')
})
})
})
})
it('6 | downloads viewer TSQL DDL', (done) => {
visitPage('view/data')
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openDownloadModal(() => {
cy.get('select')
.select('TSQL DDL')
.then(() => {
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
button.trigger('click')
const id = button[0].id
checkForFileDownloaded(id, 'ddl', () => done(), '_')
})
})
})
})
it('7 | downloads viewer PGSQL DDL', (done) => {
visitPage('view/data')
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openDownloadModal(() => {
cy.get('select')
.select('PGSQL DDL')
.then(() => {
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
button.trigger('click')
const id = button[0].id
checkForFileDownloaded(id, 'ddl', () => done(), '_')
})
})
})
})
this.afterEach(() => {
cy.visit(`${hostUrl}/SASLogon/logout`)
})
this.afterAll(() => {
cy.visit(`https://sas.4gl.io/mihmed/cypress_finish`)
})
})
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}
const checkForFileDownloaded = (
id: string,
extension: string,
callback?: any,
libDivider: string = '.'
) => {
cy.on('url:changed', (newUrl) => {
console.log('newUrl', newUrl)
})
id = id.replace('.', libDivider)
const filename = downloadsFolder + '/' + id + '.' + extension
// browser might take a while to download the file,
// so use "cy.readFile" to retry until the file exists
// and has length - and we assume that it has finished downloading then
cy.readFile(filename, { timeout: longerCommandTimeout })
.should('have.length.gt', 10)
.then((file) => {
if (callback) callback()
})
}
const openDownloadModal = (callback?: any) => {
cy.get('.btn.btn-sm.btn-outline.filterSide.dropdown-toggle')
.click()
.then(() => {
cy.get('clr-dropdown-menu button').then((buttons) => {
for (let button of buttons) {
if (button.innerText.toLowerCase().includes('download')) {
button.click()
if (callback) callback()
}
}
})
})
}
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 viyaLib
for (let node of treeNodes) {
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
viyaLib = node
break
}
}
console.log('viyaLib', viyaLib)
cy.get(viyaLib).within(() => {
cy.get(
'.clr-tree-node-content-container .clr-treenode-content p'
).click()
cy.get('.clr-treenode-link').then((innerNodes: any) => {
for (let innerNode of innerNodes) {
if (innerNode.innerText.toLowerCase().includes(tablename)) {
innerNode.click()
break
}
}
})
})
})
})
}
+246
View File
@@ -0,0 +1,246 @@
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 = 'excels_general/'
context('editor tests: ', function () {
this.beforeAll(() => {
cy.visit(`${hostUrl}/SASLogon/logout`)
cy.loginAndUpdateValidKey()
})
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
visitPage('home')
})
it('1 | Submits duplicate primary keys', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
attachExcelFile('MPE_DATADICTIONARY_duplicate_keys.xlsx', () => {
clickOnUploadPreview(() => {
confirmEditPreviewFile(() => {
submitTable(() => {
cy.get('.modal-body').then((modalBody: any) => {
if (modalBody[0].innerText.includes(`Duplicates found:`)) {
done()
}
})
})
})
})
})
})
it('2 | Submits null cells which must not be null', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
clickOnEdit(() => {
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
timeout: longerCommandTimeout
}).then(() => {
cy.get('.ht_master tbody tr').then((rows: any) => {
cy.get(rows[1].childNodes[2])
.dblclick({ force: true })
.then(() => {
cy.focused()
.clear()
.type('{enter}')
.then(() => {
submitTable(() => {
cy.get('.modal-body').then((modalBody: any) => {
if (
modalBody[0].innerHTML
.toLowerCase()
.includes(`invalid values are present`)
) {
done()
}
})
})
})
})
})
})
})
})
it('3 | Gets basic dynamic cell validation', () => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
clickOnEdit(() => {
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
timeout: longerCommandTimeout
}).then(() => {
cy.get('.ht_master tbody tr').then((rows: any) => {
cy.get(rows[1].childNodes[5])
.click({ force: true })
.then(($td) => {
cy.get('.htAutocompleteArrow', { withinSubject: $td }).should(
'exist'
)
})
})
})
})
})
it('4 | Gets advanced dynamic cell validation', () => {
openTableFromTree(libraryToOpenIncludes, 'mpe_tables')
clickOnEdit(() => {
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
timeout: longerCommandTimeout
}).then(() => {
cy.get('.ht_master tbody tr').then((rows: any) => {
cy.get(rows[1].childNodes[3])
.click({ force: true })
.then(($td) => {
cy.get('.htAutocompleteArrow', { withinSubject: $td }).should(
'exist'
)
cy.get('.htAutocompleteArrow', {
withinSubject: rows[1].childNodes[7]
}).should('exist')
cy.get('.htAutocompleteArrow', {
withinSubject: rows[1].childNodes[8]
}).should('exist')
})
})
})
})
})
})
const clickOnEdit = (callback?: any) => {
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
.click()
.then(() => {
if (callback) callback()
})
}
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 viyaLib
for (let node of treeNodes) {
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
viyaLib = node
break
}
}
cy.get(viyaLib).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 attachExcelFile = (excelFilename: string, callback?: any) => {
cy.get('.buttonBar button:last-child')
.click()
.then(() => {
cy.get('input[type="file"]#file-upload')
.attachFile(`/${fixturePath}/${excelFilename}`)
.then(() => {
cy.get('.modal-footer .btn.btn-primary').then((modalBtn) => {
modalBtn.click()
if (callback) callback()
})
})
})
}
const clickOnUploadPreview = (callback?: any) => {
cy.get('.buttonBar button.btn-primary.btn-upload-preview')
.click()
.then(() => {
if (callback) callback()
})
}
const confirmEditPreviewFile = (callback?: any) => {
cy.get('.modal-footer button.btn-success-outline')
.click()
.then(() => {
if (callback) callback()
})
}
const submitTable = (callback?: any) => {
cy.get('.btnCtrl button.btn-primary')
.click()
.then(() => {
if (callback) callback()
})
}
const submitTableMessage = (callback?: any) => {
cy.get('.modal-footer .btn.btn-sm.btn-success-outline')
.click()
.then(() => {
if (callback) callback()
})
}
const submitExcel = (callback?: any) => {
cy.get('.buttonBar button.preview-submit')
.click()
.then(() => {
if (callback) callback()
})
}
const rejectExcel = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Approve')
.then((allButtons: any) => {
for (let approvalButton of allButtons) {
if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.click()
break
}
}
cy.get('button.btn-danger')
.should('exist')
.should('not.be.disabled')
.click()
.then(() => {
cy.get('.modal-footer button.btn-success-outline')
.click()
.then(() => {
cy.get('app-history')
.should('exist')
.then(() => {
if (callback) callback()
})
})
})
})
}
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}
+527
View File
@@ -0,0 +1,527 @@
import { Callbacks } from 'cypress/types/jquery/index'
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 = 'excels/'
// TODO: 4 and 9 failing
context('excel tests: ', function () {
this.beforeAll(() => {
cy.visit(`${hostUrl}/SASLogon/logout`)
cy.loginAndUpdateValidKey()
})
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
visitPage('home')
colorLog(
`TEST START ---> ${
Cypress.mocha.getRunner().suite.ctx.currentTest.title
}`,
'#3498DB'
)
})
it('1 | Uploads regular Excel file', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('regular_excel.xlsx', () => {
submitExcel()
rejectExcel(done)
})
})
it('2 | Uploads Excel with data on the 7th tab', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('7th_tab_excel.xlsx', () => {
submitExcel()
rejectExcel(done)
})
})
it('3 | Uploads Excel with missing columns (should fail)', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('missing_columns_excel.xlsx', () => {
cy.get('.abortMsg', { timeout: longerCommandTimeout })
.should('exist')
.then((elements: any) => {
if (elements[0]) {
if (elements[0].innerText.toLowerCase().includes('missing')) done()
}
})
})
})
it('4 | Uploads Excel with formulas', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
attachExcelFile('formulas_excel.xlsx', () => {
checkResultOfFormulaUpload(done)
})
})
it('5 | Uploads Excel with no data rows', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('nodata_rows_excel.xlsx', () => {
cy.get('.abortMsg', { timeout: longerCommandTimeout })
.should('exist')
.then((elements: any) => {
if (elements[0]) {
if (
elements[0].innerText
.toLowerCase()
.includes('no relevant data found')
)
done()
}
})
})
})
it('6 | Uploads Excel with a table that is surrounded by other data', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('surrounded_data_excel.xlsx', () => {
submitExcel()
rejectExcel(done)
})
})
it('7 | Uploads Excel with a extra columns in the middle', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('extra_column_excel.xlsx', () => {
submitExcel()
rejectExcel(done)
})
})
it('8 | Uploads Excel with a duplicate column', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('duplicate_column_excel.xlsx', () => {
cy.get('.abortMsg', { timeout: longerCommandTimeout })
.should('exist')
.then((elements: any) => {
if (elements[0]) {
if (elements[0].innerText.toLowerCase().includes('missing')) done()
}
})
})
})
// it('9 | Uploads Excel with a duplicate row', (done) => {
// openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
// attachExcelFile('duplicate_row_excel.xlsx', () => {
// submitExcel(() => {
// cy.get('.abortMsg', { timeout: longerCommandTimeout })
// .should('exist')
// .then((elements: any) => {
// if (elements[0]) {
// if (elements[0].innerText.toLowerCase().includes('duplicates'))
// done()
// }
// })
// })
// })
// })
it('10 | Uploads Excel with a mixed content', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('mixed_content_excel.xlsx', () => {
submitExcel(() => {
cy.get('.modal-body').then((modalBody: any) => {
if (
modalBody[0].innerHTML
.toLowerCase()
.includes(`invalid values are present`)
) {
done()
}
})
})
})
})
it('11 | Uploads Excel with a blank columns', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('blank_columns_excel.xlsx', () => {
cy.get('.abortMsg', { timeout: longerCommandTimeout })
.should('exist')
.then((elements: any) => {
if (elements[0]) {
if (elements[0].innerText.toLowerCase().includes('missing')) done()
}
})
})
})
it('12 | Uploads Excel xls extension', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('regular_excel_xls.xls', () => {
submitExcel()
rejectExcel(done)
})
})
// For some strange reason this file breaks cypress. When uploaded manually in DC it is working.
// it('13 | Uploads Excel xlsm extension', (done) => {
// openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
// attachExcelFile('regular_excel_macro.xlsm', () => {
// submitExcel()
// rejectExcel(done)
// })
// })
it('14 | Uploads Excel with composite primary key', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
attachExcelFile('MPE_DATADICTIONARY_composite_keys.xlsx', () => {
submitExcel()
rejectExcel(done)
})
})
it('15 | Uploads Excel with missing row (empty table)', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
attachExcelFile('MPE_DATADICTIONARY_missing_row.xlsx', () => {
cy.get('.abortMsg', { timeout: longerCommandTimeout })
.should('exist')
.then((elements: any) => {
if (elements[0]) {
if (
elements[0].innerText
.toLowerCase()
.includes('no relevant data found')
)
done()
}
})
})
})
it('16 | Uploads Excel with merged cells', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
attachExcelFile('MPE_DATADICTIONARY_merged_cells.xlsx', () => {
submitExcel()
rejectExcel(done)
})
})
it('17 | Check uploaded values from excel with xls extension', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('regular_excel_xls.xls', () => {
checkResultOfXLSUpload(done)
})
})
it('18 | Uploads Excel with missing row (empty table)', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('blank_column_with_header.xlsx', () => {
cy.get('.btn-upload-preview', { timeout: 60000 })
.should('be.visible')
.then(() => {
cy.get('#hotInstance', { timeout: 30000 })
.find('div.ht_master.handsontable')
.find('div.wtHolder')
.find('div.wtHider')
.find('div.wtSpreader')
.find('table.htCore')
.find('tbody')
.should((data) => {
let allEmpty = true
for (let col = 0; col < data[0].children.length; col++) {
const cell: any = data[0].children[col].children[5]
if (cell.innerText !== '') {
allEmpty = false
break
}
}
if (allEmpty) done()
})
})
})
})
it('19 | Uploads Excel with data on random sheet surrounded with all empty cells', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('surrounded_data_all_cells_empty_excel.xlsx', () => {
checkResultOfXLSUpload(done)
})
})
it('20 | Uploads Excel with data surrounded with empty cells ', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('surrounded_data_empty_cells_excel.xlsx', () => {
checkResultOfXLSUpload(done)
})
})
it('21 | Uploads regular Excel file with first row marked for Delete (yes)', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('regular_excel_with_delete.xlsx', () => {
cy.get('.btn-upload-preview', { timeout: 60000 })
.should('be.visible')
.then(() => {
cy.get('#hotInstance', { timeout: 30000 })
.find('div.ht_master.handsontable')
.find('div.wtHolder')
.find('div.wtHider')
.find('div.wtSpreader')
.find('table.htCore')
.find('tbody')
.should((data: JQuery<HTMLTableSectionElement>) => {
const firstRowFirstCol: Partial<HTMLElement> =
data[0].children[0].children[1]
if (
firstRowFirstCol.innerText &&
!firstRowFirstCol.innerText.toLowerCase().includes('yes')
) {
done('Delete? column from file not applied')
}
})
.then(() => {
submitExcel()
rejectExcel(done)
})
})
})
})
// Large files break Cypress
// it ('? | Uploads Excel with size of 5MB', (done) => {
// attachExcelFile('5mb_excel.xlsx', () => {
// submitExcel();
// rejectExcel(done);
// });
// })
// it ('? | Uploads Excel with size of 15MB', (done) => {
// attachExcelFile('15mb_excel.xlsx', () => {
// submitExcel();
// rejectExcel(done);
// });
// })
//Large files tests end
this.afterEach(() => {
colorLog(`TEST END -------------`, '#3498DB')
})
})
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 viyaLib
for (let node of treeNodes) {
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
viyaLib = node
break
}
}
cy.get(viyaLib).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 attachExcelFile = (excelFilename: string, callback?: any) => {
cy.get('.buttonBar button:last-child')
.should('exist')
.click()
.then(() => {
cy.get('input[type="file"]#file-upload')
.attachFile(`/${fixturePath}/${excelFilename}`)
.then(() => {
cy.get('.clr-abort-modal .modal-title').then((modalTitle) => {
if (!modalTitle[0].innerHTML.includes('Abort Message')) {
cy.get('.modal-footer .btn.btn-primary').then((modalBtn) => {
modalBtn.click()
if (callback) callback()
})
} else {
if (callback) callback()
}
})
})
})
}
const submitExcel = (callback?: any) => {
cy.get('.buttonBar button.preview-submit', { timeout: longerCommandTimeout })
.click()
.then(() => {
if (callback) callback()
})
}
const rejectExcel = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Approve')
.then((allButtons: any) => {
for (let approvalButton of allButtons) {
if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.click()
break
}
}
cy.get('button.btn-danger')
.should('exist')
.should('not.be.disabled')
.click()
.then(() => {
cy.get('.modal-footer button.btn-success-outline')
.click()
.then(() => {
cy.get('app-history')
.should('exist')
.then(() => {
if (callback) callback()
})
})
})
})
}
const acceptExcel = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Approve')
.then((allButtons: any) => {
for (let approvalButton of allButtons) {
if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.click()
break
}
}
cy.get('#acceptBtn')
.should('exist')
.should('not.be.disabled')
.click()
.then(() => {
if (callback) {
callback()
}
})
})
}
const checkResultOfFormulaUpload = (callback?: any) => {
cy.get('#hotInstance', { timeout: longerCommandTimeout })
.find('div.ht_master.handsontable')
.find('div.wtHolder')
.find('div.wtHider')
.find('div.wtSpreader')
.find('table.htCore')
.find('tbody')
.should((data) => {
const cell: any = data[0].children[0].children[5]
expect(cell.innerText).to.equal('=1+1')
if (callback) callback()
})
}
const checkResultOfXLSUpload = (callback?: any) => {
cy.viewport(1280, 720)
cy.get('#hotInstance', { timeout: 30000 })
.find('div.ht_master.handsontable')
.find('div.wtHolder')
.find('div.wtHider')
.find('div.wtSpreader')
.find('table.htCore')
.find('tbody')
.should((data) => {
let cell: any = data[0].children[0].children[2]
expect(cell.innerText).to.equal('0')
cell = data[0].children[0].children[3]
expect(cell.innerText).to.equal('this is dummy data changed in excel')
cell = data[0].children[0].children[4]
expect(cell.innerText).to.equal('▼\nOption 1')
cell = data[0].children[0].children[5]
expect(cell.innerText).to.equal('42')
cell = data[0].children[0].children[6]
expect(cell.innerText).to.equal('▼\n1960-02-12')
cell = data[0].children[0].children[7]
expect(cell.innerText).to.equal('▼\n1960-01-01 00:00:42')
cell = data[0].children[0].children[8]
expect(cell.innerText).to.equal('00:00:42')
cell = data[0].children[0].children[9]
expect(cell.innerText).to.equal('3')
if (callback) callback()
})
cy.get('#hotInstance', { timeout: 30000 })
.find('div.ht_master.handsontable')
.find('div.wtHolder')
.scrollTo('right')
.find('div.wtHider')
.find('div.wtSpreader')
.find('table.htCore')
.find('tbody')
.should((data) => {
let cell: any = data[0].children[0].children[1]
cell = data[0].children[0].children[9]
expect(cell.innerText).to.equal('44')
if (callback) callback()
})
}
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}
const colorLog = (msg: string, color: string) => {
console.log('%c' + msg, 'color:' + color + ';font-weight:bold;')
}
@@ -0,0 +1,376 @@
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 = 'excels_general/'
context('filtering tests: ', function () {
this.beforeAll(() => {
cy.visit(`${hostUrl}/SASLogon/logout`, { timeout: longerCommandTimeout })
cy.loginAndUpdateValidKey()
})
this.beforeEach(() => {
cy.visit(hostUrl + appLocation, { timeout: longerCommandTimeout })
visitPage('home')
})
it('1 | filter char field', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openFilterPopup(() => {
setFilterWithValue('SOME_CHAR', 'this is dummy data', 'value', () => {
checkInfoBarIncludes(
`AND,AND,0,SOME_CHAR,=,"'this is dummy data'"`,
(includes: boolean) => {
if (includes) done()
}
)
})
})
})
it('2 | filter number field', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openFilterPopup(() => {
setFilterWithValue('SOME_NUM', '42', 'value', () => {
checkInfoBarIncludes(`AND,AND,0,SOME_NUM,=,42`, (includes: boolean) => {
if (includes) done()
})
})
})
})
it.only('3 | filter time field', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openFilterPopup(() => {
setFilterWithValue('SOME_TIME', '00:00:42', 'time', () => {
checkInfoBarIncludes(
`AND,AND,0,SOME_TIME,=,42`,
(includes: boolean) => {
if (includes) done()
}
)
})
})
})
it('3.1 | Non picker - filter time field', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openFilterPopup(() => {
setFilterWithValue('SOME_TIME', '42', 'value', () => {
checkInfoBarIncludes(
`AND,AND,0,SOME_TIME,=,42`,
(includes: boolean) => {
if (includes) done()
}
)
})
}, false)
})
it('4 | filter date field', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openFilterPopup(() => {
setFilterWithValue('SOME_DATE', '12/02/1960', 'date', () => {
checkInfoBarIncludes(
`AND,AND,0,SOME_DATE,=,42`,
(includes: boolean) => {
if (includes) done()
}
)
})
})
})
it('4.1 | Non picker - filter date field', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openFilterPopup(() => {
setFilterWithValue('SOME_DATE', '42', 'value', () => {
checkInfoBarIncludes(
`AND,AND,0,SOME_DATE,=,42`,
(includes: boolean) => {
if (includes) done()
}
)
})
}, false)
})
it('5 | filter datetime field', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openFilterPopup(() => {
setFilterWithValue(
'SOME_DATETIME',
'01/01/1960 00:00:42',
'datetime',
() => {
checkInfoBarIncludes(
`AND,AND,0,SOME_DATETIME,=,42`,
(includes: boolean) => {
if (includes) done()
}
)
}
)
})
})
it('5.1 | Non picker - filter datetime field', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openFilterPopup(() => {
setFilterWithValue('SOME_DATETIME', '42', 'value', () => {
checkInfoBarIncludes(
`AND,AND,0,SOME_DATETIME,=,42`,
(includes: boolean) => {
if (includes) done()
}
)
})
}, false)
})
it('6 | filter date field IN', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openFilterPopup(() => {
setFilterWithValue('SOME_DATE', '', 'in', () => {
checkInfoBarIncludes(
`AND,AND,0,SOME_DATE,IN,(0)`,
(includes: boolean) => {
if (includes) done()
}
)
})
})
})
it('7 | filter bestnum field BETWEEN', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openFilterPopup(() => {
setFilterWithValue('SOME_BESTNUM', '0-10', 'between', () => {
checkInfoBarIncludes(
`AND,AND,0,SOME_BESTNUM,BETWEEN,0 AND 10`,
(includes: boolean) => {
if (includes) done()
}
)
})
})
})
})
const checkInfoBarIncludes = (text: string, callback: any) => {
cy.get('.infoBar b', { timeout: longerCommandTimeout }).then((el: any) => {
const includes = el[0].innerText.toLowerCase().includes(text.toLowerCase())
if (callback) callback(includes)
})
}
const openFilterPopup = (
callback?: any,
usePickers: boolean = true,
isViewerFiltering: boolean = false
) => {
const filterButton = isViewerFiltering
? '.btn-outline.filterSide'
: '.btnCtrl .btnView'
cy.get(filterButton, { timeout: longerCommandTimeout }).then(
(optionsButton: any) => {
optionsButton.click()
if (isViewerFiltering) {
cy.wait(300)
cy.get('.dropdown-menu button').then(async (dropdownButtons: any) => {
let filterButton = null
for (let btn of dropdownButtons) {
if (btn.innerText.toLowerCase().includes('filter')) {
filterButton = btn
break
}
}
if (filterButton) {
filterButton.click()
if (usePickers) turnOnPickers()
if (callback) callback()
return
}
})
}
if (usePickers) turnOnPickers()
if (callback) callback()
}
)
}
const turnOnPickers = () => {
cy.get('#usePickers')
.should('exist')
.then((picker: any) => {
picker[0].click()
})
}
const setFilterWithValue = (
variableValue: string,
valueString: string,
valueField: 'value' | 'time' | 'date' | 'datetime' | 'in' | 'between',
callback?: any
) => {
cy.wait(600)
cy.focused().type(variableValue)
cy.wait(100)
// cy.focused().trigger('input')
cy.get('.variable-col .autocomplete-wrapper', { withinSubject: null })
.first()
.trigger('keydown', { key: 'ArrowDown' })
cy.get('.variable-col .autocomplete-wrapper', {
withinSubject: null
}).trigger('keydown', { key: 'Enter' })
cy.focused().tab()
cy.wait(100)
if (valueField === 'in') {
cy.focused().select(valueField.toUpperCase()).trigger('change')
} else if (valueField === 'between') {
cy.focused().select(valueField.toUpperCase()).trigger('change')
} else {
cy.focused().tab()
cy.wait(100)
}
switch (valueField) {
case 'value': {
cy.focused().type(valueString)
break
}
case 'time': {
cy.focused().type(valueString)
break
}
case 'date': {
cy.focused().type(valueString)
cy.focused().tab()
break
}
case 'datetime': {
const date = valueString.split(' ')[0]
const time = valueString.split(' ')[1]
cy.focused().type(date)
cy.focused().tab()
cy.focused().tab()
cy.focused().type(time)
break
}
case 'in': {
cy.get('.checkbox-vals').then(() => {
cy.focused().tab()
cy.focused().click()
cy.get('.no-values')
.should('not.exist')
.then(() => {
cy.get('clr-checkbox-wrapper input').then((inputs: any) => {
inputs[0].click()
cy.get('.in-values-modal .modal-footer button').click()
cy.get('.modal-footer .btn-success-outline').click()
if (callback) callback()
})
})
})
break
}
case 'between': {
cy.focused().tab()
const start = valueString.split('-')[0]
const end = valueString.split('-')[1]
cy.focused().type(start)
cy.focused().tab()
cy.focused().type(end)
}
default: {
break
}
}
cy.wait(100)
cy.focused().tab()
cy.wait(100)
cy.focused().tab()
cy.wait(100)
cy.focused().tab()
cy.wait(100)
cy.focused().tab()
cy.wait(100)
cy.focused().click()
if (callback) callback()
}
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 viyaLib
for (let node of treeNodes) {
if (new RegExp(libNameIncludes).test(node.innerText.toLowerCase())) {
viyaLib = node
break
}
}
cy.get(viyaLib).within(() => {
cy.get('.clr-tree-node-content-container p').click()
cy.get('.clr-treenode-link').then((innerNodes: any) => {
for (let innerNode of innerNodes) {
if (innerNode.innerText.toLowerCase().includes(tablename)) {
innerNode.click()
break
}
}
})
})
})
})
}
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}
@@ -0,0 +1,719 @@
import { arrayBufferToBase64 } from './../util/helper-functions'
import * as moment from 'moment'
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 fixturePath = 'excels_general/'
const serverType = Cypress.env('serverType')
const site_id = Cypress.env(`site_id_${serverType}`)
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
const testLicenceUserLimits = Cypress.env('testLicenceUserLimits')
/** IMPORTANT NOTICE
* Before running tests, make sure that table `MPE_USERS` is present
*/
interface EditConfigTableCells {
varName: string
varValue: string
}
context('licensing tests: ', function () {
this.beforeAll(() => {
cy.loginAndUpdateValidKey()
})
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
visitPage('home')
})
it('1 | key valid, not expired', (done) => {
let keyData = {
valid_until: moment().add(1, 'year').format('YYYY-MM-DD'),
users_allowed: 4,
hot_license_key: '',
demo: false,
site_id: site_id
}
let keys: { licenseKey: any; activationKey: any }
generateKeys(keyData, (keysGen: any) => {
keys = keysGen
cy.wait(2000)
isLicensingPage((result: boolean) => {
if (result) {
inputLicenseKeyPage(keys.licenseKey, keys.activationKey)
cy.wait(2000)
}
acceptTermsIfPresented((result: boolean) => {
if (result) {
cy.wait(10000)
}
visitPage('home')
cy.get('.nav-tree clr-tree > clr-tree-node', {
timeout: longerCommandTimeout
}).then((treeNodes: any) => {
done()
})
})
})
})
})
it('2 | Key will expire in less then 14 days, not free tier', (done) => {
// make 2 separate for this one
let keyData = {
valid_until: moment().add(10, 'day').format('YYYY-MM-DD'),
users_allowed: 4,
hot_license_key: '',
demo: false,
site_id: site_id
}
let keys: { licenseKey: any; activationKey: any }
generateKeys(keyData, (keysGen: any) => {
keys = keysGen
console.log('keys', keys)
cy.wait(2000)
acceptTermsIfPresented((result: boolean) => {
if (result) {
cy.wait(20000)
}
updateLicenseKeyQuick(keysGen, () => {
cy.wait(2000)
acceptTermsIfPresented((result: boolean) => {
if (result) {
cy.wait(20000)
}
verifyLicensingWarning('This license key will expire in ', () => {
done()
})
})
})
})
})
})
it('3 | key expired, free tier works', (done) => {
let keyData = {
valid_until: moment().subtract(1, 'day').format('YYYY-MM-DD'),
users_allowed: 4,
hot_license_key: '',
demo: false,
site_id: site_id
}
let keys: { licenseKey: any; activationKey: any }
generateKeys(keyData, (keysGen: any) => {
keys = keysGen
cy.wait(2000)
acceptTermsIfPresented((result: boolean) => {
if (result) {
cy.wait(20000)
}
cy.wait(2000)
updateLicenseKeyQuick(keysGen, () => {
cy.wait(2000)
acceptTermsIfPresented((result: boolean) => {
if (result) {
cy.wait(20000)
}
verifyLicensingPage(
'Licence key is expired - please contact',
(success: boolean) => {
if (success) {
verifyLicensingWarning(
'(FREE Tier) - Problem with licence',
() => {
done()
}
)
}
}
)
})
})
})
})
})
it('4 | key invalid, free tier works', (done) => {
let keyData = {
valid_until: moment().subtract(1, 'day').format('YYYY-MM-DD'),
users_allowed: 4,
hot_license_key: '',
demo: false,
site_id: site_id
}
let keys: { licenseKey: any; activationKey: any }
generateKeys(keyData, (keysGen: any) => {
keys = keysGen
keys.activationKey = 'invalid' + keys.activationKey
cy.wait(2000)
acceptTermsIfPresented((result: boolean) => {
if (result) {
cy.wait(20000)
}
cy.wait(2000)
updateLicenseKeyQuick(keysGen, () => {
cy.wait(2000)
acceptTermsIfPresented((result: boolean) => {
if (result) {
cy.wait(20000)
}
verifyLicensingPage(
'Licence key is invalid - please contact',
(success: boolean) => {
if (success) {
verifyLicensingWarning(
'(FREE Tier) - Problem with licence',
() => {
done()
}
)
}
}
)
})
})
})
})
})
it('5 | key for wrong organisation, free tier works', (done) => {
let keyData = {
valid_until: moment().add(1, 'year').format('YYYY-MM-DD'),
users_allowed: 4,
hot_license_key: '',
demo: false,
site_id: 100
}
let keys: { licenseKey: any; activationKey: any }
generateKeys(keyData, (keysGen: any) => {
keys = keysGen
keys.activationKey = keys.activationKey
cy.wait(2000)
acceptTermsIfPresented((result: boolean) => {
if (result) {
cy.wait(20000)
}
cy.wait(2000)
updateLicenseKeyQuick(keysGen, () => {
cy.wait(2000)
acceptTermsIfPresented((result: boolean) => {
if (result) {
cy.wait(20000)
}
verifyLicensingPage(
'SYSSITE (below) is not found',
(success: boolean) => {
if (success) {
verifyLicensingWarning(
'(FREE Tier) - Problem with licence',
() => {
done()
}
)
}
}
)
})
})
})
})
})
if (testLicenceUserLimits) {
it('4 | User try to register when limit is reached', (done) => {
let keyData = {
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
users_allowed: 10,
hot_license_key: '',
demo: false,
site_id: site_id
}
let keyData2 = {
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
users_allowed: 1,
hot_license_key: '',
demo: false,
site_id: site_id
}
generateKeys(keyData, (keysGen: any) => {
generateKeys(keyData2, (keysGen2: any) => {
cy.wait(2000)
acceptTermsIfPresented((result: boolean) => {
if (result) {
cy.wait(20000)
}
updateLicenseKeyQuick(keysGen, () => {
cy.wait(2000)
acceptTermsIfPresented((result: boolean) => {
if (result) {
cy.wait(20000)
}
updateLicenseKeyQuick(keysGen2, () => {
cy.wait(2000)
const random = Cypress._.random(0, 1000)
const newUser = {
username: `randomusername${random}notregistered`,
last_seen_at: moment().add(1, 'month').format('YYYY-MM-DD'),
registered_at: moment().add(1, 'month').format('YYYY-MM-DD')
}
updateUsersTable(
{ deleteAll: true, newUsers: [newUser] },
() => {
logout(() => {
cy.visit(hostUrl + appLocation)
// cy.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
visitPage('home')
cy.wait(2000)
verifyLicensingPage(
'The registered number of users reached the limit specified for your licence.',
(success: boolean) => {
if (success) done()
}
)
})
}
)
})
})
})
})
})
})
})
it('5 | Show warning banner when limit is exceeded', (done) => {
let keyData = {
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
users_allowed: 10,
hot_license_key: '',
demo: false,
site_id: site_id
}
let keyData2 = {
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
users_allowed: 1,
hot_license_key: '',
demo: false,
site_id: site_id
}
generateKeys(keyData, (keysGen: any) => {
generateKeys(keyData2, (keysGen2: any) => {
cy.wait(2000)
acceptTermsIfPresented((result: boolean) => {
if (result) {
cy.wait(20000)
}
updateLicenseKeyQuick(keysGen, () => {
cy.wait(2000)
acceptTermsIfPresented((result: boolean) => {
if (result) {
cy.wait(20000)
}
const random = Cypress._.random(0, 1000)
const newUser = {
username: `randomusername${random}`,
last_seen_at: moment().add(1, 'month').format('YYYY-MM-DD'),
registered_at: moment().add(1, 'month').format('YYYY-MM-DD')
}
updateUsersTable(
{ deleteAll: true, keep: username, newUsers: [newUser] },
() => {
updateLicenseKeyQuick(keysGen2, () => {
cy.wait(2000)
verifyLicensingWarning(
'The registered number of users exceeds the limit specified for your license.',
() => {
done()
}
)
})
}
)
})
})
})
})
})
})
}
})
const logout = (callback?: any) => {
cy.get('.header-actions .dropdown-toggle')
.click()
.then(() => {
cy.get('.header-actions .dropdown-menu > .separator')
.next()
.click()
.then(() => {
if (callback) callback()
})
})
}
const acceptTermsIfPresented = (callback?: any) => {
cy.url().then((url: string) => {
if (url.includes('licensing/register')) {
cy.get('.card-block')
.scrollTo('bottom')
.then(() => {
cy.get('#checkbox1')
.click()
.then(() => {
if (callback) callback(true)
})
})
} else {
if (callback) callback(false)
}
})
}
const isLicensingPage = (callback: any) => {
return cy.url().then((url: string) => {
callback(
url.includes('#/licensing/') && !url.includes('licensing/register')
)
})
}
const verifyLicensingPage = (text: string, callback: any) => {
// visitPage('home')
cy.wait(1000)
isLicensingPage((result: boolean) => {
if (result) {
cy.get('p.key-error')
.should('contain', text)
.then((treeNodes: any) => {
callback(true)
})
}
})
}
const verifyLicensingWarning = (text: string, callback: any) => {
visitPage('home')
cy.wait(1000)
cy.get("div[role='alert'] .alert-text")
.invoke('text')
.should('contain', text)
.then(() => {
callback()
})
}
const inputLicenseKeyPage = (licenseKey: string, activationKey: string) => {
cy.get('button').contains('Paste licence').click()
cy.get('.license-key-form textarea', { timeout: longerCommandTimeout })
.invoke('val', licenseKey)
.trigger('input')
.should('not.be.undefined')
cy.get('.activation-key-form textarea', { timeout: longerCommandTimeout })
.invoke('val', activationKey)
.trigger('input')
.should('not.be.undefined')
cy.get('button.apply-keys').click()
}
const updateUsersTable = (options: any, callback?: any) => {
visitPage('home')
openTableFromTree(libraryToOpenIncludes, 'mpe_users')
clickOnEdit(() => {
cy.get('.ht_master tbody tr').then((rows: any) => {
if (options.deleteAll) {
for (let row of rows) {
const user_id = row.childNodes[2]
if (!options.keep || user_id.innerText !== options.keep) {
cy.get(row.childNodes[1])
.dblclick()
.then(() => {
cy.focused().type('{selectall}').type('Yes').type('{enter}')
})
}
}
}
if (options.newUsers && options.newUsers.length) {
for (let newUser of options.newUsers) {
clickOnAddRow(() => {
cy.get('#hotInstance tbody tr:last-child').then((rows: any) => {
cy.get(rows[0].childNodes[2])
.dblclick()
.then(() => {
cy.focused()
.type('{selectall}')
.type(newUser.username)
.type('{enter}')
})
// cy.get(rows[0].childNodes[3])
// .dblclick()
// .then(() => {
// cy.focused()
// .type('{selectall}')
// .type(newUser.last_seen_at)
// .type('{enter}')
// })
// cy.get(rows[0].childNodes[4])
// .dblclick()
// .then(() => {
// cy.focused()
// .type('{selectall}')
// .type(newUser.registered_at)
// .type('{enter}')
// })
submitTable(() => {
cy.wait(2000)
approveTable(callback)
})
})
})
}
}
})
})
}
const changeLicenseKeyTable = (keys: any, callback?: any) => {
visitPage('home')
openTableFromTree(libraryToOpenIncludes, 'mpe_config')
clickOnEdit(() => {
editTableField(
[
{ varName: 'DC_ACTIVATION_KEY', varValue: keys.activationKey },
{ varName: 'DC_LICENCE_KEY', varValue: keys.licenseKey }
],
() => {
submitTable(() => {
cy.wait(2000)
approveTable(() => {
cy.reload()
if (callback) callback()
})
})
}
)
})
}
const updateLicenseKeyQuick = (keys: any, callback: any) => {
isLicensingPage((result: boolean) => {
if (!result) {
visitPage('licensing/update')
cy.wait(2000)
}
inputLicenseKeyPage(keys.licenseKey, keys.activationKey)
callback()
})
}
const generateKeys = async (licenseData: any, resultCallback?: any) => {
let keyPair = await window.crypto.subtle.generateKey(
{
name: 'RSA-OAEP',
modulusLength: 2024,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256'
},
true,
['encrypt', 'decrypt']
)
const encoded = new TextEncoder().encode(JSON.stringify(licenseData))
const cipher = await window.crypto.subtle
.encrypt(
{
name: 'RSA-OAEP'
},
keyPair.publicKey,
encoded
)
.then(
(value) => {
return value
},
(err) => {
console.log('Encrpyt error', err)
}
)
if (!cipher) {
alert('Encryptin keys failed')
throw new Error('Encryptin keys failed')
}
const privateKeyBytes = await window.crypto.subtle.exportKey(
'pkcs8',
keyPair.privateKey
)
const activationKey = await arrayBufferToBase64(privateKeyBytes)
const licenseKey = await arrayBufferToBase64(cipher)
if (resultCallback)
resultCallback({
activationKey,
licenseKey
})
}
const editTableField = (edits: EditConfigTableCells[], callback?: any) => {
cy.get('td').then((tdNodes: any) => {
for (let edit of edits) {
let correctRow = false
for (let node of tdNodes) {
if (correctRow) {
cy.get(node)
.dblclick()
.then(() => {
// textarea update on long keys
cy.focused().invoke('val', edit.varValue).type('{enter}')
})
correctRow = false
break
}
if (node.innerText.includes(edit.varName)) {
correctRow = true
}
}
}
if (callback) callback()
})
}
const openTableFromTree = (
libNameIncludes: string,
tablename: string,
callback?: any
) => {
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 viyaLib
for (let node of treeNodes) {
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
viyaLib = node
break
}
}
cy.get(viyaLib).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()
if (callback) callback()
break
}
}
})
})
})
})
}
const clickOnAddRow = (callback?: any) => {
cy.get('.btnCtrl button.btn-success')
.click()
.then(() => {
if (callback) callback()
})
}
const clickOnEdit = (callback?: any) => {
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
.click()
.then(() => {
if (callback) callback()
})
}
const submitTable = (callback?: any) => {
cy.get('.btnCtrl button.btn-primary', { timout: longerCommandTimeout })
.click()
.then(() => {
cy.get(".modal.ng-star-inserted button[type='submit']")
.click()
.then(() => {
if (callback) callback()
})
})
}
const approveTable = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Approve')
.then((allButtons: any) => {
for (let approvalButton of allButtons) {
if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.click()
break
}
}
cy.get('button#acceptBtn', { timeout: longerCommandTimeout })
.should('exist')
.should('not.be.disabled')
.click()
.then(() => {
cy.get('app-history', { timeout: longerCommandTimeout })
.should('exist')
.then(() => {
if (callback) callback()
})
})
})
}
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}
@@ -0,0 +1,149 @@
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 = 'excels/'
context('liveness tests: ', function () {
this.beforeAll(() => {
if (serverType !== 'SASJS') {
cy.visit(`${hostUrl}/SASLogon/logout`)
}
cy.loginAndUpdateValidKey(true)
})
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
visitPage('home')
})
it('1 | Login and submit test', (done) => {
cy.get('.nav-tree clr-tree > clr-tree-node', {
timeout: longerCommandTimeout
}).then((treeNodes: any) => {
libraryExistsInTree('viya', treeNodes)
? openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
: openTableFromTree('dc', 'mpe_x_test')
attachExcelFile('regular_excel.xlsx', () => {
submitExcel()
rejectExcel(done)
})
})
})
/**
* Thist part will be needed if we add more tests in future
*/
// this.afterEach(() => {
// cy.visit('https://sas.4gl.io/SASLogon/logout');
// })
})
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}
const libraryExistsInTree = (libName: string, nodes: any) => {
for (let node of nodes) {
if (node.innerText.toLowerCase().includes(libName.toLowerCase()))
return true
}
return false
}
const openTableFromTree = (
libNameIncludes: string,
tablename: string,
finish: any
) => {
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 viyaLib
for (let node of treeNodes) {
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
viyaLib = node
break
}
}
if (!viyaLib && finish) finish(false)
cy.get(viyaLib).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()
if (finish) finish(true)
break
}
}
})
})
})
})
}
const attachExcelFile = (excelFilename: string, callback?: any) => {
cy.get('.buttonBar button:last-child')
.should('exist')
.click()
.then(() => {
cy.get('input[type="file"]#file-upload').attachFile(
`/${fixturePath}/${excelFilename}`
)
cy.get('.modal-footer .btn.btn-primary').then((modalBtn) => {
modalBtn.click()
if (callback) callback()
})
})
}
const submitExcel = (callback?: any) => {
cy.get('.buttonBar button.preview-submit', { timeout: longerCommandTimeout })
.click()
.then(() => {
if (callback) callback()
})
}
const rejectExcel = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Approve')
.then((allButtons: any) => {
for (let approvalButton of allButtons) {
if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.click()
break
}
}
cy.get('button.btn-danger')
.should('exist')
.should('not.be.disabled')
.click()
.then(() => {
cy.get('.modal-footer button.btn-success-outline')
.click()
.then(() => {
cy.get('app-history')
.should('exist')
.then(() => {
if (callback) callback()
})
})
})
})
}
@@ -0,0 +1,61 @@
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 = 'excels_general/'
context('metanav tests: ', function () {
this.beforeAll(() => {
cy.visit(`${hostUrl}/SASLogon/logout`)
cy.loginAndUpdateValidKey()
})
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
cy.get('input.username').type(username)
cy.get('input.password').type(password)
cy.get('.login-group button').click()
visitPage('view/metadata')
})
it('1 | Opens metadata object', (done) => {
openFirstMetadataFromTree(() => {
// BLOCKER
// For unkown reasons, .clr-accordion-header-button always null although it is present on the page.
cy.get('.clr-accordion-header-button').then((panelNodes: any) => {
panelNodes[0].querySelector('button').click()
})
})
})
this.afterEach(() => {
cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
const openFirstMetadataFromTree = (callback?: any) => {
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 firstMetaNode
firstMetaNode = treeNodes[1]
cy.get(firstMetaNode).within(() => {
cy.get('.clr-treenode-content').click()
callback()
})
})
})
}
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}
+624
View File
@@ -0,0 +1,624 @@
import { cloneDeep } from 'lodash-es'
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 = 'excels_general/'
context('editor tests: ', function () {
this.beforeAll(() => {
cy.visit(`${hostUrl}/SASLogon/logout`)
cy.loginAndUpdateValidKey()
})
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
cy.wait(2000)
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()
}
})
visitPage('home')
})
it('1 | Add one viewbox', (done) => {
const viewbox_table = 'mpe_audit'
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
cy.get('.viewbox-open').click()
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
cy.get('.open-viewbox').then((viewboxNodes: any) => {
for (let viewboxNode of viewboxNodes) {
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
return
}
checkColumns(columns, () => {
done()
})
}
})
})
it('2 | Add two viewboxes', (done) => {
const viewboxes = [
{
viewbox_table: 'mpe_audit',
columns: ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
},
{
viewbox_table: 'mpe_alerts',
columns: [
'TX_FROM',
'ALERT_EVENT',
'ALERT_LIB',
'ALERT_DS',
'ALERT_USER'
]
}
]
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
cy.get('.viewbox-open').click()
openTableFromViewboxTree(
libraryToOpenIncludes,
viewboxes.map((viewbox) => viewbox.viewbox_table)
)
cy.get('.open-viewbox').then((viewboxNodes: any) => {
let found = 0
for (let viewboxNode of viewboxNodes) {
for (let viewbox of viewboxes) {
if (
viewboxNode.innerText.toLowerCase().includes(viewbox.viewbox_table)
)
found++
}
}
if (found < viewboxes.length) return
cy.get('.viewboxes-container .viewbox').then((viewboxNodes: any) => {
for (let viewboxNode of viewboxNodes) {
cy.get(viewboxNode).within(() => {
cy.get('.table-title').then((tableTitle) => {
const title = tableTitle[0].innerText
const viewbox = viewboxes.find((vb) =>
title.toLowerCase().includes(vb.viewbox_table)
)
if (viewbox) {
cy.get('.ht_master.handsontable .htCore thead tr').then(
(viewboxColNodes: any) => {
let allColsHtml = viewboxColNodes[0].innerHTML
for (let col of viewbox?.columns) {
if (!allColsHtml.includes(col)) return
}
done()
}
)
}
})
})
}
})
})
})
it('3 | Add viewbox, add columns', (done) => {
const viewbox_table = 'mpe_audit'
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
const additionalColumns = ['IS_PK']
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
cy.get('.viewbox-open').click()
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
cy.get('.open-viewbox').then((viewboxNodes: any) => {
for (let viewboxNode of viewboxNodes) {
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
return
}
openViewboxConfig(viewbox_table)
addColumns(additionalColumns)
checkColumns([...columns, ...additionalColumns], () => {
done()
})
}
})
})
it('4 | Add viewbox, add columns and reorder', (done) => {
const viewbox_table = 'mpe_audit'
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
const additionalColumns = ['IS_PK', 'MOVE_TYPE']
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
cy.get('.viewbox-open').click()
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
cy.get('.open-viewbox').then((viewboxNodes: any) => {
for (let viewboxNode of viewboxNodes) {
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
return
}
openViewboxConfig(viewbox_table)
addColumns(additionalColumns, () => {
cy.wait(1000)
//reorder
cy.get('.col-box.column-MOVE_TYPE')
.realMouseDown({ button: 'left', position: 'center' })
.realMouseMove(0, 10, { position: 'center' })
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
cy.get('.col-box.column-IS_PK')
.realMouseMove(0, 0, { position: 'center' })
.realMouseUp()
//reorder end
cy.wait(500)
checkColumns([...columns, ...additionalColumns.reverse()], () => {
done()
})
})
}
})
})
it('5 | Add viewbox, add columns, reorder, remove column, add again', (done) => {
const viewbox_table = 'mpe_audit'
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
const additionalColumns = ['IS_PK', 'MOVE_TYPE']
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
cy.get('.viewbox-open').click()
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
cy.get('.open-viewbox').then((viewboxNodes: any) => {
for (let viewboxNode of viewboxNodes) {
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
return
}
viewboxNode.click()
addColumns(additionalColumns, () => {
cy.wait(1000)
//reorder
cy.get('.col-box.column-MOVE_TYPE')
.realMouseDown({ button: 'left', position: 'center' })
.realMouseMove(0, 10, { position: 'center' })
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
cy.get('.col-box.column-IS_PK')
.realMouseMove(0, 0, { position: 'center' })
.realMouseUp()
//reorder end
cy.wait(500)
checkColumns([...columns, ...additionalColumns.reverse()], () => {
const colToRemove = 'MOVE_TYPE'
removeColumn(colToRemove)
checkColumns(
[
...columns,
...additionalColumns.filter((col) => col !== colToRemove)
],
() => {
addColumns([colToRemove], () => {
checkColumns(
[...columns, ...additionalColumns.reverse()],
() => {
done()
}
)
})
}
)
})
})
}
})
})
it('6 | Add viewboxes, reload and check url restored configuration', (done) => {
const viewboxes = [
{
viewbox_table: 'mpe_audit',
columns: ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM'],
additionalColumns: ['IS_PK', 'MOVE_TYPE']
},
{
viewbox_table: 'mpe_alerts',
columns: [
'TX_FROM',
'ALERT_EVENT',
'ALERT_LIB',
'ALERT_DS',
'ALERT_USER'
],
additionalColumns: ['TX_TO']
}
]
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
cy.get('.viewbox-open').click()
openTableFromViewboxTree(libraryToOpenIncludes, [
viewboxes[0].viewbox_table,
viewboxes[1].viewbox_table
])
openViewboxConfig(viewboxes[0].viewbox_table)
cy.wait(500)
addColumns(viewboxes[0].additionalColumns, () => {
cy.wait(1000)
if (viewboxes[0].viewbox_table === 'mpe_audit') {
cy.get('.col-box.column-MOVE_TYPE')
.realMouseDown({ button: 'left', position: 'center' })
.realMouseMove(0, 10, { position: 'center' })
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
cy.get('.col-box.column-IS_PK')
.realMouseMove(0, 0, { position: 'center' })
.realMouseUp()
}
cy.wait(1000)
openViewboxConfig(viewboxes[1].viewbox_table)
addColumns(viewboxes[1].additionalColumns, () => {
cy.wait(1000).reload()
let result = 0
checkColumns(
[
...viewboxes[0].columns,
...cloneDeep(viewboxes[0].additionalColumns.reverse())
],
() => {
result++
if (result === 2) done()
}
)
checkColumns(
[...viewboxes[1].columns, ...viewboxes[1].additionalColumns],
() => {
result++
if (result === 2) done()
}
)
})
})
})
it('7 | Add viewboxes and filter', () => {
const viewboxes = ['mpe_x_test', 'mpe_validations']
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
cy.get('.viewbox-open').click()
openTableFromViewboxTree(libraryToOpenIncludes, viewboxes)
cy.wait(1000)
closeViewboxModal()
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
(viewboxNodes: any) => {
for (let viewboxNode of viewboxNodes) {
cy.get(viewboxNode).within(() => {
cy.get('.table-title').then((title: any) => {
cy.get('.hot-spinner')
.should('not.exist')
.then(() => {
cy.get('clr-icon[shape="filter"]').then((filterButton) => {
filterButton[0].click()
})
if (title[0].innerText.includes('MPE_X_TEST')) {
setFilterWithValue(
'SOME_CHAR',
'this is dummy data',
'value',
() => {
cy.get('app-query', { withinSubject: null })
.should('not.exist')
.get('.ht_master.handsontable tbody tr')
.then((rowNodes) => {
const tr = rowNodes[0]
expect(rowNodes).to.have.length(1)
expect(tr.innerText).to.equal('0')
})
}
)
} else if (title[0].innerText.includes('MPE_VALIDATIONS')) {
setFilterWithValue('BASE_COL', 'ALERT_LIB', 'value', () => {
cy.get('app-query', { withinSubject: null })
.should('not.exist')
.get('.ht_master.handsontable tbody tr')
.then((rowNodes) => {
const tr = rowNodes[0]
expect(rowNodes).to.have.length(1)
expect(tr.innerText).to.contain('ALERT_LIB')
})
})
}
})
})
})
}
}
)
})
})
const checkColumns = (columns: string[], callback: () => void) => {
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
(viewboxNodes: any) => {
for (let viewboxNode of viewboxNodes) {
cy.get(viewboxNode).within(() => {
cy.get('.ht_master.handsontable thead tr th').then(
(viewboxColNodes: any) => {
for (let i = 0; i < viewboxColNodes.length; i++) {
const col = columns[i]
const colNode = viewboxColNodes[i]
if (
!colNode.innerHTML.toLowerCase().includes(col.toLowerCase())
)
return
}
callback()
}
)
})
}
}
)
}
const closeViewboxModal = () => {
cy.get('app-viewboxes .close', { withinSubject: null }).click()
}
const removeColumn = (column: string) => {
cy.get(`.col-box.column-${column} clr-icon`, { withinSubject: null }).click()
}
const addColumns = (columns: string[], callback?: () => void) => {
for (let i = 0; i < columns.length; i++) {
const column = columns[i]
cy.get('.cols-search input', { withinSubject: null }).type(column)
cy.get('.cols-search .autocomplete-wrapper', { withinSubject: null })
.first()
.trigger('keydown', { key: 'ArrowDown' })
cy.get('.cols-search .autocomplete-wrapper', { withinSubject: null })
.first()
.trigger('keydown', { key: 'Enter' })
.then(() => {
if (i === columns.length - 1 && callback) callback()
})
}
}
const openViewboxConfig = (viewbox_tablename: string) => {
cy.get('.open-viewbox').then((viewboxes: any) => {
for (let openViewbox of viewboxes) {
if (openViewbox.innerText.toLowerCase().includes(viewbox_tablename))
openViewbox.click()
}
})
}
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 viyaLib
for (let node of treeNodes) {
if (new RegExp(libNameIncludes).test(node.innerText.toLowerCase())) {
viyaLib = node
break
}
}
cy.get(viyaLib).within(() => {
cy.get('.clr-tree-node-content-container p').click()
cy.get('.clr-treenode-link').then((innerNodes: any) => {
for (let innerNode of innerNodes) {
if (innerNode.innerText.toLowerCase().includes(tablename)) {
innerNode.click()
break
}
}
})
})
})
})
}
const setFilterWithValue = (
variableValue: string,
valueString: string,
valueField: 'value' | 'time' | 'date' | 'datetime' | 'in' | 'between',
callback?: any
) => {
cy.wait(600)
cy.focused().type(variableValue)
cy.wait(100)
// cy.focused().trigger('input')
cy.get('.variable-col .autocomplete-wrapper', { withinSubject: null })
.first()
.trigger('keydown', { key: 'ArrowDown' })
cy.get('.variable-col .autocomplete-wrapper', {
withinSubject: null
}).trigger('keydown', { key: 'Enter' })
cy.focused().tab()
cy.wait(100)
if (valueField === 'in') {
cy.focused().select(valueField.toUpperCase()).trigger('change')
} else if (valueField === 'between') {
cy.focused().select(valueField.toUpperCase()).trigger('change')
} else {
cy.focused().tab()
cy.wait(100)
}
switch (valueField) {
case 'value': {
cy.focused().type(valueString)
break
}
case 'time': {
cy.focused().type(valueString)
break
}
case 'date': {
cy.focused().type(valueString)
cy.focused().tab()
break
}
case 'datetime': {
const date = valueString.split(' ')[0]
const time = valueString.split(' ')[1]
cy.focused().type(date)
cy.focused().tab()
cy.focused().tab()
cy.focused().type(time)
break
}
case 'in': {
cy.get('.checkbox-vals').then(() => {
cy.focused().tab()
cy.focused().click()
cy.get('.no-values')
.should('not.exist')
.then(() => {
cy.get('clr-checkbox-wrapper input').then((inputs: any) => {
inputs[0].click()
cy.get('.in-values-modal .modal-footer button').click()
cy.get('.modal-footer .btn-success-outline').click()
if (callback) callback()
})
})
})
break
}
case 'between': {
cy.focused().tab()
const start = valueString.split('-')[0]
const end = valueString.split('-')[1]
cy.focused().type(start)
cy.focused().tab()
cy.focused().type(end)
}
default: {
break
}
}
cy.wait(100)
cy.focused().tab()
cy.wait(100)
cy.focused().tab()
cy.wait(100)
cy.focused().tab()
cy.wait(100)
cy.focused().tab()
cy.wait(100)
cy.focused().click()
if (callback) callback()
}
const openTableFromViewboxTree = (
libNameIncludes: string,
tablenames: string[]
) => {
cy.get('.add-new clr-tree > clr-tree-node', {
timeout: longerCommandTimeout
}).then((treeNodes: any) => {
let viyaLib
for (let node of treeNodes) {
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
viyaLib = node
break
}
}
cy.get(viyaLib).within(() => {
cy.get('p')
.click()
.then(() => {
cy.get('.clr-treenode-link').then((innerNodes: any) => {
for (let innerNode of innerNodes) {
for (let tablename of tablenames) {
if (innerNode.innerText.toLowerCase().includes(tablename)) {
innerNode.click()
}
}
}
})
})
})
})
}
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -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;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' '@cds/city@1.1.0;@handsontable/angular@14.6.2;handsontable@14.6.2;hyperformula@2.7.1;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1'
}, },
(error, json) => { (error, json) => {
if (error) { if (error) {
-46
View File
@@ -1,46 +0,0 @@
module.exports = {
ci: {
collect: {
settings: {
preset: 'desktop',
chromeFlags: '--no-sandbox --disable-dev-shm-usage'
},
url: [
'http://localhost:5000/AppStream/clickme/#/home/tables',
'http://localhost:5000/AppStream/clickme/#/editor/DC996664.MPE_X_TEST',
'http://localhost:5000/AppStream/clickme/#/view/data',
'http://localhost:5000/AppStream/clickme/#/view/data/DC996664',
'http://localhost:5000/AppStream/clickme/#/view/data/DC996664.MPE_X_TEST',
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups',
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups',
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups/1',
'http://localhost:5000/AppStream/clickme/#/view/usernav/users/1',
'http://localhost:5000/AppStream/clickme/#/home/excel-maps',
'http://localhost:5000/AppStream/clickme/#/home/excel-maps/BASEL-CR2',
'http://localhost:5000/AppStream/clickme/#/home/multi-load',
'http://localhost:5000/AppStream/clickme/#/review/submitted',
'http://localhost:5000/AppStream/clickme/#/review/approve',
'http://localhost:5000/AppStream/clickme/#/review/history',
'http://localhost:5000/AppStream/clickme/#/stage/DC20221006T142649516_059582_7169',
'http://localhost:5000/AppStream/clickme/#/review/submitted/DC20221006T142649516_059582_7169',
'http://localhost:5000/AppStream/clickme/#/system'
]
},
assert: {
assertions: {
'categories:accessibility': [
'error',
{ minScore: 1, aggregationMethod: 'median' }
],
'categories:performance': [
'error',
{ minScore: 0.4, aggregationMethod: 'median' }
]
}
},
upload: {
target: 'filesystem',
outputDir: './lighthouse-reports'
}
}
}
+7023 -11552
View File
File diff suppressed because it is too large Load Diff
+30 -33
View File
@@ -31,28 +31,26 @@
"sasdocs": "sasjs doc && ./sasjs/utils/deploydocs.sh", "sasdocs": "sasjs doc && ./sasjs/utils/deploydocs.sh",
"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",
"ng": "ng"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^19.2.18", "@angular/animations": "^17.3.3",
"@angular/cdk": "^19.2.19", "@angular/cdk": "^17.3.3",
"@angular/common": "^19.2.18", "@angular/common": "^17.3.3",
"@angular/compiler": "^19.2.18", "@angular/compiler": "^17.3.3",
"@angular/core": "^19.2.18", "@angular/core": "^17.3.3",
"@angular/forms": "^19.2.18", "@angular/forms": "^17.3.3",
"@angular/platform-browser": "^19.2.18", "@angular/platform-browser": "^17.3.3",
"@angular/platform-browser-dynamic": "^19.2.18", "@angular/platform-browser-dynamic": "^17.3.3",
"@angular/router": "^19.2.18", "@angular/router": "^17.3.3",
"@cds/core": "^6.15.1", "@cds/core": "^6.10.0",
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz", "@clr/angular": "^17.0.1",
"@clr/icons": "^13.0.2", "@clr/icons": "^13.0.2",
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz", "@clr/ui": "^17.0.1",
"@handsontable/angular-wrapper": "16.0.1", "@handsontable/angular": "^14.3.0",
"@sasjs/adapter": "^4.16.3", "@sasjs/adapter": "^4.11.0",
"@sasjs/utils": "^3.5.3", "@sasjs/utils": "^3.4.0",
"@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",
@@ -62,14 +60,14 @@
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"d3-graphviz": "^5.0.2", "d3-graphviz": "^5.0.2",
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
"handsontable": "^16.0.1", "handsontable": "^14.3.0",
"https-browserify": "1.0.0", "https-browserify": "1.0.0",
"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": "^11.1.0", "jsrsasign": "^10.2.0",
"marked": "^5.0.0", "marked": "^5.0.0",
"moment": "^2.30.1", "moment": "^2.26.0",
"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",
@@ -82,22 +80,21 @@
"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": "file:libraries/xlsx-0.20.3.tgz", "xlsx": "^0.18.5",
"zone.js": "~0.15.1" "zone.js": "~0.14.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^19.2.19", "@angular-devkit/build-angular": "^17.3.3",
"@angular-eslint/builder": "19.8.1", "@angular-eslint/builder": "17.3.0",
"@angular-eslint/eslint-plugin": "19.8.1", "@angular-eslint/eslint-plugin": "17.3.0",
"@angular-eslint/eslint-plugin-template": "19.8.1", "@angular-eslint/eslint-plugin-template": "17.3.0",
"@angular-eslint/schematics": "19.8.1", "@angular-eslint/schematics": "17.3.0",
"@angular-eslint/template-parser": "19.8.1", "@angular-eslint/template-parser": "17.3.0",
"@angular/cli": "^19.2.19", "@angular/cli": "^17.3.3",
"@angular/compiler-cli": "^19.2.18", "@angular/compiler-cli": "^17.3.3",
"@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",
"@lhci/cli": "^0.12.0",
"@types/core-js": "^2.5.5", "@types/core-js": "^2.5.5",
"@types/crypto-js": "^4.2.1", "@types/crypto-js": "^4.2.1",
"@types/es6-shim": "^0.31.39", "@types/es6-shim": "^0.31.39",
@@ -129,7 +126,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.8.3", "typescript": "~5.4.4",
"wait-on": "^6.0.1", "wait-on": "^6.0.1",
"watch": "^1.0.2" "watch": "^1.0.2"
} }
-3
View File
@@ -148,8 +148,5 @@ export const globals: {
}, },
handsontable: { handsontable: {
darkTableHeaderClass: 'darkTH' darkTableHeaderClass: 'darkTH'
},
userDropdownConfig: {
closeOnDebugClick: false
} }
} }
+2 -11
View File
@@ -139,15 +139,10 @@
[routerLink]="['/']" [routerLink]="['/']"
class="nav-link" class="nav-link"
> >
<img <img class="without-text d-block d-md-none" src="images/dc-logo.svg" />
class="without-text d-block d-md-none"
src="images/dc-logo.svg"
alt="datacontroller logo without text"
/>
<img <img
class="with-text d-none d-md-block" class="with-text d-none d-md-block"
src="images/datacontroller.svg" src="images/datacontroller.svg"
alt="datacontroller logo"
/> />
</a> </a>
@@ -288,11 +283,7 @@
<!-- App Loading Page --> <!-- App Loading Page -->
<div *ngIf="!startupDataLoaded" class="app-loading"> <div *ngIf="!startupDataLoaded" class="app-loading">
<img <img class="loading-logo" src="images/datacontroller.svg" />
class="loading-logo"
src="images/datacontroller.svg"
alt="datacontroller logo"
/>
<div *ngIf="appActive === null" class="slider"> <div *ngIf="appActive === null" class="slider">
<div class="line"></div> <div class="line"></div>
+447
View File
@@ -0,0 +1,447 @@
@import '../colors.scss';
// Copyright (c) 2016 VMware, Inc. All Rights Reserved.
// This software is released under MIT license.
// The full license information can be found in LICENSE in the root directory of this project.
app-requests-modal {
z-index: 10000;
}
header.app-header {
background: $headerBackground !important;
color: #fff;
}
.logo img.without-text {
width: 30px;
}
.logo img.with-text {
width: 210px;
}
.header-hamburger-trigger {
display: block;
background: transparent;
border: 0;
margin-left: 10px;
}
.demo-expired-notice {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
left: 0;
top: 0;
height: 100vh !important;
width: 100vw !important;
z-index: 105;
background: rgba(33, 33, 33, .5);
.expired-details {
flex-direction: column;
align-items: center;
padding: 30px;
z-index: 110;
background: $headerBackground;
.expired-notice {
color: #e0e0e0;
font-size: 16px;
.mailto {
color: #8dc53e;
}
}
}
}
.main-container .update-key {
display: flex;
align-items: center;
color: white;
padding: 0px 10px;
background: #00000026;
}
.alert-icon-wrapper {
margin-top: 0 !important;
}
.nav-text {
margin-right: 20px;
}
.sidebar-toggle {
display: flex;
height: 100%;
align-items: center;
padding-left: 10px;
clr-icon {
cursor: pointer;
width: 30px;
height: 30px;
}
}
header {
.header-actions {
.dropdown {
position: unset; //without it, when opening user dropdown scrollbar was displaying without reason
}
}
.nav-link:hover {
color: #fafafa;
}
.nav-link.active {
background: #61717D;
}
}
.notf {
background: #16a57a;
color: #fffcfc;
font-size: 12px;
}
.toggle-switch input[type=checkbox]:checked+label:before {
border-color: #61717D;
background-color: #61717D;
transition: .15s ease-in;
transition-property: border-color,background-color;
}
.main-container {
min-height: 100vh !important;
}
.main-container .content-container .content-area {
padding: 0rem 1rem 1rem 1rem;
}
.content-container {
z-index: 0!important;
}
.navBarResp {
display: flex;
justify-content: center;
background: #495A67;
color: #fff;
}
::ng-deep {
.htInvalid {
background: black!important;
}
@media screen and (max-width:480px) {
h2 {
font-size: .7rem!important;
}
h3 {
font-size: .7rem;
}
}
.nav-link {
padding: 0rem 1rem 0rem 1rem;
}
body[cds-theme="light"] {
.btn-primary .btn, .btn.btn-primary {
border-color: $headerBackground;
background-color: $headerBackground;
color: #fff;
}
}
body[cds-theme="dark"] {
.btn-primary .btn, .btn.btn-primary {
border-color: #5e7382;
background-color: #5e7382;
color: #fff;
clr-icon, cds-icon {
color: #fff
}
}
}
.btn-primary .btn, .btn.btn-primary {
&:disabled {
opacity: 0.65;
}
}
.btn {
cursor: pointer;
display: inline-block;
-webkit-appearance: none!important;
border-radius: .125rem;
border: 1px solid;
min-width: 3rem;
max-width: 15rem;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
text-align: center;
text-transform: uppercase;
vertical-align: middle;
line-height: 1.5rem;
letter-spacing: .12em;
font-size: .5rem;
font-weight: 500;
height: 1.5rem;
padding: 0 .5rem;
}
.btn.btn-outline:hover {
border-color: $headerBackground;
background-color: #495A67;
color: #fff;
}
body[cds-theme="dark"] {
.btn.btn-icon.btn-dimmed {
color: #7295ae;
}
}
body[cds-theme="light"] {
.btn.btn-icon.btn-dimmed {
color: $headerBackground;
}
.btn.btn-outline {
border-color: $headerBackground;
background-color: transparent;
color: $headerBackground;
}
}
.htMobileEditorContainer .inputs textarea {
font-size: 13pt;
border: 2px solid #485967;
border-radius: 4px;
-webkit-appearance: none;
box-shadow: none;
position: absolute;
left: 14px;
right: 0px;
top: 0;
bottom: 0;
padding: 7pt;
width: 290px;
}
.htMobileEditorContainer .positionControls {
width: 333px;
position: absolute;
right: 5pt;
top: 50px;
bottom: 0;
display: flex;
justify-content: center;
}
.htMobileEditorContainer.active {
display: block;
height: 120px;
width: 350px;
}
/* Left and right */
/* Column headers */
body[cds-theme="light"] {
.wtBorder {
background-color: #495A67!important;
}
.ht_master tr:nth-of-type(odd) > td {
filter: brightness(0.95);
}
}
$darkBorderColor: #697c85;
body[cds-theme="dark"] {
.ht_master tr:nth-of-type(odd) > td {
filter: brightness(1.2);
}
.ht_master:not(.emptyColumns) ~ .handsontable tbody tr th, .ht_master:not(.emptyColumns) ~ .handsontable:not(.ht_clone_top) thead tr th:first-child {
background-color: #2d4048;
border-color: $darkBorderColor;
}
.handsontable td {
// border-right: 1px solid #697c85;
// border-bottom: 1px solid #697c85;
border-color: $darkBorderColor;
}
.handsontable tr:first-child th, .handsontable tr:first-child td {
border-color: $darkBorderColor;
}
.handsontable .handsontable.ht_clone_top .wtHider {
border-color: $darkBorderColor;
}
.handsontable .changeType {
background-color: #3c5662;
border-color: $darkBorderColor;
}
.handsontableInput {
background-color: #708b98;
}
}
.handsontable .handsontable.ht_clone_top .wtHider {
padding: 0 0 0px 0!important;
margin: 0px;
border-bottom: 3px solid #d6d3d3;
}
body[cds-theme="light"] {
.content-container {
// background: red;
background: #F5F6FF;
}
}
.datagrid-compact, .datagrid-history{
.datagrid {
border-collapse: separate;
border: 1px solid transparent;
border-radius: .125rem;
margin: 0;
margin-top: 1rem;
max-width: 100%;
width: 100%;
padding: 15px 15px 50px 15px;
}
.datagrid-foot {
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
height: 1.5rem;
padding: 0 .5rem;
line-height: calc(1.5rem - 3px);
font-size: .45833rem;
background-color: #fff;
border-top: 1px solid #ccc;
border-radius: 0px;
// border-radius: 0 0 .125rem .125rem;
}
.datagrid-footer {
position: absolute;
right: 30px;
top: 1px;
}
.datagrid .datagrid-head {
background-color: #fff;
border-bottom: 1px solid #ccc;
}
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: .083333rem;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
padding: .5rem 0;
border: 1px solid #ccc;
box-shadow: 0 1px 0.125rem hsla(0,0%,45%,.25);
min-width: 5rem;
max-width: 15rem;
border-radius: .125rem;
visibility: hidden;
z-index: 1000;
}
.table {
border-collapse: separate;
border: 1px solid transparent;
border-radius: 0px;
margin: 0;
margin-top: 1rem;
max-width: 100%;
width: 100%;
}
.table th {
font-size: .45833rem;
font-weight: 600;
letter-spacing: .03em;
vertical-align: bottom;
border-bottom: 1px solid #ccc;
text-transform: uppercase;
}
.modal-header {
border-bottom: 2px solid #e4e4e4;
padding: 0 0 .5rem 0;
margin-bottom: 1rem;
}
.main-container .content-container {
min-height: 0px;
position: relative;
}
}
.app-loading {
.loading-logo {
max-width: 400px;
width: 100%;
}
}
@media screen and (max-width: 768px) {
.navBarResp {
display: flex;
justify-content: flex-start;
background: #495A67;
color: #fff;
}
.main-container .sub-nav.clr-nav-level-1 .nav .nav-link, .main-container .sub-nav.clr-nav-level-2 .nav .nav-link, .main-container .subnav.clr-nav-level-1 .nav .nav-link, .main-container .subnav.clr-nav-level-2 .nav .nav-link {
padding: 0 .5rem 0 1rem;
width: 100%;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
border-radius: .125rem 0 0 .125rem;
color: #95c84b;
}
.card-block, .card-footer {
padding: 10px 0px 0px 0px;
}
.main-container[_ngcontent-c0] .content-container[_ngcontent-c0] .content-area[_ngcontent-c0] {
padding: 0rem 0rem 0rem 0rem;
}
}
+3 -10
View File
@@ -1,9 +1,4 @@
import { import { ChangeDetectorRef, Component, ElementRef } from '@angular/core'
ChangeDetectorRef,
Component,
ElementRef,
ViewEncapsulation
} from '@angular/core'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { VERSION } from '../environments/version' import { VERSION } from '../environments/version'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
@@ -11,7 +6,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 moment from 'moment' import * as 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'
@@ -41,9 +36,7 @@ ClarityIcons.addIcons(
@Component({ @Component({
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,
standalone: false
}) })
export class AppComponent { export class AppComponent {
private dcAdapterSettings: DcAdapterSettings | undefined private dcAdapterSettings: DcAdapterSettings | undefined
+4 -9
View File
@@ -1,4 +1,4 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' import { HttpClientModule } 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,12 +50,7 @@ import { AppSettingsService } from './services/app-settings.service'
DirectivesModule, DirectivesModule,
NgxJsonViewerModule NgxJsonViewerModule
], ],
providers: [ providers: [AppService, SasStoreService, LicensingGuard, AppSettingsService],
AppService, bootstrap: [AppComponent]
SasStoreService,
LicensingGuard,
AppSettingsService,
provideHttpClient(withInterceptorsFromDi())
]
}) })
export class AppModule {} export class AppModule {}
+1 -1
View File
@@ -5,7 +5,7 @@
<div class="card-header">Terms and Conditions</div> <div class="card-header">Terms and Conditions</div>
<div class="card-block"> <div class="card-block">
<div class="card-text"> <div class="card-text">
<p class="mt-0"> <p>
The Demo version of Data Controller is free for EVALUATION purposes The Demo version of Data Controller is free for EVALUATION purposes
only. Before proceeding with configuration, please confirm that you only. Before proceeding with configuration, please confirm that you
have read, understood, and agreed to the have read, understood, and agreed to the
@@ -0,0 +1,50 @@
.card {
margin-top: 0;
}
.btn {
margin-top: 10px;
}
.log-wrapper {
width: 100%;
background: #f0f0f0;
border: 1px solid #c9c9c9;
padding: 10px;
overflow: auto;
white-space: pre-wrap;
}
#contexts-btn {
padding: 0;
min-width: 30px;
margin-left: 10px;
height: 30px;
display: inline-flex;
justify-content: center;
align-items: center;
padding-top: 3px;
}
.validation-bar {
display: flex;
margin-top: 20px;
align-items: center;
clr-icon {
margin-right: 5px;
}
}
.autodeploy-section {
padding: 0px 15px;
.clr-checkbox-wrapper {
margin: 20px 0 20px 0;
}
.btn-autodeploy {
display: block;
margin: 15px 0 15px 0;
}
}
+21 -4
View File
@@ -1,4 +1,4 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { SasService } from '../services/sas.service' import { SasService } from '../services/sas.service'
import { SASjsConfig } from '@sasjs/adapter' import { SASjsConfig } from '@sasjs/adapter'
import { Router } from '@angular/router' import { Router } from '@angular/router'
@@ -13,9 +13,7 @@ import { DcAdapterSettings } from '../models/DcAdapterSettings'
styleUrls: ['./deploy.component.scss'], styleUrls: ['./deploy.component.scss'],
host: { host: {
class: 'content-container' class: 'content-container'
}, }
encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class DeployComponent implements OnInit { export class DeployComponent implements OnInit {
public step: number = 0 public step: number = 0
@@ -58,6 +56,25 @@ export class DeployComponent implements OnInit {
} }
ngOnInit() { ngOnInit() {
if (this.sasJsConfig.serverType === ServerType.SasViya) {
fetch('sasbuild/viya.json')
.then((res) => res.text())
.then((res) => {
let initJsonFile: any = null
try {
initJsonFile = JSON.parse(res)
} catch (err) {
console.error(err)
}
if (initJsonFile) {
this.jsonFile = initJsonFile
this.loggerService.log(this.jsonFile)
}
})
}
this.setDeployDefaults() this.setDeployDefaults()
} }
@@ -9,17 +9,14 @@
<p class="m-0 align-self-start">Done</p> <p class="m-0 align-self-start">Done</p>
<hr class="w-100" /> <hr class="w-100" />
<div <div class="deploy-status-row">
*ngIf="autoDeployStatus.deployServicePack !== null"
class="deploy-status-row"
>
<clr-icon <clr-icon
*ngIf="autoDeployStatus.deployServicePack === true" *ngIf="autoDeployStatus.deployServicePack"
class="deploy-success" class="deploy-success"
shape="success-standard" shape="success-standard"
></clr-icon> ></clr-icon>
<clr-icon <clr-icon
*ngIf="!autoDeployStatus.deployServicePack === false" *ngIf="!autoDeployStatus.deployServicePack"
class="deploy-error" class="deploy-error"
shape="times-circle" shape="times-circle"
></clr-icon> ></clr-icon>
@@ -55,7 +52,7 @@
class="deploy-error" class="deploy-error"
shape="times-circle" shape="times-circle"
></clr-icon> ></clr-icon>
LAUNCH LAUNCH / CONFIGURE
</button> </button>
<button <button
@@ -97,72 +94,20 @@
</div> </div>
<label for="dcloc" class="mt-20 clr-control-label">DC Loc</label> <label for="dcloc" class="mt-20 clr-control-label">DC Loc</label>
<div class="mb-10 clr-control-container dc-loc-input-wrapper"> <div class="mb-10 clr-control-container">
<div class="clr-input-wrapper small-mt"> <div class="clr-input-wrapper">
<input clrInput name="dcloc" [(ngModel)]="dcPath" /> <p class="mt-0">{{ dcPath }}</p>
</div> </div>
</div> </div>
<label for="dcloc" class="mt-20 clr-control-label">SAS Admin group</label> <label for="dcloc" class="mt-20 clr-control-label">SAS Admin group</label>
<div class="mb-10 clr-control-container"> <div class="mb-10 clr-control-container">
<div class="clr-input-wrapper small-mt"> <div class="clr-input-wrapper">
<select <p class="mt-0">{{ selectedAdminGroup }}</p>
*ngIf="!adminGroupsLoading"
clrSelect
name="options"
[(ngModel)]="selectedAdminGroup"
>
<option *ngFor="let adminGroup of adminGroups" [value]="adminGroup.id">
{{ adminGroup.name }}
</option>
</select>
<clr-spinner
clrInline
class="spinner-sm"
*ngIf="adminGroupsLoading"
></clr-spinner>
</div> </div>
</div> </div>
<label for="computeContext" class="mt-20 clr-control-label" <clr-checkbox-wrapper>
>Compute Context</label
>
<div class="mb-10 clr-control-container">
<div class="clr-input-wrapper small-mt">
<select
*ngIf="!computeContextsLoading"
clrSelect
name="options"
(ngModelChange)="onComputeContextChange($event)"
[(ngModel)]="selectedComputeContext"
>
<option
*ngFor="let computeContext of computeContexts"
[value]="computeContext.id"
>
{{ computeContext.name }}
</option>
</select>
<clr-spinner
clrInline
class="spinner-sm"
*ngIf="computeContextsLoading"
></clr-spinner>
</div>
</div>
<ng-container *ngIf="runningAsUser">
<label for="dcloc" class="mt-20 clr-control-label">Running as user:</label>
<div class="mb-10 clr-control-container">
<div class="clr-input-wrapper">
<p class="mt-0">{{ runningAsUser }}</p>
</div>
</div>
</ng-container>
<!-- Keeping this for a reference in case future VIYA changes and starts allowing separate backend and frontend) -->
<!-- <clr-checkbox-wrapper>
<input <input
clrCheckbox clrCheckbox
[(ngModel)]="recreateDatabase" [(ngModel)]="recreateDatabase"
@@ -171,28 +116,19 @@
checked checked
/> />
<label>Recreate database</label> <label>Recreate database</label>
</clr-checkbox-wrapper> --> </clr-checkbox-wrapper>
<hr /> <hr />
<button <button
(click)="runAutoDeploy()"
class="btn-autodeploy btn btn-primary d-inline-block mr-10"
>
Deploy
</button>
<!-- Keeping this for a reference in case future VIYA changes and starts allowing separate backend and frontend) -->
<!-- <button
(click)="executeJson()" (click)="executeJson()"
class="btn-autodeploy btn btn-primary d-inline-block mr-10" class="btn-autodeploy btn btn-primary d-inline-block mr-10"
[disabled]="!jsonFile" [disabled]="!jsonFile"
> >
Deploy {{ !jsonFile ? '(json file is not available)' : '' }} Deploy {{ !jsonFile ? '(json file is not available)' : '' }}
</button> --> </button>
<!-- <button <button
(click)="uploadJsonAuto.click()" (click)="uploadJsonAuto.click()"
class="btn-autodeploy btn btn-primary d-inline-block mr-10" class="btn-autodeploy btn btn-primary d-inline-block mr-10"
> >
@@ -204,7 +140,7 @@
hidden hidden
(click)="clearUploadInput($event)" (click)="clearUploadInput($event)"
(change)="onJsonFileChange($event)" (change)="onJsonFileChange($event)"
/> --> />
<clr-modal [(clrModalOpen)]="recreateDatabaseModal" [clrModalClosable]="false"> <clr-modal [(clrModalOpen)]="recreateDatabaseModal" [clrModalClosable]="false">
<h3 class="modal-title">Warning</h3> <h3 class="modal-title">Warning</h3>
@@ -0,0 +1,61 @@
.auto-deploy {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 100;
}
.spinner-box {
width: 400px;
padding: 20px;
border-radius: 3px;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
box-shadow: 1px 1px 8px 0px #00000082;
.buttons {
display: flex;
justify-content: space-between;
width: 100%;
}
}
.deploy-status-row {
display: flex;
align-items: center;
align-self: flex-start;
p {
margin: 0 0 0 10px;
}
}
.deploy-success {
color: #6ECF44;
}
.deploy-error {
color: #E74C3C;
// width: 20px;
// height: 20px;
}
.deploy-undeterminated {
color: #cacaca;
}
hr {
border: 0;
border-bottom: 1px solid #00000045;
}
@@ -1,36 +1,15 @@
import { import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewEncapsulation
} from '@angular/core'
import SASjs, { SASjsConfig } from '@sasjs/adapter' import SASjs, { SASjsConfig } from '@sasjs/adapter'
import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings' import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings'
import { HelperService } from 'src/app/services'
import { DeployService } from 'src/app/services/deploy.service' import { DeployService } from 'src/app/services/deploy.service'
import { EventService } from 'src/app/services/event.service' import { EventService } from 'src/app/services/event.service'
import { LoggerService } from 'src/app/services/logger.service' import { LoggerService } from 'src/app/services/logger.service'
import { SasViyaService } from 'src/app/services/sas-viya.service'
import { SasService } from 'src/app/services/sas.service' import { SasService } from 'src/app/services/sas.service'
import { ViyaApiCurrentUser } from 'src/app/viya-api-explorer/models/viya-api-current-user.model'
import {
Item,
ViyaApiIdentities
} from 'src/app/viya-api-explorer/models/viya-api-identities.model'
import { ComputeContextDetails } from 'src/app/viya-api-explorer/models/viya-compute-context-details.model'
import {
ViyaComputeContexts,
Item as ComputeContextItem
} from 'src/app/viya-api-explorer/models/viya-compute-contexts.model'
@Component({ @Component({
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,
standalone: false
}) })
export class AutomaticComponent implements OnInit { export class AutomaticComponent implements OnInit {
@Input() sasJs!: SASjs @Input() sasJs!: SASjs
@@ -42,7 +21,6 @@ export class AutomaticComponent implements OnInit {
@Output() onNavigateToHome: EventEmitter<any> = new EventEmitter<any>() @Output() onNavigateToHome: EventEmitter<any> = new EventEmitter<any>()
public selectedComputeContext: string = ''
public makeDataResponse: string = '' public makeDataResponse: string = ''
public jsonFile: any = null public jsonFile: any = null
public autodeploying: boolean = false public autodeploying: boolean = false
@@ -50,19 +28,8 @@ export class AutomaticComponent implements OnInit {
public recreateDatabaseModal: boolean = false public recreateDatabaseModal: boolean = false
public isSubmittingJson: boolean = false public isSubmittingJson: boolean = false
public isJsonSubmitted: boolean = false public isJsonSubmitted: boolean = false
/** public recreateDatabase: boolean = false
* Default was `false` when deploy was done with frontend and backend separately.
* Now we are using only streaming app, so we always want to recreate database (makedata)
*/
public recreateDatabase: boolean = true
public createDatabaseLoading: boolean = false public createDatabaseLoading: boolean = false
public adminGroupsLoading: boolean = false
public currentUserInfoLoading: boolean = false
public computeContextsLoading: boolean = false
public adminGroups: { id: string; name: string }[] = []
public runningAsUser: string | undefined
public currentUserInfo: ViyaApiCurrentUser | null = null
public computeContexts: ComputeContextItem[] = []
/** autoDeployStatus /** autoDeployStatus
* This object presents the status for two steps that we have for deploy. * This object presents the status for two steps that we have for deploy.
@@ -79,138 +46,14 @@ export class AutomaticComponent implements OnInit {
runMakeData: null runMakeData: null
} }
public sasjsConfig = this.sasService.getSasjsConfig()
/**
* makedata service will be run in a new window
* This is needed to ensure that the user can see the logs
* and the progress of the service execution.
* If this is set to `false`, the service will be run in the same window
* using the adapter request method.
*/
public deployInNewWindow: boolean = true
constructor( constructor(
private eventService: EventService, private eventService: EventService,
private deployService: DeployService, private deployService: DeployService,
private sasService: SasService, private sasService: SasService,
private sasViyaService: SasViyaService, private loggerService: LoggerService
private loggerService: LoggerService,
private helperService: HelperService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {}
this.loadData()
}
public async loadData() {
await this.getAdminGroups()
await this.getComputeContexts()
await this.getCurrentUser()
setTimeout(() => {
if (this.selectedComputeContext) {
this.onComputeContextChange(this.selectedComputeContext)
}
}, 500)
}
public async getComputeContexts() {
return new Promise<void>((resolve, reject) => {
this.computeContextsLoading = true
this.sasViyaService.getComputeContexts().subscribe(
(res: ViyaComputeContexts) => {
this.computeContextsLoading = false
const defaultContext = res.items.find(
(item: ComputeContextItem) =>
item.name === 'SAS Job Execution compute context'
)
if (defaultContext) {
this.selectedComputeContext = defaultContext.id
}
this.computeContexts = res.items
resolve()
},
(err) => {
reject(err)
}
)
})
}
public async getCurrentUser() {
return new Promise<void>((resolve, reject) => {
this.currentUserInfoLoading = true
this.sasViyaService.getCurrentUser().subscribe(
(res: ViyaApiCurrentUser) => {
this.currentUserInfoLoading = false
this.currentUserInfo = res
this.dcPath = `/export/viya/homes/${res.id}`
resolve()
},
(err) => {
console.error('Error while getting current user', err)
reject(err)
}
)
})
}
public async getAdminGroups() {
return new Promise<void>((resolve, reject) => {
this.adminGroupsLoading = true
;(this.sasViyaService
.getAdminGroups()
.subscribe((res: ViyaApiIdentities) => {
this.adminGroupsLoading = false
// Map admin groups with only needed fields
this.adminGroups = res.items.map((item: Item) => {
return {
id: item.id,
name: item.name
}
})
resolve()
}),
(err: any) => {
this.adminGroupsLoading = false
this.loggerService.error('Error while getting admin groups', err)
this.eventService.showAbortModal('admin groups', err)
reject(err)
})
})
}
public async onComputeContextChange(computeContextId: string) {
this.sasViyaService
.getComputeContextById(computeContextId)
.subscribe((res: ComputeContextDetails) => {
if (res.attributes && res.attributes.runServerAs) {
this.runningAsUser = res.attributes.runServerAs
} else {
this.runningAsUser = this.currentUserInfo?.id || 'unknown'
}
})
}
public getComputeContextName(id: string): string | undefined {
return (
this.computeContexts.find(
(context: ComputeContextItem) => context.id === id
)?.name || undefined
)
}
/** /**
* Executes sas.json file to deploy the backend * Executes sas.json file to deploy the backend
@@ -220,6 +63,7 @@ export class AutomaticComponent implements OnInit {
* to create database if checkbox is toggled on * to create database if checkbox is toggled on
*/ */
public async executeJson() { public async executeJson() {
this.autodeploying = true
this.isSubmittingJson = true this.isSubmittingJson = true
try { try {
@@ -254,19 +98,11 @@ export class AutomaticComponent implements OnInit {
} }
this.isSubmittingJson = false this.isSubmittingJson = false
}
public async runAutoDeploy(executeJson: boolean = false) {
if (!this.deployInNewWindow) this.autodeploying = true
if (executeJson) {
this.executeJson()
}
if (this.recreateDatabase) { if (this.recreateDatabase) {
this.createDatabase() this.createDatabase()
} else { } else {
if (!this.deployInNewWindow) this.autodeployDone = true this.autodeployDone = true
} }
} }
@@ -283,160 +119,45 @@ export class AutomaticComponent implements OnInit {
] ]
} }
// Get and run service using the selected context name
let selectedComputeContextName = this.sasJsConfig.contextName
if (this.selectedComputeContext.length && this.computeContexts.length) {
const computeContextName = this.getComputeContextName(
this.selectedComputeContext
)
if (computeContextName) {
selectedComputeContextName = computeContextName
}
}
/** /**
* We are overriding default `sasjsConfig` object fields with this object fields. * We are overriding default `sasjsConfig` object fields with this object fields.
* Here we want to run this request using original WEB method. * Here we want to run this request using original WEB method.
* contextName: null is the MUST field for it. * contextName: null is the MUST field for it.
*/ */
let overrideConfig = { let overrideConfig = {
useComputeApi: null, useComputeApi: false,
contextName: selectedComputeContextName, contextName: this.sasJsConfig.contextName,
debug: true debug: true
} }
if (this.deployInNewWindow) { this.sasJs
this.runMakedataInNewWindow({ .request(`services/admin/makedata`, data, overrideConfig, () => {
contextName: selectedComputeContextName, this.sasService.shouldLogin.next(true)
admin: this.selectedAdminGroup,
dcPath: this.dcPath
}) })
} else { .then((res: any) => {
this.sasJs this.autodeployDone = true
.request(`services/admin/makedata`, data, overrideConfig, () => {
this.sasService.shouldLogin.next(true)
})
.then((res: any) => {
this.autodeployDone = true
try { try {
this.makeDataResponse = JSON.stringify(res) this.makeDataResponse = JSON.stringify(res)
} catch { } catch {
this.makeDataResponse = res this.makeDataResponse = res
} }
if (res.result && res.result.length > 0) { if (res.result && res.result.length > 0) {
this.autoDeployStatus.runMakeData = true this.autoDeployStatus.runMakeData = true
} else { } else {
this.autoDeployStatus.runMakeData = false
}
if (typeof res.sasjsAbort !== 'undefined') {
const abortRes = res
const abortMsg = abortRes.sasjsAbort[0].MSG
const macMsg = abortRes.sasjsAbort[0].MAC
this.eventService.showAbortModal('makedata', abortMsg, {
SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT,
SYSERRORTEXT: abortRes.SYSERRORTEXT,
MAC: macMsg
})
}
if (this.helperService.isStreamingViya())
this.updateIndexHtmlComputeContext()
})
.catch((err: any) => {
this.eventService.showAbortModal('makedata', JSON.stringify(err))
this.autoDeployStatus.runMakeData = false this.autoDeployStatus.runMakeData = false
this.autodeployDone = true }
})
try {
this.makeDataResponse = JSON.stringify(err)
} catch {
this.makeDataResponse = err
}
})
}
}
public runMakedataInNewWindow(params: {
contextName: string
admin: string
dcPath: string
}) {
let serverUrl = this.sasjsConfig.serverUrl
let appLoc = this.sasjsConfig.appLoc
const execPath = this.sasService.getExecutionPath()
let contextname = `&_contextname=${params.contextName}`
let admin = `&admin=${params.admin}`
let dcPath = `&dcpath=${params.dcPath}`
let debug = `&_debug=131`
let programUrl =
serverUrl +
execPath +
'/?_program=' +
appLoc +
'/services/admin/makedata' +
contextname +
admin +
dcPath +
debug
window.open(programUrl)
}
/**
* Only when on Viya streamed app, this method will update the `contextname` in the `index.html` on the SAS drive
* This is needed to ensure that the DC will use the same compute context `makedata` service used to run against.
*/
public async updateIndexHtmlComputeContext() {
const filenamePath = location.search.split('/').pop()
const filename = filenamePath?.includes('.') ? filenamePath : undefined
if (!filename) {
this.eventService.showAbortModal(
null,
'We could not figure out the file name of `index.html` based on the url.'
)
return
}
const indexHtmlContent = await this.sasService.getFileContent(
`${this.appLoc}/services`,
filename
)
if (!indexHtmlContent) {
this.loggerService.error(
`Failed to get ${filename} at ${this.appLoc}/services`
)
return
}
const computeContextName = this.getComputeContextName(
this.selectedComputeContext
)
if (!computeContextName) {
this.loggerService.error(
`Compute context name not found for ID: ${this.selectedComputeContext} | List: ${JSON.stringify(this.computeContexts)}`
)
return
}
const updatedContent = indexHtmlContent.replace(
/contextname="[^"]*"/g,
`contextname="${computeContextName}"`
)
await this.sasService
.updateFileContent(`${this.appLoc}/services`, filename, updatedContent)
.catch((err: any) => { .catch((err: any) => {
this.loggerService.error(`Failed to update DataController.html: ${err}`) this.autoDeployStatus.runMakeData = false
this.autodeployDone = true
try {
this.makeDataResponse = JSON.stringify(err)
} catch {
this.makeDataResponse = err
}
}) })
} }
@@ -0,0 +1,4 @@
.clear-memory-button {
right: 10px;
top: 2px;
}
@@ -1,11 +1,4 @@
import { import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core'
Component,
Input,
OnInit,
Output,
EventEmitter,
ViewEncapsulation
} from '@angular/core'
import SASjs, { SASjsConfig } from '@sasjs/adapter' import SASjs, { SASjsConfig } from '@sasjs/adapter'
import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings' import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings'
import { RequestWrapperResponse } from 'src/app/models/request-wrapper/RequestWrapperResponse' import { RequestWrapperResponse } from 'src/app/models/request-wrapper/RequestWrapperResponse'
@@ -17,9 +10,7 @@ import { SasService } from 'src/app/services/sas.service'
@Component({ @Component({
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,
standalone: false
}) })
export class ManualComponent implements OnInit { export class ManualComponent implements OnInit {
@Input() sasJs!: SASjs @Input() sasJs!: SASjs
@@ -275,7 +266,7 @@ export class ManualComponent implements OnInit {
* contextName: null is the MUST field for it. * contextName: null is the MUST field for it.
*/ */
let overrideConfig = { let overrideConfig = {
useComputeApi: null, useComputeApi: false,
contextName: this.sasJsConfig.contextName, contextName: this.sasJsConfig.contextName,
debug: true debug: true
} }
@@ -10,13 +10,11 @@
</p> </p>
<p class="m-0 mt-10"> <p class="m-0 mt-10">
Please specify a physical directory (on the Please specify a physical directory below, to which user
<strong> {{ SYSHOSTNAME }}</strong> <strong>{{ SYSUSERID }}</strong> can write, on behalf of Data Controller:
compute server) below, to which user
<strong>{{ SYSUSERID }}</strong> can write, on behalf of Data Controller.
</p> </p>
<label class="mt-20 clr-control-label">DC Staging Directory</label> <label class="mt-20 clr-control-label">DC Directory</label>
<div class="mb-10 clr-control-container"> <div class="mb-10 clr-control-container">
<div class="clr-input-wrapper"> <div class="clr-input-wrapper">
<input <input
@@ -0,0 +1,23 @@
.clr-control-container {
width: 50vw;
}
.clr-input-wrapper {
width: 100%;
input {
width: 100%;
}
}
.thinProgress {
left: 0px;
right: 0;
width: unset;
height: 1px;
margin-top: 0 !important;
&::after {
top: 0;
}
}
@@ -1,12 +1,5 @@
import { Location } from '@angular/common' import { Location } from '@angular/common'
import { import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewEncapsulation
} from '@angular/core'
import SASjs, { SASjsConfig } from '@sasjs/adapter' import SASjs, { SASjsConfig } from '@sasjs/adapter'
import { ServerType } from '@sasjs/utils/types/serverType' import { ServerType } from '@sasjs/utils/types/serverType'
import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings' import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings'
@@ -19,9 +12,7 @@ import { SasjsService } from 'src/app/services/sasjs.service'
@Component({ @Component({
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,
standalone: false
}) })
export class SasjsConfiguratorComponent implements OnInit { export class SasjsConfiguratorComponent implements OnInit {
@Input() sasJs!: SASjs @Input() sasJs!: SASjs
@@ -7,8 +7,7 @@ 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,8 +9,7 @@ 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,8 +9,7 @@ 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,8 +6,7 @@ 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,8 +1,7 @@
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() {}
@@ -0,0 +1,238 @@
.record-edit-modal {
.column-entry {
display: flex;
justify-content: space-between;
.name-input-row {
width: 100%;
max-width: 260px;
.cell-desc {
margin-right: 30px;
margin-top: 10px;
}
}
.inputs-wrapper {
flex: 1;
display: flex;
align-items: center;
::ng-deep >*:not(.date-field):not(clr-select-container) {
flex: 1;
}
}
p {
margin-top: 0px;
}
::ng-deep {
.clr-textarea-wrapper {
margin-top: 0 !important;
}
.clr-form-control {
margin-top: 0px !important;
}
app-soft-select {
display: block;
width: 224px;
border: 1px solid #999;
color: #000;
padding: calc(.25rem + 2px) .5rem;
border-radius: .125rem;
font-size: .541667rem;
margin-right: 6px;
input {
width: 100%;
border: 0;
&:focus {
background: none;
border: 0 !important;
}
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
}
}
&:first-child p:first-child {
margin-top: 0;
}
}
.date-field {
position: relative;
display: inline-block;
textarea {
width: 230px;
}
.date-picker {
position: absolute;
right: 0;
top: 4px;
::ng-deep {
// clr-datepicker-view-manager {
// transform: unset !important;
// left: unset !important;
// right: 70px !important;
// }
.clr-input-group {
border: 0 !important;
}
}
}
}
.modal-body {
padding-bottom: 10px;
}
::ng-deep {
clr-select-container {
border: 1px solid #999;
color: #000;
border-radius: .125rem;
margin-right: 5px;
.clr-select-wrapper {
max-height: unset;
&::after {
top: 15px;
}
}
select {
height: auto;
padding: 10px;
padding-right: 20px;
border: 0 !important;
&:focus {
background: 0 0 !important;
}
&:hover {
background: transparent;
}
}
}
clr-input-container {
width: 224px;
border: 1px solid #999;
color: #000;
padding: calc(.25rem + 2px) .5rem;
border-radius: .125rem;
font-size: .541667rem;
margin-right: 6px;
input {
width: 100%;
border: 0;
&:focus {
background: none;
border: 0 !important;
}
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
&.invalid-data {
border-color: red;
}
}
.modal-dialog {
width: 80vw;
}
.clr-control-container {
width: 100%;
textarea {
width: 100%;
resize: none;
border-color: #999;
&.invalid-data {
border-color: red;
outline: 0;
}
&.not-char {
font-family: "Lucida Console", Monaco, monospace;
}
}
}
.generate-record-url {
right: 40px;
top: 40px;
font-size: 12px;
}
.generate-record-url-button {
right: 25px;
top: 5px;
}
.modal-header {
padding: 0 0 1rem 0;
}
.modal-footer {
display: flex;
align-items: center;
justify-content: space-between;
// height: 65px;
.alert {
margin: 0;
}
}
}
}
.prev-next {
display: flex;
align-items: center;
p {
margin: 0;
}
button {
margin: 0px 10px;
}
}
.focusable {
&:focus {
box-shadow: 0 0 3px 0px #5aa220;
}
}
.entry-input-left-offset {
left: -30px;
}
.validation-info-alert {
width: 310px
}
@@ -1,12 +1,5 @@
import { KeyValue } from '@angular/common' import { KeyValue } from '@angular/common'
import { import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewEncapsulation
} from '@angular/core'
import moment from 'moment' import moment from 'moment'
import { ValidateFilterSASResponse } from 'src/app/models/sas/validate-filter.model' import { ValidateFilterSASResponse } from 'src/app/models/sas/validate-filter.model'
import { QueryClause } from 'src/app/models/TableData' import { QueryClause } from 'src/app/models/TableData'
@@ -14,7 +7,6 @@ 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,9 +16,7 @@ import { EditRecordModal } from '../../models/EditRecordModal'
@Component({ @Component({
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,
standalone: false
}) })
export class EditRecordComponent implements OnInit { export class EditRecordComponent implements OnInit {
@Input() currentRecord!: EditRecordModal @Input() currentRecord!: EditRecordModal
@@ -147,63 +137,23 @@ export class EditRecordComponent implements OnInit {
}, 0) }, 0)
} }
async recordInputChange(event: any, colName: string): Promise<void> { async recordInputChange(event: any, colName: string) {
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) => {
this.updateValidationState(colName, valid) const index = this.currentRecordInvalidCols.indexOf(colName)
if (!valid) { if (valid) {
this.tryAutoPopulateNotNull(event, colName, colRules, value) if (index > -1) this.currentRecordInvalidCols.splice(index, 1)
} 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()
} }
@@ -213,8 +163,23 @@ export class EditRecordComponent implements OnInit {
} }
public copyToClip(text: string) { public copyToClip(text: string) {
navigator.clipboard.writeText(text) const modalElement = document.querySelector('#recordModalRef .modal-title')
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() {
@@ -0,0 +1,8 @@
:host {
display: block;
}
p {
margin: 0;
text-align: center;
}
@@ -1,4 +1,4 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core' import { Component, OnInit } from '@angular/core'
/** /**
* Goal of this component is to recieve array of strings where every element is one state * Goal of this component is to recieve array of strings where every element is one state
@@ -10,9 +10,7 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
@Component({ @Component({
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,
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
+5 -5
View File
@@ -36,7 +36,7 @@
<div class="clr-row card-block mt-15 d-flex justify-content-between"> <div class="clr-row card-block mt-15 d-flex justify-content-between">
<div class="clr-col-md-auto"> <div class="clr-col-md-auto">
<div class="encoding-block"> <div class="encoding-block">
<clr-radio-container class="mt-0" clrInline> <clr-radio-container class="mt-0-i" clrInline>
<clr-radio-wrapper> <clr-radio-wrapper>
<input <input
type="radio" type="radio"
@@ -193,14 +193,13 @@
libName: (libds?.split('.'))![0], libName: (libds?.split('.'))![0],
tableName: (libds?.split('.'))![1] tableName: (libds?.split('.'))![1]
} as libdsParsed" } as libdsParsed"
class="editor-title text-center mt-0" class="editor-title text-center mt-0-i"
> >
<clr-tooltip> <clr-tooltip>
<clr-icon <clr-icon
clrTooltipTrigger clrTooltipTrigger
(click)="datasetInfo = true" (click)="datasetInfo = true"
shape="info-circle" shape="info-circle"
aria-label="View dataset meta info"
class="is-highlight cursor-pointer" class="is-highlight cursor-pointer"
size="24" size="24"
></clr-icon> ></clr-icon>
@@ -408,11 +407,12 @@
<div class="hot-wrapper clr-flex-1"> <div class="hot-wrapper clr-flex-1">
<hot-table <hot-table
#hotInstance #hotInstance
hotId="hotInstance"
id="hotTable" id="hotTable"
class="edit-hot" class="edit-hot"
className="htDark"
[class.hidden]="hotTable.hidden" [class.hidden]="hotTable.hidden"
[data]="hotTable.data" [licenseKey]="hotTable.licenseKey"
[settings]="hotTableSettings"
> >
</hot-table> </hot-table>
</div> </div>
+246
View File
@@ -0,0 +1,246 @@
.card {
margin-top: 0;
border: 0;
}
.buttonBar {
padding: 2px 10px 2px 10px;
align-items: center;
}
.testRed {
color: white;
background: rgba(255,0,0, 0.8) !important;
}
hot-table {
::ng-deep {
.firstColumnHeaderStyle button.changeType {
display: none;
}
.handsontable tbody th.ht__highlight, .handsontable thead th.ht__highlight {
&.primaryKeyHeaderStyle {
background-color: #306b00b0 !important;
}
}
.primaryKeyHeaderStyle {
background-color: #306b006e !important;
}
th.readonlyCell {
div {
opacity: 0.4;
}
}
td.readonlyCell {
opacity: 0.5
}
}
}
.submit-reason {
min-height: 120px;
max-height: 120px;
height: 120px;
}
.infoBar {
margin-top:14px;
background: #495967;
color: white;
text-align:center;
padding: 3px;
font-size: 16px;
height: 30px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
span {
width: 80%;
}
&:hover {
height: unset;
white-space: normal;
span {
width: unset;
}
}
}
.pkHeader {
background: #687682;
color: #fff;
margin: -1px -1px -1px -1px;
}
.headerBar {
// padding: 13px 0px 14px 0px;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
background: var(--clr-vertical-nav-bg-color);
}
.error-icon {
width: 30px;
height: 30px;
color: red;
}
.btnCtrl {
display:flex;
justify-content:flex-end;
}
.card-header {
border-bottom: 1px solid transparent;
}
.hidden {
visibility: hidden;
}
.my-drop-zone {
border: solid 1px lightgray;
border-radius: 10px;
background: whitesmoke;
box-shadow: inset 0px 0px 4px 2px #a7a5a52b;
height: 50vh;
}
.nv-file-over {
border: solid 2px green;
} /* Default class applied to drop zones on over */
.file-drop-text{
text-align: center;
}
.nv-file-over {
border: solid 2px green;
} /* Default class applied to drop zones on over */
.file-drop-text{
text-align: center;
}
@media screen and (max-width: 768px) {
.progresStatic {
margin-top:9px!important;
}
.progress, .progress-static {
width: calc(100% - 14px);
}
}
.hotEditor {
position: relative;
}
.excel-parsing {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
.details {
margin: 0;
position: absolute;
top: -45px;
}
}
.edit-record-spinner {
display: flex;
justify-content: center;
align-items: center;
background: rgba(255, 255, 255, 0.6);
position: absolute;
top: 0px;
bottom: 0px;
width: 100%;
z-index: 500;
}
@media screen and (max-width: 480px) {
.progresStatic {
margin-top:32px!important;
}
.card-block, .card-footer {
padding: 10px 0px 0px 0px;
}
}
.content-area {
padding: 0 0.8rem 0.8rem 0.8rem !important;
padding-top: 0;
// .card {
// min-height: calc(100vh - 160px);
// }
}
.drop-area {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
justify-content: center;
align-items: flex-start;
margin: 1px;
border: 2px dashed #fff;
z-index: -1;
span {
font-size: 20px;
margin-top: 20px;
padding: 10px;
background: #dbdbdb;
border-radius: 5px;
color: black;
}
}
#submitBtn, #cancelSubmitBtn {
width: 150px;
}
.view-table {
font-size: inherit !important;
}
// When width is smaller remove the text from the buttons
// keep only the icons
@media (max-width: 992px) {
.icon-collapse {
.text {
display: none;
}
}
}
// FIXME
// Let's leave it here for a reference if there
// is an issue with viewboxes/filter modal overlaying
// we will remove it if no issues found
// .filter-modal {
// z-index: 1210;
// }
+85 -496
View File
@@ -3,7 +3,6 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
OnDestroy,
OnInit, OnInit,
QueryList, QueryList,
ViewChild, ViewChild,
@@ -17,7 +16,7 @@ import { SasStoreService } from '../services/sas-store.service'
type AOA = any[][] type AOA = any[][]
import { HotTableComponent } from '@handsontable/angular-wrapper' import { HotTableRegisterer } from '@handsontable/angular'
import { UploadFile } from '@sasjs/adapter' import { UploadFile } from '@sasjs/adapter'
import { isSpecialMissing } from '@sasjs/utils/input/validators' import { isSpecialMissing } from '@sasjs/utils/input/validators'
import CellRange from 'handsontable/3rdparty/walkontable/src/cell/range' import CellRange from 'handsontable/3rdparty/walkontable/src/cell/range'
@@ -43,7 +42,6 @@ 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'
@@ -71,16 +69,15 @@ import { ParseResult } from '../models/ParseResult.interface'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.Emulated
standalone: false
}) })
export class EditorComponent implements OnInit, AfterViewInit, OnDestroy { export class EditorComponent implements OnInit, AfterViewInit {
@ViewChildren('uploadStater') @ViewChildren('uploadStater')
uploadStaterCompList: QueryList<UploadStaterComponent> = new QueryList() uploadStaterCompList: QueryList<UploadStaterComponent> = new QueryList()
@ViewChildren('queryFilter') @ViewChildren('queryFilter')
queryFilterCompList: QueryList<QueryComponent> = new QueryList() queryFilterCompList: QueryList<QueryComponent> = new QueryList()
@ViewChild(HotTableComponent, { static: false }) @ViewChildren('hotInstance')
hotTableComponent!: HotTableComponent hotInstanceCompList: QueryList<Handsontable> = new QueryList()
@ViewChildren('fileUploadInput') @ViewChildren('fileUploadInput')
fileUploadInputCompList: QueryList<ElementRef> = new QueryList() fileUploadInputCompList: QueryList<ElementRef> = new QueryList()
@@ -122,26 +119,13 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
public hotInstance!: Handsontable public hotInstance!: Handsontable
public dcValidator: DcValidator | undefined public dcValidator: DcValidator | undefined
public hotTableSettings: Handsontable.GridSettings = {}
private updateHotTableSettings(): void {
this.hotTableSettings = {
colHeaders: this.hotTable.colHeaders,
columns: this.hotTable.columns,
height: this.hotTable.height,
licenseKey: this.hotTable.licenseKey,
readOnly: this.hotTable.readOnly,
copyPaste: this.hotTable.copyPaste,
contextMenu: true
}
}
public hotTable: HotTableInterface = { public hotTable: HotTableInterface = {
data: [], data: [],
colHeaders: [], colHeaders: [],
hidden: true, hidden: true,
columns: [], columns: [],
height: 'calc(100vh - 160px)', height: '100%',
minSpareRows: 1,
licenseKey: undefined, licenseKey: undefined,
readOnly: true, readOnly: true,
copyPaste: { copyPaste: {
@@ -178,30 +162,10 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
} }
}, },
row_above: { row_above: {
name: 'Insert Row above', name: 'Insert Row above'
callback: (
key: string,
selection: any[],
clickEvent: MouseEvent
) => {
const firstSelection = selection[0]
const targetRow = firstSelection.start.row
this.insertRowAtPosition(targetRow)
}
}, },
row_below: { row_below: {
name: 'Insert Row below', name: 'Insert Row below'
callback: (
key: string,
selection: any[],
clickEvent: MouseEvent
) => {
const firstSelection = selection[0]
const targetRow = firstSelection.start.row + 1
this.insertRowAtPosition(targetRow)
}
}, },
remove_row: { remove_row: {
name: 'Ignore row' name: 'Ignore row'
@@ -386,9 +350,6 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
public licenceState = this.licenceService.licenceState public licenceState = this.licenceService.licenceState
private ariaObserver: MutationObserver | undefined
private ariaCheckInterval: any | undefined
constructor( constructor(
private licenceService: LicenceService, private licenceService: LicenceService,
private eventService: EventService, private eventService: EventService,
@@ -399,12 +360,15 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
private route: ActivatedRoute, private route: ActivatedRoute,
private sasService: SasService, private sasService: SasService,
private cdf: ChangeDetectorRef, private cdf: ChangeDetectorRef,
private hotRegisterer: HotTableRegisterer,
private spreadsheetService: SpreadsheetService private spreadsheetService: SpreadsheetService
) { ) {
const lang = languages[window.navigator.language] const lang = languages[window.navigator.language]
if (lang) if (lang)
numbro.default.registerLanguage(languages[window.navigator.language]) numbro.default.registerLanguage(languages[window.navigator.language])
this.hotRegisterer = new HotTableRegisterer()
this.parseRestrictions() this.parseRestrictions()
this.setRestrictions() this.setRestrictions()
} }
@@ -932,11 +896,6 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
} }
this.reSetCellValidationValues() this.reSetCellValidationValues()
// Fix ARIA accessibility issues after table edit
setTimeout(() => {
this.fixAriaAccessibility()
}, 100)
}, 0) }, 0)
} }
@@ -963,9 +922,6 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
this.cellValidationSource = [] this.cellValidationSource = []
// Clear custom validation styling
this.clearDuplicateValidation()
const hot = this.hotInstance const hot = this.hotInstance
const columnSorting = hot.getPlugin('multiColumnSorting') const columnSorting = hot.getPlugin('multiColumnSorting')
const columnSortConfig = columnSorting.getSortConfig() const columnSortConfig = columnSorting.getSortConfig()
@@ -1026,58 +982,22 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
setTimeout(() => { setTimeout(() => {
const hot = this.hotInstance const hot = this.hotInstance
// Create a new empty row object with proper structure const dsInsertIndex = this.dataSource.length
const newRow = this.createEmptyRow() hot.alter('insert_row_below', dsInsertIndex, 1)
// Add the new row to the data source
this.dataSource.push(newRow)
// Update the hot table with the new data
hot.updateSettings({ data: this.dataSource }, false) hot.updateSettings({ data: this.dataSource }, false)
// Select the newly added row
hot.selectCell(this.dataSource.length - 1, 0) hot.selectCell(this.dataSource.length - 1, 0)
hot.render() hot.render()
if (this.dataSource[dsInsertIndex]) {
this.dataSource[dsInsertIndex]['noLinkOption'] = true
}
this.addingNewRow = false this.addingNewRow = false
this.reSetCellValidationValues() this.reSetCellValidationValues()
}) })
} }
/**
* Creates a new empty row object with proper structure.
* Columns with NOTNULL DQ rules are pre-populated with their RULE_VALUE.
*/
private createEmptyRow(): any {
const newRow: any = {}
this.cellValidation.forEach((rule: any) => {
const dataKey = rule.data
newRow[dataKey] = this.hotDataSchema.hasOwnProperty(dataKey)
? this.hotDataSchema[dataKey]
: ''
})
newRow['noLinkOption'] = true
return newRow
}
/**
* Inserts a new row at the specified position and updates the table
*/
private insertRowAtPosition(targetRow: number): void {
const newRow = this.createEmptyRow()
// Insert the new row at the target position
this.dataSource.splice(targetRow, 0, newRow)
// Update the hot table
const hot = this.hotInstance
hot.updateSettings({ data: this.dataSource }, false)
hot.render()
this.reSetCellValidationValues()
}
public cancelSubmit() { public cancelSubmit() {
this.dataSource = this.helperService.deepClone(this.dataSourceBeforeSubmit) this.dataSource = this.helperService.deepClone(this.dataSourceBeforeSubmit)
this.dataSourceBeforeSubmit = [] this.dataSourceBeforeSubmit = []
@@ -1157,96 +1077,51 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
} }
} }
private clearDuplicateValidation() {
const hot = this.hotInstance
// Clear previous duplicate validation styling
for (const rowIndex of this.duplicatePkIndexes) {
for (let col = 1; col <= this.readOnlyFields; col++) {
hot.removeCellMeta(rowIndex, col, 'valid')
hot.removeCellMeta(rowIndex, col, 'dupKey')
// Remove our custom class from cell metadata
const cellMeta = hot.getCellMeta(rowIndex, col)
if (cellMeta.className) {
let cleanedClassName: string
if (Array.isArray(cellMeta.className)) {
cleanedClassName = cellMeta.className
.filter((c) => c !== 'dc-invalid-cell')
.join(' ')
} else {
cleanedClassName = cellMeta.className
.replace('dc-invalid-cell', '')
.trim()
}
hot.setCellMeta(rowIndex, col, 'className', cleanedClassName)
}
}
}
this.duplicatePkIndexes = []
hot.render()
}
public validatePrimaryKeys() { public validatePrimaryKeys() {
const hot = this.hotInstance const hot = this.hotInstance
// Clear previous validation before applying new ones const myTable = hot.getData()
this.clearDuplicateValidation()
// Get data from the data source instead of hot.getData() to ensure consistency
const myTable = this.dataSource
this.pkFields = [] this.pkFields = []
for (let index = 0; index < myTable.length; index++) { for (let index = 0; index < myTable.length; index++) {
let pkRow = '' let pkRow = ''
for (let ind = 1; ind < this.readOnlyFields + 1; ind++) { for (let ind = 1; ind < this.readOnlyFields + 1; ind++) {
const colName = this.headerColumns[ind] pkRow = pkRow + '|' + myTable[index][ind]
const value = myTable[index][colName] || ''
pkRow = pkRow + '|' + value
} }
this.pkFields.push(pkRow) this.pkFields.push(pkRow)
} }
const results: any = [] const results = []
const rows = this.dataSource.length const rows = this.dataSource.length
// Only check for duplicates if we have data for (let j = 0; j < this.pkFields.length; j++) {
if (this.pkFields.length > 0) { for (let i = 0; i < this.pkFields.length; i++) {
for (let j = 0; j < this.pkFields.length; j++) { if (this.pkFields[j] === this.pkFields[i] && i !== j) {
for (let i = 0; i < this.pkFields.length; i++) { results.push(i)
if ( }
this.pkFields[j] === this.pkFields[i] && }
i !== j && }
this.pkFields[j] !== '|'
) { if (this.pkFields.length > rows) {
results.push(i) for (let n = rows; n < this.pkFields.length; n++) {
for (let p = rows; p < this.pkFields.length; p++) {
if (n < p && this.pkFields[n] === this.pkFields[p]) {
results.push(p)
} }
} }
} }
} }
// Clear any existing validation marks for all cells let cellMeta
for (let row = 0; row < myTable.length; row++) {
for (let col = 0; col < this.headerColumns.length; col++) {
const cellMeta = hot.getCellMeta(row, col)
if (cellMeta) {
cellMeta.valid = true
cellMeta.dupKey = false
}
}
}
// Mark duplicate cells as invalid
for (let k = 0; k < results.length; k++) { for (let k = 0; k < results.length; k++) {
for (let index = 1; index < this.readOnlyFields + 1; index++) { for (let index = 1; index < this.readOnlyFields + 1; index++) {
hot.setCellMeta(results[k], index, 'valid', false) cellMeta = hot.getCellMeta(results[k], index)
hot.setCellMeta(results[k], index, 'dupKey', true) cellMeta.valid = false
hot.setCellMeta(results[k], index, 'className', 'dc-invalid-cell') cellMeta.dupKey = true
hot.render()
} }
} }
this.duplicatePkIndexes = [...new Set(results.sort())] this.duplicatePkIndexes = [...new Set(results.sort())]
hot.render()
} }
/** /**
@@ -1541,26 +1416,10 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
this.dataSourceBeforeSubmit = this.helperService.deepClone(this.dataSource) this.dataSourceBeforeSubmit = this.helperService.deepClone(this.dataSource)
// Clean up the data source by removing noLinkOption property
for (let i = 0; i < this.dataSource.length; i++) { for (let i = 0; i < this.dataSource.length; i++) {
delete this.dataSource[i].noLinkOption delete this.dataSource[i].noLinkOption
} }
// Remove any completely empty rows from the end
while (this.dataSource.length > 0) {
const lastRow = this.dataSource[this.dataSource.length - 1]
const isEmpty = Object.keys(lastRow).every((key) => {
if (key === '_____DELETE__THIS__RECORD_____') return true
return !lastRow[key] || lastRow[key] === ''
})
if (isEmpty) {
this.dataSource.pop()
} else {
break
}
}
hot.updateSettings( hot.updateSettings(
{ {
data: this.dataSource, data: this.dataSource,
@@ -1578,6 +1437,17 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
EditorComponent.cnt = 0 EditorComponent.cnt = 0
EditorComponent.nonPkCnt = 0 EditorComponent.nonPkCnt = 0
// this.saveLoading = true;
/**
* Below code should be analized, not sure what is the purpose of exceedCells
*/
const myTableData = hot.getData()
// If the last row is empty, remove it before validation
if (myTableData.length > 1 && hot.isEmptyRow(myTableData.length - 1)) {
hot.alter('remove_row', myTableData.length - 1)
}
this.validatePrimaryKeys() this.validatePrimaryKeys()
@@ -1607,6 +1477,15 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
if (txt) txt.focus() if (txt) txt.focus()
}, 200) }, 200)
}) })
// let cnt = 0;
// hot.addHook("afterValidate", () => {
// this.updateSoftSelectColumns(true);
// cnt++;
// if (cnt === long) {
// this.validationDone = 1;
// }
// });
} }
public async saveTable(data: any) { public async saveTable(data: any) {
@@ -1760,20 +1639,11 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
} }
public checkInvalid() { public checkInvalid() {
// Use Angular wrapper to access Handsontable element instead of DOM queries const hotElement = (this.hotInstanceCompList.first.container as any)
if (!this.hotTableComponent || !this.hotTableComponent.hotInstance) .nativeElement
return false const invalidCells = hotElement.querySelectorAll('.htInvalid')
const hotElement = this.hotTableComponent.hotInstance.rootElement return invalidCells.length > 0
if (!hotElement) return false
// Check for standard Handsontable validation failures
const standardInvalidCells = hotElement.querySelectorAll('.htInvalid')
// Check for our custom duplicate primary key validation failures
const customInvalidCells = hotElement.querySelectorAll('.dc-invalid-cell')
return standardInvalidCells.length > 0 || customInvalidCells.length > 0
} }
public goToEditor() { public goToEditor() {
@@ -2313,7 +2183,6 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
*/ */
private setCellFilter(filter: boolean) { private setCellFilter(filter: boolean) {
const hotSelected = this.hotInstance.getSelected() const hotSelected = this.hotInstance.getSelected()
if (!hotSelected) return
const selection = hotSelected ? hotSelected[0] : hotSelected const selection = hotSelected ? hotSelected[0] : hotSelected
// When we open a dropdown we want filter disabled so value in cell // When we open a dropdown we want filter disabled so value in cell
@@ -2338,13 +2207,9 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
} }
async ngOnInit() { async ngOnInit() {
// Initialize hot table settings
this.updateHotTableSettings()
this.licenceService.hot_license_key.subscribe( this.licenceService.hot_license_key.subscribe(
(hot_license_key: string | undefined) => { (hot_license_key: string | undefined) => {
this.hotTable.licenseKey = hot_license_key this.hotTable.licenseKey = hot_license_key
this.updateHotTableSettings() // Update settings when license key changes
} }
) )
@@ -2397,202 +2262,14 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
} }
} }
ngAfterViewInit() { ngAfterViewInit() {}
// Fix ARIA accessibility issues after table initialization
setTimeout(() => {
this.fixAriaAccessibility()
}, 1000)
// Set up event listener for hot table element
// Double click to edit
setTimeout(() => {
if (this.hotTableComponent && this.hotTableComponent.hotInstance) {
const hotElement = this.hotTableComponent.hotInstance.rootElement
if (hotElement) {
hotElement.addEventListener('mousedown', (event: MouseEvent) => {
if (!this.uploadPreview) {
this.hotClicked()
}
setTimeout(() => {
const menuDebugItem: any =
document.querySelector('.debug-switch-item') || undefined
if (menuDebugItem) menuDebugItem.click()
}, 100)
})
}
}
}, 100)
}
ngOnDestroy() {
// Clean up the MutationObserver
if (this.ariaObserver) {
this.ariaObserver.disconnect()
this.ariaObserver = undefined
}
// Clean up the interval
if (this.ariaCheckInterval) {
clearInterval(this.ariaCheckInterval)
this.ariaCheckInterval = undefined
}
}
/**
* Fixes ARIA accessibility issues in the Handsontable component
* This addresses the accessibility report issues with treegrid and presentation roles
*/
private fixAriaAccessibility() {
// Use a more aggressive approach to find and fix all ARIA issues
const fixAriaIssues = () => {
// Specifically target Handsontable wrapper elements that are causing issues
const hotWrappers = document.querySelectorAll(
'.ht-wrapper, .wtHolder, [id^="ht_"]'
)
hotWrappers.forEach((wrapper) => {
// Remove problematic ARIA attributes from Handsontable wrappers
wrapper.removeAttribute('role')
wrapper.removeAttribute('aria-rowcount')
wrapper.removeAttribute('aria-colcount')
wrapper.removeAttribute('aria-multiselectable')
})
// Find all elements with problematic ARIA roles in the entire document
const allTreegridElements = document.querySelectorAll('[role="treegrid"]')
const allPresentationElements = document.querySelectorAll(
'[role="presentation"]'
)
// Fix treegrid role issues - remove them completely as they're causing problems
allTreegridElements.forEach((element) => {
element.removeAttribute('role')
element.removeAttribute('aria-rowcount')
element.removeAttribute('aria-colcount')
element.removeAttribute('aria-multiselectable')
})
// Fix presentation role issues - remove them if they contain interactive elements
allPresentationElements.forEach((element) => {
const hasInteractiveChildren =
element.querySelectorAll(
'button, input, select, textarea, [tabindex], [onclick], [contenteditable]'
).length > 0
if (hasInteractiveChildren) {
element.removeAttribute('role')
}
})
// Also fix any elements with aria-rowcount="-1" which is problematic
const negativeRowCountElements = document.querySelectorAll(
'[aria-rowcount="-1"]'
)
negativeRowCountElements.forEach((element) => {
element.removeAttribute('aria-rowcount')
})
// Ensure proper table structure
const tableElements = document.querySelectorAll('table')
tableElements.forEach((table) => {
if (!table.getAttribute('role')) {
table.setAttribute('role', 'table')
}
// Ensure table headers have proper scope
const headerCells = table.querySelectorAll('th')
headerCells.forEach((th) => {
if (!th.getAttribute('scope')) {
th.setAttribute('scope', 'col')
}
})
})
// Add proper ARIA labels to interactive elements
const interactiveElements = document.querySelectorAll(
'button, input, select, textarea, [contenteditable]'
)
interactiveElements.forEach((element) => {
if (
!element.getAttribute('aria-label') &&
!element.getAttribute('aria-labelledby')
) {
const textContent = element.textContent?.trim()
if (textContent) {
element.setAttribute('aria-label', textContent)
}
}
})
}
// Run the fix immediately
fixAriaIssues()
// Run it again after a short delay to catch any dynamically created elements
setTimeout(fixAriaIssues, 100)
setTimeout(fixAriaIssues, 500)
setTimeout(fixAriaIssues, 1000)
setTimeout(fixAriaIssues, 2000)
// Set up a periodic check to ensure accessibility fixes are maintained
if (!this.ariaCheckInterval) {
this.ariaCheckInterval = setInterval(fixAriaIssues, 3000)
}
// Set up a MutationObserver to continuously monitor for new problematic elements
if (!this.ariaObserver) {
this.ariaObserver = new MutationObserver((mutations) => {
let shouldFix = false
mutations.forEach((mutation) => {
if (
mutation.type === 'attributes' &&
(mutation.attributeName === 'role' ||
mutation.attributeName === 'aria-rowcount')
) {
shouldFix = true
}
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element
if (
element.hasAttribute('role') ||
element.hasAttribute('aria-rowcount')
) {
shouldFix = true
}
}
})
}
})
if (shouldFix) {
setTimeout(fixAriaIssues, 50)
}
})
// Start observing the entire document for changes
this.ariaObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: [
'role',
'aria-rowcount',
'aria-colcount',
'aria-multiselectable'
]
})
}
}
initSetup(response: EditorsGetDataServiceResponse) { initSetup(response: EditorsGetDataServiceResponse) {
this.hotInstance = this.hotTableComponent!.hotInstance! this.hotInstance = this.hotRegisterer.getInstance('hotInstance')
if (this.getdataError) return if (this.getdataError) return
if (!response) return if (!response) return
if (!response.data) return if (!response.data) return
if (!this.hotInstance) return
this.cols = response.data.cols this.cols = response.data.cols
this.dsmeta = response.data.dsmeta this.dsmeta = response.data.dsmeta
@@ -2612,7 +2289,7 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
this.dsNote = '' this.dsNote = ''
} }
const hot = this.hotInstance const hot: Handsontable = this.hotInstance
const approvers: Approver[] = response.data.approvers const approvers: Approver[] = response.data.approvers
@@ -2681,14 +2358,13 @@ 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 - includes NOTNULL defaults from DQ rules // default schema
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()
) )
} }
@@ -2734,11 +2410,6 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
rowHeights: 24, rowHeights: 24,
maxRows: this.licenceState.value.editor_rows_allowed || Infinity, maxRows: this.licenceState.value.editor_rows_allowed || Infinity,
invalidCellClassName: 'htInvalid', invalidCellClassName: 'htInvalid',
// Prevent automatic row creation
autoWrapRow: false,
autoWrapCol: false,
// Ensure proper data binding
bindRowsWithHeaders: false,
dropdownMenu: { dropdownMenu: {
items: { items: {
make_read_only: { make_read_only: {
@@ -2813,51 +2484,7 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
cellProperties: Handsontable.CellProperties cellProperties: Handsontable.CellProperties
) => { ) => {
const isReadonlyCol = col && this.isReadonlyCol(col) const isReadonlyCol = col && this.isReadonlyCol(col)
if (isReadonlyCol) cellProperties.className = 'readonlyCell'
// Check if this cell should be marked as invalid due to duplicate primary key values
// Only applies to primary key columns (col 1 through readOnlyFields)
const isDuplicateCell =
this.duplicatePkIndexes.includes(row) &&
col >= 1 &&
col <= this.readOnlyFields
// Handle existing CSS classes - Handsontable can provide className as string or array
const existingClasses = cellProperties.className || ''
let classes: string[]
if (Array.isArray(existingClasses)) {
// If already an array, create a copy
classes = [...existingClasses]
} else {
// If string, split by spaces and filter out empty strings
classes = existingClasses
.split(' ')
.filter((c: string) => c.length > 0)
}
// Add readonlyCell class for readonly columns to maintain original styling
if (isReadonlyCol && !classes.includes('readonlyCell')) {
classes.push('readonlyCell')
}
// Apply custom validation styling for duplicate primary key cells
// Note: Uses 'dc-invalid-cell' instead of Handsontable's 'htInvalid' class
// because Handsontable's internal validation system was removing 'htInvalid'
// causing flickering. Our custom class persists reliably.
if (isDuplicateCell) {
if (!classes.includes('dc-invalid-cell')) {
classes.push('dc-invalid-cell')
}
// Mark cell as invalid to prevent form submission
cellProperties.valid = false
// Custom flag to identify this as a duplicate key cell for cleanup
cellProperties.dupKey = true
}
// Apply the combined CSS classes back to the cell
if (classes.length > 0) {
cellProperties.className = classes.join(' ')
}
} }
}, },
false false
@@ -2876,6 +2503,22 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
this.columnHeader[0] = 'Delete?' this.columnHeader[0] = 'Delete?'
this.readOnlyFields = response.data.sasparams[0].PKCNT this.readOnlyFields = response.data.sasparams[0].PKCNT
const hotInstaceEl = document.getElementById('hotInstance')
if (hotInstaceEl) {
hotInstaceEl.addEventListener('mousedown', (event) => {
if (!this.uploadPreview) {
this.hotClicked()
}
setTimeout(() => {
const menuDebugItem: any =
document.querySelector('.debug-switch-item') || undefined
if (menuDebugItem) menuDebugItem.click()
}, 100)
})
}
hot.addHook( hot.addHook(
'afterSelection', 'afterSelection',
( (
@@ -2954,17 +2597,6 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
hot.addHook('afterRender', (isForced: boolean) => { hot.addHook('afterRender', (isForced: boolean) => {
this.eventService.dispatchEvent('resize') this.eventService.dispatchEvent('resize')
// Fix ARIA accessibility issues after each render
this.fixAriaAccessibility()
})
// Add a more frequent accessibility fix hook
hot.addHook('afterChange', () => {
// Fix ARIA accessibility issues after any data change
setTimeout(() => {
this.fixAriaAccessibility()
}, 50)
}) })
hot.addHook('afterCreateRow', (source: any, change: any) => { hot.addHook('afterCreateRow', (source: any, change: any) => {
@@ -2978,44 +2610,6 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
} }
}) })
// Add hook to prevent unwanted row creation
hot.addHook(
'beforeCreateRow',
(index: number, amount: number, source?: any) => {
// Only allow row creation through the Add Row button or context menu
if (
!this.addingNewRow &&
source !== 'ContextMenu.insert_row_above' &&
source !== 'ContextMenu.insert_row_below'
) {
return false
}
}
)
// 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
@@ -3066,10 +2660,5 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
} }
hot.render() hot.render()
// Fix ARIA accessibility issues after table initialization
setTimeout(() => {
this.fixAriaAccessibility()
}, 500)
} }
} }
+2 -2
View File
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { ClarityModule } from '@clr/angular' import { ClarityModule } from '@clr/angular'
import { HotTableModule } from '@handsontable/angular-wrapper' import { HotTableModule } from '@handsontable/angular'
import { registerAllModules } from 'handsontable/registry' import { registerAllModules } from 'handsontable/registry'
import { AppSharedModule } from '../app-shared.module' import { AppSharedModule } from '../app-shared.module'
import { DirectivesModule } from '../directives/directives.module' import { DirectivesModule } from '../directives/directives.module'
@@ -28,7 +28,7 @@ registerAllModules()
FormsModule, FormsModule,
EditorRoutingModule, EditorRoutingModule,
ClarityModule, ClarityModule,
HotTableModule, HotTableModule.forRoot(),
AppSharedModule, AppSharedModule,
DirectivesModule, DirectivesModule,
SharedModule, SharedModule,
+85
View File
@@ -0,0 +1,85 @@
@import '../../colors.scss';
::ng-deep body[cds-theme="dark"] {
.group-info {
background-color: $headerBackground;
border-color: $headerBackground;
}
.group-data {
background-color: $headerBackground;
border-color: $headerBackground;
}
.member-table tbody{
tr:hover{
background-color: #29404b;
}
}
}
::ng-deep body[cds-theme="light"] {
.group-info{
background-color: #f9f9f9;
border-color: #a7a7a7;
box-shadow: 0px 2px 5px #dad7d7;
}
.group-data {
background-color: #f9f9f9;
border-color: #a7a7a7;
box-shadow: 0px 2px 5px #dad7d7;
}
.member-table tbody{
tr:hover{
background-color: #e6e6e6;
}
}
}
.sidebar-height{
height: 100%;
}
.group-info-text{
display: inline;
font-size: 20px;
}
.group-info{
border: 1px solid;
border-radius: 3px;
}
.group-info td{
text-align: center;
}
.group-data{
border: 1px solid;
border-radius: 3px;
}
.group-data{
min-height: auto;
h3, h5{
text-align: center;
}
.member-table{
width: 100%;
}
.member-table tbody{
tr:hover{
cursor: pointer;
}
}
}
.table-container{
overflow: auto;
}
@media screen and (max-width: 768px){
.group-data{
min-height: unset !important;
}
}
+2 -4
View File
@@ -1,5 +1,5 @@
import { Location } from '@angular/common' import { Location } from '@angular/common'
import { Component, OnInit, ViewEncapsulation } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { SASjsConfig } from '@sasjs/adapter' import { SASjsConfig } from '@sasjs/adapter'
import { ServerType } from '@sasjs/utils/types/serverType' import { ServerType } from '@sasjs/utils/types/serverType'
@@ -14,9 +14,7 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
styleUrls: ['./group.component.scss'], styleUrls: ['./group.component.scss'],
host: { host: {
class: 'content-container' class: 'content-container'
}, }
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
+5 -5
View File
@@ -123,11 +123,11 @@
</div> </div>
<div *ngIf="!loading" class="no-table-selected"> <div *ngIf="!loading" class="no-table-selected">
<img <clr-icon
src="images/select-table.png" shape="warning-standard"
class="select-table-icon" size="60"
alt="select table icon" class="is-info icon-dc-fill"
/> ></clr-icon>
<p <p
*ngIf="treeNodeLibraries?.length! > 0" *ngIf="treeNodeLibraries?.length! > 0"
class="text-center color-gray mt-10" class="text-center color-gray mt-10"
+32
View File
@@ -0,0 +1,32 @@
clr-tree-node button {
white-space: nowrap;
}
.card-block {
height: 100%;
padding: 0;
}
.no-table-selected {
position: relative;
height: 100%;
}
::ng-deep {
// .badge.badge-info {
// background: #6a9235!important;
// color: #fff;
// }
clr-icon.is-blue, clr-icon.is-info {
fill: #6a9235;
}
}
.spinner-wrapper-fullpage {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
+2 -4
View File
@@ -3,7 +3,7 @@
* This software is released under MIT license. * This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project. * The full license information can be found in LICENSE in the root directory of this project.
*/ */
import { Component, AfterContentInit, ViewEncapsulation } from '@angular/core' import { Component, AfterContentInit } from '@angular/core'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { globals } from '../_globals' import { globals } from '../_globals'
@@ -18,9 +18,7 @@ import { LicenceService } from '../services/licence.service'
templateUrl: './home.component.html', templateUrl: './home.component.html',
host: { host: {
class: 'content-container' class: 'content-container'
}, }
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
@@ -0,0 +1,51 @@
:host {
height: calc(100% - 96px);
padding: 20px 20px;
}
.card {
margin-top: 0;
}
.key-error {
font-size: 16px;
}
.misskey {
color: #E74C3C;
}
.license-key-form, .activation-key-form {
padding: 0;
.clr-control-container {
width: 100%;
textarea {
width: 100%;
height: 170px;
max-height: 170px;
min-height: 170px;
resize: none;
}
}
}
.apply-keys {
height: 40px;
}
.drop-area {
display: flex;
justify-content: center;
align-items: center;
padding: 15px;
border: 2px dashed #b2b2b2;
border-radius: 4px;
cursor: pointer;
margin: 10px 0;
}
clr-tabs button {
box-shadow: none !important
}
@@ -1,4 +1,4 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core' import { Component, OnInit } 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'
@@ -14,9 +14,7 @@ enum LicenseActions {
@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,
standalone: false
}) })
export class LicensingComponent implements OnInit { export class LicensingComponent implements OnInit {
public action: LicenseActions | null = null public action: LicenseActions | null = null
@@ -53,7 +51,6 @@ export class LicensingComponent implements OnInit {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router,
private licenceService: LicenceService, private licenceService: LicenceService,
private sasService: SasService, private sasService: SasService,
private appService: AppService private appService: AppService
@@ -126,9 +123,7 @@ export class LicensingComponent implements OnInit {
res.adapterResponse.return[0] && res.adapterResponse.return[0] &&
res.adapterResponse.return[0].MSG === 'SUCCESS' res.adapterResponse.return[0].MSG === 'SUCCESS'
) { ) {
this.router.navigateByUrl('/').then(() => { location.replace(location.href.split('#')[0])
window.location.reload()
})
} }
}) })
.finally(() => { .finally(() => {
+14 -2
View File
@@ -239,7 +239,13 @@
<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 (click)="downloadPNG()" clrDropdownItem>PNG</div> <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
@@ -360,7 +366,13 @@
<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 (click)="renderToDownload('PNG')" clrDropdownItem>PNG</div> <div
*ngIf="!helperService.isMicrosoft"
(click)="renderToDownload('PNG')"
clrDropdownItem
>
PNG
</div>
<div (click)="downloadDot(); cancelRenderingGraph()" clrDropdownItem> <div (click)="downloadDot(); cancelRenderingGraph()" clrDropdownItem>
Dot Dot
</div> </div>
@@ -0,0 +1,85 @@
@import '../../colors.scss';
.toggle-switch input[type=checkbox]:checked+label:before {
border-color: $headerBackground;
background-color: $headerBackground !important;
transition: .15s ease-in;
transition-property: border-color,background-color;
}
#graph{
height: calc(100vh - 195px);
overflow: hidden;
text-align: center;
display: block;
width: 100%;
border: 1px solid #e4e4e4;
margin-top: 10px;
}
.selection-wrapper {
width: 100%;
max-width: 670px;
}
.column-active {
background: #d8e3e9;
color: black;
}
.content-area {
padding: 0.5rem !important;
.card {
min-height: calc(100vh - 120px);
.card-block {
padding: 0.5rem 0.35rem !important;
}
}
}
clr-tree-node button {
white-space: nowrap;
}
.btn-group.direction {
margin-left: var(--cds-global-space-6);
}
.graph-render-spinner {
position: absolute;
top: 0;
width: 100%;
display: flex;
justify-content: center;
margin-top: 10px;
}
.biglineage-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-footer {
p {
margin: 0;
}
}
.lineage-title-wrapper {
left: 12px;
}
.max-depth-input {
width: 100%;
}
@media (max-width: 768px) {
.toggle-switch-container {
margin-bottom: 20px;
}
}
+47 -17
View File
@@ -1,4 +1,4 @@
import { Component, ViewEncapsulation } from '@angular/core' import { Component } from '@angular/core'
import { Location } from '@angular/common' import { Location } from '@angular/common'
import { globals } from '../_globals' import { globals } from '../_globals'
import * as d3Viz from 'd3-graphviz' import * as d3Viz from 'd3-graphviz'
@@ -18,9 +18,7 @@ const moment = require('moment')
templateUrl: './lineage.component.html', templateUrl: './lineage.component.html',
host: { host: {
class: 'content-container' class: 'content-container'
}, }
encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class LineageComponent { export class LineageComponent {
public switchFlag: boolean = false public switchFlag: boolean = false
@@ -747,13 +745,28 @@ 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()
let downloadLink = document.createElement('a') if (navigator.appVersion.toString().indexOf('.NET') > 0) {
downloadLink.href = this.getSVGURL() window.navigator.msSaveBlob(this.getSVGBlob(), this.constructName('svg'))
downloadLink.download = this.constructName('svg') } else {
downloadLink.click() let downloadLink = document.createElement('a')
downloadLink.href = this.getSVGURL()
downloadLink.download = this.constructName('svg')
document.body.appendChild(downloadLink)
downloadLink.click()
document.body.removeChild(downloadLink)
}
} }
async downloadPNG() { async downloadPNG() {
@@ -781,11 +794,16 @@ 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' })
var url = window.URL.createObjectURL(blob) if (navigator.appVersion.toString().indexOf('.NET') > 0) {
a.href = url window.navigator.msSaveBlob(blob, this.constructName('csv'))
a.download = this.constructName('csv') } else {
a.click() var url = window.URL.createObjectURL(blob)
window.URL.revokeObjectURL(url) a.href = url
a.download = this.constructName('csv')
a.click()
window.URL.revokeObjectURL(url)
a.remove()
}
} }
private getDotUrl() { private getDotUrl() {
@@ -794,11 +812,23 @@ 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() {
let downloadLink = document.createElement('a') if (navigator.appVersion.toString().indexOf('.NET') > 0) {
downloadLink.href = this.getDotUrl() window.navigator.msSaveBlob(this.getDotBlob(), this.constructName('txt'))
downloadLink.download = this.constructName('txt') } else {
downloadLink.click() let downloadLink = document.createElement('a')
downloadLink.href = this.getDotUrl()
downloadLink.download = this.constructName('txt')
document.body.appendChild(downloadLink)
downloadLink.click()
document.body.removeChild(downloadLink)
}
} }
public showSvg() { public showSvg() {
@@ -0,0 +1,82 @@
::ng-deep body[cds-theme="dark"] {
.object-header:hover {
background-color: #405560;
}
}
::ng-deep body[cds-theme="light"] {
.objects-col {
background: white;
}
.object-header:hover {
background-color: #d8e3e9;
}
}
.objects-col{
height: 75vh;
overflow: scroll;
border: 1px solid #cccccc;
border-radius: 4px;
}
.cols-head {
border: 1px solid #cccccc;
padding: 10px;
display: flex;
}
.object-text {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-left: 10px;
flex: 1;
}
.repo-dropdown{
margin-right: 15px;
margin-left: 15px;
margin-bottom: 10px;
}
.clr-accordion-title{
width: 100%;
}
.float-right{
margin: 0px;
float: right;
}
.full-width{
width: 100%;
}
.object-uri{
margin: 0px;
margin-top: 5px;
}
.object-header{
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 3px;
padding-right: 3px;
}
.object-header:hover{
border-radius: 3px;
}
.datagrid-host{
display: unset !important;
}
.card {
margin-top: 0;
flex: 1;
display: flex;
flex-direction: column;
}
.content-area {
padding: 0.5rem !important;
display: flex;
flex-direction: column;
}
@@ -1,5 +1,5 @@
import { Location } from '@angular/common' import { Location } from '@angular/common'
import { Component, OnInit, ViewEncapsulation } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { ClrDatagridStringFilterInterface } from '@clr/angular' import { ClrDatagridStringFilterInterface } from '@clr/angular'
import { Observable, of } from 'rxjs' import { Observable, of } from 'rxjs'
@@ -50,9 +50,7 @@ class ValueFilter implements ClrDatagridStringFilterInterface<MetaData> {
styleUrls: ['./metadata.component.scss'], styleUrls: ['./metadata.component.scss'],
host: { host: {
class: 'content-container' class: 'content-container'
}, }
encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class MetadataComponent implements OnInit { export class MetadataComponent implements OnInit {
metaDataList: Array<any> | undefined metaDataList: Array<any> | undefined
@@ -100,11 +98,6 @@ export class MetadataComponent implements OnInit {
} }
this.pageSize = 5 this.pageSize = 5
// Initialize filters for accessibility
this.typeFilter = new TypeFilter()
this.nameFilter = new NameFilter()
this.valueFilter = new ValueFilter()
if ( if (
globals.metadata.metaDataList && globals.metadata.metaDataList &&
globals.metadata.metaRepositories && globals.metadata.metaRepositories &&
@@ -8,5 +8,4 @@ export interface Libinfo {
LIBID: string LIBID: string
LIBSIZE: number LIBSIZE: number
TABLE_CNT: number TABLE_CNT: number
CATALOG_CNT: number
} }
@@ -1,14 +1,30 @@
import { BaseSASResponse } from './common/BaseSASResponse' import { BaseSASResponse } from './common/BaseSASResponse'
export interface EditorsStageDataSASResponse extends BaseSASResponse { export interface EditorsStageDataSASResponse extends BaseSASResponse {
SYSDATE: string
SYSTIME: string
sasparams: Sasparam[] sasparams: Sasparam[]
_DEBUG: string
_PROGRAM: string
AUTOEXEC: string AUTOEXEC: string
MF_GETUSER: string
SYSCC: string
SYSENCODING: string SYSENCODING: string
SYSERRORTEXT: string
SYSHOSTINFOLONG: string SYSHOSTINFOLONG: string
SYSHOSTNAME: string
SYSPROCESSID: string SYSPROCESSID: string
SYSPROCESSMODE: string SYSPROCESSMODE: string
SYSPROCESSNAME: string SYSPROCESSNAME: string
SYSJOBID: string
SYSSCPL: string
SYSSITE: string
SYSTCPIPHOSTNAME: string SYSTCPIPHOSTNAME: string
SYSUSERID: string
SYSVLONG: string
SYSWARNINGTEXT: string
END_DTTM: string
MEMSIZE: string
} }
export interface Sasparam { export interface Sasparam {
@@ -1,28 +0,0 @@
import { BaseSASResponse } from './common/BaseSASResponse'
export interface PublicGetChangeinfo extends BaseSASResponse {
jsparams: Jsparam[]
}
export interface Jsparam {
TABLE_ID: string
SUBMIT_STATUS_CD: string
BASE_LIB: string
BASE_DS: string
SUBMITTED_BY_NM: string
SUBMITTED_ON: number
SUBMITTED_REASON_TXT: string
INPUT_OBS: number
INPUT_VARS: number
NUM_OF_APPROVALS_REQUIRED: number
NUM_OF_APPROVALS_REMAINING: number
REVIEWED_BY_NM: string
REVIEWED_ON?: any
TABLE_NM: string
BASE_TABLE: string
REVIEWED_ON_DTTM: string
SUBMITTED_ON_DTTM: string
LIB_ENGINE: string
ALLOW_RESTORE: string
REASON: string
}
@@ -127,7 +127,7 @@
class="no-table-selected pointer-events-none" class="no-table-selected pointer-events-none"
> >
<clr-icon <clr-icon
shape="upload-cloud" shape="warning-standard"
size="40" size="40"
class="is-info icon-dc-fill" class="is-info icon-dc-fill"
></clr-icon> ></clr-icon>
@@ -166,10 +166,13 @@
> >
<hot-table <hot-table
#hotInstanceUserDataset hotId="hotInstanceUserDataset"
id="hotTableUserDataset" id="hotTableUserDataset"
class="mt-15" class="mt-15"
[settings]="hotUserDatasetsSettings" [afterGetColHeader]="afterGetColHeader"
[settings]="hotUserDatasets"
[licenseKey]="hotTableLicenseKey"
stretchH="all"
> >
</hot-table> </hot-table>
@@ -357,10 +360,17 @@
</div> </div>
<hot-table <hot-table
#hotInstanceMain hotId="hotInstance"
id="hotTable" id="hotTable"
class="mt-15" class="mt-15"
[settings]="hotMainTableSettings" [afterGetColHeader]="afterGetColHeader"
[className]="['htDark', 'htCustomHidden']"
[licenseKey]="hotTableLicenseKey"
[multiColumnSorting]="true"
[viewportRowRenderingOffset]="50"
[manualColumnResize]="true"
[filters]="true"
stretchH="all"
> >
</hot-table> </hot-table>
</ng-container> </ng-container>
@@ -0,0 +1,54 @@
.no-table-selected {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
position: absolute;
background: var(--clr-vertical-nav-bg-color);
z-index: 10;
width: 100%;
height: 100%;
top: 0;
}
.header-row {
padding: 15px 0;
border-bottom: 1px solid #d3d3d3;
}
.dataset-input-wrapper {
max-width: 500px;
width: 100%;
textarea {
min-height: 200px;
height: 200px;
}
}
.submit-reason {
min-height: 70px;
max-height: 70px;
height: 70px;
}
.log-wrapper {
margin: 0 10px;
height: auto;
}
::ng-deep td.not-matched {
background-color: #ff000054;
}
.dataset-selection-actions {
border-top: 1px solid #d3d3d3;
}
.licence-limit-notice {
color: var(--cds-alias-status-warning-dark);
}
.submission-results {
border-bottom: 1px solid #d3d3d3;
}
@@ -4,9 +4,7 @@ import {
ElementRef, ElementRef,
HostBinding, HostBinding,
OnInit, OnInit,
AfterViewInit, ViewChild
ViewChild,
ViewEncapsulation
} from '@angular/core' } from '@angular/core'
import { import {
EventService, EventService,
@@ -23,7 +21,7 @@ import { HotTableInterface } from '../models/HotTable.interface'
import { Col } from '../shared/dc-validator/models/col.model' import { Col } from '../shared/dc-validator/models/col.model'
import { SpreadsheetService } from '../services/spreadsheet.service' import { SpreadsheetService } from '../services/spreadsheet.service'
import Handsontable from 'handsontable' import Handsontable from 'handsontable'
import { HotTableComponent } from '@handsontable/angular-wrapper' import { HotTableRegisterer } from '@handsontable/angular'
import { EditorsStageDataSASResponse } from '../models/sas/editors-stagedata.model' import { EditorsStageDataSASResponse } from '../models/sas/editors-stagedata.model'
import { CellChange, ChangeSource } from 'handsontable/common' import { CellChange, ChangeSource } from 'handsontable/common'
import { baseAfterGetColHeader } from '../shared/utils/hot.utils' import { baseAfterGetColHeader } from '../shared/utils/hot.utils'
@@ -47,11 +45,9 @@ enum FileLoadingState {
@Component({ @Component({
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,
standalone: false
}) })
export class MultiDatasetComponent implements OnInit, AfterViewInit { export class MultiDatasetComponent implements OnInit {
@HostBinding('class.content-container') contentContainerClass = true @HostBinding('class.content-container') contentContainerClass = true
@ViewChild('contentArea', { static: true }) contentAreaRef!: ElementRef @ViewChild('contentArea', { static: true }) contentAreaRef!: ElementRef
@@ -91,13 +87,7 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
public hotInstance!: Handsontable public hotInstance!: Handsontable
public hotInstanceUserDataset!: Handsontable public hotInstanceUserDataset!: Handsontable
@ViewChild('hotInstanceMain', { static: false }) private hotRegisterer: HotTableRegisterer
hotTableMainComponent!: HotTableComponent
@ViewChild('hotInstanceUserDataset', { static: false })
hotTableUserDatasetComponent!: HotTableComponent
public hotMainTableSettings: Handsontable.GridSettings = {}
public hotUserDatasetsSettings: Handsontable.GridSettings = {}
public showSubmitReasonModal: boolean = false public showSubmitReasonModal: boolean = false
public submitReasonMessage: string = '' public submitReasonMessage: string = ''
@@ -144,36 +134,7 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
} }
}, },
manualRowMove: true, manualRowMove: true,
columnSorting: true, columnSorting: true
afterGetColHeader: baseAfterGetColHeader,
stretchH: 'all'
}
private initializeHotSettings() {
this.hotMainTableSettings = {
className: ['htDark'],
licenseKey: this.hotTableLicenseKey,
multiColumnSorting: true,
viewportRowRenderingOffset: 50,
manualColumnResize: true,
autoColumnSize: true,
filters: true,
stretchH: 'all',
afterGetColHeader: baseAfterGetColHeader,
modifyColWidth: this.maxWidthCheker
}
// Exclude data from settings for HOT v16 - it will be loaded manually
const { data, ...settingsWithoutData } = this.hotUserDatasets
this.hotUserDatasetsSettings = {
...settingsWithoutData,
licenseKey: this.hotTableLicenseKey
}
}
public maxWidthCheker(width: any, col: any) {
if (width > 200) return 200
else return width
} }
public afterGetColHeader = baseAfterGetColHeader public afterGetColHeader = baseAfterGetColHeader
@@ -186,28 +147,16 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
private spreadsheetService: SpreadsheetService, private spreadsheetService: SpreadsheetService,
private sasService: SasService, private sasService: SasService,
private cdr: ChangeDetectorRef private cdr: ChangeDetectorRef
) {} ) {
this.hotRegisterer = new HotTableRegisterer()
}
ngOnInit() { ngOnInit() {
this.licenceService.hot_license_key.subscribe( this.licenceService.hot_license_key.subscribe(
(hot_license_key: string | undefined) => { (hot_license_key: string | undefined) => {
this.hotTableLicenseKey = hot_license_key this.hotTableLicenseKey = hot_license_key
this.initializeHotSettings()
} }
) )
this.initializeHotSettings()
}
ngAfterViewInit() {
// Ensure HOT instances are properly initialized after view is ready
setTimeout(() => {
if (this.hotTableUserDatasetComponent && !this.hotInstanceUserDataset) {
this.initUserInputHot()
}
if (this.hotTableMainComponent && !this.hotInstance) {
this.initHot()
}
}, 50)
} }
ngAfterContentInit(): void { ngAfterContentInit(): void {
@@ -282,10 +231,7 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
} }
this.initUserInputHot() this.initUserInputHot()
// Call onAutoDetectColumns after HOT is initialized this.onAutoDetectColumns()
setTimeout(() => {
this.onAutoDetectColumns()
}, 100)
} else if (matchedExtension === 'csv') { } else if (matchedExtension === 'csv') {
this.onMultiCsvFiles(event.target.files) this.onMultiCsvFiles(event.target.files)
} else { } else {
@@ -444,112 +390,84 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
initHot() { initHot() {
setTimeout(() => { setTimeout(() => {
if (this.hotTableMainComponent?.hotInstance) { this.hotInstance = this.hotRegisterer.getInstance('hotInstance')
this.hotInstance = this.hotTableMainComponent.hotInstance
// Set height of parsed data to full height of the page content area // Set height of parsed data to full height of the page content area
const contentAreaHeight = this.contentAreaRef.nativeElement.clientHeight const contentAreaHeight = this.contentAreaRef.nativeElement.clientHeight
const hotHeight = `${contentAreaHeight - 160}px` const hotHeight = `${contentAreaHeight - 160}px`
if (this.activeParsedDataset) { if (this.activeParsedDataset) {
// Update settings without data - data will be loaded manually this.hotInstance.updateSettings({
this.hotInstance.updateSettings({ data: this.activeParsedDataset.datasource || [],
colHeaders: this.activeParsedDataset.datasetInfo.headerColumns, colHeaders: this.activeParsedDataset.datasetInfo.headerColumns,
columns: columns: this.activeParsedDataset.datasetInfo.dcValidator?.getRules(),
this.activeParsedDataset.datasetInfo.dcValidator?.getRules(), readOnly: true,
readOnly: true, height: hotHeight || '300px',
height: hotHeight || '300px', className: 'htDark'
className: ['htDark'] })
})
// Trigger change detection to avoid ExpressionChangedAfterItHasBeenCheckedError
this.cdr.detectChanges()
// Load data manually - this is required for HOT v16 Angular wrapper
setTimeout(() => {
if (
this.activeParsedDataset &&
this.activeParsedDataset.datasource
) {
this.hotInstance.loadData(this.activeParsedDataset.datasource)
this.hotInstance.render()
}
}, 100)
}
} }
}, 100) })
} }
initUserInputHot() { initUserInputHot() {
setTimeout(() => { setTimeout(() => {
if (this.hotTableUserDatasetComponent?.hotInstance) { this.hotInstanceUserDataset = this.hotRegisterer.getInstance(
this.hotInstanceUserDataset = 'hotInstanceUserDataset'
this.hotTableUserDatasetComponent.hotInstance )
// Load initial data manually after instance is ready this.hotInstanceUserDataset.addHook(
setTimeout(() => { 'beforeChange',
if (this.hotUserDatasets.data) { (changes: (CellChange | null)[], source: ChangeSource) => {
this.hotInstanceUserDataset.loadData(this.hotUserDatasets.data) if (changes) {
this.hotInstanceUserDataset.render() for (let change of changes) {
} if (change && change[3]) {
}, 50) change[3] = change[3].toUpperCase()
this.hotInstanceUserDataset.addHook(
'beforeChange',
(changes: (CellChange | null)[], source: ChangeSource) => {
if (changes) {
for (let change of changes) {
if (change && change[3]) {
change[3] = change[3].toUpperCase()
}
} }
} }
} }
) }
)
this.hotInstanceUserDataset.addHook( this.hotInstanceUserDataset.addHook(
'afterChange', 'afterChange',
async (changes: CellChange[] | null, source: ChangeSource) => { async (changes: CellChange[] | null, source: ChangeSource) => {
if (changes) { if (changes) {
if (source === 'edit') { if (source === 'edit') {
await this.onUserInputDatasetsChange() await this.onUserInputDatasetsChange()
}
for (let change of changes) {
const row = change[0] as number
this.markUnmatchedRows(row)
}
this.dynamicCellValidations()
this.hotInstanceUserDataset.render()
} }
}
)
this.hotInstanceUserDataset.addHook( for (let change of changes) {
'afterRemoveRow', const row = change[0] as number
async (
index: number,
amount: number,
physicalRows: number[],
source?: Handsontable.ChangeSource | undefined
) => {
await this.onUserInputDatasetsChange()
for (let row of physicalRows) {
this.markUnmatchedRows(row) this.markUnmatchedRows(row)
} }
this.dynamicCellValidations()
this.hotInstanceUserDataset.render()
} }
) }
} )
}, 100)
this.hotInstanceUserDataset.addHook(
'afterRemoveRow',
async (
index: number,
amount: number,
physicalRows: number[],
source?: Handsontable.ChangeSource | undefined
) => {
await this.onUserInputDatasetsChange()
for (let row of physicalRows) {
this.markUnmatchedRows(row)
}
}
)
})
} }
dynamicCellValidations() { dynamicCellValidations() {
if (!this.hotInstanceUserDataset) return
const hotData = this.hotInstanceUserDataset.getData() const hotData = this.hotInstanceUserDataset.getData()
hotData.forEach((row, rowIndex) => { hotData.forEach((row, rowIndex) => {
@@ -563,8 +481,6 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
} }
markUnmatchedRows(row: number) { markUnmatchedRows(row: number) {
if (!this.hotInstanceUserDataset) return
const dataAtRow = this.hotInstanceUserDataset.getDataAtRow(row) as number[] const dataAtRow = this.hotInstanceUserDataset.getDataAtRow(row) as number[]
const dataset = `${dataAtRow[0]}.${dataAtRow[1]}` const dataset = `${dataAtRow[0]}.${dataAtRow[1]}`
const cellMetaAtRow = this.hotInstanceUserDataset.getCellMetaAtRow(row) const cellMetaAtRow = this.hotInstanceUserDataset.getCellMetaAtRow(row)
@@ -638,20 +554,6 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
* convention. {@link isValidDatasetFormat} * convention. {@link isValidDatasetFormat}
*/ */
async onAutoDetectColumns() { async onAutoDetectColumns() {
// Wait for hotInstanceUserDataset to be ready
if (!this.hotInstanceUserDataset) {
let attempts = 0
const maxAttempts = 20
while (!this.hotInstanceUserDataset && attempts < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, 100))
attempts++
}
if (!this.hotInstanceUserDataset) {
console.warn('hotInstanceUserDataset not ready after waiting')
return
}
}
let passwordError = false let passwordError = false
await this.parseExcelSheetNames() await this.parseExcelSheetNames()
@@ -712,13 +614,7 @@ export class MultiDatasetComponent implements OnInit, AfterViewInit {
} }
} }
if (this.hotInstanceUserDataset) { this.hotInstanceUserDataset.updateData(hotReadyData)
// Load data manually - this is required for HOT v16 Angular wrapper
setTimeout(() => {
this.hotInstanceUserDataset.loadData(hotReadyData)
this.hotInstanceUserDataset.render()
}, 100)
}
this.dynamicCellValidations() this.dynamicCellValidations()
} }
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { ClarityModule } from '@clr/angular' import { ClarityModule } from '@clr/angular'
import { HotTableModule } from '@handsontable/angular-wrapper' import { HotTableModule } from '@handsontable/angular'
import { registerAllModules } from 'handsontable/registry' import { registerAllModules } from 'handsontable/registry'
import { AppSharedModule } from '../app-shared.module' import { AppSharedModule } from '../app-shared.module'
import { DirectivesModule } from '../directives/directives.module' import { DirectivesModule } from '../directives/directives.module'
@@ -1,4 +1,4 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core' import { Component, OnInit } from '@angular/core'
@Component({ @Component({
selector: 'app-not-found', selector: 'app-not-found',
@@ -6,9 +6,7 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
styleUrls: ['./not-found.component.scss'], styleUrls: ['./not-found.component.scss'],
host: { host: {
class: 'content-container' class: 'content-container'
}, }
encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class NotFoundComponent implements OnInit { export class NotFoundComponent implements OnInit {
constructor() {} constructor() {}
+1 -2
View File
@@ -2,8 +2,7 @@ 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,8 +1,7 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import moment from 'moment' import * as 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 -2
View File
@@ -1,8 +1,7 @@
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 {
/** /**
+1 -2
View File
@@ -2,8 +2,7 @@ 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,8 +1,7 @@
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 -2
View File
@@ -1,8 +1,7 @@
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 {
+1 -2
View File
@@ -2,8 +2,7 @@ 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,8 +1,7 @@
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 -2
View File
@@ -1,8 +1,7 @@
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 {
+307
View File
@@ -0,0 +1,307 @@
::ng-deep {
body[cds-theme="dark"] {
.clause-logic {
background: #192a30;
}
.clause-query {
background: #263e48;
}
}
body[cds-theme="light"] {
.clause-logic {
background: #e9e9e9;
}
.clause-query {
background: #fbf8f8;
}
}
}
.content {
display: flex;
.clauses-container {
display: flex;
flex-direction: column;
.clause-logic {
display: flex;
justify-content: center;
flex-direction: column;
padding: 15px;
}
.clause-query {
padding: 30px 0px 20px 20px;
display: flex;
justify-content: center;
flex-direction: column;
position: relative;
& > .clr-row {
justify-content: space-between;
&:not(:last-child) {
padding-bottom: 15px;
margin-bottom: 15px;
border-bottom: 1px solid rgba(0, 0, 0, 0.16)
}
}
.remove-group-clause-button {
position: absolute;
top: 0px;
right: 10px;
color: gray;
}
.variable-col {
display: flex;
align-items: flex-start;
padding-bottom: 1px;
.datalist-wrapper {
width: 100%;
input {
width: 100%;
}
}
}
.operator-col {
display: flex;
align-items: flex-start;
clr-select-container {
height: 45px;
margin-top: 0;
width: 100%;
}
}
.value-col {
display: flex;
align-items: flex-start;
padding-bottom: 1px;
.checkbox-vals {
width: 100%;
padding: 0px 5px;
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
clr-checkbox-container {
margin-top: 0;
}
section {
max-height: 120px;
overflow-y: scroll;
}
}
.single-field-vals {
width: 100%;
::ng-deep {
.clr-control-container {
width: 100%;
.clr-input-wrapper {
max-width: none;
.clr-input-group {
width: 100%;
}
}
}
}
& > input {
width: 100%;
}
input[type=time] {
width: 100%;
padding-right: 17px;
}
}
.range-vals {
width: 100%;
::ng-deep {
.clr-control-container {
width: 100%;
.clr-input-wrapper {
max-width: none;
.clr-input-group {
width: 100%;
}
}
}
}
.from {
margin-bottom: 10px;
& > input {
width: 100%;
}
input[type=time] {
width: 100%;
padding-right: 17px;
}
}
.from, .to {
min-width: 100px;
& > input {
width: 100%;
}
input[type=time] {
width: 100%;
padding-right: 17px;
}
}
}
.contains-vals {
width: 100%;
::ng-deep {
.clr-control-container {
width: 100%;
.clr-input-wrapper {
max-width: none;
.clr-input-group {
width: 100%;
}
}
}
}
& > input {
width: 100%;
}
input[type=time] {
width: 100%;
padding-right: 17px;
}
}
}
.clause-buttons {
display: flex;
justify-content: space-around;
align-items: center;
flex-direction: row;
align-items: center;
button {
min-width: auto;
}
}
}
}
}
.invalid-clause {
border-left: 2px solid #d94b31;
}
.clause-row {
clr-icon {
margin: 0;
}
}
.clause-row:after {
position: relative;
content: "";
height: .41667rem;
width: .41667rem;
top: .29167rem;
right: .25rem;
background-image: url(data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org…%2C9.84%2C3.24a0.68%2C0.68%2C0%2C1%2C1%2C1%2C1Z%22%2F%3E%0A%3C%2Fsvg%3E%0A);
background-repeat: no-repeat;
background-size: contain;
vertical-align: middle;
margin: 0;
}
::ng-deep body[cds-theme="dark"] {
.line-numbers {
border-color: #989797 !important;
}
}
pre[class*="language-"] {
padding: 8px;
margin: 0;
border-radius: 1px;
display: flex;
justify-content: center;
align-items: center;
min-height: 66px;
position: relative;
span.spinner {
position: absolute;
left: 10px;
top: 10px;
}
code {
white-space: pre-wrap;
word-break: break-word;
}
}
.input-val {
border: 0px;
background: #fbf8f8;
border-bottom: 1px solid #999999;
}
clr-date-container {
margin-top: 2px !important;
}
input[type="time"] {
border: 0;
background: transparent;
border-bottom: 1px solid #b3b3b3;
&:focus {
outline: none;
}
}
.in-values-modal {
.modal-footer {
border-top: 1px solid #d8d8d8;
margin-top: 10px;
}
}
.progress, .progress-static {
background-color: transparent;
width: 100%;
height: 4px;
top: 3px;
}
+2 -5
View File
@@ -6,8 +6,7 @@ import {
OnDestroy, OnDestroy,
ChangeDetectorRef, ChangeDetectorRef,
LOCALE_ID, LOCALE_ID,
Input, Input
ViewEncapsulation
} from '@angular/core' } from '@angular/core'
import { SasStoreService } from '../services/sas-store.service' import { SasStoreService } from '../services/sas-store.service'
import { globals } from '../_globals' import { globals } from '../_globals'
@@ -28,9 +27,7 @@ registerLocaleData(localeEnGB)
selector: 'app-query', selector: 'app-query',
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,
standalone: false
}) })
export class QueryComponent export class QueryComponent
implements AfterViewInit, AfterContentInit, OnDestroy implements AfterViewInit, AfterContentInit, OnDestroy
@@ -0,0 +1,210 @@
@import '../../../colors.scss';
.loader {
display:flex;
justify-content: center;
height:75vh;
align-items:center;
flex-direction:column
}
.modalLarge {
width: 50rem!important;
}
.addedRow {
border: 1px solid rgba(9, 77, 117, 0.2);
border-radius: 5px;
}
.deletedRow {
border: 1px solid rgba(70, 71, 70, 0.2);
border-radius: 5px;
}
::ng-deep body[cds-theme="dark"] {
table {
.updatedRow {
background: #93971e;
}
.addedRow {
background: rgb(86 153 95);
}
.deletedRow {
background: rgb(138 90 90);
}
}
}
::ng-deep body[cds-theme="light"] {
table {
.updatedRow {
background: #fafda8;
}
.addedRow {
background: rgb(146, 208, 154);
}
.deletedRow {
background: rgb(230, 179, 179);
}
}
}
.updatedRow {
border: 1px solid rgba(9, 117, 9, 0.2);
border-radius: 5px;
}
.table {
border: 0px solid;
}
.ch {
background: rgba(0,0,0,.1);
border: 1px solid rgba(104, 100, 0, 0.4);
border-radius: 5px;
}
.ch:hover {
background: rgba(252, 135, 120, 0.4);
}
.tooltip .tooltip-content.tooltip-top-right, .tooltip.tooltip-top-right>.tooltip-content, .tooltip>.tooltip-content {
font-size: .54167rem;
font-weight: 400;
letter-spacing: normal;
background: $headerBackground;
border-radius: .125rem;
color: #f0f1ec;;
line-height: .75rem;
margin: 0;
padding: .375rem .5rem;
width: 235px;
position: absolute;
top: auto;
bottom: 100%;
left: 12px;
right: auto;
border-bottom-left-radius: 0;
margin-bottom: .66667rem;
}
.tooltip .tooltip-content.tooltip-top-right:before, .tooltip.tooltip-top-right>.tooltip-content:before, .tooltip>.tooltip-content:before {
position: absolute;
bottom: -.375rem;
left: 0;
top: auto;
right: auto;
content: "";
border-left: .25rem solid $headerBackground;
border-top: .20833rem solid $headerBackground;
border-right: .25rem solid transparent;
border-bottom: .20833rem solid transparent;
}
.table {
border: 0px solid;
}
.toggle-switch input[type=checkbox]:checked+label:before {
border-color: $headerBackground;
background-color: $headerBackground !important;
transition: .15s ease-in;
transition-property: border-color,background-color;
}
.tableCont {
overflow:auto;
margin: 15px 10px 10px 10px;
td {
word-break: break-word;
}
}
.approvalInfo {
display: flex;
justify-content: flex-end
}
.approvalBack {
display: flex;
justify-content: flex-start;
}
@media screen and (max-width:768px) {
.approvalInfo {
display: flex;
justify-content: center;
margin-top: 15px;
}
.approvalBack {
display: flex;
justify-content: center;
margin-bottom: 15px;
}
.card {
margin-top:0rem!important;
min-height: calc(100vh - 0px)!important;
}
.table td.left, .table th.left {
text-align: left;
width: 150px!important;
flex: 0
}
}
.table td.left, .table th.left {
text-align: left;
flex: 1;
width: 300px!important;
}
.tooll {
position: absolute;
background: #e6b3b3;
color: $headerBackground;
top: 0px;
height: 36px;
width: 100%;
left: 0px;
justify-content: center;
align-items: center;
display: flex;
}
#acceptBtn, #rejectBtn {
width: 175px
}
.formatted-values-toggle {
min-width: 75px
}
clr-modal {
::ng-deep {
.modal-body-wrapper {
overflow: auto;
}
}
}
.rows-notice {
display: flex;
align-items: center;
margin-right: 10px;
color: #6a6a6a;
font-size: 15px;
clr-icon {
margin: 0;
}
}
@@ -1,11 +1,6 @@
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { SasStoreService } from '../../services/sas-store.service' import { SasStoreService } from '../../services/sas-store.service'
import { import { Component, AfterViewInit, OnDestroy } from '@angular/core'
Component,
AfterViewInit,
OnDestroy,
ViewEncapsulation
} from '@angular/core'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { EventService } from '../../services/event.service' import { EventService } from '../../services/event.service'
@@ -13,8 +8,6 @@ import {
AuditorsPostdataSASResponse, AuditorsPostdataSASResponse,
Param Param
} from '../../models/sas/auditors-postdata.model' } from '../../models/sas/auditors-postdata.model'
import { PublicGetChangeinfo } from 'src/app/models/sas/public-getchangeinfo.model'
import { SasService } from 'src/app/services'
interface ChangesObj { interface ChangesObj {
ind: any ind: any
@@ -29,9 +22,7 @@ interface ChangesObj {
styleUrls: ['./approve-details.component.scss'], styleUrls: ['./approve-details.component.scss'],
host: { host: {
class: 'content-container' class: 'content-container'
}, }
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
@@ -79,11 +70,9 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
public diffsLimit: boolean = false public diffsLimit: boolean = false
public recordsLimit: number = 100 public recordsLimit: number = 100
public refreshStartupserviceAfterApprove: boolean = false
constructor( constructor(
private sasStoreService: SasStoreService, private sasStoreService: SasStoreService,
private sasService: SasService,
private eventService: EventService, private eventService: EventService,
private router: ActivatedRoute, private router: ActivatedRoute,
private route: Router private route: Router
@@ -167,9 +156,6 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
await this.sasStoreService await this.sasStoreService
.approveTable(approveParams, 'SASControlTable', 'auditors/postdata') .approveTable(approveParams, 'SASControlTable', 'auditors/postdata')
.then((res: any) => { .then((res: any) => {
// If we are approving MPE_TABLES we will arm the trigger for the reload of startup data to se the updated tables
if (this.refreshStartupserviceAfterApprove)
this.sasService.reloadStartupData()
this.route.navigateByUrl('/review/history') this.route.navigateByUrl('/review/history')
}) })
.catch((err: any) => { .catch((err: any) => {
@@ -184,7 +170,7 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
public async callChangesInfo(tableId: any) { public async callChangesInfo(tableId: any) {
await this.sasStoreService await this.sasStoreService
.getChangeInfo(tableId) .getChangeInfo(tableId)
.then((res: PublicGetChangeinfo) => { .then((res: any) => {
this.tableDetails = res.jsparams[0] this.tableDetails = res.jsparams[0]
this.jsParams = res.jsparams[0] this.jsParams = res.jsparams[0]
@@ -197,11 +183,6 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
} }
this.keysArray = keysArray this.keysArray = keysArray
// If we are approving MPE_TABLES we will arm the trigger for the reload of startup data to se the updated tables
// After user approved if armed, reload will be triggered
if (res.jsparams[0].BASE_DS === 'MPE_TABLES')
this.refreshStartupserviceAfterApprove = true
}) })
.catch((err: any) => { .catch((err: any) => {
this.acceptLoading = false this.acceptLoading = false
@@ -368,12 +349,13 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
this.params = param this.params = param
this.response = res this.response = res
this.calcDiff() this.calcDiff()
this.callChangesInfo(this.tableId)
}) })
.catch((err: any) => err) .catch((err: any) => err)
.finally(() => { .finally(() => {
this.loadingTable = true this.loadingTable = true
}) })
this.callChangesInfo(this.tableId)
} }
) )
if (typeof this.router.snapshot.params['tableId'] === 'undefined') { if (typeof this.router.snapshot.params['tableId'] === 'undefined') {
@@ -396,7 +378,6 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
this.params = param this.params = param
this.response = res this.response = res
this.calcDiff() this.calcDiff()
this.callChangesInfo(this.tableId)
}) })
.catch((err: any) => { .catch((err: any) => {
this.acceptLoading = false this.acceptLoading = false
@@ -405,6 +386,8 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
this.loadingTable = true this.loadingTable = true
this.setFocus() this.setFocus()
}) })
this.callChangesInfo(this.tableId)
} }
ngOnDestroy() { ngOnDestroy() {
@@ -33,34 +33,12 @@
<div class="clr-col-md-12" ng-if="loaded"> <div class="clr-col-md-12" ng-if="loaded">
<div *ngIf="approveList && remained !== 0"> <div *ngIf="approveList && remained !== 0">
<clr-datagrid class="datagrid-compact datagrid-custom-footer"> <clr-datagrid class="datagrid-compact datagrid-custom-footer">
<clr-dg-column [clrDgField]="'submitter'"> <clr-dg-column [clrDgField]="'submitter'">SUBMITTER</clr-dg-column>
SUBMITTER <clr-dg-column [clrDgField]="'baseTable'">BASE TABLE</clr-dg-column>
<clr-dg-string-filter <clr-dg-column [clrDgField]="'submitted'">SUBMITTED</clr-dg-column>
[clrDgStringFilter]="submitterFilter" <clr-dg-column [clrDgField]="'submitReason'"
aria-label="Filter submitter" >SUBMIT REASON</clr-dg-column
></clr-dg-string-filter> >
</clr-dg-column>
<clr-dg-column [clrDgField]="'baseTable'">
BASE TABLE
<clr-dg-string-filter
[clrDgStringFilter]="baseTableFilter"
aria-label="Filter base table"
></clr-dg-string-filter>
</clr-dg-column>
<clr-dg-column [clrDgField]="'submitted'">
SUBMITTED
<clr-dg-string-filter
[clrDgStringFilter]="submittedFilter"
aria-label="Filter submitted date"
></clr-dg-string-filter>
</clr-dg-column>
<clr-dg-column [clrDgField]="'submitReason'">
SUBMIT REASON
<clr-dg-string-filter
[clrDgStringFilter]="submitReasonFilter"
aria-label="Filter submit reason"
></clr-dg-string-filter>
</clr-dg-column>
<clr-dg-column>ACTION</clr-dg-column> <clr-dg-column>ACTION</clr-dg-column>
<clr-dg-column>DOWNLOAD</clr-dg-column> <clr-dg-column>DOWNLOAD</clr-dg-column>
@@ -73,19 +51,15 @@
<clr-dg-cell>{{ approveItem.submitReason }}</clr-dg-cell> <clr-dg-cell>{{ approveItem.submitReason }}</clr-dg-cell>
<clr-dg-cell> <clr-dg-cell>
<div <div
class="clr-row d-flex justify-content-around" class="clr-row"
role="toolbar" role="tooltip"
aria-label="Table actions" class="d-flex justify-content-around"
> >
<a <a
class="column-center links tooltip tooltip-md tooltip-bottom-left color-green" class="column-center links tooltip tooltip-md tooltip-bottom-left color-green"
(click)="getClicked(i)" (click)="getClicked(i)"
> >
<clr-icon <clr-icon shape="check" size="24"></clr-icon>
shape="check"
size="24"
aria-hidden="true"
></clr-icon>
<span class="tooltip-content">Go to review page screen</span> <span class="tooltip-content">Go to review page screen</span>
</a> </a>
<a <a
@@ -96,12 +70,10 @@
*ngIf="!approveItem.rejectLoading" *ngIf="!approveItem.rejectLoading"
shape="ban" shape="ban"
size="22" size="22"
aria-hidden="true"
></clr-icon> ></clr-icon>
<clr-spinner <clr-spinner
*ngIf="approveItem.rejectLoading" *ngIf="approveItem.rejectLoading"
[clrSmall]="true" [clrSmall]="true"
aria-hidden="true"
></clr-spinner> ></clr-spinner>
<span class="tooltip-content">Reject</span> <span class="tooltip-content">Reject</span>
</a> </a>
@@ -109,11 +81,7 @@
class="column-center links tooltip tooltip-md tooltip-bottom-left color-blue" class="column-center links tooltip tooltip-md tooltip-bottom-left color-blue"
(click)="getTable(approveItem.tableId)" (click)="getTable(approveItem.tableId)"
> >
<clr-icon <clr-icon shape="code" size="28"></clr-icon>
shape="code"
size="28"
aria-hidden="true"
></clr-icon>
<span class="tooltip-content">Go to staged data screen</span> <span class="tooltip-content">Go to staged data screen</span>
</a> </a>
</div> </div>
@@ -121,7 +89,6 @@
<clr-dg-cell class="p-0 d-flex justify-content-center"> <clr-dg-cell class="p-0 d-flex justify-content-center">
<button <button
class="btn btn-success" class="btn btn-success"
aria-label="Download audit file"
[id]="approveItem.tableId" [id]="approveItem.tableId"
(click)=" (click)="
download(approveItem.tableId); $event.stopPropagation() download(approveItem.tableId); $event.stopPropagation()
@@ -0,0 +1,43 @@
@import '../../../colors.scss';
.column-center {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.datagrid .datagrid-column .datagrid-column-title{
outline: none!important;
}
.links {
font-weight: 700;cursor: pointer;
}
.tooltip.tooltip-bottom-left>.tooltip-content, .tooltip .tooltip-content.tooltip-bottom-left {
background: $headerBackground !important;
}
.tooltip.tooltip-bottom-left>.tooltip-content:before, .tooltip .tooltip-content.tooltip-bottom-left:before {
border-right: .25rem solid $headerBackground;
border-bottom: .20833rem solid $headerBackground;
}
.noBorder {
border-bottom: 1px solid transparent!important;
}
.approvals-list-wrapper {
height: 70vh;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
.noapprovals-info-wrapper {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
height: calc(100vh - 200px);
}
@@ -1,14 +1,8 @@
import { import { Component, OnInit, ChangeDetectorRef } from '@angular/core'
Component,
OnInit,
ChangeDetectorRef,
ViewEncapsulation
} from '@angular/core'
import { SasStoreService } from '../../services/sas-store.service' import { SasStoreService } from '../../services/sas-store.service'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { SasService } from '../../services/sas.service' import { SasService } from '../../services/sas.service'
import { EventService } from '../../services/event.service' import { EventService } from '../../services/event.service'
import { ClrDatagridStringFilterInterface } from '@clr/angular'
interface ApproveData { interface ApproveData {
tableId: string tableId: string
@@ -20,39 +14,13 @@ interface ApproveData {
rejectLoading?: boolean rejectLoading?: boolean
} }
class SubmitterFilter implements ClrDatagridStringFilterInterface<ApproveData> {
accepts(data: ApproveData, search: string): boolean {
return data.submitter.toLowerCase().indexOf(search.toLowerCase()) >= 0
}
}
class BaseTableFilter implements ClrDatagridStringFilterInterface<ApproveData> {
accepts(data: ApproveData, search: string): boolean {
return data.baseTable.toLowerCase().indexOf(search.toLowerCase()) >= 0
}
}
class SubmittedFilter implements ClrDatagridStringFilterInterface<ApproveData> {
accepts(data: ApproveData, search: string): boolean {
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
}
}
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<ApproveData> {
accepts(data: ApproveData, search: string): boolean {
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
}
}
@Component({ @Component({
selector: 'app-approve', selector: 'app-approve',
templateUrl: './approve.component.html', templateUrl: './approve.component.html',
styleUrls: ['./approve.component.scss'], styleUrls: ['./approve.component.scss'],
host: { host: {
class: 'content-container' class: 'content-container'
}, }
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
@@ -61,12 +29,6 @@ export class ApproveComponent implements OnInit {
public loaded: boolean = false public loaded: boolean = false
public itemsNum: number = 10 public itemsNum: number = 10
// Filter instances for datagrid accessibility
public submitterFilter = new SubmitterFilter()
public baseTableFilter = new BaseTableFilter()
public submittedFilter = new SubmittedFilter()
public submitReasonFilter = new SubmitReasonFilter()
constructor( constructor(
private sasStoreService: SasStoreService, private sasStoreService: SasStoreService,
private eventService: EventService, private eventService: EventService,
@@ -24,19 +24,19 @@
<a <a
*ngIf="ind < 1" *ngIf="ind < 1"
(click)="getTable(approveData[col])" (click)="getTable(approveData[col])"
class="cursor-pointer table-link" class="cursor-pointer"
>{{ approveData[col] }}</a >{{ approveData[col] }}</a
> >
<div *ngIf="ind < 2 && ind >= 1"> <div *ngIf="ind < 2 && ind >= 1">
<a <a
(click)="getBaseTable(approveData[col])" (click)="getBaseTable(approveData[col])"
class="cursor-pointer table-link" class="cursor-pointer"
>VIEW</a >VIEW</a
> >
<span> / </span> <span> / </span>
<a <a
(click)="getEditTable(approveData[col])" (click)="getEditTable(approveData[col])"
class="cursor-pointer table-link" class="cursor-pointer"
>EDIT</a >EDIT</a
> >
</div> </div>
@@ -47,12 +47,7 @@
</table> </table>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button <button type="button" class="btn btn-outline" (click)="openModal = false">
type="button"
aria-label="Close modal"
class="btn btn-outline"
(click)="openModal = false"
>
OK OK
</button> </button>
</div> </div>
@@ -85,48 +80,16 @@
class="datagrid-history datagrid-custom-footer" class="datagrid-history datagrid-custom-footer"
*ngIf="loaded" *ngIf="loaded"
> >
<clr-dg-column [clrDgField]="'basetable'"> <clr-dg-column [clrDgField]="'basetable'">BASE_TABLE</clr-dg-column>
BASE_TABLE <clr-dg-column [clrDgField]="'status'">STATUS</clr-dg-column>
<clr-dg-string-filter <clr-dg-column [clrDgField]="'submitter'">SUBMITTER</clr-dg-column>
[clrDgStringFilter]="baseTableFilter" <clr-dg-column [clrDgField]="'submittedReason'"
aria-label="Filter base table" >SUBMIT REASON</clr-dg-column
></clr-dg-string-filter> >
</clr-dg-column> <clr-dg-column [clrDgField]="'submitted'">SUBMITTED</clr-dg-column>
<clr-dg-column [clrDgField]="'status'"> <clr-dg-column [clrDgField]="'reviewed'"
STATUS >APPROVED / REJECTED</clr-dg-column
<clr-dg-string-filter >
[clrDgStringFilter]="statusFilter"
aria-label="Filter status"
></clr-dg-string-filter>
</clr-dg-column>
<clr-dg-column [clrDgField]="'submitter'">
SUBMITTER
<clr-dg-string-filter
[clrDgStringFilter]="submitterFilter"
aria-label="Filter submitter"
></clr-dg-string-filter>
</clr-dg-column>
<clr-dg-column [clrDgField]="'submittedReason'">
SUBMIT REASON
<clr-dg-string-filter
[clrDgStringFilter]="submitReasonFilter"
aria-label="Filter submit reason"
></clr-dg-string-filter>
</clr-dg-column>
<clr-dg-column [clrDgField]="'submitted'">
SUBMITTED
<clr-dg-string-filter
[clrDgStringFilter]="submittedFilter"
aria-label="Filter submitted date"
></clr-dg-string-filter>
</clr-dg-column>
<clr-dg-column [clrDgField]="'reviewed'">
APPROVED / REJECTED
<clr-dg-string-filter
[clrDgStringFilter]="reviewedFilter"
aria-label="Filter reviewed date"
></clr-dg-string-filter>
</clr-dg-column>
<clr-dg-column>DOWNLOAD</clr-dg-column> <clr-dg-column>DOWNLOAD</clr-dg-column>
<clr-dg-row <clr-dg-row
@@ -156,7 +119,6 @@
<clr-dg-cell class="verCenter">{{ historyItem.reviewed }}</clr-dg-cell> <clr-dg-cell class="verCenter">{{ historyItem.reviewed }}</clr-dg-cell>
<clr-dg-cell class="verCenter p-0 d-flex justify-content-center"> <clr-dg-cell class="verCenter p-0 d-flex justify-content-center">
<button <button
aria-label="Download audit file"
class="btn btn-success" class="btn btn-success"
(click)="download(historyItem.tableId); $event.stopPropagation()" (click)="download(historyItem.tableId); $event.stopPropagation()"
> >
@@ -0,0 +1,37 @@
.rejected {
color: #f83126;
font-weight: bold
}
.accepted {
color: #3fc424;
font-weight: bold
}
.hsCell {
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
padding: 7px;
}
.btCell {
display: flex !important;
justify-content: center !important;
}
.verCenter {
display: flex;
align-items: center;
word-break: break-all;
}
.load-more {
input {
width: 90px;
}
}
#noDataContainer {
height: calc(100vh - 200px);
}

Some files were not shown because too many files have changed in this diff Show More