Compare commits
107 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ef1015f33b | ||
b43dfb5cf4 | |||
|
225e693d1f | ||
|
fda91770be | ||
d512876e0b | |||
|
2ba4b5383e | ||
ecc3184609 | |||
|
712b384848 | ||
|
26cdd73331 | ||
|
919aa6dcfe | ||
|
3bb3093b49 | ||
|
9c12250558 | ||
|
6c843f64fb | ||
|
3fda7dc5b0 | ||
|
22ec7f0340 | ||
|
378461dcbb | ||
|
905c7b9d3c | ||
|
670ec2c71c | ||
|
b419cd5078 | ||
|
b1db4ea590 | ||
822ddb1274 | |||
|
672dd6d4f1 | ||
|
b3ac73d903 | ||
|
f8554dd5e7 | ||
88679c0c9a | |||
|
a08a717ca8 | ||
c8b6fdbfdb | |||
|
b495c41626 | ||
|
7f4be474c6 | ||
7f6f68fcbb | |||
|
03fd7db033 | ||
|
9dc5c66f7b | ||
|
aa1b08632e | ||
|
6bbe354c9e | ||
|
8ff429793b | ||
|
70d010127a | ||
|
696717c509 | ||
|
71c308d052 | ||
|
bed5b320ad | ||
b0e827412e | |||
|
63e9af402e | ||
fd55105f62 | |||
|
c2e3b362e7 | ||
|
5d2d60d040 | ||
0e59f5406f | |||
|
e7cb471c0b | ||
|
0465089207 | ||
|
4f2f59907c | ||
|
7d85328d41 | ||
f2a9329196 | |||
|
7a8231615c | ||
|
0db6b25327 | ||
e91f6f01a6 | |||
|
0b4042af60 | ||
|
519d8953b5 | ||
14a616fc1b | |||
bfe5a8626f | |||
|
4ecd186e5c | ||
|
8c60473c15 | ||
bb126eba5b | |||
|
d1998422d2 | ||
|
69f687a85f | ||
|
2aa19d1dca | ||
|
e44a25dcc3 | ||
|
efb5ffa906 | ||
|
b4c586a859 | ||
|
e874143a95 | ||
|
e4dbab8b16 | ||
|
f6d7d6f90c | ||
|
063c90caf4 | ||
|
2011c2eee7 | ||
|
24545f2acd | ||
a7c81245ff | |||
|
4f2c993b2d | ||
|
e5f8e500c1 | ||
|
a61e2de140 | ||
881d2b060e | |||
|
4830c6d219 | ||
|
4c3c9ac88c | ||
7e1c610a4d | |||
|
8139f495ce | ||
|
89ab296151 | ||
|
a0dc92c403 | ||
|
86134f478a | ||
|
9a2addc18e | ||
9264ce2a60 | |||
|
cbd69df708 | ||
|
ca7caa25b6 | ||
c10330627f | |||
d80c59afce | |||
|
abdbb67471 | ||
|
037a97b6ff | ||
|
a0a529ad38 | ||
|
72239558af | ||
|
d2097ad6dd | ||
1bd542cddb | |||
fb1c1ee874 | |||
|
624a7a8f37 | ||
|
381378f532 | ||
af05486c0e | |||
|
4b558948d9 | ||
|
d9cff42f5c | ||
2a3d2b8d0d | |||
|
d0f453d291 | ||
|
8e65dd0eae | ||
|
da4d0b28c7 | ||
|
6c96ef7fb0 |
@@ -1,11 +1,23 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Avoid commits to the master branch
|
||||
BRANCH=`git rev-parse --abbrev-ref HEAD`
|
||||
REGEX="^(master|development)$"
|
||||
# Using `--silent` helps for showing any errs in the first line of the response
|
||||
# The first line is picked up by the VS Code GIT UI popup when rc is not 0
|
||||
|
||||
if [[ "$BRANCH" =~ $REGEX ]]; then
|
||||
echo "You are on branch $BRANCH. Are you sure you want to commit to this branch?"
|
||||
echo "If so, commit with -n to bypass the pre-commit hook."
|
||||
exit 1
|
||||
if npm run --silent lint:check:silent ; then
|
||||
exit 0
|
||||
else
|
||||
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
|
||||
)
|
@@ -10,7 +10,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.14.0
|
||||
node-version: 20.15.1
|
||||
|
||||
- name: Install Google Chrome
|
||||
run: |
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.14.0
|
||||
node-version: 20.15.1
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: |
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
- run: apt install -y ./google-chrome*.deb;
|
||||
- run: export CHROME_BIN=/usr/bin/google-chrome
|
||||
- run: apt-get update -y
|
||||
- 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-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb
|
||||
- run: apt -y install jq
|
||||
|
||||
- name: Write cypress credentials
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
||||
cat ./cypress.config.ts
|
||||
# Start frontend and run cypress
|
||||
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"
|
||||
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"
|
||||
|
||||
- name: Zip Cypress videos
|
||||
if: always()
|
||||
|
101
.gitea/workflows/lighthouse.yaml
Normal file
101
.gitea/workflows/lighthouse.yaml
Normal file
@@ -0,0 +1,101 @@
|
||||
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: [20.15.1]
|
||||
|
||||
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
|
@@ -80,7 +80,7 @@ jobs:
|
||||
- run: apt install -y ./google-chrome*.deb;
|
||||
- run: export CHROME_BIN=/usr/bin/google-chrome
|
||||
- run: apt-get update -y
|
||||
- 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-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb
|
||||
- run: apt -y install jq
|
||||
|
||||
- name: Write cypress credentials
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
||||
cat ./cypress.config.ts
|
||||
# Start frontend and run cypress
|
||||
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"
|
||||
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"
|
||||
|
||||
- name: Zip Cypress videos
|
||||
if: always()
|
||||
@@ -237,20 +237,20 @@ jobs:
|
||||
cd sas
|
||||
sasjs c -t viya
|
||||
rm -rf sasjsbuild/tests
|
||||
sed -i -e 's/servertype="SASJS"/servertype="SASVIYA"/g' sasjsbuild/services/clickme.html
|
||||
sasjs b -t viya
|
||||
cp sasjsbuild/viya.sas ./demostream_viya.sas
|
||||
# compile Viya Full deploy (without web)
|
||||
rm -rf sasjsbuild/services/web
|
||||
rm sasjsbuild/services/clickme.html
|
||||
sed -i -e 's/servertype="SASJS"/servertype="SASVIYA"/g' sasjsbuild/services/DC.html
|
||||
sasjs b -t viya
|
||||
cp sasjsbuild/viya.sas ./viya.sas
|
||||
cp sasjsbuild/viya.json ./viya.json
|
||||
# compile Viya Full deploy (without web)
|
||||
rm -rf sasjsbuild/services/web
|
||||
rm sasjsbuild/services/DC.html
|
||||
sasjs b -t viya
|
||||
cp sasjsbuild/viya.sas ./viya_noweb.sas
|
||||
cp sasjsbuild/viya.json ./viya_noweb.json
|
||||
|
||||
- name: Zip Frontend (including viya.json for full viya deploy)
|
||||
run: |
|
||||
cd sas
|
||||
cp sasjsbuild/viya.json ../client/dist
|
||||
cp sasjsbuild/viya.json ../client/dist/viya.json
|
||||
cd ..
|
||||
zip -r frontend.zip ./client/dist
|
||||
|
||||
@@ -277,8 +277,8 @@ jobs:
|
||||
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=@sas/demostream_sas9.sas
|
||||
curl -k $URL -F attachment=@sas/demostream_viya.sas
|
||||
curl -k $URL -F attachment=@sas/viya.sas
|
||||
curl -k $URL -F attachment=@sas/sasjs_server.json.zip
|
||||
curl -k $URL -F attachment=@sas/sas9.sas
|
||||
curl -k $URL -F attachment=@sas/viya.sas
|
||||
curl -k $URL -F attachment=@sas/viya.json
|
||||
curl -k $URL -F attachment=@sas/viya_noweb.sas
|
||||
curl -k $URL -F attachment=@sas/viya_noweb.json
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,3 +21,4 @@ sasjsresults
|
||||
.sasjsrc
|
||||
client/.npmrc
|
||||
*~
|
||||
.lighthouseci
|
145
CHANGELOG.md
145
CHANGELOG.md
@@ -1,3 +1,148 @@
|
||||
## [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)
|
||||
|
||||
|
||||
|
34
README.md
34
README.md
@@ -23,10 +23,42 @@ _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).
|
||||
|
||||
For more information:
|
||||
An individual Viya deploy can be done in just 2 lines of #SAS code!
|
||||
|
||||
```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
|
||||
* Docs: https://docs.datacontroller.io
|
||||
* Code: https://code.datacontroller.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,27 +32,26 @@ context('excel tests: ', function () {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('regular.csv', () => {
|
||||
cy.get('#approval-btn', { 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')
|
||||
// .then((data) => {
|
||||
// let cell: any = data[0].children[0].children[1]
|
||||
// expect(cell.innerText).to.equal('0')
|
||||
// cell = data[0].children[0].children[2]
|
||||
// expect(cell.innerText).to.equal('44')
|
||||
// cell = data[0].children[0].children[3]
|
||||
// expect(cell.innerText).to.equal('abc')
|
||||
// cell = data[0].children[0].children[6]
|
||||
// expect(cell.innerText).to.equal('Option abc')
|
||||
// })
|
||||
// })
|
||||
cy.get('#approval-btn', { 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')
|
||||
// .then((data) => {
|
||||
// let cell: any = data[0].children[0].children[1]
|
||||
// expect(cell.innerText).to.equal('0')
|
||||
// cell = data[0].children[0].children[2]
|
||||
// expect(cell.innerText).to.equal('44')
|
||||
// cell = data[0].children[0].children[3]
|
||||
// expect(cell.innerText).to.equal('abc')
|
||||
// cell = data[0].children[0].children[6]
|
||||
// expect(cell.innerText).to.equal('Option abc')
|
||||
// })
|
||||
// })
|
||||
})
|
||||
})
|
||||
|
||||
|
@@ -217,11 +217,7 @@ const rejectExcel = (callback?: any) => {
|
||||
.should('contain', 'Approve')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('approve')
|
||||
) {
|
||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
|
@@ -34,93 +34,162 @@ context('excel multi load tests: ', function () {
|
||||
|
||||
it('1 | Uploads Excel file with multiple sheets, 3 sheets including data, 2 sheets matched with dataset', (done) => {
|
||||
attachExcelFile('multi_load_test_2.xlsx', () => {
|
||||
checkHotUserDatasetTable('hotTableUserDataset', [
|
||||
[library, mpeXTestTable],
|
||||
[library, mpeTablesTable]
|
||||
], () => {
|
||||
cy.get('#continue-btn').trigger('click').then(() => {
|
||||
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()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
checkHotUserDatasetTable(
|
||||
'hotTableUserDataset',
|
||||
[
|
||||
[library, mpeXTestTable],
|
||||
[library, mpeTablesTable]
|
||||
],
|
||||
() => {
|
||||
cy.get('#continue-btn')
|
||||
.trigger('click')
|
||||
.then(() => {
|
||||
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) => {
|
||||
attachExcelFile('multi_load_test_1.xlsx', () => {
|
||||
checkHotUserDatasetTable('hotTableUserDataset', [
|
||||
[library, mpeXTestTable],
|
||||
[library, mpeTablesTable]
|
||||
], () => {
|
||||
cy.get('#continue-btn').trigger('click').then(() => {
|
||||
checkIfTreeHasTables([`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], `${library}.${mpeXTestTable}`, (includes: boolean) => {
|
||||
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()
|
||||
checkHotUserDatasetTable(
|
||||
'hotTableUserDataset',
|
||||
[
|
||||
[library, mpeXTestTable],
|
||||
[library, mpeTablesTable]
|
||||
],
|
||||
() => {
|
||||
cy.get('#continue-btn')
|
||||
.trigger('click')
|
||||
.then(() => {
|
||||
checkIfTreeHasTables(
|
||||
[`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`],
|
||||
`${library}.${mpeXTestTable}`,
|
||||
(includes: boolean) => {
|
||||
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) => {
|
||||
if (valid) done()
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
hasSuccessSubmits(2, (valid: boolean) => {
|
||||
if (valid) done()
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('3 | Uploads Excel file with multiple sheets, 1 sheets has 2 tables', (done) => {
|
||||
attachExcelFile('multi_load_test_1.xlsx', () => {
|
||||
checkHotUserDatasetTable('hotTableUserDataset', [
|
||||
[library, mpeXTestTable],
|
||||
[library, mpeTablesTable]
|
||||
], () => {
|
||||
cy.get('#continue-btn').trigger('click').then(() => {
|
||||
checkIfTreeHasTables([`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], `${library}.${mpeXTestTable}`, (includes: boolean) => {
|
||||
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']
|
||||
], () => {
|
||||
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()
|
||||
checkHotUserDatasetTable(
|
||||
'hotTableUserDataset',
|
||||
[
|
||||
[library, mpeXTestTable],
|
||||
[library, mpeTablesTable]
|
||||
],
|
||||
() => {
|
||||
cy.get('#continue-btn')
|
||||
.trigger('click')
|
||||
.then(() => {
|
||||
checkIfTreeHasTables(
|
||||
[`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`],
|
||||
`${library}.${mpeXTestTable}`,
|
||||
(includes: boolean) => {
|
||||
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'
|
||||
]
|
||||
],
|
||||
() => {
|
||||
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(2, (valid: boolean) => {
|
||||
if (valid) done()
|
||||
})
|
||||
|
||||
})
|
||||
hasSuccessSubmits(
|
||||
2,
|
||||
(valid: boolean) => {
|
||||
if (valid) done()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -142,25 +211,31 @@ const attachExcelFile = (excelFilename: string, callback?: any) => {
|
||||
})
|
||||
}
|
||||
|
||||
const checkHotUserDatasetTable = (hotId: string, dataToContain: any[][], callback?: () => void) => {
|
||||
const checkHotUserDatasetTable = (
|
||||
hotId: string,
|
||||
dataToContain: any[][],
|
||||
callback?: () => void
|
||||
) => {
|
||||
cy.get(`#${hotId}`, { timeout: longerCommandTimeout })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
.find('div.wtSpreader')
|
||||
.find('table.htCore')
|
||||
.find('tbody')
|
||||
.then((data) => {
|
||||
cy.wait(2000).then(() => {
|
||||
for (let rowI = 0; rowI < dataToContain.length; rowI++) {
|
||||
for (let colI = 0; colI < dataToContain[rowI].length; colI++) {
|
||||
expect(data[0].children[rowI].children[colI]).to.contain(dataToContain[rowI][colI])
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
.find('div.wtSpreader')
|
||||
.find('table.htCore')
|
||||
.find('tbody')
|
||||
.then((data) => {
|
||||
cy.wait(2000).then(() => {
|
||||
for (let rowI = 0; rowI < dataToContain.length; rowI++) {
|
||||
for (let colI = 0; colI < dataToContain[rowI].length; colI++) {
|
||||
expect(data[0].children[rowI].children[colI]).to.contain(
|
||||
dataToContain[rowI][colI]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (callback) callback()
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const clickOnTreeNode = (clickOnNode: string, callback?: () => void) => {
|
||||
@@ -174,7 +249,11 @@ const clickOnTreeNode = (clickOnNode: string, callback?: () => void) => {
|
||||
})
|
||||
}
|
||||
|
||||
const checkIfTreeHasTables = (tables: string[], clickOnNode?: string, callback?: (includes: boolean) => void) => {
|
||||
const checkIfTreeHasTables = (
|
||||
tables: string[],
|
||||
clickOnNode?: string,
|
||||
callback?: (includes: boolean) => void
|
||||
) => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node').then((treeNodes: any) => {
|
||||
let datasets = tables
|
||||
let nodesCorrect = true
|
||||
@@ -207,16 +286,26 @@ const submitTables = () => {
|
||||
cy.wait(1000)
|
||||
}
|
||||
|
||||
const hasSuccessSubmits = (expectedNoOfSubmits: number, callback: (valid: boolean) => void) => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="success"]').should('be.visible').then(($nodes) => {
|
||||
callback(expectedNoOfSubmits === $nodes.length)
|
||||
})
|
||||
const hasSuccessSubmits = (
|
||||
expectedNoOfSubmits: number,
|
||||
callback: (valid: boolean) => void
|
||||
) => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="success"]')
|
||||
.should('be.visible')
|
||||
.then(($nodes) => {
|
||||
callback(expectedNoOfSubmits === $nodes.length)
|
||||
})
|
||||
}
|
||||
|
||||
const hasErrorTables = (expectedNoOfErrors: number, callback: (valid: boolean) => void) => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="danger"]').should('be.visible').then(($nodes) => {
|
||||
callback(expectedNoOfErrors === $nodes.length)
|
||||
})
|
||||
const hasErrorTables = (
|
||||
expectedNoOfErrors: number,
|
||||
callback: (valid: boolean) => void
|
||||
) => {
|
||||
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) => {
|
||||
|
@@ -234,7 +234,7 @@ context('excel tests: ', function () {
|
||||
cy.get('.btn-upload-preview', { timeout: 60000 })
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
cy.get('#hotInstance', { timeout: 30000 })
|
||||
cy.get('#hotTable', { timeout: 30000 })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
@@ -283,7 +283,7 @@ context('excel tests: ', function () {
|
||||
cy.get('.btn-upload-preview', { timeout: 60000 })
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
cy.get('#hotInstance', { timeout: 30000 })
|
||||
cy.get('#hotTable', { timeout: 30000 })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
@@ -399,11 +399,7 @@ const rejectExcel = (callback?: any) => {
|
||||
.should('contain', 'Approve')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('approve')
|
||||
) {
|
||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
@@ -432,11 +428,7 @@ const acceptExcel = (callback?: any) => {
|
||||
.should('contain', 'Approve')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('approve')
|
||||
) {
|
||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
@@ -455,7 +447,7 @@ const acceptExcel = (callback?: any) => {
|
||||
}
|
||||
|
||||
const checkResultOfFormulaUpload = (callback?: any) => {
|
||||
cy.get('#hotInstance', { timeout: longerCommandTimeout })
|
||||
cy.get('#hotTable', { timeout: longerCommandTimeout })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
@@ -471,7 +463,7 @@ const checkResultOfFormulaUpload = (callback?: any) => {
|
||||
|
||||
const checkResultOfXLSUpload = (callback?: any) => {
|
||||
cy.viewport(1280, 720)
|
||||
cy.get('#hotInstance', { timeout: 30000 })
|
||||
cy.get('#hotTable', { timeout: 30000 })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
@@ -500,7 +492,7 @@ const checkResultOfXLSUpload = (callback?: any) => {
|
||||
if (callback) callback()
|
||||
})
|
||||
|
||||
cy.get('#hotInstance', { timeout: 30000 })
|
||||
cy.get('#hotTable', { timeout: 30000 })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.scrollTo('right')
|
||||
|
@@ -16,7 +16,6 @@ context('filtering tests: ', function () {
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation, { timeout: longerCommandTimeout })
|
||||
|
||||
|
||||
visitPage('home')
|
||||
})
|
||||
|
||||
@@ -299,14 +298,16 @@ const setFilterWithValue = (
|
||||
cy.get('.no-values')
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.in-values-modal clr-checkbox-wrapper input').then((inputs: any) => {
|
||||
inputs[0].click()
|
||||
cy.get('.in-values-modal .modal-footer button').click()
|
||||
cy.get('.in-values-modal 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()
|
||||
cy.get('.modal-footer .btn-success-outline').click()
|
||||
|
||||
if (callback) callback()
|
||||
})
|
||||
if (callback) callback()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
@@ -23,7 +23,6 @@ interface EditConfigTableCells {
|
||||
|
||||
context('licensing tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
|
||||
cy.loginAndUpdateValidKey()
|
||||
})
|
||||
|
||||
@@ -371,8 +370,6 @@ context('licensing tests: ', function () {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
|
||||
const logout = (callback?: any) => {
|
||||
@@ -697,11 +694,7 @@ const approveTable = (callback?: any) => {
|
||||
.should('contain', 'Approve')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('approve')
|
||||
) {
|
||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
|
@@ -18,7 +18,6 @@ context('liveness tests: ', function () {
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
|
||||
|
||||
visitPage('home')
|
||||
})
|
||||
|
||||
@@ -125,11 +124,7 @@ const rejectExcel = (callback?: any) => {
|
||||
.should('contain', 'Approve')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('approve')
|
||||
) {
|
||||
if (approvalButton.innerText.toLowerCase().includes('approve')) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
|
@@ -76,7 +76,8 @@ context('editor tests: ', function () {
|
||||
cy.get('.viewbox-open').click()
|
||||
openTableFromViewboxTree(
|
||||
libraryToOpenIncludes,
|
||||
viewboxes.map((viewbox) => viewbox.viewbox_table))
|
||||
viewboxes.map((viewbox) => viewbox.viewbox_table)
|
||||
)
|
||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
||||
let found = 0
|
||||
|
||||
@@ -91,32 +92,34 @@ context('editor tests: ', function () {
|
||||
|
||||
if (found < viewboxes.length) return
|
||||
|
||||
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).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()
|
||||
}
|
||||
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).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()
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -395,11 +398,13 @@ context('editor tests: ', function () {
|
||||
})
|
||||
|
||||
const removeAllColumns = () => {
|
||||
cy.get('.configuration-wrapper clr-icon[shape="trash"]').then(removeNodes => {
|
||||
for (let removeNode of removeNodes) {
|
||||
removeNode.click()
|
||||
cy.get('.configuration-wrapper clr-icon[shape="trash"]').then(
|
||||
(removeNodes) => {
|
||||
for (let removeNode of removeNodes) {
|
||||
removeNode.click()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const checkColumns = (columns: string[], callback: () => void) => {
|
||||
@@ -412,7 +417,7 @@ const checkColumns = (columns: string[], callback: () => void) => {
|
||||
console.log('viewboxColNode', viewboxColNodes)
|
||||
console.log('columns', columns)
|
||||
for (let i = 0; i < viewboxColNodes.length; i++) {
|
||||
const col = columns[i]|| ''
|
||||
const col = columns[i] || ''
|
||||
const colNode = viewboxColNodes[i]
|
||||
|
||||
if (
|
||||
|
@@ -1,255 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
@@ -1,246 +0,0 @@
|
||||
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}`)
|
||||
}
|
@@ -1,527 +0,0 @@
|
||||
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;')
|
||||
}
|
@@ -1,376 +0,0 @@
|
||||
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}`)
|
||||
}
|
@@ -1,719 +0,0 @@
|
||||
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}`)
|
||||
}
|
@@ -1,149 +0,0 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
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}`)
|
||||
}
|
@@ -1,624 +0,0 @@
|
||||
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}`)
|
||||
}
|
@@ -10,7 +10,7 @@ const check = (cwd) => {
|
||||
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;',
|
||||
excludePackages:
|
||||
'@cds/city@1.1.0;@handsontable/angular@15.3.0;handsontable@15.3.0;hyperformula@2.7.1;hyperformula@3.0.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1'
|
||||
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;handsontable@16.0.1;hyperformula@2.7.1;hyperformula@3.0.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1'
|
||||
},
|
||||
(error, json) => {
|
||||
if (error) {
|
||||
|
42
client/lighthouserc.js
Normal file
42
client/lighthouserc.js
Normal file
@@ -0,0 +1,42 @@
|
||||
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' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3140
client/package-lock.json
generated
3140
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,8 @@
|
||||
"sasdocs": "sasjs doc && ./sasjs/utils/deploydocs.sh",
|
||||
"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:serve": "compodoc -s --name 'Data Controller Client'"
|
||||
"compodoc:serve": "compodoc -s --name 'Data Controller Client'",
|
||||
"lighthouse": "lhci autorun"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@@ -48,8 +49,8 @@
|
||||
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
|
||||
"@clr/icons": "^13.0.2",
|
||||
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
|
||||
"@handsontable/angular": "^15.3.0",
|
||||
"@sasjs/adapter": "^4.11.0",
|
||||
"@handsontable/angular-wrapper": "16.0.1",
|
||||
"@sasjs/adapter": "^4.12.2",
|
||||
"@sasjs/utils": "^3.4.0",
|
||||
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
||||
"@types/d3-graphviz": "^2.6.7",
|
||||
@@ -60,7 +61,7 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"d3-graphviz": "^5.0.2",
|
||||
"fs-extra": "^7.0.1",
|
||||
"handsontable": "^15.3.0",
|
||||
"handsontable": "^16.0.1",
|
||||
"https-browserify": "1.0.0",
|
||||
"hyperformula": "^2.5.0",
|
||||
"iconv-lite": "^0.5.0",
|
||||
@@ -95,6 +96,7 @@
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"@compodoc/compodoc": "^1.1.21",
|
||||
"@cypress/webpack-preprocessor": "^5.17.1",
|
||||
"@lhci/cli": "^0.12.0",
|
||||
"@types/core-js": "^2.5.5",
|
||||
"@types/crypto-js": "^4.2.1",
|
||||
"@types/es6-shim": "^0.31.39",
|
||||
|
@@ -98,14 +98,14 @@
|
||||
|
||||
<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="clr-input-wrapper">
|
||||
<div class="clr-input-wrapper small-mt">
|
||||
<input clrInput name="dcloc" [(ngModel)]="dcPath" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="dcloc" class="mt-20 clr-control-label">SAS Admin group</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<div class="clr-input-wrapper small-mt">
|
||||
<select
|
||||
*ngIf="!adminGroupsLoading"
|
||||
clrSelect
|
||||
@@ -124,6 +124,42 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="computeContext" class="mt-20 clr-control-label"
|
||||
>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>
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
} from '@angular/core'
|
||||
import SASjs, { SASjsConfig } from '@sasjs/adapter'
|
||||
import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings'
|
||||
import { HelperService } from 'src/app/services'
|
||||
import { DeployService } from 'src/app/services/deploy.service'
|
||||
import { EventService } from 'src/app/services/event.service'
|
||||
import { LoggerService } from 'src/app/services/logger.service'
|
||||
@@ -18,6 +19,11 @@ 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({
|
||||
selector: 'app-automatic-deploy',
|
||||
@@ -35,6 +41,7 @@ export class AutomaticComponent implements OnInit {
|
||||
|
||||
@Output() onNavigateToHome: EventEmitter<any> = new EventEmitter<any>()
|
||||
|
||||
public selectedComputeContext: string = ''
|
||||
public makeDataResponse: string = ''
|
||||
public jsonFile: any = null
|
||||
public autodeploying: boolean = false
|
||||
@@ -50,8 +57,11 @@ export class AutomaticComponent implements OnInit {
|
||||
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
|
||||
* This object presents the status for two steps that we have for deploy.
|
||||
@@ -68,51 +78,137 @@ export class AutomaticComponent implements OnInit {
|
||||
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(
|
||||
private eventService: EventService,
|
||||
private deployService: DeployService,
|
||||
private sasService: SasService,
|
||||
private sasViyaService: SasViyaService,
|
||||
private loggerService: LoggerService
|
||||
private loggerService: LoggerService,
|
||||
private helperService: HelperService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getAdminGroups()
|
||||
this.getCurrentUser()
|
||||
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() {
|
||||
this.currentUserInfoLoading = true
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.currentUserInfoLoading = true
|
||||
|
||||
this.sasViyaService
|
||||
.getCurrentUser()
|
||||
.subscribe((res: ViyaApiCurrentUser) => {
|
||||
this.currentUserInfoLoading = false
|
||||
this.sasViyaService.getCurrentUser().subscribe(
|
||||
(res: ViyaApiCurrentUser) => {
|
||||
this.currentUserInfoLoading = false
|
||||
|
||||
this.currentUserInfo = res
|
||||
this.currentUserInfo = res
|
||||
|
||||
this.dcPath = `/export/viya/homes/${res.id}`
|
||||
})
|
||||
this.dcPath = `/export/viya/homes/${res.id}`
|
||||
|
||||
resolve()
|
||||
},
|
||||
(err) => {
|
||||
console.error('Error while getting current user', err)
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public async getAdminGroups() {
|
||||
this.adminGroupsLoading = true
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
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'
|
||||
}
|
||||
})
|
||||
}),
|
||||
(err: any) => {
|
||||
this.adminGroupsLoading = false
|
||||
this.loggerService.error('Error while getting admin groups', err)
|
||||
this.eventService.showAbortModal('admin groups', err)
|
||||
}
|
||||
}
|
||||
|
||||
public getComputeContextName(id: string): string | undefined {
|
||||
return (
|
||||
this.computeContexts.find(
|
||||
(context: ComputeContextItem) => context.id === id
|
||||
)?.name || undefined
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,7 +256,7 @@ export class AutomaticComponent implements OnInit {
|
||||
}
|
||||
|
||||
public async runAutoDeploy(executeJson: boolean = false) {
|
||||
this.autodeploying = true
|
||||
if (!this.deployInNewWindow) this.autodeploying = true
|
||||
|
||||
if (executeJson) {
|
||||
this.executeJson()
|
||||
@@ -169,7 +265,7 @@ export class AutomaticComponent implements OnInit {
|
||||
if (this.recreateDatabase) {
|
||||
this.createDatabase()
|
||||
} else {
|
||||
this.autodeployDone = true
|
||||
if (!this.deployInNewWindow) this.autodeployDone = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +282,19 @@ 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.
|
||||
* Here we want to run this request using original WEB method.
|
||||
@@ -193,51 +302,140 @@ export class AutomaticComponent implements OnInit {
|
||||
*/
|
||||
let overrideConfig = {
|
||||
useComputeApi: null,
|
||||
contextName: this.sasJsConfig.contextName,
|
||||
contextName: selectedComputeContextName,
|
||||
debug: true
|
||||
}
|
||||
|
||||
this.sasJs
|
||||
.request(`services/admin/makedata`, data, overrideConfig, () => {
|
||||
this.sasService.shouldLogin.next(true)
|
||||
if (this.deployInNewWindow) {
|
||||
this.runMakedataInNewWindow({
|
||||
contextName: selectedComputeContextName,
|
||||
admin: this.selectedAdminGroup,
|
||||
dcPath: this.dcPath
|
||||
})
|
||||
.then((res: any) => {
|
||||
this.autodeployDone = true
|
||||
} else {
|
||||
this.sasJs
|
||||
.request(`services/admin/makedata`, data, overrideConfig, () => {
|
||||
this.sasService.shouldLogin.next(true)
|
||||
})
|
||||
.then((res: any) => {
|
||||
this.autodeployDone = true
|
||||
|
||||
try {
|
||||
this.makeDataResponse = JSON.stringify(res)
|
||||
} catch {
|
||||
this.makeDataResponse = res
|
||||
}
|
||||
try {
|
||||
this.makeDataResponse = JSON.stringify(res)
|
||||
} catch {
|
||||
this.makeDataResponse = res
|
||||
}
|
||||
|
||||
if (res.result && res.result.length > 0) {
|
||||
this.autoDeployStatus.runMakeData = true
|
||||
} else {
|
||||
if (res.result && res.result.length > 0) {
|
||||
this.autoDeployStatus.runMakeData = true
|
||||
} 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.autodeployDone = true
|
||||
|
||||
if (typeof res.sasjsAbort !== 'undefined') {
|
||||
const abortRes = res
|
||||
const abortMsg = abortRes.sasjsAbort[0].MSG
|
||||
const macMsg = abortRes.sasjsAbort[0].MAC
|
||||
try {
|
||||
this.makeDataResponse = JSON.stringify(err)
|
||||
} catch {
|
||||
this.makeDataResponse = err
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.eventService.showAbortModal('makedata', abortMsg, {
|
||||
SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT,
|
||||
SYSERRORTEXT: abortRes.SYSERRORTEXT,
|
||||
MAC: macMsg
|
||||
})
|
||||
}
|
||||
})
|
||||
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) => {
|
||||
this.eventService.showAbortModal('makedata', JSON.stringify(err))
|
||||
this.autoDeployStatus.runMakeData = false
|
||||
this.autodeployDone = true
|
||||
|
||||
try {
|
||||
this.makeDataResponse = JSON.stringify(err)
|
||||
} catch {
|
||||
this.makeDataResponse = err
|
||||
}
|
||||
this.loggerService.error(`Failed to update DataController.html: ${err}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -408,12 +408,11 @@
|
||||
<div class="hot-wrapper clr-flex-1">
|
||||
<hot-table
|
||||
#hotInstance
|
||||
hotId="hotInstance"
|
||||
id="hotTable"
|
||||
class="edit-hot"
|
||||
className="htDark"
|
||||
[class.hidden]="hotTable.hidden"
|
||||
[licenseKey]="hotTable.licenseKey"
|
||||
[data]="hotTable.data"
|
||||
[settings]="hotTableSettings"
|
||||
>
|
||||
</hot-table>
|
||||
</div>
|
||||
|
@@ -3,6 +3,7 @@ import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
@@ -16,7 +17,7 @@ import { SasStoreService } from '../services/sas-store.service'
|
||||
|
||||
type AOA = any[][]
|
||||
|
||||
import { HotTableRegisterer } from '@handsontable/angular'
|
||||
import { HotTableComponent } from '@handsontable/angular-wrapper'
|
||||
import { UploadFile } from '@sasjs/adapter'
|
||||
import { isSpecialMissing } from '@sasjs/utils/input/validators'
|
||||
import CellRange from 'handsontable/3rdparty/walkontable/src/cell/range'
|
||||
@@ -71,13 +72,13 @@ import { ParseResult } from '../models/ParseResult.interface'
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class EditorComponent implements OnInit, AfterViewInit {
|
||||
export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChildren('uploadStater')
|
||||
uploadStaterCompList: QueryList<UploadStaterComponent> = new QueryList()
|
||||
@ViewChildren('queryFilter')
|
||||
queryFilterCompList: QueryList<QueryComponent> = new QueryList()
|
||||
@ViewChildren('hotInstance')
|
||||
hotInstanceCompList: QueryList<Handsontable> = new QueryList()
|
||||
@ViewChild(HotTableComponent, { static: false })
|
||||
hotTableComponent!: HotTableComponent
|
||||
@ViewChildren('fileUploadInput')
|
||||
fileUploadInputCompList: QueryList<ElementRef> = new QueryList()
|
||||
|
||||
@@ -119,13 +120,26 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
public hotInstance!: Handsontable
|
||||
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 = {
|
||||
data: [],
|
||||
colHeaders: [],
|
||||
hidden: true,
|
||||
columns: [],
|
||||
height: '100%',
|
||||
minSpareRows: 1,
|
||||
height: 'calc(100vh - 160px)',
|
||||
licenseKey: undefined,
|
||||
readOnly: true,
|
||||
copyPaste: {
|
||||
@@ -162,10 +176,30 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
},
|
||||
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: {
|
||||
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: {
|
||||
name: 'Ignore row'
|
||||
@@ -350,6 +384,9 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
|
||||
public licenceState = this.licenceService.licenceState
|
||||
|
||||
private ariaObserver: MutationObserver | undefined
|
||||
private ariaCheckInterval: any | undefined
|
||||
|
||||
constructor(
|
||||
private licenceService: LicenceService,
|
||||
private eventService: EventService,
|
||||
@@ -360,15 +397,12 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
private route: ActivatedRoute,
|
||||
private sasService: SasService,
|
||||
private cdf: ChangeDetectorRef,
|
||||
private hotRegisterer: HotTableRegisterer,
|
||||
private spreadsheetService: SpreadsheetService
|
||||
) {
|
||||
const lang = languages[window.navigator.language]
|
||||
if (lang)
|
||||
numbro.default.registerLanguage(languages[window.navigator.language])
|
||||
|
||||
this.hotRegisterer = new HotTableRegisterer()
|
||||
|
||||
this.parseRestrictions()
|
||||
this.setRestrictions()
|
||||
}
|
||||
@@ -896,6 +930,11 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
this.reSetCellValidationValues()
|
||||
|
||||
// Fix ARIA accessibility issues after table edit
|
||||
setTimeout(() => {
|
||||
this.fixAriaAccessibility()
|
||||
}, 100)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
@@ -922,6 +961,9 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
|
||||
this.cellValidationSource = []
|
||||
|
||||
// Clear custom validation styling
|
||||
this.clearDuplicateValidation()
|
||||
|
||||
const hot = this.hotInstance
|
||||
const columnSorting = hot.getPlugin('multiColumnSorting')
|
||||
const columnSortConfig = columnSorting.getSortConfig()
|
||||
@@ -982,22 +1024,54 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
setTimeout(() => {
|
||||
const hot = this.hotInstance
|
||||
|
||||
const dsInsertIndex = this.dataSource.length
|
||||
hot.alter('insert_row_below', dsInsertIndex, 1)
|
||||
// Create a new empty row object with proper structure
|
||||
const newRow = this.createEmptyRow()
|
||||
|
||||
// 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)
|
||||
|
||||
// Select the newly added row
|
||||
hot.selectCell(this.dataSource.length - 1, 0)
|
||||
hot.render()
|
||||
|
||||
if (this.dataSource[dsInsertIndex]) {
|
||||
this.dataSource[dsInsertIndex]['noLinkOption'] = true
|
||||
}
|
||||
|
||||
this.addingNewRow = false
|
||||
|
||||
this.reSetCellValidationValues()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new empty row object with proper structure
|
||||
*/
|
||||
private createEmptyRow(): any {
|
||||
const newRow: any = {}
|
||||
this.headerColumns.forEach((col: string) => {
|
||||
newRow[col] = ''
|
||||
})
|
||||
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() {
|
||||
this.dataSource = this.helperService.deepClone(this.dataSourceBeforeSubmit)
|
||||
this.dataSourceBeforeSubmit = []
|
||||
@@ -1077,51 +1151,96 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
public validatePrimaryKeys() {
|
||||
private clearDuplicateValidation() {
|
||||
const hot = this.hotInstance
|
||||
|
||||
const myTable = hot.getData()
|
||||
this.pkFields = []
|
||||
for (let index = 0; index < myTable.length; index++) {
|
||||
let pkRow = ''
|
||||
for (let ind = 1; ind < this.readOnlyFields + 1; ind++) {
|
||||
pkRow = pkRow + '|' + myTable[index][ind]
|
||||
}
|
||||
this.pkFields.push(pkRow)
|
||||
}
|
||||
|
||||
const results = []
|
||||
const rows = this.dataSource.length
|
||||
|
||||
for (let j = 0; j < this.pkFields.length; j++) {
|
||||
for (let i = 0; i < this.pkFields.length; i++) {
|
||||
if (this.pkFields[j] === this.pkFields[i] && i !== j) {
|
||||
results.push(i)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.pkFields.length > rows) {
|
||||
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)
|
||||
this.duplicatePkIndexes = []
|
||||
hot.render()
|
||||
}
|
||||
|
||||
public validatePrimaryKeys() {
|
||||
const hot = this.hotInstance
|
||||
|
||||
// Clear previous validation before applying new ones
|
||||
this.clearDuplicateValidation()
|
||||
|
||||
// Get data from the data source instead of hot.getData() to ensure consistency
|
||||
const myTable = this.dataSource
|
||||
this.pkFields = []
|
||||
|
||||
for (let index = 0; index < myTable.length; index++) {
|
||||
let pkRow = ''
|
||||
for (let ind = 1; ind < this.readOnlyFields + 1; ind++) {
|
||||
const colName = this.headerColumns[ind]
|
||||
const value = myTable[index][colName] || ''
|
||||
pkRow = pkRow + '|' + value
|
||||
}
|
||||
this.pkFields.push(pkRow)
|
||||
}
|
||||
|
||||
const results: any = []
|
||||
const rows = this.dataSource.length
|
||||
|
||||
// Only check for duplicates if we have data
|
||||
if (this.pkFields.length > 0) {
|
||||
for (let j = 0; j < this.pkFields.length; j++) {
|
||||
for (let i = 0; i < this.pkFields.length; i++) {
|
||||
if (
|
||||
this.pkFields[j] === this.pkFields[i] &&
|
||||
i !== j &&
|
||||
this.pkFields[j] !== '|'
|
||||
) {
|
||||
results.push(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cellMeta
|
||||
// Clear any existing validation marks for all cells
|
||||
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 index = 1; index < this.readOnlyFields + 1; index++) {
|
||||
cellMeta = hot.getCellMeta(results[k], index)
|
||||
cellMeta.valid = false
|
||||
cellMeta.dupKey = true
|
||||
hot.render()
|
||||
hot.setCellMeta(results[k], index, 'valid', false)
|
||||
hot.setCellMeta(results[k], index, 'dupKey', true)
|
||||
hot.setCellMeta(results[k], index, 'className', 'dc-invalid-cell')
|
||||
}
|
||||
}
|
||||
|
||||
this.duplicatePkIndexes = [...new Set(results.sort())]
|
||||
hot.render()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1416,10 +1535,26 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
|
||||
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++) {
|
||||
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(
|
||||
{
|
||||
data: this.dataSource,
|
||||
@@ -1437,17 +1572,6 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
|
||||
EditorComponent.cnt = 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()
|
||||
|
||||
@@ -1477,15 +1601,6 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
if (txt) txt.focus()
|
||||
}, 200)
|
||||
})
|
||||
|
||||
// let cnt = 0;
|
||||
// hot.addHook("afterValidate", () => {
|
||||
// this.updateSoftSelectColumns(true);
|
||||
// cnt++;
|
||||
// if (cnt === long) {
|
||||
// this.validationDone = 1;
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
public async saveTable(data: any) {
|
||||
@@ -1639,11 +1754,20 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
public checkInvalid() {
|
||||
const hotElement = (this.hotInstanceCompList.first.container as any)
|
||||
.nativeElement
|
||||
const invalidCells = hotElement.querySelectorAll('.htInvalid')
|
||||
// Use Angular wrapper to access Handsontable element instead of DOM queries
|
||||
if (!this.hotTableComponent || !this.hotTableComponent.hotInstance)
|
||||
return false
|
||||
|
||||
return invalidCells.length > 0
|
||||
const hotElement = this.hotTableComponent.hotInstance.rootElement
|
||||
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() {
|
||||
@@ -2183,6 +2307,7 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
*/
|
||||
private setCellFilter(filter: boolean) {
|
||||
const hotSelected = this.hotInstance.getSelected()
|
||||
if (!hotSelected) return
|
||||
const selection = hotSelected ? hotSelected[0] : hotSelected
|
||||
|
||||
// When we open a dropdown we want filter disabled so value in cell
|
||||
@@ -2207,9 +2332,13 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// Initialize hot table settings
|
||||
this.updateHotTableSettings()
|
||||
|
||||
this.licenceService.hot_license_key.subscribe(
|
||||
(hot_license_key: string | undefined) => {
|
||||
this.hotTable.licenseKey = hot_license_key
|
||||
this.updateHotTableSettings() // Update settings when license key changes
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2262,14 +2391,202 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
this.hotInstance = this.hotRegisterer.getInstance('hotInstance')
|
||||
this.hotInstance = this.hotTableComponent!.hotInstance!
|
||||
|
||||
if (this.getdataError) return
|
||||
if (!response) return
|
||||
if (!response.data) return
|
||||
if (!this.hotInstance) return
|
||||
|
||||
this.cols = response.data.cols
|
||||
this.dsmeta = response.data.dsmeta
|
||||
@@ -2289,7 +2606,7 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
this.dsNote = ''
|
||||
}
|
||||
|
||||
const hot: Handsontable = this.hotInstance
|
||||
const hot = this.hotInstance
|
||||
|
||||
const approvers: Approver[] = response.data.approvers
|
||||
|
||||
@@ -2410,6 +2727,11 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
rowHeights: 24,
|
||||
maxRows: this.licenceState.value.editor_rows_allowed || Infinity,
|
||||
invalidCellClassName: 'htInvalid',
|
||||
// Prevent automatic row creation
|
||||
autoWrapRow: false,
|
||||
autoWrapCol: false,
|
||||
// Ensure proper data binding
|
||||
bindRowsWithHeaders: false,
|
||||
dropdownMenu: {
|
||||
items: {
|
||||
make_read_only: {
|
||||
@@ -2484,7 +2806,51 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
cellProperties: Handsontable.CellProperties
|
||||
) => {
|
||||
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
|
||||
@@ -2503,22 +2869,6 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
this.columnHeader[0] = 'Delete?'
|
||||
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(
|
||||
'afterSelection',
|
||||
(
|
||||
@@ -2597,6 +2947,17 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
|
||||
hot.addHook('afterRender', (isForced: boolean) => {
|
||||
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) => {
|
||||
@@ -2610,6 +2971,21 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
})
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
hot.addHook('beforePaste', (data: any, cords: any) => {
|
||||
const startCol = cords[0].startCol
|
||||
|
||||
@@ -2660,5 +3036,10 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
hot.render()
|
||||
|
||||
// Fix ARIA accessibility issues after table initialization
|
||||
setTimeout(() => {
|
||||
this.fixAriaAccessibility()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
import { HotTableModule } from '@handsontable/angular'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { registerAllModules } from 'handsontable/registry'
|
||||
import { AppSharedModule } from '../app-shared.module'
|
||||
import { DirectivesModule } from '../directives/directives.module'
|
||||
@@ -28,7 +28,7 @@ registerAllModules()
|
||||
FormsModule,
|
||||
EditorRoutingModule,
|
||||
ClarityModule,
|
||||
HotTableModule.forRoot(),
|
||||
HotTableModule,
|
||||
AppSharedModule,
|
||||
DirectivesModule,
|
||||
SharedModule,
|
||||
|
@@ -99,6 +99,11 @@ export class MetadataComponent implements OnInit {
|
||||
}
|
||||
this.pageSize = 5
|
||||
|
||||
// Initialize filters for accessibility
|
||||
this.typeFilter = new TypeFilter()
|
||||
this.nameFilter = new NameFilter()
|
||||
this.valueFilter = new ValueFilter()
|
||||
|
||||
if (
|
||||
globals.metadata.metaDataList &&
|
||||
globals.metadata.metaRepositories &&
|
||||
|
@@ -8,4 +8,5 @@ export interface Libinfo {
|
||||
LIBID: string
|
||||
LIBSIZE: number
|
||||
TABLE_CNT: number
|
||||
CATALOG_CNT: number
|
||||
}
|
||||
|
@@ -1,30 +1,14 @@
|
||||
import { BaseSASResponse } from './common/BaseSASResponse'
|
||||
|
||||
export interface EditorsStageDataSASResponse extends BaseSASResponse {
|
||||
SYSDATE: string
|
||||
SYSTIME: string
|
||||
sasparams: Sasparam[]
|
||||
_DEBUG: string
|
||||
_PROGRAM: string
|
||||
AUTOEXEC: string
|
||||
MF_GETUSER: string
|
||||
SYSCC: string
|
||||
SYSENCODING: string
|
||||
SYSERRORTEXT: string
|
||||
SYSHOSTINFOLONG: string
|
||||
SYSHOSTNAME: string
|
||||
SYSPROCESSID: string
|
||||
SYSPROCESSMODE: string
|
||||
SYSPROCESSNAME: string
|
||||
SYSJOBID: string
|
||||
SYSSCPL: string
|
||||
SYSSITE: string
|
||||
SYSTCPIPHOSTNAME: string
|
||||
SYSUSERID: string
|
||||
SYSVLONG: string
|
||||
SYSWARNINGTEXT: string
|
||||
END_DTTM: string
|
||||
MEMSIZE: string
|
||||
}
|
||||
|
||||
export interface Sasparam {
|
||||
|
28
client/src/app/models/sas/public-getchangeinfo.model.ts
Normal file
28
client/src/app/models/sas/public-getchangeinfo.model.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
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
|
||||
}
|
@@ -166,13 +166,10 @@
|
||||
>
|
||||
|
||||
<hot-table
|
||||
hotId="hotInstanceUserDataset"
|
||||
#hotInstanceUserDataset
|
||||
id="hotTableUserDataset"
|
||||
class="mt-15"
|
||||
[afterGetColHeader]="afterGetColHeader"
|
||||
[settings]="hotUserDatasets"
|
||||
[licenseKey]="hotTableLicenseKey"
|
||||
stretchH="all"
|
||||
[settings]="hotUserDatasetsSettings"
|
||||
>
|
||||
</hot-table>
|
||||
|
||||
@@ -360,17 +357,10 @@
|
||||
</div>
|
||||
|
||||
<hot-table
|
||||
hotId="hotInstance"
|
||||
#hotInstanceMain
|
||||
id="hotTable"
|
||||
class="mt-15"
|
||||
[afterGetColHeader]="afterGetColHeader"
|
||||
[className]="['htDark', 'htCustomHidden']"
|
||||
[licenseKey]="hotTableLicenseKey"
|
||||
[multiColumnSorting]="true"
|
||||
[viewportRowRenderingOffset]="50"
|
||||
[manualColumnResize]="true"
|
||||
[filters]="true"
|
||||
stretchH="all"
|
||||
[settings]="hotMainTableSettings"
|
||||
>
|
||||
</hot-table>
|
||||
</ng-container>
|
||||
|
@@ -4,6 +4,7 @@ import {
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
OnInit,
|
||||
AfterViewInit,
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core'
|
||||
@@ -22,7 +23,7 @@ import { HotTableInterface } from '../models/HotTable.interface'
|
||||
import { Col } from '../shared/dc-validator/models/col.model'
|
||||
import { SpreadsheetService } from '../services/spreadsheet.service'
|
||||
import Handsontable from 'handsontable'
|
||||
import { HotTableRegisterer } from '@handsontable/angular'
|
||||
import { HotTableComponent } from '@handsontable/angular-wrapper'
|
||||
import { EditorsStageDataSASResponse } from '../models/sas/editors-stagedata.model'
|
||||
import { CellChange, ChangeSource } from 'handsontable/common'
|
||||
import { baseAfterGetColHeader } from '../shared/utils/hot.utils'
|
||||
@@ -49,7 +50,7 @@ enum FileLoadingState {
|
||||
styleUrls: ['./multi-dataset.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class MultiDatasetComponent implements OnInit {
|
||||
export class MultiDatasetComponent implements OnInit, AfterViewInit {
|
||||
@HostBinding('class.content-container') contentContainerClass = true
|
||||
@ViewChild('contentArea', { static: true }) contentAreaRef!: ElementRef
|
||||
|
||||
@@ -89,7 +90,13 @@ export class MultiDatasetComponent implements OnInit {
|
||||
|
||||
public hotInstance!: Handsontable
|
||||
public hotInstanceUserDataset!: Handsontable
|
||||
private hotRegisterer: HotTableRegisterer
|
||||
@ViewChild('hotInstanceMain', { static: false })
|
||||
hotTableMainComponent!: HotTableComponent
|
||||
@ViewChild('hotInstanceUserDataset', { static: false })
|
||||
hotTableUserDatasetComponent!: HotTableComponent
|
||||
|
||||
public hotMainTableSettings: Handsontable.GridSettings = {}
|
||||
public hotUserDatasetsSettings: Handsontable.GridSettings = {}
|
||||
|
||||
public showSubmitReasonModal: boolean = false
|
||||
public submitReasonMessage: string = ''
|
||||
@@ -136,7 +143,36 @@ export class MultiDatasetComponent implements OnInit {
|
||||
}
|
||||
},
|
||||
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
|
||||
@@ -149,16 +185,28 @@ export class MultiDatasetComponent implements OnInit {
|
||||
private spreadsheetService: SpreadsheetService,
|
||||
private sasService: SasService,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {
|
||||
this.hotRegisterer = new HotTableRegisterer()
|
||||
}
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.licenceService.hot_license_key.subscribe(
|
||||
(hot_license_key: string | undefined) => {
|
||||
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 {
|
||||
@@ -233,7 +281,10 @@ export class MultiDatasetComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.initUserInputHot()
|
||||
this.onAutoDetectColumns()
|
||||
// Call onAutoDetectColumns after HOT is initialized
|
||||
setTimeout(() => {
|
||||
this.onAutoDetectColumns()
|
||||
}, 100)
|
||||
} else if (matchedExtension === 'csv') {
|
||||
this.onMultiCsvFiles(event.target.files)
|
||||
} else {
|
||||
@@ -392,84 +443,112 @@ export class MultiDatasetComponent implements OnInit {
|
||||
|
||||
initHot() {
|
||||
setTimeout(() => {
|
||||
this.hotInstance = this.hotRegisterer.getInstance('hotInstance')
|
||||
if (this.hotTableMainComponent?.hotInstance) {
|
||||
this.hotInstance = this.hotTableMainComponent.hotInstance
|
||||
|
||||
// Set height of parsed data to full height of the page content area
|
||||
const contentAreaHeight = this.contentAreaRef.nativeElement.clientHeight
|
||||
const hotHeight = `${contentAreaHeight - 160}px`
|
||||
// Set height of parsed data to full height of the page content area
|
||||
const contentAreaHeight = this.contentAreaRef.nativeElement.clientHeight
|
||||
const hotHeight = `${contentAreaHeight - 160}px`
|
||||
|
||||
if (this.activeParsedDataset) {
|
||||
this.hotInstance.updateSettings({
|
||||
data: this.activeParsedDataset.datasource || [],
|
||||
colHeaders: this.activeParsedDataset.datasetInfo.headerColumns,
|
||||
columns: this.activeParsedDataset.datasetInfo.dcValidator?.getRules(),
|
||||
readOnly: true,
|
||||
height: hotHeight || '300px',
|
||||
className: 'htDark'
|
||||
})
|
||||
if (this.activeParsedDataset) {
|
||||
// Update settings without data - data will be loaded manually
|
||||
this.hotInstance.updateSettings({
|
||||
colHeaders: this.activeParsedDataset.datasetInfo.headerColumns,
|
||||
columns:
|
||||
this.activeParsedDataset.datasetInfo.dcValidator?.getRules(),
|
||||
readOnly: true,
|
||||
height: hotHeight || '300px',
|
||||
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() {
|
||||
setTimeout(() => {
|
||||
this.hotInstanceUserDataset = this.hotRegisterer.getInstance(
|
||||
'hotInstanceUserDataset'
|
||||
)
|
||||
if (this.hotTableUserDatasetComponent?.hotInstance) {
|
||||
this.hotInstanceUserDataset =
|
||||
this.hotTableUserDatasetComponent.hotInstance
|
||||
|
||||
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()
|
||||
// Load initial data manually after instance is ready
|
||||
setTimeout(() => {
|
||||
if (this.hotUserDatasets.data) {
|
||||
this.hotInstanceUserDataset.loadData(this.hotUserDatasets.data)
|
||||
this.hotInstanceUserDataset.render()
|
||||
}
|
||||
}, 50)
|
||||
|
||||
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(
|
||||
'afterChange',
|
||||
async (changes: CellChange[] | null, source: ChangeSource) => {
|
||||
if (changes) {
|
||||
if (source === 'edit') {
|
||||
await this.onUserInputDatasetsChange()
|
||||
this.hotInstanceUserDataset.addHook(
|
||||
'afterChange',
|
||||
async (changes: CellChange[] | null, source: ChangeSource) => {
|
||||
if (changes) {
|
||||
if (source === 'edit') {
|
||||
await this.onUserInputDatasetsChange()
|
||||
}
|
||||
|
||||
for (let change of changes) {
|
||||
const row = change[0] as number
|
||||
|
||||
this.markUnmatchedRows(row)
|
||||
}
|
||||
|
||||
this.dynamicCellValidations()
|
||||
|
||||
this.hotInstanceUserDataset.render()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
for (let change of changes) {
|
||||
const row = change[0] as number
|
||||
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)
|
||||
}
|
||||
|
||||
this.dynamicCellValidations()
|
||||
|
||||
this.hotInstanceUserDataset.render()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
dynamicCellValidations() {
|
||||
if (!this.hotInstanceUserDataset) return
|
||||
|
||||
const hotData = this.hotInstanceUserDataset.getData()
|
||||
|
||||
hotData.forEach((row, rowIndex) => {
|
||||
@@ -483,6 +562,8 @@ export class MultiDatasetComponent implements OnInit {
|
||||
}
|
||||
|
||||
markUnmatchedRows(row: number) {
|
||||
if (!this.hotInstanceUserDataset) return
|
||||
|
||||
const dataAtRow = this.hotInstanceUserDataset.getDataAtRow(row) as number[]
|
||||
const dataset = `${dataAtRow[0]}.${dataAtRow[1]}`
|
||||
const cellMetaAtRow = this.hotInstanceUserDataset.getCellMetaAtRow(row)
|
||||
@@ -556,6 +637,20 @@ export class MultiDatasetComponent implements OnInit {
|
||||
* convention. {@link isValidDatasetFormat}
|
||||
*/
|
||||
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
|
||||
|
||||
await this.parseExcelSheetNames()
|
||||
@@ -616,7 +711,13 @@ export class MultiDatasetComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
this.hotInstanceUserDataset.updateData(hotReadyData)
|
||||
if (this.hotInstanceUserDataset) {
|
||||
// Load data manually - this is required for HOT v16 Angular wrapper
|
||||
setTimeout(() => {
|
||||
this.hotInstanceUserDataset.loadData(hotReadyData)
|
||||
this.hotInstanceUserDataset.render()
|
||||
}, 100)
|
||||
}
|
||||
this.dynamicCellValidations()
|
||||
}
|
||||
|
||||
|
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
import { HotTableModule } from '@handsontable/angular'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { registerAllModules } from 'handsontable/registry'
|
||||
import { AppSharedModule } from '../app-shared.module'
|
||||
import { DirectivesModule } from '../directives/directives.module'
|
||||
|
@@ -13,6 +13,8 @@ import {
|
||||
AuditorsPostdataSASResponse,
|
||||
Param
|
||||
} from '../../models/sas/auditors-postdata.model'
|
||||
import { PublicGetChangeinfo } from 'src/app/models/sas/public-getchangeinfo.model'
|
||||
import { SasService } from 'src/app/services'
|
||||
|
||||
interface ChangesObj {
|
||||
ind: any
|
||||
@@ -76,9 +78,11 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
|
||||
|
||||
public diffsLimit: boolean = false
|
||||
public recordsLimit: number = 100
|
||||
public refreshStartupserviceAfterApprove: boolean = false
|
||||
|
||||
constructor(
|
||||
private sasStoreService: SasStoreService,
|
||||
private sasService: SasService,
|
||||
private eventService: EventService,
|
||||
private router: ActivatedRoute,
|
||||
private route: Router
|
||||
@@ -162,6 +166,9 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
|
||||
await this.sasStoreService
|
||||
.approveTable(approveParams, 'SASControlTable', 'auditors/postdata')
|
||||
.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')
|
||||
})
|
||||
.catch((err: any) => {
|
||||
@@ -176,7 +183,7 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
|
||||
public async callChangesInfo(tableId: any) {
|
||||
await this.sasStoreService
|
||||
.getChangeInfo(tableId)
|
||||
.then((res: any) => {
|
||||
.then((res: PublicGetChangeinfo) => {
|
||||
this.tableDetails = res.jsparams[0]
|
||||
this.jsParams = res.jsparams[0]
|
||||
|
||||
@@ -189,6 +196,11 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
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) => {
|
||||
this.acceptLoading = false
|
||||
|
@@ -33,12 +33,34 @@
|
||||
<div class="clr-col-md-12" ng-if="loaded">
|
||||
<div *ngIf="approveList && remained !== 0">
|
||||
<clr-datagrid class="datagrid-compact datagrid-custom-footer">
|
||||
<clr-dg-column [clrDgField]="'submitter'">SUBMITTER</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'baseTable'">BASE TABLE</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitted'">SUBMITTED</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitReason'"
|
||||
>SUBMIT REASON</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]="'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>DOWNLOAD</clr-dg-column>
|
||||
|
||||
@@ -51,15 +73,19 @@
|
||||
<clr-dg-cell>{{ approveItem.submitReason }}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<div
|
||||
class="clr-row"
|
||||
role="tooltip"
|
||||
class="d-flex justify-content-around"
|
||||
class="clr-row d-flex justify-content-around"
|
||||
role="toolbar"
|
||||
aria-label="Table actions"
|
||||
>
|
||||
<a
|
||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-green"
|
||||
(click)="getClicked(i)"
|
||||
>
|
||||
<clr-icon shape="check" size="24"></clr-icon>
|
||||
<clr-icon
|
||||
shape="check"
|
||||
size="24"
|
||||
aria-hidden="true"
|
||||
></clr-icon>
|
||||
<span class="tooltip-content">Go to review page screen</span>
|
||||
</a>
|
||||
<a
|
||||
@@ -70,10 +96,12 @@
|
||||
*ngIf="!approveItem.rejectLoading"
|
||||
shape="ban"
|
||||
size="22"
|
||||
aria-hidden="true"
|
||||
></clr-icon>
|
||||
<clr-spinner
|
||||
*ngIf="approveItem.rejectLoading"
|
||||
[clrSmall]="true"
|
||||
aria-hidden="true"
|
||||
></clr-spinner>
|
||||
<span class="tooltip-content">Reject</span>
|
||||
</a>
|
||||
@@ -81,7 +109,11 @@
|
||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-blue"
|
||||
(click)="getTable(approveItem.tableId)"
|
||||
>
|
||||
<clr-icon shape="code" size="28"></clr-icon>
|
||||
<clr-icon
|
||||
shape="code"
|
||||
size="28"
|
||||
aria-hidden="true"
|
||||
></clr-icon>
|
||||
<span class="tooltip-content">Go to staged data screen</span>
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -8,6 +8,7 @@ import { SasStoreService } from '../../services/sas-store.service'
|
||||
import { Router } from '@angular/router'
|
||||
import { SasService } from '../../services/sas.service'
|
||||
import { EventService } from '../../services/event.service'
|
||||
import { ClrDatagridStringFilterInterface } from '@clr/angular'
|
||||
|
||||
interface ApproveData {
|
||||
tableId: string
|
||||
@@ -19,6 +20,32 @@ interface ApproveData {
|
||||
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({
|
||||
selector: 'app-approve',
|
||||
templateUrl: './approve.component.html',
|
||||
@@ -35,6 +62,12 @@ export class ApproveComponent implements OnInit {
|
||||
public loaded: boolean = false
|
||||
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(
|
||||
private sasStoreService: SasStoreService,
|
||||
private eventService: EventService,
|
||||
|
@@ -85,16 +85,48 @@
|
||||
class="datagrid-history datagrid-custom-footer"
|
||||
*ngIf="loaded"
|
||||
>
|
||||
<clr-dg-column [clrDgField]="'basetable'">BASE_TABLE</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'status'">STATUS</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitter'">SUBMITTER</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submittedReason'"
|
||||
>SUBMIT REASON</clr-dg-column
|
||||
>
|
||||
<clr-dg-column [clrDgField]="'submitted'">SUBMITTED</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'reviewed'"
|
||||
>APPROVED / REJECTED</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]="'status'">
|
||||
STATUS
|
||||
<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-row
|
||||
|
@@ -8,6 +8,55 @@ import {
|
||||
EventService,
|
||||
SasService
|
||||
} from 'src/app/services'
|
||||
import { ClrDatagridStringFilterInterface } from '@clr/angular'
|
||||
|
||||
interface HistoryData {
|
||||
tableId: string
|
||||
basetable: string
|
||||
status: string
|
||||
submitter: string
|
||||
submittedReason: string
|
||||
submitted: string
|
||||
reviewed: string
|
||||
}
|
||||
|
||||
class BaseTableFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||
accepts(data: HistoryData, search: string): boolean {
|
||||
return data.basetable.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||
}
|
||||
}
|
||||
|
||||
class StatusFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||
accepts(data: HistoryData, search: string): boolean {
|
||||
return data.status.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||
}
|
||||
}
|
||||
|
||||
class SubmitterFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||
accepts(data: HistoryData, search: string): boolean {
|
||||
return data.submitter.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||
}
|
||||
}
|
||||
|
||||
class SubmitReasonFilter
|
||||
implements ClrDatagridStringFilterInterface<HistoryData>
|
||||
{
|
||||
accepts(data: HistoryData, search: string): boolean {
|
||||
return data.submittedReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||
}
|
||||
}
|
||||
|
||||
class SubmittedFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||
accepts(data: HistoryData, search: string): boolean {
|
||||
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||
}
|
||||
}
|
||||
|
||||
class ReviewedFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||
accepts(data: HistoryData, search: string): boolean {
|
||||
return data.reviewed.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-history',
|
||||
@@ -29,6 +78,14 @@ export class HistoryComponent implements OnInit {
|
||||
public approveData: any = {}
|
||||
public sasjsConfig: SASjsConfig = new SASjsConfig()
|
||||
|
||||
// Filter instances for datagrid accessibility
|
||||
public baseTableFilter = new BaseTableFilter()
|
||||
public statusFilter = new StatusFilter()
|
||||
public submitterFilter = new SubmitterFilter()
|
||||
public submitReasonFilter = new SubmitReasonFilter()
|
||||
public submittedFilter = new SubmittedFilter()
|
||||
public reviewedFilter = new ReviewedFilter()
|
||||
|
||||
public histParams: { HIST: number; STARTROW: number; NOBS: number } = {
|
||||
HIST: 0,
|
||||
STARTROW: 1,
|
||||
|
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
import { HotTableModule } from '@handsontable/angular'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { DirectivesModule } from '../directives/directives.module'
|
||||
import { SharedModule } from '../shared/shared.module'
|
||||
import { ApproveDetailsComponent } from './approve-details/approve-details.component'
|
||||
@@ -23,7 +23,7 @@ import { HistoryComponent } from './history/history.component'
|
||||
FormsModule,
|
||||
ReviewRoutingModule,
|
||||
ClarityModule,
|
||||
HotTableModule.forRoot(),
|
||||
HotTableModule,
|
||||
DirectivesModule,
|
||||
SharedModule
|
||||
]
|
||||
|
@@ -44,10 +44,20 @@
|
||||
<div *ngIf="submitterList && remained !== 0">
|
||||
<clr-datagrid class="datagrid-compact datagrid-custom-footer">
|
||||
<clr-dg-column>BASE TABLE</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitted'">SUBMITTED</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitReason'"
|
||||
>SUBMIT REASON</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 class="d-flex justify-content-center"
|
||||
>ACTION</clr-dg-column
|
||||
>
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { SasStoreService, EventService, SasService } from '../../services'
|
||||
import { ClrDatagridStringFilterInterface } from '@clr/angular'
|
||||
|
||||
interface SubmitterData {
|
||||
tableId: string
|
||||
@@ -16,6 +17,22 @@ interface SubmitterData {
|
||||
approver: string
|
||||
}
|
||||
|
||||
class SubmittedFilter
|
||||
implements ClrDatagridStringFilterInterface<SubmitterData>
|
||||
{
|
||||
accepts(data: SubmitterData, search: string): boolean {
|
||||
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||
}
|
||||
}
|
||||
|
||||
class SubmitReasonFilter
|
||||
implements ClrDatagridStringFilterInterface<SubmitterData>
|
||||
{
|
||||
accepts(data: SubmitterData, search: string): boolean {
|
||||
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-submitter',
|
||||
templateUrl: './submitter.component.html',
|
||||
@@ -37,6 +54,10 @@ export class SubmitterComponent implements OnInit, AfterViewInit {
|
||||
private _readySub!: Subscription
|
||||
private _backToSub!: Subscription
|
||||
|
||||
// Filter instances for datagrid accessibility
|
||||
public submittedFilter = new SubmittedFilter()
|
||||
public submitReasonFilter = new SubmitReasonFilter()
|
||||
|
||||
constructor(
|
||||
private sasStoreService: SasStoreService,
|
||||
private eventService: EventService,
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import cloneDeep from 'lodash-es/cloneDeep'
|
||||
import * as CryptoMD5 from 'crypto-js/md5'
|
||||
import { SasService } from './sas.service'
|
||||
|
||||
const librariesToShow = 50
|
||||
|
||||
@@ -12,7 +13,7 @@ export class HelperService {
|
||||
public loadMoreCount: number = librariesToShow
|
||||
public isMicrosoft: boolean = false
|
||||
|
||||
constructor() {
|
||||
constructor(private sasService: SasService) {
|
||||
this.isMicrosoft = this.isIEorEDGE()
|
||||
console.log('Is IE or Edge?', this.isMicrosoft)
|
||||
}
|
||||
@@ -314,6 +315,20 @@ export class HelperService {
|
||||
}
|
||||
}
|
||||
|
||||
public isStreamingViya(): boolean {
|
||||
const serverType = this.sasService.getServerType()
|
||||
|
||||
if (serverType !== 'SASVIYA') return false
|
||||
|
||||
if (
|
||||
location.search.toLowerCase().includes('?_file=') &&
|
||||
location.pathname.toLowerCase().includes('/sasjobexecution')
|
||||
)
|
||||
return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Required type is NodeJS.Timeout
|
||||
// But NodeJS is not available in browser so we have to go with any
|
||||
private debounceTimeout: any
|
||||
|
@@ -1,6 +1,12 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import {
|
||||
HttpClient,
|
||||
HttpContext,
|
||||
HttpErrorResponse,
|
||||
HttpHeaders,
|
||||
HttpParams
|
||||
} from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Observable } from 'rxjs'
|
||||
import { catchError, Observable, throwError } from 'rxjs'
|
||||
import { Collection } from '../viya-api-explorer/models/collection.model'
|
||||
import { AppStoreService } from './app-store.service'
|
||||
import { ViyaApis } from '../viya-api-explorer/models/viya-apis.models'
|
||||
@@ -8,6 +14,8 @@ import { ViyaApiFolderMembers } from '../viya-api-explorer/models/viya-api-folde
|
||||
import { ViyaApiFolder } from '../viya-api-explorer/models/viya-api-folder.model'
|
||||
import { ViyaApiIdentities } from '../viya-api-explorer/models/viya-api-identities.model'
|
||||
import { ViyaApiCurrentUser } from '../viya-api-explorer/models/viya-api-current-user.model'
|
||||
import { ViyaComputeContexts } from '../viya-api-explorer/models/viya-compute-contexts.model'
|
||||
import { ComputeContextDetails } from '../viya-api-explorer/models/viya-compute-context-details.model'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -75,7 +83,7 @@ export class SasViyaService {
|
||||
|
||||
this.serverUrl = adapterConfig?.serverUrl || ''
|
||||
|
||||
//example collection request
|
||||
// example collection request
|
||||
// this.getByCollection('jobs').subscribe((res) => {
|
||||
// console.log('res', res)
|
||||
// })
|
||||
@@ -95,7 +103,7 @@ export class SasViyaService {
|
||||
* @returns
|
||||
*/
|
||||
getByUrl(url: string): Observable<Collection> {
|
||||
return this.http.get<Collection>(`${this.serverUrl}${url}`, {
|
||||
return this.get<Collection>(`${this.serverUrl}${url}`, {
|
||||
withCredentials: true
|
||||
})
|
||||
}
|
||||
@@ -106,23 +114,32 @@ export class SasViyaService {
|
||||
* @returns
|
||||
*/
|
||||
getByCollection(apiCollection: string): Observable<Collection> {
|
||||
return this.http.get<Collection>(`${this.serverUrl}${apiCollection}`, {
|
||||
return this.get<Collection>(`${this.serverUrl}${apiCollection}`, {
|
||||
withCredentials: true
|
||||
})
|
||||
}
|
||||
|
||||
getComputeContexts(): Observable<any> {
|
||||
return this.http.get<any>(`${this.serverUrl}/compute/contexts`, {
|
||||
getComputeContexts(): Observable<ViyaComputeContexts> {
|
||||
return this.get<ViyaComputeContexts>(`${this.serverUrl}/compute/contexts`, {
|
||||
withCredentials: true
|
||||
})
|
||||
}
|
||||
|
||||
getComputeContextById(id: string): Observable<ComputeContextDetails> {
|
||||
return this.get<ComputeContextDetails>(
|
||||
`${this.serverUrl}/compute/contexts/${id}`,
|
||||
{
|
||||
withCredentials: true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Path to the folder
|
||||
* @returns The folder info object
|
||||
*/
|
||||
getFolderByPath(path: string): Observable<ViyaApiFolder> {
|
||||
return this.http.get<ViyaApiFolder>(
|
||||
return this.get<ViyaApiFolder>(
|
||||
`${this.serverUrl}/folders/folders/@item?path=${path}`,
|
||||
{
|
||||
withCredentials: true
|
||||
@@ -131,7 +148,7 @@ export class SasViyaService {
|
||||
}
|
||||
|
||||
getFolderMembers(folderId: string): Observable<ViyaApiFolderMembers> {
|
||||
return this.http.get<ViyaApiFolderMembers>(
|
||||
return this.get<ViyaApiFolderMembers>(
|
||||
`${this.serverUrl}/folders/folders/${folderId}/members`,
|
||||
{
|
||||
withCredentials: true
|
||||
@@ -140,7 +157,7 @@ export class SasViyaService {
|
||||
}
|
||||
|
||||
getAdminGroups(limit: number = 5000): Observable<ViyaApiIdentities> {
|
||||
return this.http.get<ViyaApiIdentities>(
|
||||
return this.get<ViyaApiIdentities>(
|
||||
`${this.serverUrl}/identities/groups?sortBy=name&limit=${limit}`,
|
||||
{
|
||||
withCredentials: true
|
||||
@@ -149,11 +166,54 @@ export class SasViyaService {
|
||||
}
|
||||
|
||||
getCurrentUser(): Observable<ViyaApiCurrentUser> {
|
||||
return this.http.get<ViyaApiCurrentUser>(
|
||||
return this.get<ViyaApiCurrentUser>(
|
||||
`${this.serverUrl}/identities/users/@currentUser`,
|
||||
{
|
||||
withCredentials: true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
get<T>(
|
||||
url: string,
|
||||
options?: {
|
||||
headers?:
|
||||
| HttpHeaders
|
||||
| {
|
||||
[header: string]: string | string[]
|
||||
}
|
||||
context?: HttpContext
|
||||
observe?: 'body'
|
||||
params?:
|
||||
| HttpParams
|
||||
| {
|
||||
[param: string]:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| ReadonlyArray<string | number | boolean>
|
||||
}
|
||||
reportProgress?: boolean
|
||||
responseType?: 'json'
|
||||
withCredentials?: boolean
|
||||
transferCache?:
|
||||
| {
|
||||
includeHeaders?: string[]
|
||||
}
|
||||
| boolean
|
||||
}
|
||||
): Observable<T> {
|
||||
return this.http.get<T>(url, options).pipe(
|
||||
catchError((err: HttpErrorResponse) => {
|
||||
console.log('url', url)
|
||||
console.log('err.status', err.status)
|
||||
if (err.status === 449 || err.status === 401) {
|
||||
// Retry once if we got a 449
|
||||
return this.http.get<T>(url, options)
|
||||
}
|
||||
// Otherwise propagate the error
|
||||
return throwError(() => err)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -333,6 +333,10 @@ export class SasService {
|
||||
})
|
||||
}
|
||||
|
||||
public reloadStartupData() {
|
||||
this.loadStartupServiceEmitter.emit()
|
||||
}
|
||||
|
||||
public getLicenseSiteId(): string[] {
|
||||
return this.license_site_id.value || []
|
||||
}
|
||||
@@ -361,13 +365,18 @@ export class SasService {
|
||||
}
|
||||
},
|
||||
(err: any) => {
|
||||
if (err.error.includes('Unauthorized')) {
|
||||
const errorMessage =
|
||||
typeof err.error === 'string'
|
||||
? err.error
|
||||
: JSON.stringify(err.error || err)
|
||||
|
||||
if (errorMessage.includes('Unauthorized')) {
|
||||
this.shouldLogin.next(true)
|
||||
|
||||
this.shouldLogin.subscribe((res: boolean) => {
|
||||
if (res === false) location.reload()
|
||||
})
|
||||
} else if (err.error.includes(`Folder doesn't exist.`)) {
|
||||
} else if (errorMessage.includes(`Folder doesn't exist.`)) {
|
||||
console.warn(
|
||||
'SASjs SAS services are not present on the current appLoc.'
|
||||
)
|
||||
@@ -415,7 +424,11 @@ export class SasService {
|
||||
}
|
||||
},
|
||||
(err: any) => {
|
||||
if (err.error.includes(`Folder doesn't exist.`)) {
|
||||
const errorMessage =
|
||||
typeof err.error === 'string'
|
||||
? err.error
|
||||
: JSON.stringify(err.error || err)
|
||||
if (errorMessage.includes(`Folder doesn't exist.`)) {
|
||||
reject()
|
||||
}
|
||||
}
|
||||
@@ -657,4 +670,17 @@ export class SasService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Viya specific functions
|
||||
public getFileContent(folderPath: string, fileName: string) {
|
||||
return this.sasjsAdapter.getFileContent(folderPath, fileName)
|
||||
}
|
||||
|
||||
public updateFileContent(
|
||||
folderPath: string,
|
||||
fileName: string,
|
||||
content: string
|
||||
) {
|
||||
return this.sasjsAdapter.updateFileContent(folderPath, fileName, content)
|
||||
}
|
||||
}
|
||||
|
@@ -386,27 +386,9 @@
|
||||
</div>
|
||||
|
||||
<hot-table
|
||||
*ngIf="viewboxTableIndex > -1"
|
||||
[hotId]="'hotInstance_viewbox_' + viewbox.id"
|
||||
id="hotTable"
|
||||
className="htDark"
|
||||
[readOnly]="true"
|
||||
[modifyColWidth]="maxWidthCheker"
|
||||
[copyPaste]="viewboxTables[viewboxTableIndex].hotTable.copyPaste"
|
||||
[contextMenu]="viewboxTables[viewboxTableIndex].hotTable.contextMenu"
|
||||
[multiColumnSorting]="true"
|
||||
[viewportRowRenderingOffset]="50"
|
||||
[data]="viewboxTables[viewboxTableIndex].hotTable.data"
|
||||
[colHeaders]="viewboxTables[viewboxTableIndex].hotTable.colHeaders"
|
||||
[columns]="viewboxTables[viewboxTableIndex].hotTable.columns"
|
||||
[filters]="true"
|
||||
[dropdownMenu]="viewboxTables[viewboxTableIndex].hotTable.dropdownMenu"
|
||||
[height]="viewboxTables[viewboxTableIndex].hotTable.height"
|
||||
stretchH="all"
|
||||
[cells]="viewboxTables[viewboxTableIndex].hotTable.cells"
|
||||
[maxRows]="viewboxTables[viewboxTableIndex].hotTable.maxRows"
|
||||
[manualColumnResize]="true"
|
||||
[licenseKey]="viewboxTables[viewboxTableIndex].hotTable.licenseKey"
|
||||
*ngIf="viewboxTableIndex > -1 && viewboxHotSettings.get(viewbox.id)"
|
||||
[settings]="viewboxHotSettings.get(viewbox.id) || {}"
|
||||
[id]="'hotTable_' + viewbox.id"
|
||||
></hot-table>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -21,9 +21,9 @@ import {
|
||||
ViewEncapsulation
|
||||
} from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { HotTableRegisterer } from '@handsontable/angular'
|
||||
import { SASjsConfig } from '@sasjs/adapter'
|
||||
import Handsontable from 'handsontable'
|
||||
import { HotTableComponent } from '@handsontable/angular-wrapper'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { FilterQuery, FilterGroup } from 'src/app/models/FilterQuery'
|
||||
@@ -54,6 +54,8 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@ViewChildren('resizeBox') resizeBoxQuery!: QueryList<ElementRef> //make query list, handle multiple
|
||||
@ViewChildren('dragHandleCorner')
|
||||
dragHandleCornerQuery!: QueryList<ElementRef>
|
||||
@ViewChildren(HotTableComponent)
|
||||
hotTableComponents!: QueryList<HotTableComponent>
|
||||
|
||||
private _viewboxModal: boolean = false
|
||||
get viewboxModal(): boolean {
|
||||
@@ -111,7 +113,7 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
},
|
||||
columns: [],
|
||||
cols: [],
|
||||
height: '100%',
|
||||
height: 200, //WORKAROUND: Changed from '100%' to fixed pixel value because otherwize hot does not load
|
||||
settings: {},
|
||||
hiddenColumns: true,
|
||||
manualColumnMove: false,
|
||||
@@ -119,8 +121,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
licenseKey: undefined,
|
||||
dropdownMenu: undefined
|
||||
}
|
||||
|
||||
public viewboxHotSettings: Map<number, Handsontable.GridSettings> = new Map()
|
||||
public viewboxTables: ViewboxTable[] = []
|
||||
private hotTableRegisterer: HotTableRegisterer
|
||||
|
||||
public filteringViewbox: Viewbox | undefined
|
||||
|
||||
@@ -150,9 +153,7 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private cdf: ChangeDetectorRef
|
||||
) {
|
||||
this.hotTableRegisterer = new HotTableRegisterer()
|
||||
}
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Load libraries
|
||||
@@ -207,7 +208,17 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
//set handles for box resize
|
||||
// Set handles for box resize and ensure HOT components are properly initialized
|
||||
setTimeout(() => {
|
||||
this.setAllHandleTransform()
|
||||
|
||||
// Force refresh of any existing HOT instances after view init
|
||||
this.viewboxes.forEach((viewbox) => {
|
||||
if (this.getViewboxTableIndex(viewbox) > -1) {
|
||||
this.refreshTableAfterResize(viewbox)
|
||||
}
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// Maximum number of open viewboxes reached
|
||||
@@ -304,6 +315,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (viewboxTable) {
|
||||
viewboxTable.hotTable.data = res.viewdata
|
||||
|
||||
// Update settings with new data
|
||||
this.createViewboxTableSettings(viewbox)
|
||||
|
||||
resolve(null)
|
||||
} else {
|
||||
resolve(null)
|
||||
@@ -413,6 +427,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
viewbox.query = this.helperService.deepClone(res.query)
|
||||
viewbox.filterText = res.sasparams[0].FILTER_TEXT
|
||||
|
||||
// Create settings for this viewbox
|
||||
this.createViewboxTableSettings(viewbox)
|
||||
|
||||
setTimeout(() => {
|
||||
this.updateHotColumns(
|
||||
viewboxTable!.hotTable.colHeadersHidden || [],
|
||||
@@ -421,30 +438,34 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
// HOT Settings are bound in HTML but some settings due to timing issues
|
||||
// requires to be updated after the HOT is instanced
|
||||
// after the update `render` method is called
|
||||
const hotInstance = this.getViewboxHotInstance(viewbox.id)
|
||||
// Use a longer timeout to ensure the HOT component is fully initialized
|
||||
setTimeout(() => {
|
||||
const hotInstance = this.getViewboxHotInstance(viewbox.id)
|
||||
|
||||
hotInstance?.updateSettings({
|
||||
manualColumnMove: viewboxTable!.hotTable.manualColumnMove,
|
||||
afterGetColHeader: (col: number, th: any) => {
|
||||
const column = hotInstance?.colToProp(col) as string
|
||||
if (hotInstance) {
|
||||
hotInstance.updateSettings({
|
||||
manualColumnMove: viewboxTable!.hotTable.manualColumnMove,
|
||||
afterGetColHeader: (col: number, th: any) => {
|
||||
const column = hotInstance?.colToProp(col) as string
|
||||
|
||||
// header columns styling - primary keys
|
||||
const isPKCol =
|
||||
column &&
|
||||
viewboxTable!.hotTable.headerPks.indexOf(column) > -1
|
||||
// header columns styling - primary keys
|
||||
const isPKCol =
|
||||
column &&
|
||||
viewboxTable!.hotTable.headerPks.indexOf(column) > -1
|
||||
|
||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||
// Dark mode
|
||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||
// Dark mode
|
||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||
}
|
||||
})
|
||||
hotInstance.render()
|
||||
}
|
||||
})
|
||||
hotInstance?.render()
|
||||
|
||||
if (this.selectedViewbox) {
|
||||
this.resetSelectedViewbox(viewbox)
|
||||
}
|
||||
})
|
||||
if (this.selectedViewbox) {
|
||||
this.resetSelectedViewbox(viewbox)
|
||||
}
|
||||
}, 500)
|
||||
}, 100)
|
||||
|
||||
resolve()
|
||||
})
|
||||
@@ -490,6 +511,68 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and store Handsontable settings for a specific viewbox
|
||||
* @param viewbox
|
||||
*/
|
||||
private createViewboxTableSettings(viewbox: Viewbox): void {
|
||||
const viewboxTableIndex = this.getViewboxTableIndex(viewbox)
|
||||
if (viewboxTableIndex === -1) {
|
||||
this.viewboxHotSettings.set(viewbox.id, {})
|
||||
return
|
||||
}
|
||||
|
||||
const viewboxTable = this.viewboxTables[viewboxTableIndex]
|
||||
const calculatedHeight = this.calculateTableHeight(viewbox)
|
||||
|
||||
// HOT v16 settings - data will be loaded manually after initialization
|
||||
const settings: Handsontable.GridSettings = {
|
||||
colHeaders: viewboxTable.hotTable.colHeaders,
|
||||
columns: viewboxTable.hotTable.columns,
|
||||
height: calculatedHeight,
|
||||
readOnly: true,
|
||||
modifyColWidth: this.maxWidthCheker,
|
||||
copyPaste: viewboxTable.hotTable.copyPaste,
|
||||
contextMenu: viewboxTable.hotTable.contextMenu,
|
||||
multiColumnSorting: true,
|
||||
viewportRowRenderingOffset: 50,
|
||||
filters: true,
|
||||
dropdownMenu: viewboxTable.hotTable.dropdownMenu,
|
||||
stretchH: 'all',
|
||||
cells: viewboxTable.hotTable.cells,
|
||||
maxRows: viewboxTable.hotTable.maxRows || Infinity,
|
||||
manualColumnResize: true,
|
||||
rowHeaders: true,
|
||||
licenseKey: viewboxTable.hotTable.licenseKey
|
||||
}
|
||||
|
||||
// Force a new object reference to trigger change detection
|
||||
this.viewboxHotSettings.set(viewbox.id, { ...settings })
|
||||
|
||||
// Force change detection to ensure the template updates
|
||||
setTimeout(() => {
|
||||
this.cdf.detectChanges()
|
||||
|
||||
// Try to get the HOT instance and force a render
|
||||
setTimeout(() => {
|
||||
const hotInstance = this.getViewboxHotInstance(viewbox.id)
|
||||
if (hotInstance) {
|
||||
// Load data manually - this is required for HOT v16 Angular wrapper
|
||||
hotInstance.loadData(viewboxTable.hotTable.data)
|
||||
hotInstance.render()
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored Handsontable settings for a specific viewbox
|
||||
* @param viewbox
|
||||
*/
|
||||
getViewboxTableSettings(viewbox: Viewbox): Handsontable.GridSettings {
|
||||
return this.viewboxHotSettings.get(viewbox.id) || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Viewbox resize
|
||||
* @param dragHandle
|
||||
@@ -513,6 +596,12 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.helperService.debounceCall(1000, () => {
|
||||
this.viewboxChanged()
|
||||
this.eventService.dispatchEvent('resize')
|
||||
|
||||
// Refresh all viewbox tables after resize and update their settings
|
||||
this.viewboxes.forEach((viewbox) => {
|
||||
// Settings will include updated heights when accessed
|
||||
this.refreshTableAfterResize(viewbox)
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -672,6 +761,12 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
setTimeout(() => {
|
||||
this.setAllHandleTransform()
|
||||
|
||||
// Refresh all tables after snap to grid
|
||||
this.viewboxes.forEach((viewbox) => {
|
||||
// Settings will include correct height when accessed
|
||||
this.refreshTableAfterResize(viewbox)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -713,6 +808,12 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
viewbox.minimized = false
|
||||
|
||||
this.viewboxChanged()
|
||||
|
||||
// Refresh table after restoring
|
||||
setTimeout(() => {
|
||||
// Settings will include correct height when accessed
|
||||
this.refreshTableAfterResize(viewbox)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
collapse(viewbox: Viewbox) {
|
||||
@@ -723,6 +824,12 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
expand(viewbox: Viewbox) {
|
||||
viewbox.collapsed = false
|
||||
this.viewboxChanged()
|
||||
|
||||
// Refresh table after expanding
|
||||
setTimeout(() => {
|
||||
// Settings will include correct height when accessed
|
||||
this.refreshTableAfterResize(viewbox)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -739,6 +846,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (index > -1) this.viewboxes.splice(index, 1)
|
||||
if (viewtableIndex > -1) this.viewboxTables.splice(viewtableIndex, 1)
|
||||
|
||||
// Clean up settings for this viewbox
|
||||
this.viewboxHotSettings.delete(viewbox.id)
|
||||
|
||||
if (this.selectedViewbox?.id === viewbox.id) {
|
||||
this.unsetSelectedViewbox()
|
||||
}
|
||||
@@ -1036,6 +1146,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
viewboxTable.hotTable.data = res.viewdata
|
||||
|
||||
// Update settings with new data
|
||||
this.createViewboxTableSettings(viewbox)
|
||||
})
|
||||
.catch((err: any) => {
|
||||
this.loggerService.error(err)
|
||||
@@ -1064,6 +1177,8 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.updateHiddenColumnsHot(hiddenColProps, viewboxId)
|
||||
|
||||
this.setColumnOrder(viewboxId)
|
||||
|
||||
// Settings will be regenerated when accessed
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1158,19 +1273,87 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate available height for Handsontable
|
||||
* @param viewbox The viewbox to calculate height for
|
||||
* @returns Available height in pixels
|
||||
*/
|
||||
calculateTableHeight(viewbox: Viewbox): number {
|
||||
// Calculate the exact height of the content div
|
||||
const dragHandleHeight = 20
|
||||
const searchFormHeight = 36
|
||||
const padding = 2
|
||||
// Return the exact remaining height for the table with minimum height
|
||||
const calculatedHeight =
|
||||
viewbox.height - dragHandleHeight - searchFormHeight - padding
|
||||
|
||||
return calculatedHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh Handsontable instance after resize
|
||||
* @param viewbox The viewbox to refresh
|
||||
*/
|
||||
refreshTableAfterResize(viewbox: Viewbox): void {
|
||||
const hotInstance = this.getViewboxHotInstance(viewbox.id)
|
||||
if (hotInstance) {
|
||||
// Force the table to recalculate its dimensions
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// Update height setting and refresh
|
||||
hotInstance.updateSettings({
|
||||
height: this.calculateTableHeight(viewbox)
|
||||
})
|
||||
hotInstance.refreshDimensions()
|
||||
hotInstance.render()
|
||||
} catch (error) {
|
||||
// If refresh fails, try again later
|
||||
setTimeout(() => {
|
||||
try {
|
||||
hotInstance.updateSettings({
|
||||
height: this.calculateTableHeight(viewbox)
|
||||
})
|
||||
hotInstance.refreshDimensions()
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'Failed to refresh HOT dimensions for viewbox',
|
||||
viewbox.id,
|
||||
e
|
||||
)
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param viewboxId
|
||||
* @returns HOT Instance from the given Viewbox
|
||||
*/
|
||||
private getViewboxHotInstance(viewboxId?: number): Handsontable | undefined {
|
||||
if (!viewboxId) return
|
||||
if (!viewboxId || !this.hotTableComponents) return
|
||||
|
||||
const hotInstance = this.hotTableRegisterer.getInstance(
|
||||
`hotInstance_viewbox_${viewboxId}`
|
||||
)
|
||||
// Find the component corresponding to this viewbox
|
||||
// Since we only show one table per viewbox and they're rendered in order,
|
||||
// we can match by the viewbox's position in the array
|
||||
const viewboxIndex = this.viewboxes.findIndex((vb) => vb.id === viewboxId)
|
||||
if (viewboxIndex === -1) return
|
||||
|
||||
return hotInstance
|
||||
// Get the HOT component at this index
|
||||
const hotComponents = this.hotTableComponents.toArray()
|
||||
let hotComponentIndex = 0
|
||||
|
||||
// Count how many viewboxes before this one have loaded tables
|
||||
for (let i = 0; i < viewboxIndex; i++) {
|
||||
if (this.getViewboxTableIndex(this.viewboxes[i]) > -1) {
|
||||
hotComponentIndex++
|
||||
}
|
||||
}
|
||||
|
||||
const hotTableComponent = hotComponents[hotComponentIndex]
|
||||
return hotTableComponent?.hotInstance || undefined
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -4,7 +4,11 @@ import { ClarityModule } from '@clr/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ViewboxesComponent } from './viewboxes.component'
|
||||
import { QueryModule } from 'src/app/query/query.module'
|
||||
import { HotTableModule } from '@handsontable/angular'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { registerAllModules } from 'handsontable/registry'
|
||||
|
||||
// register Handsontable's modules
|
||||
registerAllModules()
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||
import { AutocompleteModule } from '../autocomplete/autocomplete.module'
|
||||
import { DcTreeModule } from '../dc-tree/dc-tree.module'
|
||||
|
@@ -46,6 +46,10 @@
|
||||
rejected: tableDetails?.REVIEW_STATUS_ID === 'REJECTED',
|
||||
accepted: tableDetails?.REVIEW_STATUS_ID === 'APPROVED'
|
||||
}"
|
||||
[attr.aria-label]="
|
||||
'Review status: ' + tableDetails?.REVIEW_STATUS_ID
|
||||
"
|
||||
role="status"
|
||||
>
|
||||
{{ tableDetails?.REVIEW_STATUS_ID }}
|
||||
</span>
|
||||
@@ -61,6 +65,7 @@
|
||||
class="btn btn-sm btn-outline text-center mr-5i"
|
||||
(click)="viewerTableScreen()"
|
||||
[disabled]="revertingChanges"
|
||||
aria-label="View base table"
|
||||
>
|
||||
View base table
|
||||
</button>
|
||||
@@ -74,6 +79,7 @@
|
||||
"
|
||||
(click)="approveTableScreen()"
|
||||
[disabled]="revertingChanges"
|
||||
aria-label="Approve table"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
@@ -81,14 +87,16 @@
|
||||
class="btn btn-sm btn-info-outline text-center mr-5i"
|
||||
(click)="goBack()"
|
||||
[disabled]="revertingChanges"
|
||||
aria-label="Edit base table"
|
||||
>
|
||||
Edit base table
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-success text-center mr-5i min-w-0"
|
||||
(click)="download(tableDetails?.TABLE_ID)"
|
||||
aria-label="Download audit file"
|
||||
>
|
||||
<clr-icon shape="download"></clr-icon>
|
||||
<clr-icon shape="download" aria-hidden="true"></clr-icon>
|
||||
</button>
|
||||
|
||||
<clr-tooltip>
|
||||
@@ -98,6 +106,7 @@
|
||||
clrTooltipTrigger
|
||||
[clrLoading]="revertingChanges"
|
||||
class="btn btn-sm btn-danger text-center mt-20"
|
||||
aria-label="Revert this and all subsequent changes"
|
||||
>
|
||||
REVERT
|
||||
|
||||
@@ -116,18 +125,10 @@
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<hot-table
|
||||
hotId="hotInstance"
|
||||
id="hotTable"
|
||||
className="htDark"
|
||||
[data]="hotTable.data"
|
||||
[colHeaders]="hotTable.colHeaders"
|
||||
[columns]="hotTable.columns"
|
||||
[maxRows]="hotTable.maxRows"
|
||||
[height]="hotTable.height"
|
||||
[licenseKey]="hotTable.licenseKey"
|
||||
[afterGetColHeader]="hotTable.afterGetColHeader"
|
||||
stretchH="all"
|
||||
[cells]="hotTable.cells"
|
||||
[settings]="hotTableSettings"
|
||||
aria-label="Staged data table"
|
||||
>
|
||||
<!--[licenseKey]=null-->
|
||||
</hot-table>
|
||||
|
@@ -1,10 +1,17 @@
|
||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core'
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ViewEncapsulation,
|
||||
ViewChild,
|
||||
AfterViewInit
|
||||
} from '@angular/core'
|
||||
import { SasStoreService } from '../services/sas-store.service'
|
||||
import { Router } from '@angular/router'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { SasService } from '../services/sas.service'
|
||||
import { EventService } from '../services/event.service'
|
||||
import { HotTableInterface } from '../models/HotTable.interface'
|
||||
import Handsontable from 'handsontable'
|
||||
import { LicenceService } from '../services/licence.service'
|
||||
import { globals } from '../_globals'
|
||||
import { EditorsRestoreServiceResponse } from '../models/sas/editors-restore.model'
|
||||
@@ -19,7 +26,7 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class StageComponent implements OnInit {
|
||||
export class StageComponent implements OnInit, AfterViewInit {
|
||||
public table_id: any
|
||||
public jsParams: any
|
||||
public keysArray: any
|
||||
@@ -32,12 +39,42 @@ export class StageComponent implements OnInit {
|
||||
colHeaders: [],
|
||||
columns: [],
|
||||
height: 500,
|
||||
settings: {},
|
||||
settings: {
|
||||
// Disable problematic ARIA attributes that cause accessibility issues
|
||||
ariaTags: false,
|
||||
// Use grid role instead of treegrid for better accessibility
|
||||
tableClassName: 'htCenter',
|
||||
// Disable focus management to avoid focus catcher issues
|
||||
outsideClickDeselects: false,
|
||||
// Use simpler accessibility mode
|
||||
autoWrapRow: false,
|
||||
autoWrapCol: false
|
||||
},
|
||||
licenseKey: undefined,
|
||||
maxRows: this.licenceState.value.stage_rows_allowed || Infinity,
|
||||
afterGetColHeader: (column, th, headerLevel) => {
|
||||
// Dark mode
|
||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||
},
|
||||
afterInit: () => {
|
||||
// Fix accessibility issues with focus catcher inputs
|
||||
this.fixFocusCatcherAccessibility()
|
||||
}
|
||||
}
|
||||
|
||||
get hotTableSettings(): Handsontable.GridSettings {
|
||||
return {
|
||||
...this.hotTable.settings,
|
||||
colHeaders: this.hotTable.colHeaders,
|
||||
columns: this.hotTable.columns,
|
||||
maxRows: this.hotTable.maxRows,
|
||||
height: this.hotTable.height,
|
||||
licenseKey: this.hotTable.licenseKey,
|
||||
afterGetColHeader: this.hotTable.afterGetColHeader,
|
||||
afterInit: this.hotTable.afterInit,
|
||||
stretchH: 'all',
|
||||
cells: this.hotTable.cells,
|
||||
className: 'htDark'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +206,13 @@ export class StageComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
// Additional accessibility fixes after view is initialized
|
||||
setTimeout(() => {
|
||||
this.fixFocusCatcherAccessibility()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
revertChanges() {
|
||||
this.revertingChanges = true
|
||||
|
||||
@@ -204,4 +248,27 @@ export class StageComponent implements OnInit {
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
|
||||
private fixFocusCatcherAccessibility() {
|
||||
// Add labels to focus catcher inputs to fix accessibility issues
|
||||
setTimeout(() => {
|
||||
const focusCatchers = document.querySelectorAll('.htFocusCatcher')
|
||||
focusCatchers.forEach((input: any, index: number) => {
|
||||
if (input) {
|
||||
// Add proper accessibility attributes
|
||||
input.setAttribute('aria-label', `Table focus catcher ${index + 1}`)
|
||||
input.setAttribute('aria-hidden', 'true')
|
||||
input.setAttribute('tabindex', '-1')
|
||||
input.setAttribute('role', 'presentation')
|
||||
// Add a hidden label element
|
||||
const label = document.createElement('label')
|
||||
label.setAttribute('for', input.id || `htFocusCatcher${index}`)
|
||||
label.setAttribute('aria-hidden', 'true')
|
||||
label.style.display = 'none'
|
||||
label.textContent = `Table focus catcher ${index + 1}`
|
||||
input.parentNode?.insertBefore(label, input)
|
||||
}
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { StageComponent } from './stage.component'
|
||||
import { HotTableModule } from '@handsontable/angular'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
|
||||
|
@@ -598,6 +598,12 @@
|
||||
{{ libinfo[0] ? libinfo[0].TABLE_CNT : '' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="libinfo[0].CATALOG_CNT !== null">
|
||||
<td class="m-0">CATALOG_CNT:</td>
|
||||
<td class="m-0 font-bold">
|
||||
{{ libinfo[0] ? libinfo[0].CATALOG_CNT : '' }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -615,32 +621,16 @@
|
||||
</div>
|
||||
|
||||
<div *ngIf="!noData && !noDataReqErr && table" class="clr-flex-1">
|
||||
<hot-table
|
||||
hotId="hotInstance"
|
||||
id="hotTable"
|
||||
className="htDark"
|
||||
[multiColumnSorting]="true"
|
||||
[viewportRowRenderingOffset]="50"
|
||||
[data]="hotTable.data"
|
||||
[colHeaders]="hotTable.colHeaders"
|
||||
[columns]="hotTable.columns"
|
||||
[copyPaste]="hotTable.copyPaste"
|
||||
[contextMenu]="hotTable.contextMenu"
|
||||
[filters]="true"
|
||||
[dropdownMenu]="hotTable.dropdownMenu"
|
||||
[height]="hotTable.height"
|
||||
stretchH="all"
|
||||
[modifyColWidth]="maxWidthCheker"
|
||||
[cells]="hotTable.cells"
|
||||
[maxRows]="hotTable.maxRows"
|
||||
[manualColumnResize]="true"
|
||||
[afterGetColHeader]="hotTable.afterGetColHeader"
|
||||
[rowHeaders]="hotTable.rowHeaders"
|
||||
[rowHeaderWidth]="hotTable.rowHeaderWidth"
|
||||
[rowHeights]="hotTable.rowHeights"
|
||||
[licenseKey]="hotTable.licenseKey"
|
||||
>
|
||||
</hot-table>
|
||||
<div class="hot-wrapper clr-flex-1">
|
||||
<hot-table
|
||||
#hotInstance
|
||||
id="hotTable"
|
||||
class="view-hot"
|
||||
[data]="hotTable.data"
|
||||
[settings]="hotTableSettings"
|
||||
>
|
||||
</hot-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
@@ -3,9 +3,11 @@ import {
|
||||
AfterContentInit,
|
||||
ChangeDetectorRef,
|
||||
AfterViewInit,
|
||||
OnDestroy,
|
||||
ViewChildren,
|
||||
QueryList,
|
||||
ViewEncapsulation
|
||||
ViewEncapsulation,
|
||||
ViewChild
|
||||
} from '@angular/core'
|
||||
import { SasStoreService } from '../services/sas-store.service'
|
||||
import { Subscription } from 'rxjs'
|
||||
@@ -16,7 +18,7 @@ import { globals } from '../_globals'
|
||||
|
||||
import { EventService } from '../services/event.service'
|
||||
import { HelperService } from '../services/helper.service'
|
||||
import { HotTableRegisterer } from '@handsontable/angular'
|
||||
import { HotTableComponent } from '@handsontable/angular-wrapper'
|
||||
import { SasService } from '../services/sas.service'
|
||||
import { SASjsConfig } from '@sasjs/adapter'
|
||||
import { QueryComponent } from '../query/query.component'
|
||||
@@ -49,10 +51,15 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
||||
export class ViewerComponent
|
||||
implements AfterContentInit, AfterViewInit, OnDestroy
|
||||
{
|
||||
@ViewChildren('queryFilter')
|
||||
queryFilterCompList: QueryList<QueryComponent> = new QueryList()
|
||||
|
||||
@ViewChild('hotInstance', { static: false })
|
||||
hotInstanceViewChild!: Handsontable
|
||||
|
||||
public libraries!: Array<any>
|
||||
public librariesPaging: boolean = false
|
||||
public librariesSearch: string = ''
|
||||
@@ -95,7 +102,35 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
||||
public sasjsConfig: SASjsConfig = new SASjsConfig()
|
||||
public searchLoading: boolean = false
|
||||
public searchNumeric: boolean = false
|
||||
private hotTableRegisterer: HotTableRegisterer
|
||||
@ViewChild(HotTableComponent, { static: false })
|
||||
hotTableComponent!: HotTableComponent
|
||||
|
||||
public hotTableSettings: Handsontable.GridSettings = {}
|
||||
|
||||
private updateHotTableSettings(): void {
|
||||
this.hotTableSettings = {
|
||||
multiColumnSorting: true,
|
||||
viewportRowRenderingOffset: 30,
|
||||
colHeaders: this.hotTable.colHeaders,
|
||||
columns: this.hotTable.columns,
|
||||
copyPaste: this.hotTable.copyPaste,
|
||||
contextMenu: this.hotTable.contextMenu,
|
||||
filters: true,
|
||||
dropdownMenu: this.hotTable.dropdownMenu,
|
||||
height: this.hotTable.height,
|
||||
stretchH: 'all',
|
||||
modifyColWidth: this.maxWidthCheker,
|
||||
cells: this.hotTable.cells,
|
||||
maxRows: this.hotTable.maxRows,
|
||||
manualColumnResize: true,
|
||||
afterGetColHeader: this.hotTable.afterGetColHeader,
|
||||
rowHeaders: this.hotTable.rowHeaders,
|
||||
rowHeaderWidth: this.hotTable.rowHeaderWidth,
|
||||
rowHeights: this.hotTable.rowHeights,
|
||||
licenseKey: this.hotTable.licenseKey,
|
||||
className: 'htDark'
|
||||
}
|
||||
}
|
||||
public numberOfRows: number | null = null
|
||||
public headerPks: string[] = []
|
||||
public $dataFormats: $DataFormats | null = null
|
||||
@@ -107,11 +142,14 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
||||
public licenceState = this.licenceService.licenceState
|
||||
public Infinity = Infinity
|
||||
|
||||
private ariaObserver: MutationObserver | undefined
|
||||
private ariaCheckInterval: any | undefined
|
||||
|
||||
public hotTable: HotTableInterface = {
|
||||
data: [],
|
||||
colHeaders: [],
|
||||
columns: [],
|
||||
height: '100%',
|
||||
height: 'calc(100vh - 182px)',
|
||||
maxRows: this.licenceState.value.viewer_rows_allowed || Infinity,
|
||||
settings: {},
|
||||
licenseKey: undefined,
|
||||
@@ -119,6 +157,13 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
||||
return ' '
|
||||
},
|
||||
afterGetColHeader: (col: number, th: any, headerLevel: number) => {
|
||||
const column = this.hotInstance?.colToProp(col) as string
|
||||
|
||||
// header columns styling - primary keys
|
||||
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
||||
|
||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||
|
||||
// Dark mode
|
||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||
},
|
||||
@@ -193,7 +238,6 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
||||
private location: Location,
|
||||
private cdf: ChangeDetectorRef
|
||||
) {
|
||||
this.hotTableRegisterer = new HotTableRegisterer()
|
||||
this.sasjsConfig = this.sasService.getSasjsConfig()
|
||||
}
|
||||
|
||||
@@ -213,6 +257,7 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
||||
this.licenceService.hot_license_key.subscribe(
|
||||
(hot_license_key: string | undefined) => {
|
||||
this.hotTable.licenseKey = hot_license_key
|
||||
this.updateHotTableSettings() // Update settings when license key changes
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -530,7 +575,6 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
||||
`#search_${library.LIBRARYREF}`
|
||||
)
|
||||
|
||||
this.loggerService.log('[libTreeSearchInput]', libTreeSearchInput)
|
||||
if (libTreeSearchInput) libTreeSearchInput.focus()
|
||||
|
||||
if (library && library.libinfo) this.libinfo = library.libinfo
|
||||
@@ -819,27 +863,37 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
||||
this.versions = res.versions || []
|
||||
this.setDSNote()
|
||||
this.queryText = res.sasparams[0].FILTER_TEXT
|
||||
let columns: any[] = []
|
||||
let colArr = []
|
||||
|
||||
for (let key in res.viewdata[0]) {
|
||||
if (key) {
|
||||
colArr.push(key)
|
||||
// Initialize columns only if we have data
|
||||
if (res.viewdata && res.viewdata.length > 0) {
|
||||
let columns: any[] = []
|
||||
let colArr = []
|
||||
|
||||
for (let key in res.viewdata[0]) {
|
||||
if (key) {
|
||||
colArr.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = 0; index < colArr.length; index++) {
|
||||
columns.push({ data: colArr[index] })
|
||||
}
|
||||
|
||||
this.hotTable.colHeaders = colArr
|
||||
this.hotTable.columns = columns
|
||||
} else {
|
||||
// Set empty arrays if no data
|
||||
this.hotTable.colHeaders = []
|
||||
this.hotTable.columns = []
|
||||
}
|
||||
|
||||
for (let index = 0; index < colArr.length; index++) {
|
||||
columns.push({ data: colArr[index] })
|
||||
// Set cells function
|
||||
this.hotTable.cells = () => {
|
||||
return { readOnly: true }
|
||||
}
|
||||
|
||||
let cells = function () {
|
||||
let cellProperties = { readOnly: true }
|
||||
return cellProperties
|
||||
}
|
||||
|
||||
this.hotTable.colHeaders = colArr
|
||||
this.hotTable.columns = columns
|
||||
this.hotTable.cells = cells
|
||||
// Update hot table settings after data is loaded
|
||||
this.updateHotTableSettings()
|
||||
|
||||
this.tableFlag = false
|
||||
let ds = []
|
||||
@@ -911,6 +965,11 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
||||
//That is intorduced by HOT update
|
||||
if (!this.noData && !this.noDataReqErr && libDataset) this.setupHot()
|
||||
|
||||
// Fix ARIA accessibility issues after data loading
|
||||
setTimeout(() => {
|
||||
this.fixAriaAccessibility()
|
||||
}, 1500)
|
||||
|
||||
/**
|
||||
* This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area.
|
||||
* Without it, hot table does not steal focus, so it doesn't close the nav.
|
||||
@@ -1060,12 +1119,12 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
||||
private setupHot() {
|
||||
setTimeout(() => {
|
||||
if (!this.loadingTableView && this.libDataset) {
|
||||
this.hotInstance = this.hotTableRegisterer.getInstance('hotInstance')
|
||||
this.hotInstance = this.hotTableComponent?.hotInstance
|
||||
|
||||
if (this.hotInstance) {
|
||||
this.hotInstance.updateSettings({
|
||||
height: this.hotTable.height,
|
||||
modifyColWidth: function (width: any, col: any) {
|
||||
modifyColWidth: (width: any, col: any) => {
|
||||
if (width > 500) return 500
|
||||
else return width
|
||||
},
|
||||
@@ -1081,8 +1140,26 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||
}
|
||||
})
|
||||
|
||||
// Add hooks for accessibility fixes
|
||||
this.hotInstance.addHook('afterRender', () => {
|
||||
// Fix ARIA accessibility issues after each render
|
||||
this.fixAriaAccessibility()
|
||||
})
|
||||
|
||||
this.hotInstance.addHook('afterChange', () => {
|
||||
// Fix ARIA accessibility issues after any data change
|
||||
setTimeout(() => {
|
||||
this.fixAriaAccessibility()
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fix ARIA accessibility issues after table setup
|
||||
setTimeout(() => {
|
||||
this.fixAriaAccessibility()
|
||||
}, 500)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
@@ -1148,7 +1225,173 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit() {}
|
||||
ngAfterViewInit() {
|
||||
// Fix ARIA accessibility issues after table initialization
|
||||
setTimeout(() => {
|
||||
this.fixAriaAccessibility()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
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'
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async ngAfterContentInit() {
|
||||
if (this.hotTable.data.length > 0) {
|
||||
|
@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ViewerComponent } from './viewer.component'
|
||||
import { ViewRouteComponent } from '../routes/view-route/view-route.component'
|
||||
import { HotTableModule } from '@handsontable/angular'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { ViewerRoutingModule } from './viewer-routing.module'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
@@ -36,7 +36,7 @@ import { MetadataComponent } from '../metadata/metadata.component'
|
||||
ClipboardModule,
|
||||
FormsModule,
|
||||
ClarityModule,
|
||||
HotTableModule.forRoot(),
|
||||
HotTableModule,
|
||||
AppSharedModule,
|
||||
SharedModule,
|
||||
PipesModule,
|
||||
|
@@ -0,0 +1,34 @@
|
||||
export interface ComputeContextDetails {
|
||||
attributes?: Attributes
|
||||
createdBy: string
|
||||
creationTimeStamp: string
|
||||
description: string
|
||||
id: string
|
||||
launchContext: LaunchContext
|
||||
launchType: string
|
||||
links: Link[]
|
||||
modifiedBy: string
|
||||
modifiedTimeStamp: string
|
||||
name: string
|
||||
version: number
|
||||
}
|
||||
|
||||
export interface Link {
|
||||
method: string
|
||||
rel: string
|
||||
href: string
|
||||
uri: string
|
||||
type?: string
|
||||
responseType?: string
|
||||
itemType?: string
|
||||
}
|
||||
|
||||
export interface LaunchContext {
|
||||
contextId: string
|
||||
}
|
||||
|
||||
export interface Attributes {
|
||||
reuseServerProcesses: string
|
||||
runServerAs: string
|
||||
serverMinAvailable: string
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
export interface ViyaComputeContexts {
|
||||
accept: string
|
||||
count: number
|
||||
items: Item[]
|
||||
limit: number
|
||||
links: Link2[]
|
||||
name: string
|
||||
start: number
|
||||
version: number
|
||||
}
|
||||
|
||||
export interface Link2 {
|
||||
method: string
|
||||
rel: string
|
||||
href: string
|
||||
uri: string
|
||||
type: string
|
||||
itemType: string
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
createdBy: string
|
||||
id: string
|
||||
links: Link[]
|
||||
name: string
|
||||
version: number
|
||||
}
|
||||
|
||||
export interface Link {
|
||||
method: string
|
||||
rel: string
|
||||
href: string
|
||||
uri: string
|
||||
type?: string
|
||||
responseType?: string
|
||||
}
|
@@ -125,30 +125,9 @@
|
||||
|
||||
<div class="clr-flex-1">
|
||||
<hot-table
|
||||
hotId="hotInstance"
|
||||
id="hot-table"
|
||||
className="htDark"
|
||||
[multiColumnSorting]="true"
|
||||
[viewportRowRenderingOffset]="50"
|
||||
[data]="selectedTab === TabsEnum.Rules ? xlmapRules : xlData"
|
||||
[colHeaders]="
|
||||
selectedTab === TabsEnum.Rules ? xlmapRulesHeaders : xlUploadHeader
|
||||
"
|
||||
[columns]="
|
||||
selectedTab === TabsEnum.Rules ? xlmapRulesColumns : xlUploadColumns
|
||||
"
|
||||
[filters]="true"
|
||||
[height]="'100%'"
|
||||
stretchH="all"
|
||||
[afterGetColHeader]="afterGetColHeader"
|
||||
[modifyColWidth]="maxWidthChecker"
|
||||
[cells]="getCellConfiguration"
|
||||
[maxRows]="hotTableMaxRows"
|
||||
[manualColumnResize]="true"
|
||||
[rowHeaders]="rowHeaders"
|
||||
[rowHeaderWidth]="15"
|
||||
[rowHeights]="20"
|
||||
[licenseKey]="hotTableLicenseKey"
|
||||
[settings]="hotTableSettings"
|
||||
>
|
||||
</hot-table>
|
||||
</div>
|
||||
|
@@ -23,6 +23,7 @@ import {
|
||||
} from '../services'
|
||||
import { getCellAddress, getFinishingCell } from './utils/xl.utils'
|
||||
import { blobToFile, byteArrayToBinaryString } from './utils/file.utils'
|
||||
import Handsontable from 'handsontable'
|
||||
import { UploadFileResponse } from '../models/UploadFile'
|
||||
|
||||
interface XLMapRule {
|
||||
@@ -136,6 +137,34 @@ export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
|
||||
public hotTableMaxRows =
|
||||
this.licenceState.value.viewer_rows_allowed || Infinity
|
||||
|
||||
get hotTableSettings(): Handsontable.GridSettings {
|
||||
return {
|
||||
multiColumnSorting: true,
|
||||
viewportRowRenderingOffset: 50,
|
||||
colHeaders:
|
||||
this.selectedTab === this.TabsEnum.Rules
|
||||
? this.xlmapRulesHeaders
|
||||
: this.xlUploadHeader,
|
||||
columns:
|
||||
this.selectedTab === this.TabsEnum.Rules
|
||||
? this.xlmapRulesColumns
|
||||
: this.xlUploadColumns,
|
||||
filters: true,
|
||||
height: '100%',
|
||||
stretchH: 'all',
|
||||
afterGetColHeader: this.afterGetColHeader,
|
||||
modifyColWidth: this.maxWidthChecker,
|
||||
cells: this.getCellConfiguration,
|
||||
maxRows: this.hotTableMaxRows,
|
||||
manualColumnResize: true,
|
||||
rowHeaders: this.rowHeaders,
|
||||
rowHeaderWidth: 15,
|
||||
rowHeights: 20,
|
||||
licenseKey: this.hotTableLicenseKey,
|
||||
className: 'htDark'
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private eventService: EventService,
|
||||
private licenceService: LicenceService,
|
||||
|
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
import { HotTableModule } from '@handsontable/angular'
|
||||
import { HotTableModule } from '@handsontable/angular-wrapper'
|
||||
import { registerAllModules } from 'handsontable/registry'
|
||||
import { AppSharedModule } from '../app-shared.module'
|
||||
import { DirectivesModule } from '../directives/directives.module'
|
||||
|
@@ -45,7 +45,7 @@
|
||||
|
||||
<sasjs
|
||||
serverUrl=""
|
||||
appLoc="/Public/app/dc"
|
||||
appLoc="/Public/app/devtest"
|
||||
serverType="SASJS"
|
||||
loginMechanism="Redirected"
|
||||
debug="false"
|
||||
|
@@ -937,11 +937,6 @@ app-multi-dataset {
|
||||
.dataset-input-wrapper {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
|
||||
textarea {
|
||||
min-height: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-reason {
|
||||
@@ -1042,12 +1037,12 @@ app-approve {
|
||||
// HISTORY.COMPONENT
|
||||
app-history {
|
||||
.rejected {
|
||||
color: #f83126;
|
||||
color: #92201a;
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
.accepted {
|
||||
color: #3fc424;
|
||||
color: #105c26;
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
@@ -1206,6 +1201,8 @@ app-viewer {
|
||||
}
|
||||
|
||||
hot-table {
|
||||
height: calc(100vh - 200px);
|
||||
|
||||
.handsontable tbody th.ht__highlight, .handsontable thead th.ht__highlight {
|
||||
&.primaryKeyHeaderStyle {
|
||||
background-color: #306b00b0 !important;
|
||||
@@ -3442,6 +3439,10 @@ app-approve-details {
|
||||
width: 175px
|
||||
}
|
||||
|
||||
#rejectBtn {
|
||||
background-color: #a62f16 !important;
|
||||
}
|
||||
|
||||
.formatted-values-toggle {
|
||||
min-width: 75px
|
||||
}
|
||||
@@ -3630,12 +3631,12 @@ app-excel-password-modal {
|
||||
// STAGE.COMPONENT
|
||||
app-stage {
|
||||
.rejected {
|
||||
color: #f83126;
|
||||
color: #92201a;
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
.accepted {
|
||||
color: #3fc424;
|
||||
color: #105c26;
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
@@ -3644,6 +3645,25 @@ app-stage {
|
||||
margin-top:10px;
|
||||
color: #007cbb;
|
||||
}
|
||||
|
||||
// Accessibility fixes for handsontable focus catchers
|
||||
.htFocusCatcher {
|
||||
// Hide from screen readers but maintain functionality
|
||||
position: absolute !important;
|
||||
left: -9999px !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
border: 0 !important;
|
||||
margin: -1px !important;
|
||||
padding: 0 !important;
|
||||
|
||||
// Ensure it's not focusable by screen readers
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body[cds-theme="dark"] {
|
||||
@@ -3884,6 +3904,12 @@ app-header-actions {
|
||||
|
||||
// END OF CSP WORKAROUND
|
||||
|
||||
.clr-input-wrapper.small-mt {
|
||||
.clr-form-control {
|
||||
margin-top: 5px !important;
|
||||
}
|
||||
}
|
||||
|
||||
body[cds-theme="dark"] {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: $trackColor $thumbColor;
|
||||
@@ -4740,7 +4766,6 @@ body[cds-theme="dark"] {
|
||||
}
|
||||
|
||||
.handsontable.listbox {
|
||||
padding: 5px 0px 5px 5px;
|
||||
box-shadow: 0px 4px 20px 0px #00000070;
|
||||
}
|
||||
|
||||
@@ -4750,6 +4775,12 @@ body[cds-theme="dark"] {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.handsontable td.dc-invalid-cell {
|
||||
background: #e62700ad !important;
|
||||
border: 1px solid red !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.handsontable .numericListbox {
|
||||
text-align: right;
|
||||
}
|
||||
|
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dcfrontend",
|
||||
"version": "6.14.7",
|
||||
"version": "7.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dcfrontend",
|
||||
"version": "6.14.7",
|
||||
"version": "7.1.1",
|
||||
"hasInstallScript": true,
|
||||
"devDependencies": {
|
||||
"@saithodev/semantic-release-gitea": "^2.1.0",
|
||||
@@ -16,7 +16,7 @@
|
||||
"@semantic-release/npm": "11.0.0",
|
||||
"@semantic-release/release-notes-generator": "^11.0.4",
|
||||
"commit-and-tag-version": "^11.2.2",
|
||||
"prettier": "3.2.5"
|
||||
"prettier": "3.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -6690,9 +6690,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
|
||||
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
18
package.json
18
package.json
@@ -1,25 +1,28 @@
|
||||
{
|
||||
"name": "dcfrontend",
|
||||
"version": "6.14.10",
|
||||
"version": "7.2.1",
|
||||
"description": "Data Controller",
|
||||
"devDependencies": {
|
||||
"@saithodev/semantic-release-gitea": "^2.1.0",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/commit-analyzer": "^10.0.1",
|
||||
"@semantic-release/npm": "11.0.0",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/npm": "11.0.0",
|
||||
"@semantic-release/release-notes-generator": "^11.0.4",
|
||||
"commit-and-tag-version": "^11.2.2",
|
||||
"prettier": "3.2.5"
|
||||
"prettier": "3.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
"install": "cd client && npm i && cd ../sas && npm i",
|
||||
"build-frontend": "cd client && npm run build",
|
||||
"release": "commit-and-tag-version",
|
||||
"lint": "npm run lint:fix",
|
||||
"lint:fix": "npx prettier --write \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/integration/*.tests.ts\"",
|
||||
"lint:check": "npx prettier --check \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/integration/*.tests.ts\"",
|
||||
"jo": "echo"
|
||||
"lint:fix": "npx prettier --write \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/e2e/*.cy.ts\"",
|
||||
"lint:fix:silent": "npx prettier --log-level silent --write \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/e2e/*.cy.ts\"",
|
||||
"lint:check": "npx prettier --check \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/e2e/*.cy.ts\"",
|
||||
"lint:check:silent": "npx prettier --log-level silent --check \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/e2e/*.cy.ts\"",
|
||||
"jo": "echo",
|
||||
"prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks && git config core.autocrlf false || true"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -29,5 +32,6 @@
|
||||
"//": [
|
||||
"Readme",
|
||||
"We must set private: true so that semantic-release/npm plugin will update the package.json version but not try to release it as NPM package"
|
||||
]
|
||||
],
|
||||
"dependencies": {}
|
||||
}
|
||||
|
@@ -13,6 +13,12 @@ if (fs.existsSync(sessionStoragePath)){
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
let controlTableText = ''
|
||||
|
||||
if (_WEBIN_FILENAME1.includes('SASControlTable')) controlTableText = _WEBIN_FILEREF1.toString()
|
||||
|
||||
|
||||
|
||||
let webouts = {
|
||||
MPE_X_TEST: `{"SYSDATE" : "26SEP22"
|
||||
,"SYSTIME" : "08:48"
|
||||
|
213
sas/package-lock.json
generated
213
sas/package-lock.json
generated
@@ -6,8 +6,8 @@
|
||||
"": {
|
||||
"name": "dc-sas",
|
||||
"dependencies": {
|
||||
"@sasjs/cli": "^4.12.5",
|
||||
"@sasjs/core": "^4.58.1"
|
||||
"@sasjs/cli": "^4.12.10",
|
||||
"@sasjs/core": "^4.59.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@coolaj86/urequest": {
|
||||
@@ -30,29 +30,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/adapter": {
|
||||
"version": "4.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.11.3.tgz",
|
||||
"integrity": "sha512-KF6G4vzs4l4efjpCD02og3kB44uFfJ1u2UWu749VdHtLKNN9l+PO26/moR+YAmRmmz2I9sC3X09fZE1nlN6zgw==",
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.12.2.tgz",
|
||||
"integrity": "sha512-OV5mx3N2Hywhp0M+SBLTuG42x/LDnMxrb2/pwG4RQbhfzvAwdHVEoXRouDJ49RMSY9s6TJcwUPh+Xzafl5sG/g==",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "3.5.2",
|
||||
"axios": "1.8.2",
|
||||
"axios-cookiejar-support": "5.0.5",
|
||||
"form-data": "4.0.0",
|
||||
"form-data": "4.0.4",
|
||||
"https": "1.0.0",
|
||||
"tough-cookie": "4.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/cli": {
|
||||
"version": "4.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.12.5.tgz",
|
||||
"integrity": "sha512-y6JFATKlTyTl0gRPpDBPL1rwZsyeuyp5uEz7HMA7raSzQuNa6QZ1oO1Er91I7+cLUg0Ndh5aSNGKYOdBRStQ2g==",
|
||||
"version": "4.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.12.10.tgz",
|
||||
"integrity": "sha512-eWh7cTtIEH9PnKdooRWZ8yWVle4jPb0D7k+QKaPSBIW+ZbtyS87yNVU4MwkonSKCHneuhvrMMo8YV8QrqoUo7Q==",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "4.11.3",
|
||||
"@sasjs/core": "4.57.0",
|
||||
"@sasjs/adapter": "4.12.2",
|
||||
"@sasjs/core": "4.59.1",
|
||||
"@sasjs/lint": "2.4.3",
|
||||
"@sasjs/utils": "3.5.2",
|
||||
"adm-zip": "0.5.10",
|
||||
@@ -77,15 +77,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/cli/node_modules/@sasjs/core": {
|
||||
"version": "4.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.57.0.tgz",
|
||||
"integrity": "sha512-iJiLnW4oY15InGerXXWtrjc1YpJ9UDz72+r7Odfr/yYR7RxIhtXGjYQIqyQu6US+cS/0b2pi12LZB6VnfMS/pA==",
|
||||
"version": "4.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.59.1.tgz",
|
||||
"integrity": "sha512-52GNI4nIll5YivI8uobWrucE6TkHcTjcbKTr/YPC9eTauC4sh0V0MptebfAJ5E6vE5P2WevNZGr42KdDpckLpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sasjs/core": {
|
||||
"version": "4.58.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.58.1.tgz",
|
||||
"integrity": "sha512-Qp6KAtp1VZcmN5HLGSIUE9H41qpFuihWLbjNygOYp+NRs/Y8VagpHrYeyIQbh3cSgchiJEMXudLql8hoU06wpg==",
|
||||
"version": "4.59.5",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.59.5.tgz",
|
||||
"integrity": "sha512-PAPinHjuFi3P/MxkZHGSF0iLWoBFnpxon7/SshHPcBRmSQwmFOvh9X8xdE8ZClqY9XDOwFZ6sqeOQ7GYZliLfw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sasjs/lint": {
|
||||
@@ -308,9 +308,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -347,6 +348,19 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -561,6 +575,20 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
@@ -577,6 +605,51 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
@@ -620,12 +693,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
@@ -667,6 +743,43 @@
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
@@ -687,6 +800,18 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -701,6 +826,33 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -748,9 +900,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-cookie-agent/node_modules/agent-base": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
@@ -1054,6 +1206,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
|
@@ -22,13 +22,13 @@
|
||||
"serverdata": "sasjs request services/admin/makedata -d deploy/makeDataServer.json -l sasjsresults/makedata_server.log -o sasjsresults/makedata_server.json -t server",
|
||||
"viya": "npm run viyacbd && sasjs test",
|
||||
"viyacbd": "sasjs folder delete /Public/app/viya && sasjs cbd -t viya && npm run viyadata",
|
||||
"viyadata": "sasjs request services/admin/makedata -d deploy/makeDataViya.json -l sasjsresults/makedata.log -o sasjsresults/output.json",
|
||||
"viyadata": "sasjs request services/admin/makedata -d deploy/makeDataViya.json -l sasjsresults/makedata.log -o sasjsresults/output.json -t viya",
|
||||
"v4data": "sasjs request services/admin/makedata -d deploy/makeDataV4.json -l sasjsresults/makedatav4.log -o sasjsresults/outputv4.json -t v4",
|
||||
"sasdocs": "sasjs doc && ./sasjs/utils/deploydocs.sh"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/cli": "^4.12.5",
|
||||
"@sasjs/core": "^4.58.1"
|
||||
"@sasjs/cli": "^4.12.10",
|
||||
"@sasjs/core": "^4.59.5"
|
||||
}
|
||||
}
|
||||
|
17
sas/sasjs/db/datactrl/mpe_datacatalog_cats.ddl
Normal file
17
sas/sasjs/db/datactrl/mpe_datacatalog_cats.ddl
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
@file mpe_datacatalog_CATS.ddl
|
||||
@brief ddl file
|
||||
@details
|
||||
|
||||
@version 9.3
|
||||
@author 4GL Apps Ltd
|
||||
@copyright 4GL Apps Ltd
|
||||
**/
|
||||
|
||||
create table &curlib..mpe_datacatalog_CATS(
|
||||
TX_FROM float format=datetime19.,
|
||||
TX_TO float format=datetime19.,
|
||||
libref char(8) label='Library Name',
|
||||
memname char(64) label='Member Name',
|
||||
constraint pk_mpe_datacatalog_CATS
|
||||
primary key(libref,memname,tx_to));
|
21
sas/sasjs/db/datactrl/mpe_datacatalog_objs.ddl
Normal file
21
sas/sasjs/db/datactrl/mpe_datacatalog_objs.ddl
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
@file mpe_datacatalog_CATS.ddl
|
||||
@brief ddl file
|
||||
@details
|
||||
|
||||
@version 9.3
|
||||
@author 4GL Apps Ltd
|
||||
@copyright 4GL Apps Ltd
|
||||
**/
|
||||
|
||||
create table &curlib..mpe_datacatalog_OBJS(
|
||||
TX_FROM float format=datetime19.,
|
||||
TX_TO float format=datetime19.,
|
||||
libref char(8) label='Library Name',
|
||||
memname char(64) label='Member Name',
|
||||
objname char(32) label='Object Name',
|
||||
objtype char(8) label='Object Type',
|
||||
objdesc char(256) label='Object Description',
|
||||
alias char(32) label='Object Alias',
|
||||
constraint pk_mpe_datacatalog_OBJS
|
||||
primary key(libref,memname,objname,objtype,tx_to));
|
20
sas/sasjs/db/datactrl/mpe_datastatus_cats.ddl
Normal file
20
sas/sasjs/db/datactrl/mpe_datastatus_cats.ddl
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
@file mpe_datacatalog_CATS.ddl
|
||||
@brief ddl file
|
||||
@details
|
||||
|
||||
@version 9.3
|
||||
@author 4GL Apps Ltd
|
||||
@copyright 4GL Apps Ltd
|
||||
**/
|
||||
|
||||
create table &curlib..mpe_datastatus_CATS(
|
||||
TX_FROM float format=datetime19.,
|
||||
TX_TO float format=datetime19.,
|
||||
libref char(8) label='Library Name',
|
||||
memname char(64) label='Member Name',
|
||||
nobjs num label='Number of objects',
|
||||
created num format=DATETIME. label='Date Created',
|
||||
modified num format=DATETIME. label='Date Modified',
|
||||
constraint pk_mpe_datastatus_CATS
|
||||
primary key(libref,memname,tx_to));
|
@@ -14,5 +14,6 @@ create table &curlib..mpe_datastatus_libs(
|
||||
libref char(8) label='Library Name',
|
||||
libsize num format=SIZEKMG. label='Size of file',
|
||||
table_cnt num label='Number of Tables',
|
||||
catalog_cnt num label='Number of Catalogs',
|
||||
constraint pk_mpe_datastatus_libs
|
||||
primary key(libref,tx_to));
|
||||
|
22
sas/sasjs/db/datactrl/mpe_datastatus_objs.ddl
Normal file
22
sas/sasjs/db/datactrl/mpe_datastatus_objs.ddl
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
@file mpe_datacatalog_CATS.ddl
|
||||
@brief ddl file
|
||||
@details
|
||||
|
||||
@version 9.3
|
||||
@author 4GL Apps Ltd
|
||||
@copyright 4GL Apps Ltd
|
||||
**/
|
||||
|
||||
create table &curlib..mpe_datastatus_OBJS(
|
||||
TX_FROM float format=datetime19.,
|
||||
TX_TO float format=datetime19.,
|
||||
libref char(8) label='Library Name',
|
||||
memname char(64) label='Member Name',
|
||||
objname char(32) label='Object Name',
|
||||
objtype char(8) label='Object Type',
|
||||
created num format=DATETIME. label='Date Created',
|
||||
modified num format=DATETIME. label='Date Modified',
|
||||
level num label='Library Concatenation Level',
|
||||
constraint pk_mpe_datastatus_OBJS
|
||||
primary key(libref,memname,objname,objtype,tx_to));
|
@@ -14,8 +14,8 @@ create table &curlib..mpe_datastatus_tabs(
|
||||
libref char(8) label='Library Name',
|
||||
dsn char(64) label='Member Name',
|
||||
filesize num format=SIZEKMG. label='Size of file',
|
||||
crdate num format=DATETIME. informat=DATETIME. label='Date Created',
|
||||
modate num format=DATETIME. informat=DATETIME. label='Date Modified',
|
||||
crdate num format=DATETIME. label='Date Created',
|
||||
modate num format=DATETIME. label='Date Modified',
|
||||
nobs num label='Number of Physical (Actual, inc. deleted) Observations',
|
||||
constraint pk_mpe_datastatus_tabs
|
||||
primary key(libref,dsn,tx_to));
|
||||
|
@@ -29,7 +29,7 @@ create table dc.mpe_datacatalog_TABS(
|
||||
nvar num label='Number of Variables',
|
||||
compress char(8) label='Compression Routine',
|
||||
pk_fields char(512)
|
||||
label='Primary Key Fields (identified by being in a constraint that is both Unique and Not Null)',
|
||||
label='Primary Key Fields (in a constraint that is both Unique and Not Null)',
|
||||
constraint pk
|
||||
primary key(libref,dsn,tx_to));
|
||||
|
||||
@@ -67,8 +67,8 @@ create table dc.mpe_datacatalog_vars(
|
||||
libref char(8) label='Library Name',
|
||||
dsn char(64) label='Member Name',
|
||||
filesize num format=SIZEKMG. label='Size of file',
|
||||
crdate num format=DATETIME. informat=DATETIME. label='Date Created',
|
||||
modate num format=DATETIME. informat=DATETIME. label='Date Modified',
|
||||
crdate num format=DATETIME. label='Date Created',
|
||||
modate num format=DATETIME. label='Date Modified',
|
||||
nobs num label='Number of Physical (Actual, inc. deleted) Observations',
|
||||
constraint pk
|
||||
primary key(libref,dsn,tx_to));
|
71
sas/sasjs/db/migrations/20250611_v7.0_release.sas
Normal file
71
sas/sasjs/db/migrations/20250611_v7.0_release.sas
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
@file
|
||||
@brief migration script to move from v6.8.2 to v7.0 of data controller
|
||||
|
||||
BREAKING CHANGE - 1 new column and 4 additional tables for capturing catalogs
|
||||
|
||||
Be sure to run this using the correct system account
|
||||
(eg the regular DC account)
|
||||
|
||||
On SAS 9 you may wish to run proc metalib or refresh in DI Studio afterwards
|
||||
to see the new tables in the VIEW page
|
||||
|
||||
proc metalib;
|
||||
omr (library="&DC_LIBNAME");
|
||||
folder="&root/data";
|
||||
update_rule=(delete);
|
||||
run;
|
||||
|
||||
**/
|
||||
|
||||
%let dclib=YOURDCLIB;
|
||||
|
||||
libname &dclib "/YOUR/DATACONTROLLER/LIBRARY/PATH";
|
||||
|
||||
proc sql;
|
||||
create table work.BACKUP as select * from &dclib..mpe_datastatus_libs;
|
||||
alter table &dclib..mpe_datastatus_libs add catalog_cnt num;
|
||||
|
||||
create table &dclib..mpe_datacatalog_CATS(
|
||||
TX_FROM float format=datetime19.,
|
||||
TX_TO float format=datetime19.,
|
||||
libref char(8) label='Library Name',
|
||||
memname char(64) label='Member Name',
|
||||
constraint pk_mpe_datacatalog_CATS
|
||||
primary key(libref,memname,tx_to));
|
||||
|
||||
create table &dclib..mpe_datacatalog_OBJS(
|
||||
TX_FROM float format=datetime19.,
|
||||
TX_TO float format=datetime19.,
|
||||
libref char(8) label='Library Name',
|
||||
memname char(64) label='Member Name',
|
||||
objname char(32) label='Object Name',
|
||||
objtype char(8) label='Object Type',
|
||||
objdesc char(256) label='Object Description',
|
||||
alias char(32) label='Object Alias',
|
||||
constraint pk_mpe_datacatalog_OBJS
|
||||
primary key(libref,memname,objname,objtype,tx_to));
|
||||
|
||||
create table &dclib..mpe_datastatus_CATS(
|
||||
TX_FROM float format=datetime19.,
|
||||
TX_TO float format=datetime19.,
|
||||
libref char(8) label='Library Name',
|
||||
memname char(64) label='Member Name',
|
||||
nobjs num label='Number of objects',
|
||||
created num format=DATETIME. label='Date Created',
|
||||
modified num format=DATETIME. label='Date Modified',
|
||||
constraint pk_mpe_datastatus_CATS
|
||||
primary key(libref,memname,tx_to));
|
||||
|
||||
create table &dclib..mpe_datastatus_OBJS(
|
||||
TX_FROM float format=datetime19.,
|
||||
TX_TO float format=datetime19.,
|
||||
libref char(8) label='Library Name',
|
||||
memname char(64) label='Member Name',
|
||||
objname char(32) label='Object Name',
|
||||
objtype char(8) label='Object Type',
|
||||
created num format=DATETIME. label='Date Created',
|
||||
modified num format=DATETIME. label='Date Modified',
|
||||
level num label='Library Concatenation Level',
|
||||
constraint pk_mpe_datastatus_OBJS
|
||||
primary key(libref,memname,objname,objtype,tx_to));
|
@@ -14,14 +14,31 @@
|
||||
**/
|
||||
|
||||
%macro mpe_dsmeta(libds, outds=dsmeta);
|
||||
%local ddsd ddld notes lenstmt;
|
||||
%local ddsd ddld notes lenstmt memname;
|
||||
%let lenstmt=length ods_table $18 name $100 value $1000;
|
||||
%let libds=%upcase(&libds);
|
||||
%mp_dsmeta(&libds, outds=&outds)
|
||||
|
||||
%if "%scan(&libds,2,-)" ne "FC" %then %do;
|
||||
%let memname=%scan(&libds,2,.);
|
||||
%mp_dsmeta(&libds, outds=&outds)
|
||||
%end;
|
||||
%else %do;
|
||||
%let memname=%scan(&libds,2,.-);
|
||||
data &outds;
|
||||
&lenstmt;
|
||||
set sashelp.vcatalg;
|
||||
ods_table=cats(objtype);
|
||||
name=cats(objname);
|
||||
value=catx(' ',objdesc,'(modified:',put(modified,datetime19.),')');
|
||||
where libname="%scan(&libds,1,.)" and memname="&memname";
|
||||
keep ods_table name value;
|
||||
run;
|
||||
proc sort; by ods_table name;run;
|
||||
%end;
|
||||
|
||||
data _null_;
|
||||
set &mpelib..mpe_datadictionary;
|
||||
where &dc_dttmtfmt < tx_to & dd_source=%upcase("&libds") & dd_type='TABLE';
|
||||
where &dc_dttmtfmt < tx_to & dd_source="&memname" & dd_type='TABLE';
|
||||
call symputx('ddsd',dd_shortdesc,'l');
|
||||
call symputx('ddld',dd_longdesc,'l');
|
||||
run;
|
||||
|
@@ -34,4 +34,15 @@ run;
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
%mpe_dsmeta(FMTONLY.DCFMTS-FC,outds=test2)
|
||||
data work.test2;
|
||||
set work.test2;
|
||||
putlog (_all_)(=);
|
||||
run;
|
||||
|
||||
%mp_assertdsobs(work.test2,
|
||||
desc=Test 2 - records returned,
|
||||
test=EQUALS 5,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
|
@@ -106,6 +106,19 @@ proc datasets lib=&lib noprint;
|
||||
/nomiss unique;
|
||||
quit;
|
||||
proc sql;
|
||||
create table &lib..mpe_datacatalog_CATS(
|
||||
TX_FROM float ¬null format=datetime19.,
|
||||
TX_TO float format=datetime19.,
|
||||
libref char(8) label='Library Name',
|
||||
memname char(64) label='Member Name'
|
||||
);quit;
|
||||
proc datasets lib=&lib noprint;
|
||||
modify mpe_datacatalog_CATS;
|
||||
index create
|
||||
pk_mpe_datacatalog_CATS=(tx_to libref memname)
|
||||
/nomiss unique;
|
||||
quit;
|
||||
proc sql;
|
||||
create table &lib..mpe_datacatalog_libs(
|
||||
TX_FROM num ¬null format=datetime19.3,
|
||||
TX_TO num ¬null format=datetime19.3,
|
||||
@@ -125,6 +138,23 @@ proc datasets lib=&lib noprint;
|
||||
/nomiss unique;
|
||||
quit;
|
||||
proc sql;
|
||||
create table &lib..mpe_datacatalog_OBJS(
|
||||
TX_FROM num ¬null format=datetime19.,
|
||||
TX_TO num ¬null format=datetime19.,
|
||||
libref char(8) ¬null label='Library Name',
|
||||
memname char(64) ¬null label='Member Name',
|
||||
objname char(32) ¬null label='Object Name',
|
||||
objtype char(8) ¬null label='Object Type',
|
||||
objdesc char(256) label='Object Description',
|
||||
alias char(32) label='Object Alias'
|
||||
);quit;
|
||||
proc datasets lib=&lib noprint;
|
||||
modify mpe_datacatalog_OBJS;
|
||||
index create
|
||||
pk_mpe_datacatalog_OBJS=(libref memname objname objtype tx_to)
|
||||
/nomiss unique;
|
||||
quit;
|
||||
proc sql;
|
||||
create table &lib..mpe_datacatalog_TABS(
|
||||
TX_FROM num ¬null format=datetime19.3,
|
||||
TX_TO num ¬null format=datetime19.3,
|
||||
@@ -137,7 +167,7 @@ create table &lib..mpe_datacatalog_TABS(
|
||||
nvar num label='Number of Variables',
|
||||
compress char(8) label='Compression Routine',
|
||||
pk_fields char(512)
|
||||
label='Primary Key Fields (identified by being in a constraint that is both Unique and Not Null)'
|
||||
label='Primary Key Fields - in a constraint being both Unique and Not Null'
|
||||
);quit;
|
||||
proc datasets lib=&lib noprint;
|
||||
modify mpe_datacatalog_TABS;
|
||||
@@ -169,12 +199,29 @@ proc datasets lib=&lib noprint;
|
||||
/nomiss unique;
|
||||
quit;
|
||||
proc sql;
|
||||
create table &lib..mpe_datastatus_CATS(
|
||||
TX_FROM float format=datetime19.,
|
||||
TX_TO float format=datetime19.,
|
||||
libref char(8) label='Library Name',
|
||||
memname char(64) label='Member Name',
|
||||
nobjs num ¬null label='Number of objects',
|
||||
created num ¬null format=DATETIME. label='Date Created',
|
||||
modified num format=DATETIME. label='Date Modified'
|
||||
);quit;
|
||||
proc datasets lib=&lib noprint;
|
||||
modify mpe_datastatus_CATS;
|
||||
index create
|
||||
pk_mpe_datastatus_cats=(libref memname tx_to)
|
||||
/nomiss unique;
|
||||
quit;
|
||||
proc sql;
|
||||
create table &lib..mpe_datastatus_libs(
|
||||
TX_FROM num ¬null format=datetime19.3,
|
||||
TX_TO num ¬null format=datetime19.3,
|
||||
libref char(8) label='Library Name',
|
||||
libsize num format=SIZEKMG. label='Size of library',
|
||||
table_cnt num label='Number of Tables'
|
||||
table_cnt num label='Number of Tables',
|
||||
catalog_cnt num label='Number of Catalogs'
|
||||
);quit;
|
||||
proc datasets lib=&lib noprint;
|
||||
modify mpe_datastatus_libs;
|
||||
@@ -183,14 +230,32 @@ proc datasets lib=&lib noprint;
|
||||
/nomiss unique;
|
||||
quit;
|
||||
proc sql;
|
||||
create table &lib..mpe_datastatus_OBJS(
|
||||
TX_FROM float ¬null format=datetime19.,
|
||||
TX_TO float ¬null format=datetime19.,
|
||||
libref char(8) label='Library Name',
|
||||
memname char(64) label='Member Name',
|
||||
objname char(32) label='Object Name',
|
||||
objtype char(8) label='Object Type',
|
||||
created num ¬null format=DATETIME. label='Date Created',
|
||||
modified num format=DATETIME. label='Date Modified',
|
||||
level num label='Library Concatenation Level'
|
||||
);quit;
|
||||
proc datasets lib=&lib noprint;
|
||||
modify mpe_datastatus_OBJS;
|
||||
index create
|
||||
pk_mpe_datastatus_OBJS=(libref memname objname objtype tx_to)
|
||||
/nomiss unique;
|
||||
quit;
|
||||
proc sql;
|
||||
create table &lib..mpe_datastatus_tabs(
|
||||
TX_FROM num ¬null format=datetime19.3,
|
||||
TX_TO num ¬null format=datetime19.3,
|
||||
libref char(8) label='Library Name',
|
||||
dsn char(64) label='Member Name',
|
||||
filesize num format=SIZEKMG. label='Size of file',
|
||||
crdate num format=DATETIME. informat=DATETIME. label='Date Created',
|
||||
modate num format=DATETIME. informat=DATETIME. label='Date Modified',
|
||||
crdate num format=DATETIME. label='Date Created',
|
||||
modate num format=DATETIME. label='Date Modified',
|
||||
nobs num label='Number of Physical (Actual, inc. deleted) Observations'
|
||||
);quit;
|
||||
proc datasets lib=&lib noprint;
|
||||
|
26
sas/sasjs/macros/mpe_makedatamodel.test.sas
Normal file
26
sas/sasjs/macros/mpe_makedatamodel.test.sas
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
@file
|
||||
@brief Testing mpe_refreshtables macro
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mpe_makedatamodel.sas
|
||||
@li mp_assert.sas
|
||||
@li mp_assertscope.sas
|
||||
|
||||
@author 4GL Apps Ltd
|
||||
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
|
||||
and may not be re-distributed or re-sold without the express permission of
|
||||
4GL Apps Ltd.
|
||||
|
||||
**/
|
||||
|
||||
%mp_assertscope(SNAPSHOT)
|
||||
%mpe_makedatamodel(lib=WORK)
|
||||
%mp_assertscope(COMPARE,
|
||||
desc=Checking macro variables against previous snapshot
|
||||
)
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&syscc = 0),
|
||||
desc=Checking error condition
|
||||
)
|
140
sas/sasjs/macros/mpe_refreshcatalogs.sas
Normal file
140
sas/sasjs/macros/mpe_refreshcatalogs.sas
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
@file mpe_refreshtables.sas
|
||||
@brief Refreshes the data catalog
|
||||
@details Assumes library is already assigned.
|
||||
Usage:
|
||||
|
||||
%mpe_refreshcatalogs(sashelp)
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li bitemporal_dataloader.sas
|
||||
|
||||
@version 9.3
|
||||
@author 4GL Apps Ltd
|
||||
**/
|
||||
|
||||
%macro mpe_refreshcatalogs(lib,cat=#all);
|
||||
%let lib=%upcase(&lib);
|
||||
%let cat=%upcase(&cat);
|
||||
|
||||
%put running &sysmacroname &lib for &cat;
|
||||
proc sql;
|
||||
create table work.catdata as
|
||||
select libname as libref,
|
||||
memname,
|
||||
objname,
|
||||
objtype,
|
||||
objdesc,
|
||||
created,
|
||||
modified,
|
||||
alias,
|
||||
level
|
||||
from dictionary.catalogs
|
||||
where upcase(libname)="&lib"
|
||||
%if &cat ne #ALL %then %do;
|
||||
and upcase(memname)="&cat"
|
||||
%end;
|
||||
;
|
||||
|
||||
%mp_abort(iftrue= (&syscc ne 0)
|
||||
,mac=&_program
|
||||
,msg=%str(syscc=&syscc afer &lib objects extraction)
|
||||
)
|
||||
|
||||
/* load mpe_datacatalog_CATS */
|
||||
proc sql;
|
||||
create table datacats as select distinct libref,memname from catdata;
|
||||
|
||||
%bitemporal_dataloader(base_lib=&mpelib
|
||||
,base_dsn=mpe_datacatalog_CATS
|
||||
,append_dsn=datacats
|
||||
,PK=LIBREF MEMNAME
|
||||
,etlsource=&sysmacroname
|
||||
,loadtype=TXTEMPORAL
|
||||
,tech_from=TX_FROM
|
||||
,tech_to=TX_TO
|
||||
%if &cat = #ALL %then %do;
|
||||
,close_vars=LIBREF
|
||||
%end;
|
||||
,dclib=&mpelib
|
||||
)
|
||||
|
||||
/* load mpe_datacatalog_objsS */
|
||||
proc sql;
|
||||
create table dataobjs as
|
||||
select distinct libref,
|
||||
memname,
|
||||
objname,
|
||||
objtype,
|
||||
objdesc,
|
||||
alias
|
||||
from catdata;
|
||||
quit;
|
||||
|
||||
%bitemporal_dataloader(base_lib=&mpelib
|
||||
,base_dsn=mpe_datacatalog_OBJS
|
||||
,append_dsn=dataobjs
|
||||
,PK=LIBREF MEMNAME OBJNAME OBJTYPE
|
||||
,etlsource=&sysmacroname
|
||||
,loadtype=TXTEMPORAL
|
||||
,tech_from=TX_FROM
|
||||
,tech_to=TX_TO
|
||||
%if &cat = #ALL %then %do;
|
||||
,close_vars=LIBREF MEMNAME
|
||||
%end;
|
||||
,dclib=&mpelib
|
||||
)
|
||||
|
||||
%put load mpe_datastatus_OBJS;
|
||||
proc sql;
|
||||
create table statusobjs as
|
||||
select distinct libref,
|
||||
memname,
|
||||
objname,
|
||||
objtype,
|
||||
created,
|
||||
modified,
|
||||
level
|
||||
from catdata;
|
||||
|
||||
%bitemporal_dataloader(base_lib=&mpelib
|
||||
,base_dsn=mpe_datastatus_OBJS
|
||||
,append_dsn=statusobjs
|
||||
,PK=LIBREF MEMNAME OBJNAME OBJTYPE
|
||||
,etlsource=&sysmacroname
|
||||
,loadtype=TXTEMPORAL
|
||||
,tech_from=TX_FROM
|
||||
,tech_to=TX_TO
|
||||
%if &cat = #ALL %then %do;
|
||||
,close_vars=LIBREF MEMNAME
|
||||
%end;
|
||||
,dclib=&mpelib
|
||||
)
|
||||
|
||||
%put load mpe_datastatus_cats;
|
||||
proc sql;
|
||||
create table statuscats as
|
||||
select libref,
|
||||
memname,
|
||||
count(*) as nobjs,
|
||||
min(created) as created,
|
||||
max(modified) as modified
|
||||
from catdata
|
||||
group by 1,2;
|
||||
|
||||
%bitemporal_dataloader(base_lib=&mpelib
|
||||
,base_dsn=mpe_datastatus_cats
|
||||
,append_dsn=statuscats
|
||||
,PK=LIBREF MEMNAME
|
||||
,etlsource=&sysmacroname
|
||||
,loadtype=TXTEMPORAL
|
||||
,tech_from=TX_FROM
|
||||
,tech_to=TX_TO
|
||||
%if &cat = #ALL %then %do;
|
||||
,close_vars=LIBREF
|
||||
%end;
|
||||
,dclib=&mpelib
|
||||
)
|
||||
|
||||
|
||||
%mend mpe_refreshcatalogs;
|
38
sas/sasjs/macros/mpe_refreshcatalogs.test.sas
Normal file
38
sas/sasjs/macros/mpe_refreshcatalogs.test.sas
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
@file
|
||||
@brief Testing mpe_refreshcatalogs macro
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mpe_refreshcatalogs.sas
|
||||
@li mp_assert.sas
|
||||
@li mp_assertscope.sas
|
||||
@li mp_ds2md.sas
|
||||
|
||||
@author 4GL Apps Ltd
|
||||
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
|
||||
and may not be re-distributed or re-sold without the express permission of
|
||||
4GL Apps Ltd.
|
||||
|
||||
**/
|
||||
|
||||
%mp_assertscope(SNAPSHOT)
|
||||
%mpe_refreshcatalogs(FMTONLY)
|
||||
%mp_assertscope(COMPARE,
|
||||
desc=Checking macro variables against previous snapshot
|
||||
)
|
||||
|
||||
/* make sure that the process picks up the catalog */
|
||||
proc sql noprint;
|
||||
create table work.test1 as
|
||||
select *
|
||||
from &mpelib..mpe_datacatalog_cats(where=(&dc_dttmtfmt. lt tx_to))
|
||||
where libref="FMTONLY";
|
||||
%let test1=0;
|
||||
select count(*) into: test1 from work.test1;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&test1>0),
|
||||
desc=Checking fmtonly.dcfmts was picked up
|
||||
)
|
||||
|
||||
%mp_ds2md(work.test1)
|
@@ -48,11 +48,6 @@ create table cols as
|
||||
,msg=%str(syscc=&syscc afer &lib cols extraction)
|
||||
)
|
||||
|
||||
%mp_abort(iftrue= (&syscc ne 0)
|
||||
,mac=&_program
|
||||
,msg=%str(syscc=&syscc afer &lib indexes extraction)
|
||||
)
|
||||
|
||||
%if &engine=SQLSVR %then %do;
|
||||
proc sql;
|
||||
connect using &lib;
|
||||
@@ -175,6 +170,11 @@ create table cols as
|
||||
|
||||
%end;
|
||||
|
||||
%mp_abort(iftrue= (&syscc ne 0)
|
||||
,mac=&_program
|
||||
,msg=%str(syscc=&syscc afer &lib indexes extraction)
|
||||
)
|
||||
|
||||
/* load columns */
|
||||
%bitemporal_dataloader(base_lib=&mpelib
|
||||
,base_dsn=mpe_datacatalog_vars
|
||||
@@ -295,7 +295,7 @@ proc sql;
|
||||
%if &ds ne #ALL %then %do;
|
||||
and upcase(memname)="&ds"
|
||||
%end;
|
||||
;
|
||||
;
|
||||
%end;
|
||||
|
||||
%bitemporal_dataloader(base_lib=&mpelib
|
||||
@@ -314,12 +314,32 @@ proc sql;
|
||||
|
||||
%if &ds = #ALL %then %do;
|
||||
proc sql;
|
||||
create table statuslibs as select
|
||||
create table work.sumcat as
|
||||
select libname as libref,
|
||||
count(distinct memname) as catalog_cnt
|
||||
from dictionary.catalogs
|
||||
where upcase(libname)="&lib"
|
||||
group by 1;
|
||||
create table work.sumdsn as select
|
||||
libref
|
||||
,sum(filesize) as libsize
|
||||
,count(*) as table_cnt
|
||||
from statustabs
|
||||
group by 1;
|
||||
create table work.libs as
|
||||
select libref from work.sumcat
|
||||
union
|
||||
select libref from work.sumdsn;
|
||||
create table work.statuslibs as
|
||||
select a.libref,
|
||||
b.libsize,
|
||||
b.table_cnt,
|
||||
c.catalog_cnt
|
||||
from work.libs a
|
||||
left join work.sumdsn b
|
||||
on a.libref=b.libref
|
||||
left join work.sumcat c
|
||||
on a.libref=c.libref;
|
||||
|
||||
%bitemporal_dataloader(base_lib=&mpelib
|
||||
,base_dsn=mpe_datastatus_libs
|
||||
|
50
sas/sasjs/macros/mpe_refreshtables.test.sas
Normal file
50
sas/sasjs/macros/mpe_refreshtables.test.sas
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
@file
|
||||
@brief Testing mpe_refreshtables macro
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mpe_refreshtables.sas
|
||||
@li mp_assert.sas
|
||||
@li mp_assertscope.sas
|
||||
|
||||
@author 4GL Apps Ltd
|
||||
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
|
||||
and may not be re-distributed or re-sold without the express permission of
|
||||
4GL Apps Ltd.
|
||||
|
||||
**/
|
||||
|
||||
%mp_assertscope(SNAPSHOT)
|
||||
%mpe_refreshtables(FMTONLY)
|
||||
%mp_assertscope(COMPARE,
|
||||
desc=Checking macro variables against previous snapshot
|
||||
)
|
||||
|
||||
/* make sure that the process picks up a library that contains only a single
|
||||
catalog */
|
||||
proc sql;
|
||||
create table work.libinfo as
|
||||
select a.engine,
|
||||
a.libname,
|
||||
a.paths,
|
||||
a.perms,
|
||||
a.owners,
|
||||
a.schemas,
|
||||
a.libid,
|
||||
b.libsize,
|
||||
b.table_cnt,
|
||||
b.catalog_cnt
|
||||
from &mpelib..mpe_datacatalog_libs(where=(&dc_dttmtfmt. lt tx_to)) a
|
||||
left join &mpelib..mpe_datastatus_libs(where=(&dc_dttmtfmt. lt tx_to)) b
|
||||
on a.libref=b.libref
|
||||
where a.libref="&libref";
|
||||
|
||||
%let test1=0;
|
||||
data _null_;
|
||||
set work.libinfo;
|
||||
call symputx('test1',table_cnt);
|
||||
run;
|
||||
%mp_assert(
|
||||
iftrue=(&test1>0),
|
||||
desc=Checking fmtonly.dcfmts was picked up
|
||||
)
|
@@ -94,50 +94,18 @@
|
||||
},
|
||||
{
|
||||
"name": "viya",
|
||||
"serverUrl": "https://sas.4gl.io",
|
||||
"serverUrl": "https://viya-f0g8ht62vq.engage.sas.com",
|
||||
"serverType": "SASVIYA",
|
||||
"httpsAgentOptions": {
|
||||
"allowInsecureRequests": false
|
||||
},
|
||||
"appLoc": "/Public/app/dcplaceholder",
|
||||
"macroFolders": [
|
||||
"sasjs/targets/viya/macros_viya"
|
||||
],
|
||||
"programFolders": [
|
||||
"sasjs/db/datactrl"
|
||||
],
|
||||
"binaryFolders": [],
|
||||
"buildConfig": {
|
||||
"initProgram": "sasjs/utils/buildinitviya.sas",
|
||||
"buildResultsFolder": "sasjsresults",
|
||||
"buildOutputFolder": "sasjsbuild",
|
||||
"buildOutputFileName": "viya.sas"
|
||||
},
|
||||
"serviceConfig": {
|
||||
"initProgram": "sasjs/utils/serviceinitviya.sas",
|
||||
"serviceFolders": [
|
||||
"sasjs/targets/viya/services_viya/viya_users",
|
||||
"sasjs/targets/viya/services_viya/admin",
|
||||
"sasjs/targets/viya/services_viya/public"
|
||||
]
|
||||
},
|
||||
"deployConfig": {
|
||||
"deployServicePack": true,
|
||||
"deployScripts": [
|
||||
"sasjs/utils/viyadeploy.sh"
|
||||
]
|
||||
},
|
||||
"contextName": "SAS Job Execution compute context"
|
||||
},
|
||||
{
|
||||
"name": "viyacloud",
|
||||
"serverUrl": "https://4gl.viyacloud.sas.com",
|
||||
"serverType": "SASVIYA",
|
||||
"httpsAgentOptions": {
|
||||
"rejectUnauthorized": false,
|
||||
"allowInsecureRequests": true
|
||||
},
|
||||
"appLoc": "/30.SASApps/app/dc",
|
||||
"macroFolders": [
|
||||
"sasjs/targets/viya/macros_viya"
|
||||
],
|
||||
@@ -146,36 +114,31 @@
|
||||
],
|
||||
"buildConfig": {
|
||||
"initProgram": "sasjs/utils/buildinitviya.sas",
|
||||
"termProgram": "sasjs/utils/buildtermviya.sas",
|
||||
"macroVars": {
|
||||
"dcpath": "/opt/sas/viya/config/var/tmp/dc",
|
||||
"adminGroup": "DataBuilders"
|
||||
},
|
||||
"buildResultsFolder": "sasjsresults",
|
||||
"buildOutputFolder": "sasjsbuild",
|
||||
"buildOutputFileName": "viya.sas"
|
||||
"buildOutputFileName": "viya.sas",
|
||||
"termProgram": "",
|
||||
"macroVars": {}
|
||||
},
|
||||
"serviceConfig": {
|
||||
"initProgram": "sasjs/utils/serviceinitviya.sas",
|
||||
"serviceFolders": [
|
||||
"sasjs/targets/viya/services_viya/viya_users",
|
||||
"sasjs/targets/viya/services_viya/admin",
|
||||
"sasjs/targets/viya/services_viya/public"
|
||||
],
|
||||
"initProgram": "sasjs/utils/serviceinitviya.sas",
|
||||
"termProgram": "",
|
||||
"macroVars": {}
|
||||
},
|
||||
"streamConfig": {
|
||||
"streamWeb": true,
|
||||
"streamWebFolder": "webv",
|
||||
"streamWebFolder": "web",
|
||||
"webSourcePath": "../client/dist",
|
||||
"streamServiceName": "clickme",
|
||||
"streamServiceName": "DC",
|
||||
"streamLogo": "favicon.ico",
|
||||
"assetPaths": []
|
||||
},
|
||||
"deployConfig": {
|
||||
"deployServicePack": true
|
||||
},
|
||||
"contextName": "Datacontroller compute context"
|
||||
"contextName": "SAS Job Execution compute context"
|
||||
},
|
||||
{
|
||||
"name": "vtest",
|
||||
|
@@ -675,7 +675,7 @@ data xl_rules;
|
||||
keep xl_column xl_rule;
|
||||
run;
|
||||
|
||||
%mpe_dsmeta(&libds, outds=dsmeta)
|
||||
%mpe_dsmeta(&orig_libds, outds=dsmeta)
|
||||
|
||||
%mpe_getversions(&mpelib,
|
||||
%scan(&orig_libds,1,.),
|
||||
|
@@ -54,7 +54,10 @@ data _null_;
|
||||
/* if a TXTEMPORAL table then filter as such */
|
||||
call symputx('txfrom',var_txfrom);
|
||||
call symputx('txto',var_txto);
|
||||
run;
|
||||
|
||||
/* if a format, extract relevant info */
|
||||
data _null_;
|
||||
ds=symget('ds');
|
||||
is_fmt=0;
|
||||
if subpad(cats(reverse(ds)),1,3)=:'CF-' then do;
|
||||
|
@@ -76,3 +76,37 @@ run;
|
||||
test=EQUALS 3,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/* test 3 - when there is a format catalog / nothing else in the library */
|
||||
data work.params3;
|
||||
length name $32 value $1000;
|
||||
name='type';value='SAS';output;
|
||||
name='table';value="FMTONLY.DCFMTS-FC";output;
|
||||
name='filter';value='0';output;
|
||||
run;
|
||||
|
||||
%mx_testservice(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputparams=work.params3,
|
||||
outref=web3,
|
||||
viyaresult=WEBOUT_TXT
|
||||
)
|
||||
|
||||
data work.results3;
|
||||
infile web3;
|
||||
input;
|
||||
putlog _infile_;
|
||||
if _infile_=:'datalines4;' then do;
|
||||
output;
|
||||
output;
|
||||
output;
|
||||
stop;
|
||||
end;
|
||||
if _n_>100 then stop;
|
||||
run;
|
||||
|
||||
%mp_assertdsobs(work.results3,
|
||||
desc=datalines file is successfully returned for LONE format catalog,
|
||||
test=EQUALS 3,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
@@ -74,8 +74,9 @@ create table work.libinfo as
|
||||
a.schemas,
|
||||
a.libid,
|
||||
b.libsize,
|
||||
b.table_cnt
|
||||
from &mpelib..mpe_datacatalog_libs(where=(&dc_dttmtfmt. lt tx_to)) a
|
||||
b.table_cnt,
|
||||
b.catalog_cnt
|
||||
from &mpelib..mpe_datacatalog_libs(where=(&dc_dttmtfmt. lt tx_to)) a
|
||||
inner join &mpelib..mpe_datastatus_libs(where=(&dc_dttmtfmt. lt tx_to)) b
|
||||
on a.libref=b.libref
|
||||
where a.libref="&libref";
|
||||
|
@@ -355,7 +355,7 @@ run;
|
||||
|
||||
%mp_getcols(&libds, outds=cols)
|
||||
|
||||
%mpe_dsmeta(&libds, outds=dsmeta)
|
||||
%mpe_dsmeta(&orig_libds, outds=dsmeta)
|
||||
|
||||
%mpe_getversions(&mpelib,
|
||||
%scan(&orig_libds,1,.),
|
||||
|
@@ -167,7 +167,8 @@ create table work.libinfo as
|
||||
a.schemas,
|
||||
a.libid,
|
||||
coalesce(b.libsize,0) as libsize,
|
||||
coalesce(b.table_cnt,0) as table_cnt
|
||||
coalesce(b.table_cnt,0) as table_cnt,
|
||||
coalesce(b.catalog_cnt,0) as catalog_cnt
|
||||
from &mpelib..mpe_datacatalog_libs(where=(&dc_dttmtfmt. lt tx_to)) a
|
||||
left join &mpelib..mpe_datastatus_libs(where=(&dc_dttmtfmt. lt tx_to)) b
|
||||
on a.libref=b.libref
|
||||
|
@@ -9,6 +9,7 @@
|
||||
<h4> SAS Macros </h4>
|
||||
@li mpe_refreshlibs.sas
|
||||
@li dc_assignlib.sas
|
||||
@li mpe_refreshcatalogs.sas
|
||||
@li mpe_refreshtables.sas
|
||||
@li mm_getrepos.sas
|
||||
|
||||
@@ -44,6 +45,7 @@ run;
|
||||
%dc_assignlib(WRITE,&libref) /* write just in order to assign direct lib */
|
||||
%mpe_refreshlibs(lib=&libref)
|
||||
%mpe_refreshtables(&libref)
|
||||
%mpe_refreshcatalogs(&libref)
|
||||
%end;
|
||||
%else %do xx=1 %to &repocnt;
|
||||
options metarepository=&&repo&xx;
|
||||
@@ -73,6 +75,7 @@ run;
|
||||
%do i=1 %to &libcnt;
|
||||
%dc_assignlib(WRITE,&&lib&i)
|
||||
%mpe_refreshtables(&&lib&i)
|
||||
%mpe_refreshcatalogs(&&lib&i)
|
||||
%end;
|
||||
%end;
|
||||
|
||||
|
@@ -9,6 +9,7 @@
|
||||
<h4> SAS Macros </h4>
|
||||
@li mpe_refreshlibs.sas
|
||||
@li dc_assignlib.sas
|
||||
@li mpe_refreshcatalogs.sas
|
||||
@li mpe_refreshtables.sas
|
||||
|
||||
|
||||
@@ -34,6 +35,8 @@ data libraries;
|
||||
str=cats('%mpe_refreshtables(',libref,')');
|
||||
put str;
|
||||
putlog str;
|
||||
str=cats('%mpe_refreshcatalogs(',libref,')');
|
||||
put str;
|
||||
run;
|
||||
%inc executor/source2;
|
||||
|
||||
|
@@ -10,6 +10,7 @@
|
||||
@li mpe_refreshlibs.sas
|
||||
@li dc_assignlib.sas
|
||||
@li mpe_refreshtables.sas
|
||||
@li mpe_refreshcatalogs.sas
|
||||
|
||||
|
||||
@version 3.4
|
||||
@@ -19,19 +20,29 @@
|
||||
4GL Apps Ltd.
|
||||
**/
|
||||
|
||||
%macro dc_refreshcatalog();
|
||||
%macro dc_refreshcatalog(libref);
|
||||
|
||||
%mpe_refreshlibs()
|
||||
|
||||
filename executor catalog 'work.code.code.source';
|
||||
data libraries;
|
||||
set &mpelib..mpe_datacatalog_libs;
|
||||
where &dc_dttmtfmt. le TX_TO;
|
||||
file executor;
|
||||
str=cats('%mpe_refreshtables(',libref,')');
|
||||
put str;
|
||||
putlog str;
|
||||
run;
|
||||
%inc executor;
|
||||
%if #&libref# ne ## %then %do;
|
||||
%put &sysmacroname: assigning specific libref, &libref;
|
||||
%dc_assignlib(WRITE,&libref) /* write just in order to assign direct lib */
|
||||
%mpe_refreshlibs(lib=&libref)
|
||||
%mpe_refreshtables(&libref)
|
||||
%mpe_refreshcatalogs(&libref)
|
||||
%end;
|
||||
%else %do;
|
||||
%mpe_refreshlibs()
|
||||
filename executor catalog 'work.code.code.source';
|
||||
data libraries;
|
||||
set &mpelib..mpe_datacatalog_libs;
|
||||
where &dc_dttmtfmt. le TX_TO;
|
||||
file executor;
|
||||
str=cats('%mpe_refreshtables(',libref,')');
|
||||
put str;
|
||||
str=cats('%mpe_refreshcatalogs(',libref,')');
|
||||
put str;
|
||||
putlog str;
|
||||
run;
|
||||
%inc executor;
|
||||
%end;
|
||||
|
||||
%mend dc_refreshcatalog;
|
||||
|
@@ -7,6 +7,7 @@
|
||||
@li mf_getapploc.sas
|
||||
@li mf_mkdir.sas
|
||||
@li mf_trimstr.sas
|
||||
@li mp_abort.sas
|
||||
@li mpe_getvars.sas
|
||||
@li mpe_makedata.sas
|
||||
@li mpe_makedatamodel.sas
|
||||
@@ -50,9 +51,33 @@ options noquotelenmax;
|
||||
%put &=admin;
|
||||
|
||||
%mf_mkdir(&dcpath)
|
||||
%mp_abort(iftrue= (&syscc ne 0)
|
||||
,mac=&_program
|
||||
,msg=%str(Unable to create &dcpath using &sysuserid)
|
||||
)
|
||||
%mf_mkdir(&dcpath/secret)
|
||||
%mf_mkdir(&dcpath/dc_staging)
|
||||
|
||||
/* check we have physical permissions to the DCLIB folder */
|
||||
data _null_;
|
||||
putlog "dcpath=&dcpath/permTest.txt";
|
||||
putlog "sysuserid=&sysuserid";
|
||||
data _null_;
|
||||
file "&dcpath/permTest.txt";
|
||||
run;
|
||||
%mp_abort(iftrue= (&syscc ne 0)
|
||||
,mac=&_program
|
||||
,msg=%str(User &sysuserid does not have WRITE permissions to: &dcpath )
|
||||
)
|
||||
filename delfile "&dcpath/permTest.txt";
|
||||
data _null_;
|
||||
rc=fdelete('delfile');
|
||||
run;
|
||||
%mp_abort(iftrue= (&syscc ne 0)
|
||||
,mac=&_program..sas
|
||||
,msg=%str(User &sysuserid could create (but not delete) &dcpath/permTest.txt )
|
||||
)
|
||||
|
||||
|
||||
libname &dclib "&dcpath";
|
||||
%global admin;
|
||||
@@ -60,11 +85,23 @@ libname &dclib "&dcpath";
|
||||
%mpe_makedatamodel(lib=&dclib)
|
||||
%mpe_makedata(lib=&dclib,mpeadmins=&admin,path=%str(&dcpath))
|
||||
|
||||
%mp_abort(iftrue=(&syscc ne 0)
|
||||
,mac=&sysmacroname
|
||||
,msg=%str(Err during &dclib build)
|
||||
)
|
||||
|
||||
|
||||
/* sample data library */
|
||||
%mf_mkdir(&dcpath/dc_demo)
|
||||
libname dcdemo "&dcpath/dc_demo";
|
||||
%mpe_makesampledata(outlib=DCDEMO)
|
||||
|
||||
%mp_abort(iftrue=(&syscc ne 0)
|
||||
,mac=&sysmacroname
|
||||
,msg=%str(Err during demo data build)
|
||||
)
|
||||
|
||||
|
||||
/* the DC precode is stored in the root of the project */
|
||||
%let root=%mf_getapploc(&_program)/services;
|
||||
%put &=root;
|
||||
@@ -167,7 +204,7 @@ run;
|
||||
*/
|
||||
%mp_abort(iftrue=(&syscc ne 0)
|
||||
,mac=&sysmacroname
|
||||
,msg=%str(Err during DB build)
|
||||
,msg=%str(Err during settings job creation)
|
||||
)
|
||||
|
||||
|
||||
|
@@ -21,7 +21,7 @@ REMOVE THAT LAST MACRO
|
||||
%let syscc=0;
|
||||
%global apploc _program dclib defaultcontext _debug sasjs_mdebug dc_dttmtfmt;
|
||||
|
||||
%let defaultcontext=Datacontroller compute context;
|
||||
%let defaultcontext=SAS Job Execution compute context;
|
||||
%let sasjs_mdebug=0;
|
||||
|
||||
options mprint mprintnest nobomfile lrecl=32767;
|
||||
|
@@ -28,7 +28,7 @@
|
||||
|
||||
%global apploc _program;
|
||||
|
||||
%let defaultcontext=Datacontroller compute context;
|
||||
%let defaultcontext=SAS Job Execution compute context;
|
||||
|
||||
data _null_;
|
||||
length _pgm $1000;
|
||||
@@ -60,6 +60,9 @@ run;
|
||||
%let testloc=%sysfunc(pathname(&DC_LIBREF))/fmt%mf_getuniquefileref();
|
||||
%mf_mkdir(&testloc)
|
||||
libname dctest "&testloc";
|
||||
/* test library with only one format catalog */
|
||||
%mf_mkdir(&testloc/fmtonly)
|
||||
libname fmtonly "&testloc/fmtonly";
|
||||
|
||||
/* add formats */
|
||||
PROC FORMAT library=dctest.dcfmts;
|
||||
@@ -99,6 +102,10 @@ run;
|
||||
proc append base=&dc_libref..mpe_tables data=work.append;
|
||||
run;
|
||||
|
||||
/* lone format catalog */
|
||||
proc format cntlin=work.fmts library=fmtonly.dcfmts;
|
||||
run;
|
||||
|
||||
/* add some other tables */
|
||||
%mp_coretable(LOCKTABLE,libds=dctest.locktable)
|
||||
%mp_coretable(DIFFTABLE,libds=dctest.difftable)
|
||||
@@ -130,6 +137,7 @@ options mprint;
|
||||
put _infile_;
|
||||
if last then do;
|
||||
put "libname dctest '&testloc';";
|
||||
put "libname fmtonly '&testloc/fmtonly';";
|
||||
end;
|
||||
run;
|
||||
data _null_;
|
||||
@@ -151,6 +159,7 @@ options mprint;
|
||||
put _infile_;
|
||||
if last then do;
|
||||
put "libname dctest '&testloc';";
|
||||
put "libname fmtonly '&testloc/fmtonly';";
|
||||
end;
|
||||
run;
|
||||
%ms_deletefile(&root/services/public/settings.sas)
|
||||
@@ -182,6 +191,7 @@ options mprint;
|
||||
put _infile_;
|
||||
if last then do;
|
||||
put "libname dctest '&testloc';";
|
||||
put "libname fmtonly '&testloc/fmtonly';";
|
||||
end;
|
||||
run;
|
||||
%mm_deletestp(target=&root/services/public/Data_Controller_Settings)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user