Compare commits
	
		
			188 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 26ce95f7c1 | |||
|  | 4924df2ef3 | ||
| 2e141a5d52 | |||
|  | cb1978bcaf | ||
|  | 387f5122f1 | ||
|  | db5887de21 | ||
| fe24d9bcbd | |||
|  | 6c6b1cbf46 | ||
|  | 4d65c9c999 | ||
| 4417279275 | |||
|  | 365f12996d | ||
|  | 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 | ||
|  | 997f09adde | ||
|  | 0e8503ed2b | ||
| 97dfcd79b1 | |||
| 9a12a2e41f | |||
|  | 5c114e562b | ||
| ae696a0be0 | |||
|  | 22d46a5dcc | ||
|  | f3125ff464 | ||
|  | 9682b548e6 | ||
|  | ec11a74265 | ||
| 4be0614604 | |||
|  | 27cbff2bc5 | ||
|  | c41c8963f2 | ||
|  | 7249d4fa29 | ||
|  | 2e0c60cc0d | ||
| 7c5e47f5e4 | |||
|  | f9decbd366 | ||
|  | 1dc69341ca | ||
|  | 75ae19fa8e | ||
|  | 9de04e9a0c | ||
|  | 983f59cd51 | ||
|  | 7b5e7ae184 | ||
|  | 6e96b1daec | ||
|  | 9604661f3b | ||
|  | 4bd215491f | ||
|  | 6a7dd451b5 | ||
|  | 841201adab | ||
|  | e013e62776 | ||
|  | 6c171a6394 | ||
|  | a377f6e8d6 | ||
| 0337318e0b | |||
|  | 2844c70f95 | ||
|  | 7e11c8f375 | ||
| 23cbbce964 | |||
|  | c63fcdd465 | ||
|  | 82412b2659 | ||
| eef3832e40 | |||
| 63b75a1c61 | |||
| d1f0879f0a | |||
|  | 36416aab2e | ||
|  | 7f3577c3ef | ||
|  | 69f883034f | ||
|  | 1c56af01d0 | ||
|   | 43c0f73c21 | ||
|   | e8cd3d63da | ||
|  | b7f564cb21 | ||
|   | 3f5cb1e2de | ||
|  | 7d8c0472f0 | ||
| b8b516ba77 | |||
|   | 83b3d775b6 | ||
|  | aea252ccc6 | ||
| d8908f9c7f | |||
|   | 95289aa952 | ||
|  | 149e318a87 | ||
|  | 8657826e60 | ||
|   | a45f5bb3b2 | ||
|   | ae9a91a7a1 | ||
|  | 3638bde633 | ||
| 3a2361f42c | |||
|  | 1bd0eef913 | ||
| 85aa3b38b7 | |||
| 678859a68d | |||
|  | e531acee3f | ||
|  | 4a45ebfe3b | ||
| 8fb9a344f2 | |||
|  | 8829b60220 | ||
|  | 4e64f28732 | ||
| a0749de700 | |||
| f9623e046e | |||
|   | bce1fd57ef | 
| @@ -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 | ||||
							
								
								
									
										269
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										269
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,3 +1,272 @@ | ||||
| ## [7.2.4](https://git.datacontroller.io/dc/dc/compare/v7.2.3...v7.2.4) (2025-10-14) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * ensure reload after applying licence key ([cb1978b](https://git.datacontroller.io/dc/dc/commit/cb1978bcaf23b0bf45b5d3b78b9707fd4e48a5f4)) | ||||
| * snyk report security patches ([387f512](https://git.datacontroller.io/dc/dc/commit/387f5122f1ea6dff55d23c9223f17737283a94d3)) | ||||
|  | ||||
| ## [7.2.3](https://git.datacontroller.io/dc/dc/compare/v7.2.2...v7.2.3) (2025-10-02) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * opening second table in viewer throws an error ([6c6b1cb](https://git.datacontroller.io/dc/dc/commit/6c6b1cbf460e5291ec746af017e764b894fff8d5)) | ||||
|  | ||||
| ## [7.2.2](https://git.datacontroller.io/dc/dc/compare/v7.2.1...v7.2.2) (2025-09-23) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * jsrsasign,  @sasjs/cli bump ([365f129](https://git.datacontroller.io/dc/dc/commit/365f12996db3ef50a4f4f099d5af15696c43bb42)) | ||||
|  | ||||
| ## [7.2.1](https://git.datacontroller.io/dc/dc/compare/v7.2.0...v7.2.1) (2025-08-08) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * removing localhost from index.html ([225e693](https://git.datacontroller.io/dc/dc/commit/225e693d1fd4381f2b8ce42fecb508f0a9e9dad8)) | ||||
|  | ||||
| # [7.2.0](https://git.datacontroller.io/dc/dc/compare/v7.1.1...v7.2.0) (2025-08-08) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * **ci:** cypress dependency package not available anymore ([26cdd73](https://git.datacontroller.io/dc/dc/commit/26cdd733315ef8babe9498ce93f6eb29c587dabd)) | ||||
| * **hot v16 migration:** multi dataset fixed issues, and cypress tests adapted ([712b384](https://git.datacontroller.io/dc/dc/commit/712b3848480a8769d149e00b0d2de91396022b66)) | ||||
| * obsolete cypress deps ([2ba4b53](https://git.datacontroller.io/dc/dc/commit/2ba4b5383e23bff8dfeb82b0ef473e5871c94709)) | ||||
| * remaining hot migrations - handsontable/angular-wrapper ([b419cd5](https://git.datacontroller.io/dc/dc/commit/b419cd507837e846e9dfcc6b729254d56cc196e6)) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * lighthouse accessibility check pipeline ([670ec2c](https://git.datacontroller.io/dc/dc/commit/670ec2c71cb2d24e9d79e297a8cbc6136aa315c8)) | ||||
|  | ||||
| ## [7.1.1](https://git.datacontroller.io/dc/dc/compare/v7.1.0...v7.1.1) (2025-07-24) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * **viewboxes:** hot v16 fails to load because of relative height `100%` ([672dd6d](https://git.datacontroller.io/dc/dc/commit/672dd6d4f1fda27e3706dd7caa42b45922319497)) | ||||
|  | ||||
| # [7.1.0](https://git.datacontroller.io/dc/dc/compare/v7.0.3...v7.1.0) (2025-07-23) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * adapter bump ([b495c41](https://git.datacontroller.io/dc/dc/commit/b495c41626c85b7c4141d9361e4d3a826efd6c05)) | ||||
| * bumping CLI to 4.12.10 ([a08a717](https://git.datacontroller.io/dc/dc/commit/a08a717ca8d49e8a7d63f3fd91c6a7d42a1d6d8b)) | ||||
| * bumping sasjs/core and sasjs/cli ([63e9af4](https://git.datacontroller.io/dc/dc/commit/63e9af402ed65f6be4426e76ee1376a40e6ed097)) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * improving accessibility score up to 100, hot update to v16.0.1 ([71c308d](https://git.datacontroller.io/dc/dc/commit/71c308d052400ecedc03f8020a5a69471ac6b116)) | ||||
|  | ||||
| ## [7.0.3](https://git.datacontroller.io/dc/dc/compare/v7.0.2...v7.0.3) (2025-06-26) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * makedata vars ([e7cb471](https://git.datacontroller.io/dc/dc/commit/e7cb471c0b60058b03fe8cbed5e3e2e70dd72e26)) | ||||
| * viya deploy makedata missing params ([7a82316](https://git.datacontroller.io/dc/dc/commit/7a8231615cb56710351fae5868e8fdeed54d180c)) | ||||
|  | ||||
| ## [7.0.2](https://git.datacontroller.io/dc/dc/compare/v7.0.1...v7.0.2) (2025-06-21) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * **viya deploy:** run makedata in new window to ensure logs are available for the user ([0b4042a](https://git.datacontroller.io/dc/dc/commit/0b4042af6011fdc65cfaaa5d4b1d8f48cd67f3b3)) | ||||
|  | ||||
| ## [7.0.1](https://git.datacontroller.io/dc/dc/compare/v7.0.0...v7.0.1) (2025-06-11) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * refresh process ([4ecd186](https://git.datacontroller.io/dc/dc/commit/4ecd186e5cb22dd436f2d7f1200956f4e3f27425)) | ||||
|  | ||||
| # [7.0.0](https://git.datacontroller.io/dc/dc/compare/v6.16.2...v7.0.0) (2025-06-11) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * bumping adapter to re-enable JES API method ([e874143](https://git.datacontroller.io/dc/dc/commit/e874143a95d0ac2e56c0793e04b979c27f96d74b)) | ||||
| * commit git hooks checking lint ([69f687a](https://git.datacontroller.io/dc/dc/commit/69f687a85f1cc562346b6167813d617cb9bd3404)) | ||||
| * ensuring apploc is not case sensitive. Closes [#171](https://git.datacontroller.io/dc/dc/issues/171) ([24545f2](https://git.datacontroller.io/dc/dc/commit/24545f2acdd5bd73cbe062526f2bd043269cc6a3)) | ||||
| * export unregistered formats ([f6d7d6f](https://git.datacontroller.io/dc/dc/commit/f6d7d6f90c978ac8c071471dfb67a60834424de5)), closes [#158](https://git.datacontroller.io/dc/dc/issues/158) | ||||
| * reload startupservice after user approves the MPE_TABLES page ([e5f8e50](https://git.datacontroller.io/dc/dc/commit/e5f8e500c125ee233c6f7af5ad0077c0ed6abfcb)) | ||||
| * showing catalog_cnt in libinfo ([e44a25d](https://git.datacontroller.io/dc/dc/commit/e44a25dcc39ba4b9714257c60da84c2dfa613a85)), closes [#160](https://git.datacontroller.io/dc/dc/issues/160) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * adding 4 new tables for catalogs ([e4dbab8](https://git.datacontroller.io/dc/dc/commit/e4dbab8b1654b24e610e4b0603d1cf2b02a451e2)) | ||||
| * capturing catalog specific information, closes [#159](https://git.datacontroller.io/dc/dc/issues/159) ([b4c586a](https://git.datacontroller.io/dc/dc/commit/b4c586a859929e0122cd46449e43d4ca597b8b2b)) | ||||
| * viewer added catalog_cnt ([2aa19d1](https://git.datacontroller.io/dc/dc/commit/2aa19d1dca747f41274a032cde78d8ba73d66224)) | ||||
|  | ||||
|  | ||||
| ### BREAKING CHANGES | ||||
|  | ||||
| * Introduction of 4 new tables for capturing information related to catalogs and their objects.  Migration script prepared and available in the DB folder (usual place) | ||||
|  | ||||
| ## [6.16.2](https://git.datacontroller.io/dc/dc/compare/v6.16.1...v6.16.2) (2025-06-06) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * streaming viya deploy `isStreaming` function stability fix ([4830c6d](https://git.datacontroller.io/dc/dc/commit/4830c6d2191cb47abcc7919bc1d49e55595e6121)) | ||||
|  | ||||
| ## [6.16.1](https://git.datacontroller.io/dc/dc/compare/v6.16.0...v6.16.1) (2025-06-06) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * viya deploy updating index html based on URL ([86134f4](https://git.datacontroller.io/dc/dc/commit/86134f478ae0b9426e01bfcc9ca4ee597ca733f7)) | ||||
| * viya streamed app deploy page flow fix ([89ab296](https://git.datacontroller.io/dc/dc/commit/89ab2961513b245eeea48d1867c6496d3261761e)) | ||||
|  | ||||
| # [6.16.0](https://git.datacontroller.io/dc/dc/compare/v6.15.2...v6.16.0) (2025-06-05) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * adapter bump ([ca7caa2](https://git.datacontroller.io/dc/dc/commit/ca7caa25b6eea1bd4579fb8b67ec9b211a893079)) | ||||
| * automatic viya deploy timing issue ([037a97b](https://git.datacontroller.io/dc/dc/commit/037a97b6ffa27b40891531ae6812ebe5b5e71e34)) | ||||
| * bump core to ensure ff works on viya streaming deploy ([cbd69df](https://git.datacontroller.io/dc/dc/commit/cbd69df708edf3a8446115ca7315fac3557dcf97)), closes [#156](https://git.datacontroller.io/dc/dc/issues/156) | ||||
| * viya deploy load data timing ([abdbb67](https://git.datacontroller.io/dc/dc/commit/abdbb674713796e5308eb4272197a5c253868a85)) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * viya deploy, update the index.html contextname ([7223955](https://git.datacontroller.io/dc/dc/commit/72239558af2ee50cdfc71b7e185e6661ab568ba1)) | ||||
|  | ||||
| ## [6.15.2](https://git.datacontroller.io/dc/dc/compare/v6.15.1...v6.15.2) (2025-06-04) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * pipeline updates for DC.html ([624a7a8](https://git.datacontroller.io/dc/dc/commit/624a7a8f37f0265cf576da310ac330c75aa417cf)) | ||||
|  | ||||
| ## [6.15.1](https://git.datacontroller.io/dc/dc/compare/v6.15.0...v6.15.1) (2025-06-04) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * updating pipeline to default to streaming on viya ([4b55894](https://git.datacontroller.io/dc/dc/commit/4b558948d997f456ff25a12a58827fe0d2075493)) | ||||
|  | ||||
| # [6.15.0](https://git.datacontroller.io/dc/dc/compare/v6.14.10...v6.15.0) (2025-06-04) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * makedata with context name ([da4d0b2](https://git.datacontroller.io/dc/dc/commit/da4d0b28c7109afd6f96455e1e0e80a40d25a942)) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * viya deploy context ([6c96ef7](https://git.datacontroller.io/dc/dc/commit/6c96ef7fb0a55754a84ff0a8bbab838b78c1acaf)) | ||||
|  | ||||
| ## [6.14.10](https://git.datacontroller.io/dc/dc/compare/v6.14.9...v6.14.10) (2025-06-02) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * bump core ([0e8503e](https://git.datacontroller.io/dc/dc/commit/0e8503ed2bb22a0fc3924ac929e7f19626772e0a)) | ||||
| * default to home directory for SAS Drive in Viya ([9682b54](https://git.datacontroller.io/dc/dc/commit/9682b548e6106d99d97dcc023a35d93addfd5170)) | ||||
|  | ||||
| ## [6.14.9](https://git.datacontroller.io/dc/dc/compare/v6.14.8...v6.14.9) (2025-06-02) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * default DC path for viya ([f3125ff](https://git.datacontroller.io/dc/dc/commit/f3125ff4641e47e33cb203228f5b1014ea3343bc)) | ||||
|  | ||||
| ## [6.14.8](https://git.datacontroller.io/dc/dc/compare/v6.14.7...v6.14.8) (2025-05-28) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * CSP issues, clarity local library build, fixed some style issues ([841201a](https://git.datacontroller.io/dc/dc/commit/841201adab582149b1cca3a42e75f7cac75167f9)) | ||||
| * deploy page, makedata error handling, added local build of clarity, to address clr-stack-view CSP issues (inline styles) ([7b5e7ae](https://git.datacontroller.io/dc/dc/commit/7b5e7ae18414152f9b9d8f2d94fc94de43152003)) | ||||
| * improved deploy flow for Viya ([9604661](https://git.datacontroller.io/dc/dc/commit/9604661f3b76111387bc9474cc26348d73ab112e)) | ||||
| * requests modal causing VIYA CSP errors ([1dc6934](https://git.datacontroller.io/dc/dc/commit/1dc69341cadb837e1f11624d5cf35788bbb98d96)) | ||||
| * sas viya service init timing issue ([9de04e9](https://git.datacontroller.io/dc/dc/commit/9de04e9a0ce016e1a9fb8b19c656077079ddcf2f)) | ||||
| * scss of components transferred to the global styles.scss so we do not cause CSP (inline styles) issues when streaming to Viya ([6c171a6](https://git.datacontroller.io/dc/dc/commit/6c171a6394aba8104fe0f50aa8a4e6b9fa8023a2)) | ||||
| * viya deploy page improved flow ([4bd2154](https://git.datacontroller.io/dc/dc/commit/4bd215491f8cdc68f78bade68e7cb98e07edc81e)) | ||||
|  | ||||
| ## [6.14.7](https://git.datacontroller.io/dc/dc/compare/v6.14.6...v6.14.7) (2025-05-08) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * updated hot, clarity and improved accessibility score. ([2844c70](https://git.datacontroller.io/dc/dc/commit/2844c70f9507036216b8b621900c2bb9010c1d34)) | ||||
|  | ||||
| ## [6.14.6](https://git.datacontroller.io/dc/dc/compare/v6.14.5...v6.14.6) (2025-04-03) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * history table modal links styling ([c63fcdd](https://git.datacontroller.io/dc/dc/commit/c63fcdd465950ada439d7d69622a3886e8f3a783)) | ||||
|  | ||||
| ## [6.14.5](https://git.datacontroller.io/dc/dc/compare/v6.14.4...v6.14.5) (2025-03-24) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * improving accessibility lighthouse score ([7f3577c](https://git.datacontroller.io/dc/dc/commit/7f3577c3ef9f44e55a58bc64fbf89a3a64006dd4)) | ||||
| * prevent errors when using sqlrc in a DI job in a HOOK ([d1f0879](https://git.datacontroller.io/dc/dc/commit/d1f0879f0acf7e816c80f7635fd02f4f284214ed)) | ||||
| * user profile style fix, new select library and table icons ([69f8830](https://git.datacontroller.io/dc/dc/commit/69f883034fabbed31aa5d832e20561c4ae3042db)) | ||||
|  | ||||
| ## [6.14.4](https://git.datacontroller.io/dc/dc/compare/v6.14.3...v6.14.4) (2025-03-18) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * removing cli dependency warnings2 ([43c0f73](https://git.datacontroller.io/dc/dc/commit/43c0f73c2189ff762986a964caae6b0b108164fc)) | ||||
|  | ||||
| ## [6.14.3](https://git.datacontroller.io/dc/dc/compare/v6.14.2...v6.14.3) (2025-03-15) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * NLDAT & NLDATM formats are now being staged ([3f5cb1e](https://git.datacontroller.io/dc/dc/commit/3f5cb1e2defe390220e904e4bf04a165cb31fec4)) | ||||
|  | ||||
| ## [6.14.2](https://git.datacontroller.io/dc/dc/compare/v6.14.1...v6.14.2) (2025-03-10) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * improving instructions for setup ([83b3d77](https://git.datacontroller.io/dc/dc/commit/83b3d775b6e33653b087ca9f4eb3ad5b0dbbd479)) | ||||
|  | ||||
| ## [6.14.1](https://git.datacontroller.io/dc/dc/compare/v6.14.0...v6.14.1) (2025-03-05) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * handle national language datetime formats ([149e318](https://git.datacontroller.io/dc/dc/commit/149e318a8787be0109f25aeec3a1270ea75a97b2)) | ||||
| * updating logic to use NLDAT formats ([95289aa](https://git.datacontroller.io/dc/dc/commit/95289aa9524d3cb2b1c248cfb84f6b0d0a490c32)) | ||||
|  | ||||
| # [6.14.0](https://git.datacontroller.io/dc/dc/compare/v6.13.2...v6.14.0) (2025-02-26) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * uses SORTSEQ=LINGUISTIC for the services/metanav/metadetails service ([a45f5bb](https://git.datacontroller.io/dc/dc/commit/a45f5bb3b27a86da5f55ff28c9c7669956484ddf)) | ||||
|  | ||||
| ## [6.13.2](https://git.datacontroller.io/dc/dc/compare/v6.13.1...v6.13.2) (2025-02-26) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * get metadata email if exists ([1bd0eef](https://git.datacontroller.io/dc/dc/commit/1bd0eef913593579771c647d80010ce628c00819)) | ||||
|  | ||||
| ## [6.13.1](https://git.datacontroller.io/dc/dc/compare/v6.13.0...v6.13.1) (2025-02-18) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * Avoiding LATIN1 unprintables in various UI locations ([bce1fd5](https://git.datacontroller.io/dc/dc/commit/bce1fd57ef397cfdd030552c3f424a8407174729)) | ||||
| * updated @sasjs/adapter, crypto-browserify ([4e64f28](https://git.datacontroller.io/dc/dc/commit/4e64f28732868b07ec2ab403912bd384c62c7e1d)) | ||||
|  | ||||
| # [6.13.0](https://git.datacontroller.io/dc/dc/compare/v6.12.3...v6.13.0) (2025-01-31) | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										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}`) | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								client/libraries/clr-angular-17.9.0.tgz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								client/libraries/clr-angular-17.9.0.tgz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								client/libraries/clr-ui-17.9.0.tgz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								client/libraries/clr-ui-17.9.0.tgz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -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@14.4.0;handsontable@14.4.0;hyperformula@2.7.0;jackspeak@2.2.0;path-scurry@1.7.0' | ||||
|           '@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' } | ||||
|         ] | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										13594
									
								
								client/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										13594
									
								
								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": { | ||||
| @@ -44,28 +45,28 @@ | ||||
|     "@angular/platform-browser": "^17.3.3", | ||||
|     "@angular/platform-browser-dynamic": "^17.3.3", | ||||
|     "@angular/router": "^17.3.3", | ||||
|     "@cds/core": "^6.10.0", | ||||
|     "@clr/angular": "^17.0.1", | ||||
|     "@cds/core": "^6.15.1", | ||||
|     "@clr/angular": "file:libraries/clr-angular-17.9.0.tgz", | ||||
|     "@clr/icons": "^13.0.2", | ||||
|     "@clr/ui": "^17.0.1", | ||||
|     "@handsontable/angular": "^14.3.0", | ||||
|     "@sasjs/adapter": "4.10.2", | ||||
|     "@clr/ui": "file:libraries/clr-ui-17.9.0.tgz", | ||||
|     "@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", | ||||
|     "@types/text-encoding": "0.0.35", | ||||
|     "base64-arraybuffer": "^0.2.0", | ||||
|     "buffer": "^5.4.3", | ||||
|     "crypto-browserify": "3.12.0", | ||||
|     "crypto-browserify": "^3.12.1", | ||||
|     "crypto-js": "^4.2.0", | ||||
|     "d3-graphviz": "^5.0.2", | ||||
|     "fs-extra": "^7.0.1", | ||||
|     "handsontable": "^14.3.0", | ||||
|     "handsontable": "^16.0.1", | ||||
|     "https-browserify": "1.0.0", | ||||
|     "hyperformula": "^2.5.0", | ||||
|     "iconv-lite": "^0.5.0", | ||||
|     "jquery-datetimepicker": "^2.5.21", | ||||
|     "jsrsasign": "^10.2.0", | ||||
|     "jsrsasign": "^11.1.0", | ||||
|     "marked": "^5.0.0", | ||||
|     "moment": "^2.26.0", | ||||
|     "ngx-clipboard": "^16.0.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", | ||||
|   | ||||
| @@ -148,5 +148,8 @@ export const globals: { | ||||
|   }, | ||||
|   handsontable: { | ||||
|     darkTableHeaderClass: 'darkTH' | ||||
|   }, | ||||
|   userDropdownConfig: { | ||||
|     closeOnDebugClick: false | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -139,10 +139,15 @@ | ||||
|         [routerLink]="['/']" | ||||
|         class="nav-link" | ||||
|       > | ||||
|         <img class="without-text d-block d-md-none" src="images/dc-logo.svg" /> | ||||
|         <img | ||||
|           class="without-text d-block d-md-none" | ||||
|           src="images/dc-logo.svg" | ||||
|           alt="datacontroller logo without text" | ||||
|         /> | ||||
|         <img | ||||
|           class="with-text d-none d-md-block" | ||||
|           src="images/datacontroller.svg" | ||||
|           alt="datacontroller logo" | ||||
|         /> | ||||
|       </a> | ||||
|  | ||||
| @@ -283,7 +288,11 @@ | ||||
|  | ||||
| <!-- App Loading Page --> | ||||
| <div *ngIf="!startupDataLoaded" class="app-loading"> | ||||
|   <img class="loading-logo" src="images/datacontroller.svg" /> | ||||
|   <img | ||||
|     class="loading-logo" | ||||
|     src="images/datacontroller.svg" | ||||
|     alt="datacontroller logo" | ||||
|   /> | ||||
|  | ||||
|   <div *ngIf="appActive === null" class="slider"> | ||||
|     <div class="line"></div> | ||||
|   | ||||
| @@ -1,447 +0,0 @@ | ||||
| @import '../colors.scss'; | ||||
|  | ||||
| // Copyright (c) 2016 VMware, Inc. All Rights Reserved. | ||||
| // This software is released under MIT license. | ||||
| // The full license information can be found in LICENSE in the root directory of this project. | ||||
| app-requests-modal { | ||||
|   z-index: 10000; | ||||
| } | ||||
|  | ||||
| header.app-header { | ||||
|   background: $headerBackground !important; | ||||
|   color: #fff; | ||||
| } | ||||
|  | ||||
| .logo img.without-text { | ||||
|   width: 30px; | ||||
| } | ||||
|  | ||||
| .logo img.with-text { | ||||
|   width: 210px; | ||||
| } | ||||
|  | ||||
| .header-hamburger-trigger { | ||||
|   display: block; | ||||
|   background: transparent; | ||||
|   border: 0; | ||||
|   margin-left: 10px; | ||||
| } | ||||
|  | ||||
| .demo-expired-notice { | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   position: fixed; | ||||
|   left: 0; | ||||
|   top: 0; | ||||
|   height: 100vh !important; | ||||
|   width: 100vw !important; | ||||
|   z-index: 105; | ||||
|   background: rgba(33, 33, 33, .5); | ||||
|  | ||||
|   .expired-details { | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     padding: 30px; | ||||
|     z-index: 110; | ||||
|     background: $headerBackground; | ||||
|  | ||||
|     .expired-notice { | ||||
|       color: #e0e0e0; | ||||
|       font-size: 16px; | ||||
|  | ||||
|       .mailto { | ||||
|         color: #8dc53e; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .main-container .update-key { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   color: white; | ||||
|   padding: 0px 10px; | ||||
|   background: #00000026; | ||||
| } | ||||
|  | ||||
| .alert-icon-wrapper { | ||||
|   margin-top: 0 !important; | ||||
| } | ||||
|  | ||||
| .nav-text { | ||||
|   margin-right: 20px; | ||||
| } | ||||
|  | ||||
| .sidebar-toggle { | ||||
|   display: flex; | ||||
|   height: 100%; | ||||
|   align-items: center; | ||||
|   padding-left: 10px; | ||||
|  | ||||
|   clr-icon { | ||||
|     cursor: pointer; | ||||
|     width: 30px; | ||||
|     height: 30px; | ||||
|   } | ||||
| } | ||||
|  | ||||
| header { | ||||
|   .header-actions { | ||||
|     .dropdown { | ||||
|       position: unset; //without it, when opening user dropdown scrollbar was displaying without reason | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .nav-link:hover { | ||||
|       color: #fafafa; | ||||
|   } | ||||
|  | ||||
|   .nav-link.active { | ||||
|     background: #61717D; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .notf { | ||||
|   background: #16a57a; | ||||
|   color: #fffcfc; | ||||
|   font-size: 12px; | ||||
| } | ||||
|  | ||||
|  | ||||
| .toggle-switch input[type=checkbox]:checked+label:before { | ||||
|   border-color: #61717D; | ||||
|   background-color: #61717D; | ||||
|   transition: .15s ease-in; | ||||
|   transition-property: border-color,background-color; | ||||
| } | ||||
|  | ||||
| .main-container { | ||||
|   min-height: 100vh !important; | ||||
| } | ||||
|  | ||||
| .main-container .content-container .content-area { | ||||
|   padding: 0rem 1rem 1rem 1rem; | ||||
| } | ||||
|  | ||||
| .content-container { | ||||
|   z-index: 0!important; | ||||
| } | ||||
|  | ||||
| .navBarResp { | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   background: #495A67; | ||||
|   color: #fff; | ||||
| } | ||||
|  | ||||
| ::ng-deep { | ||||
|   .htInvalid { | ||||
|     background: black!important; | ||||
|   } | ||||
|  | ||||
|   @media screen and (max-width:480px) { | ||||
|     h2 { | ||||
|       font-size: .7rem!important; | ||||
|     } | ||||
|     h3 { | ||||
|       font-size: .7rem; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .nav-link { | ||||
|     padding: 0rem 1rem 0rem 1rem; | ||||
|   } | ||||
|  | ||||
|   body[cds-theme="light"] { | ||||
|     .btn-primary .btn, .btn.btn-primary { | ||||
|       border-color: $headerBackground; | ||||
|       background-color: $headerBackground; | ||||
|       color: #fff; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   body[cds-theme="dark"] { | ||||
|     .btn-primary .btn, .btn.btn-primary { | ||||
|       border-color: #5e7382; | ||||
|       background-color: #5e7382; | ||||
|       color: #fff; | ||||
|  | ||||
|       clr-icon, cds-icon { | ||||
|         color: #fff | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .btn-primary .btn, .btn.btn-primary { | ||||
|     &:disabled { | ||||
|       opacity: 0.65; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .btn { | ||||
|     cursor: pointer; | ||||
|     display: inline-block; | ||||
|     -webkit-appearance: none!important; | ||||
|     border-radius: .125rem; | ||||
|     border: 1px solid; | ||||
|     min-width: 3rem; | ||||
|     max-width: 15rem; | ||||
|     white-space: nowrap; | ||||
|     text-overflow: ellipsis; | ||||
|     overflow: hidden; | ||||
|     text-align: center; | ||||
|     text-transform: uppercase; | ||||
|     vertical-align: middle; | ||||
|     line-height: 1.5rem; | ||||
|     letter-spacing: .12em; | ||||
|     font-size: .5rem; | ||||
|     font-weight: 500; | ||||
|     height: 1.5rem; | ||||
|     padding: 0 .5rem; | ||||
|   } | ||||
|  | ||||
|   .btn.btn-outline:hover { | ||||
|     border-color: $headerBackground; | ||||
|     background-color: #495A67; | ||||
|     color: #fff; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   body[cds-theme="dark"] { | ||||
|     .btn.btn-icon.btn-dimmed { | ||||
|       color: #7295ae; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   body[cds-theme="light"] { | ||||
|     .btn.btn-icon.btn-dimmed { | ||||
|       color: $headerBackground; | ||||
|     } | ||||
|  | ||||
|     .btn.btn-outline { | ||||
|       border-color: $headerBackground; | ||||
|       background-color: transparent; | ||||
|       color: $headerBackground; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .htMobileEditorContainer .inputs textarea { | ||||
|     font-size: 13pt; | ||||
|     border: 2px solid #485967; | ||||
|     border-radius: 4px; | ||||
|     -webkit-appearance: none; | ||||
|     box-shadow: none; | ||||
|     position: absolute; | ||||
|     left: 14px; | ||||
|     right: 0px; | ||||
|     top: 0; | ||||
|     bottom: 0; | ||||
|     padding: 7pt; | ||||
|     width: 290px; | ||||
| } | ||||
|  | ||||
| .htMobileEditorContainer .positionControls { | ||||
|     width: 333px; | ||||
|     position: absolute; | ||||
|     right: 5pt; | ||||
|     top: 50px; | ||||
|     bottom: 0; | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
| } | ||||
|  | ||||
| .htMobileEditorContainer.active { | ||||
|     display: block; | ||||
|     height: 120px; | ||||
|     width: 350px; | ||||
| } | ||||
|  | ||||
|  | ||||
|   /* Left and right */ | ||||
|  | ||||
|   /* Column headers */ | ||||
|  | ||||
|   body[cds-theme="light"] { | ||||
|     .wtBorder { | ||||
|       background-color: #495A67!important; | ||||
|     } | ||||
|  | ||||
|     .ht_master tr:nth-of-type(odd) > td { | ||||
|       filter: brightness(0.95); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   $darkBorderColor: #697c85; | ||||
|  | ||||
|   body[cds-theme="dark"] { | ||||
|     .ht_master tr:nth-of-type(odd) > td { | ||||
|       filter: brightness(1.2); | ||||
|     } | ||||
|  | ||||
|     .ht_master:not(.emptyColumns) ~ .handsontable tbody tr th, .ht_master:not(.emptyColumns) ~ .handsontable:not(.ht_clone_top) thead tr th:first-child { | ||||
|       background-color: #2d4048; | ||||
|       border-color: $darkBorderColor; | ||||
|     } | ||||
|  | ||||
|     .handsontable td { | ||||
|       // border-right: 1px solid #697c85; | ||||
|       // border-bottom: 1px solid #697c85; | ||||
|       border-color: $darkBorderColor; | ||||
|     } | ||||
|  | ||||
|     .handsontable tr:first-child th, .handsontable tr:first-child td { | ||||
|       border-color: $darkBorderColor; | ||||
|     } | ||||
|  | ||||
|     .handsontable .handsontable.ht_clone_top .wtHider { | ||||
|       border-color: $darkBorderColor; | ||||
|     } | ||||
|  | ||||
|     .handsontable .changeType { | ||||
|       background-color: #3c5662; | ||||
|       border-color: $darkBorderColor; | ||||
|     } | ||||
|  | ||||
|     .handsontableInput { | ||||
|       background-color: #708b98; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .handsontable .handsontable.ht_clone_top .wtHider { | ||||
|     padding: 0 0 0px 0!important; | ||||
|     margin: 0px; | ||||
|     border-bottom: 3px solid #d6d3d3; | ||||
|   } | ||||
|  | ||||
|   body[cds-theme="light"] { | ||||
|     .content-container { | ||||
|       // background: red; | ||||
|       background: #F5F6FF; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .datagrid-compact, .datagrid-history{ | ||||
|     .datagrid { | ||||
|       border-collapse: separate; | ||||
|       border: 1px solid transparent; | ||||
|       border-radius: .125rem; | ||||
|       margin: 0; | ||||
|       margin-top: 1rem; | ||||
|       max-width: 100%; | ||||
|       width: 100%; | ||||
|       padding: 15px 15px 50px 15px; | ||||
|     } | ||||
|     .datagrid-foot { | ||||
|       -webkit-box-pack: end; | ||||
|       -ms-flex-pack: end; | ||||
|       justify-content: flex-end; | ||||
|       height: 1.5rem; | ||||
|       padding: 0 .5rem; | ||||
|       line-height: calc(1.5rem - 3px); | ||||
|       font-size: .45833rem; | ||||
|       background-color: #fff; | ||||
|       border-top: 1px solid #ccc; | ||||
|       border-radius: 0px; | ||||
|       // border-radius: 0 0 .125rem .125rem; | ||||
|     } | ||||
|     .datagrid-footer { | ||||
|       position: absolute; | ||||
|       right: 30px; | ||||
|       top: 1px; | ||||
|     } | ||||
|     .datagrid .datagrid-head { | ||||
|       background-color: #fff; | ||||
|       border-bottom: 1px solid #ccc; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .dropdown-menu { | ||||
|     position: absolute; | ||||
|     top: 100%; | ||||
|     left: 0; | ||||
|     margin-top: .083333rem; | ||||
|     display: -webkit-box; | ||||
|     display: -ms-flexbox; | ||||
|     display: flex; | ||||
|     -webkit-box-orient: vertical; | ||||
|     -webkit-box-direction: normal; | ||||
|     -ms-flex-direction: column; | ||||
|     flex-direction: column; | ||||
|     padding: .5rem 0; | ||||
|     border: 1px solid #ccc; | ||||
|     box-shadow: 0 1px 0.125rem hsla(0,0%,45%,.25); | ||||
|     min-width: 5rem; | ||||
|     max-width: 15rem; | ||||
|     border-radius: .125rem; | ||||
|     visibility: hidden; | ||||
|     z-index: 1000; | ||||
|   } | ||||
|  | ||||
|   .table { | ||||
|     border-collapse: separate; | ||||
|     border: 1px solid transparent; | ||||
|     border-radius: 0px; | ||||
|     margin: 0; | ||||
|     margin-top: 1rem; | ||||
|     max-width: 100%; | ||||
|     width: 100%; | ||||
|   } | ||||
|  | ||||
|   .table th { | ||||
|     font-size: .45833rem; | ||||
|     font-weight: 600; | ||||
|     letter-spacing: .03em; | ||||
|     vertical-align: bottom; | ||||
|     border-bottom: 1px solid #ccc; | ||||
|     text-transform: uppercase; | ||||
|   } | ||||
|  | ||||
|   .modal-header { | ||||
|     border-bottom: 2px solid #e4e4e4; | ||||
|     padding: 0 0 .5rem 0; | ||||
|     margin-bottom: 1rem; | ||||
|   } | ||||
|  | ||||
|   .main-container .content-container { | ||||
|     min-height: 0px; | ||||
|     position: relative; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .app-loading { | ||||
|   .loading-logo { | ||||
|     max-width: 400px; | ||||
|     width: 100%; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @media screen and (max-width: 768px) { | ||||
|   .navBarResp { | ||||
|     display: flex; | ||||
|     justify-content: flex-start; | ||||
|     background: #495A67; | ||||
|     color: #fff; | ||||
|   } | ||||
|  | ||||
|   .main-container .sub-nav.clr-nav-level-1 .nav .nav-link, .main-container .sub-nav.clr-nav-level-2 .nav .nav-link, .main-container .subnav.clr-nav-level-1 .nav .nav-link, .main-container .subnav.clr-nav-level-2 .nav .nav-link { | ||||
|       padding: 0 .5rem 0 1rem; | ||||
|       width: 100%; | ||||
|       max-width: 100%; | ||||
|       overflow: hidden; | ||||
|       text-overflow: ellipsis; | ||||
|       border-radius: .125rem 0 0 .125rem; | ||||
|       color: #95c84b; | ||||
|   } | ||||
|  | ||||
|  | ||||
|  | ||||
|   .card-block, .card-footer { | ||||
|     padding: 10px 0px 0px 0px; | ||||
|   } | ||||
|  | ||||
|   .main-container[_ngcontent-c0] .content-container[_ngcontent-c0] .content-area[_ngcontent-c0] { | ||||
|     padding: 0rem 0rem 0rem 0rem; | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -1,4 +1,9 @@ | ||||
| import { ChangeDetectorRef, Component, ElementRef } from '@angular/core' | ||||
| import { | ||||
|   ChangeDetectorRef, | ||||
|   Component, | ||||
|   ElementRef, | ||||
|   ViewEncapsulation | ||||
| } from '@angular/core' | ||||
| import { Router } from '@angular/router' | ||||
| import { VERSION } from '../environments/version' | ||||
| import { ActivatedRoute } from '@angular/router' | ||||
| @@ -36,7 +41,8 @@ ClarityIcons.addIcons( | ||||
| @Component({ | ||||
|   selector: 'my-app', | ||||
|   templateUrl: './app.component.html', | ||||
|   styleUrls: ['./app.component.scss'] | ||||
|   styleUrls: ['./app.component.scss'], | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class AppComponent { | ||||
|   private dcAdapterSettings: DcAdapterSettings | undefined | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|       <div class="card-header">Terms and Conditions</div> | ||||
|       <div class="card-block"> | ||||
|         <div class="card-text"> | ||||
|           <p> | ||||
|           <p class="mt-0"> | ||||
|             The Demo version of Data Controller is free for EVALUATION purposes | ||||
|             only. Before proceeding with configuration, please confirm that you | ||||
|             have read, understood, and agreed to the | ||||
|   | ||||
| @@ -1,50 +0,0 @@ | ||||
| .card { | ||||
|     margin-top: 0; | ||||
| } | ||||
|  | ||||
| .btn { | ||||
|     margin-top: 10px; | ||||
| } | ||||
|  | ||||
| .log-wrapper { | ||||
|     width: 100%; | ||||
|     background: #f0f0f0; | ||||
|     border: 1px solid #c9c9c9; | ||||
|     padding: 10px; | ||||
|     overflow: auto; | ||||
|     white-space: pre-wrap; | ||||
| } | ||||
|  | ||||
| #contexts-btn { | ||||
|     padding: 0; | ||||
|     min-width: 30px; | ||||
|     margin-left: 10px; | ||||
|     height: 30px; | ||||
|     display: inline-flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     padding-top: 3px; | ||||
| } | ||||
|  | ||||
| .validation-bar { | ||||
|     display: flex; | ||||
|     margin-top: 20px; | ||||
|     align-items: center; | ||||
|  | ||||
|     clr-icon { | ||||
|         margin-right: 5px; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .autodeploy-section { | ||||
|     padding: 0px 15px; | ||||
|  | ||||
|     .clr-checkbox-wrapper { | ||||
|         margin: 20px 0 20px 0; | ||||
|     } | ||||
|  | ||||
|     .btn-autodeploy { | ||||
|         display: block; | ||||
|         margin: 15px 0 15px 0; | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| import { Component, OnInit, ViewEncapsulation } from '@angular/core' | ||||
| import { SasService } from '../services/sas.service' | ||||
| import { SASjsConfig } from '@sasjs/adapter' | ||||
| import { Router } from '@angular/router' | ||||
| @@ -13,7 +13,8 @@ import { DcAdapterSettings } from '../models/DcAdapterSettings' | ||||
|   styleUrls: ['./deploy.component.scss'], | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class DeployComponent implements OnInit { | ||||
|   public step: number = 0 | ||||
| @@ -56,25 +57,6 @@ export class DeployComponent implements OnInit { | ||||
|   } | ||||
|  | ||||
|   ngOnInit() { | ||||
|     if (this.sasJsConfig.serverType === ServerType.SasViya) { | ||||
|       fetch('sasbuild/viya.json') | ||||
|         .then((res) => res.text()) | ||||
|         .then((res) => { | ||||
|           let initJsonFile: any = null | ||||
|  | ||||
|           try { | ||||
|             initJsonFile = JSON.parse(res) | ||||
|           } catch (err) { | ||||
|             console.error(err) | ||||
|           } | ||||
|  | ||||
|           if (initJsonFile) { | ||||
|             this.jsonFile = initJsonFile | ||||
|             this.loggerService.log(this.jsonFile) | ||||
|           } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     this.setDeployDefaults() | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -9,14 +9,17 @@ | ||||
|       <p class="m-0 align-self-start">Done</p> | ||||
|       <hr class="w-100" /> | ||||
|  | ||||
|       <div class="deploy-status-row"> | ||||
|       <div | ||||
|         *ngIf="autoDeployStatus.deployServicePack !== null" | ||||
|         class="deploy-status-row" | ||||
|       > | ||||
|         <clr-icon | ||||
|           *ngIf="autoDeployStatus.deployServicePack" | ||||
|           *ngIf="autoDeployStatus.deployServicePack === true" | ||||
|           class="deploy-success" | ||||
|           shape="success-standard" | ||||
|         ></clr-icon> | ||||
|         <clr-icon | ||||
|           *ngIf="!autoDeployStatus.deployServicePack" | ||||
|           *ngIf="!autoDeployStatus.deployServicePack === false" | ||||
|           class="deploy-error" | ||||
|           shape="times-circle" | ||||
|         ></clr-icon> | ||||
| @@ -52,7 +55,7 @@ | ||||
|             class="deploy-error" | ||||
|             shape="times-circle" | ||||
|           ></clr-icon> | ||||
|           LAUNCH / CONFIGURE | ||||
|           LAUNCH | ||||
|         </button> | ||||
|  | ||||
|         <button | ||||
| @@ -94,20 +97,72 @@ | ||||
| </div> | ||||
|  | ||||
| <label for="dcloc" class="mt-20 clr-control-label">DC Loc</label> | ||||
| <div class="mb-10 clr-control-container"> | ||||
|   <div class="clr-input-wrapper"> | ||||
|     <p class="mt-0">{{ dcPath }}</p> | ||||
| <div class="mb-10 clr-control-container dc-loc-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"> | ||||
|     <p class="mt-0">{{ selectedAdminGroup }}</p> | ||||
|   <div class="clr-input-wrapper small-mt"> | ||||
|     <select | ||||
|       *ngIf="!adminGroupsLoading" | ||||
|       clrSelect | ||||
|       name="options" | ||||
|       [(ngModel)]="selectedAdminGroup" | ||||
|     > | ||||
|       <option *ngFor="let adminGroup of adminGroups" [value]="adminGroup.id"> | ||||
|         {{ adminGroup.name }} | ||||
|       </option> | ||||
|     </select> | ||||
|     <clr-spinner | ||||
|       clrInline | ||||
|       class="spinner-sm" | ||||
|       *ngIf="adminGroupsLoading" | ||||
|     ></clr-spinner> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
| <clr-checkbox-wrapper> | ||||
| <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> | ||||
|   <input | ||||
|     clrCheckbox | ||||
|     [(ngModel)]="recreateDatabase" | ||||
| @@ -116,19 +171,28 @@ | ||||
|     checked | ||||
|   /> | ||||
|   <label>Recreate database</label> | ||||
| </clr-checkbox-wrapper> | ||||
| </clr-checkbox-wrapper> --> | ||||
|  | ||||
| <hr /> | ||||
|  | ||||
| <button | ||||
|   (click)="runAutoDeploy()" | ||||
|   class="btn-autodeploy btn btn-primary d-inline-block mr-10" | ||||
| > | ||||
|   Deploy | ||||
| </button> | ||||
|  | ||||
| <!-- Keeping this for a reference in case future VIYA changes and starts allowing separate backend and frontend) --> | ||||
|  | ||||
| <!-- <button | ||||
|   (click)="executeJson()" | ||||
|   class="btn-autodeploy btn btn-primary d-inline-block mr-10" | ||||
|   [disabled]="!jsonFile" | ||||
| > | ||||
|   Deploy {{ !jsonFile ? '(json file is not available)' : '' }} | ||||
| </button> | ||||
| </button> --> | ||||
|  | ||||
| <button | ||||
| <!-- <button | ||||
|   (click)="uploadJsonAuto.click()" | ||||
|   class="btn-autodeploy btn btn-primary d-inline-block mr-10" | ||||
| > | ||||
| @@ -140,7 +204,7 @@ | ||||
|   hidden | ||||
|   (click)="clearUploadInput($event)" | ||||
|   (change)="onJsonFileChange($event)" | ||||
| /> | ||||
| /> --> | ||||
|  | ||||
| <clr-modal [(clrModalOpen)]="recreateDatabaseModal" [clrModalClosable]="false"> | ||||
|   <h3 class="modal-title">Warning</h3> | ||||
|   | ||||
| @@ -1,61 +0,0 @@ | ||||
| .auto-deploy { | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|  | ||||
|     position: fixed; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|     top: 0; | ||||
|     bottom: 0; | ||||
|     background: rgba(0, 0, 0, 0.4); | ||||
|  | ||||
|     z-index: 100; | ||||
| } | ||||
|  | ||||
| .spinner-box { | ||||
|     width: 400px; | ||||
|     padding: 20px; | ||||
|     border-radius: 3px; | ||||
|     background: #fff; | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     flex-direction: column; | ||||
|     box-shadow: 1px 1px 8px 0px #00000082; | ||||
|  | ||||
|     .buttons { | ||||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|         width: 100%; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .deploy-status-row { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     align-self: flex-start; | ||||
|  | ||||
|     p { | ||||
|         margin: 0 0 0 10px; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .deploy-success { | ||||
|     color: #6ECF44; | ||||
| } | ||||
|  | ||||
| .deploy-error { | ||||
|     color: #E74C3C; | ||||
|     // width: 20px; | ||||
|     // height: 20px; | ||||
| } | ||||
|  | ||||
| .deploy-undeterminated { | ||||
|     color: #cacaca; | ||||
| } | ||||
|  | ||||
| hr { | ||||
|     border: 0; | ||||
|     border-bottom: 1px solid #00000045; | ||||
| } | ||||
| @@ -1,15 +1,35 @@ | ||||
| import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||||
| import { | ||||
|   Component, | ||||
|   EventEmitter, | ||||
|   Input, | ||||
|   OnInit, | ||||
|   Output, | ||||
|   ViewEncapsulation | ||||
| } 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' | ||||
| import { SasViyaService } from 'src/app/services/sas-viya.service' | ||||
| import { SasService } from 'src/app/services/sas.service' | ||||
| import { ViyaApiCurrentUser } from 'src/app/viya-api-explorer/models/viya-api-current-user.model' | ||||
| import { | ||||
|   Item, | ||||
|   ViyaApiIdentities | ||||
| } from 'src/app/viya-api-explorer/models/viya-api-identities.model' | ||||
| import { ComputeContextDetails } from 'src/app/viya-api-explorer/models/viya-compute-context-details.model' | ||||
| import { | ||||
|   ViyaComputeContexts, | ||||
|   Item as ComputeContextItem | ||||
| } from 'src/app/viya-api-explorer/models/viya-compute-contexts.model' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-automatic-deploy', | ||||
|   templateUrl: './automatic.component.html', | ||||
|   styleUrls: ['./automatic.component.scss'] | ||||
|   styleUrls: ['./automatic.component.scss'], | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class AutomaticComponent implements OnInit { | ||||
|   @Input() sasJs!: SASjs | ||||
| @@ -21,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 | ||||
| @@ -28,8 +49,19 @@ export class AutomaticComponent implements OnInit { | ||||
|   public recreateDatabaseModal: boolean = false | ||||
|   public isSubmittingJson: boolean = false | ||||
|   public isJsonSubmitted: boolean = false | ||||
|   public recreateDatabase: boolean = false | ||||
|   /** | ||||
|    * Default was `false` when deploy was done with frontend and backend separately. | ||||
|    * Now we are using only streaming app, so we always want to recreate database (makedata) | ||||
|    */ | ||||
|   public recreateDatabase: boolean = true | ||||
|   public createDatabaseLoading: boolean = false | ||||
|   public 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. | ||||
| @@ -46,14 +78,138 @@ 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 loggerService: LoggerService | ||||
|     private sasViyaService: SasViyaService, | ||||
|     private loggerService: LoggerService, | ||||
|     private helperService: HelperService | ||||
|   ) {} | ||||
|  | ||||
|   ngOnInit(): void {} | ||||
|   ngOnInit(): void { | ||||
|     this.loadData() | ||||
|   } | ||||
|  | ||||
|   public async loadData() { | ||||
|     await this.getAdminGroups() | ||||
|     await this.getComputeContexts() | ||||
|     await this.getCurrentUser() | ||||
|  | ||||
|     setTimeout(() => { | ||||
|       if (this.selectedComputeContext) { | ||||
|         this.onComputeContextChange(this.selectedComputeContext) | ||||
|       } | ||||
|     }, 500) | ||||
|   } | ||||
|  | ||||
|   public async getComputeContexts() { | ||||
|     return new Promise<void>((resolve, reject) => { | ||||
|       this.computeContextsLoading = true | ||||
|  | ||||
|       this.sasViyaService.getComputeContexts().subscribe( | ||||
|         (res: ViyaComputeContexts) => { | ||||
|           this.computeContextsLoading = false | ||||
|  | ||||
|           const defaultContext = res.items.find( | ||||
|             (item: ComputeContextItem) => | ||||
|               item.name === 'SAS Job Execution compute context' | ||||
|           ) | ||||
|  | ||||
|           if (defaultContext) { | ||||
|             this.selectedComputeContext = defaultContext.id | ||||
|           } | ||||
|  | ||||
|           this.computeContexts = res.items | ||||
|  | ||||
|           resolve() | ||||
|         }, | ||||
|         (err) => { | ||||
|           reject(err) | ||||
|         } | ||||
|       ) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   public async getCurrentUser() { | ||||
|     return new Promise<void>((resolve, reject) => { | ||||
|       this.currentUserInfoLoading = true | ||||
|  | ||||
|       this.sasViyaService.getCurrentUser().subscribe( | ||||
|         (res: ViyaApiCurrentUser) => { | ||||
|           this.currentUserInfoLoading = false | ||||
|  | ||||
|           this.currentUserInfo = res | ||||
|  | ||||
|           this.dcPath = `/export/viya/homes/${res.id}` | ||||
|  | ||||
|           resolve() | ||||
|         }, | ||||
|         (err) => { | ||||
|           console.error('Error while getting current user', err) | ||||
|           reject(err) | ||||
|         } | ||||
|       ) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   public async getAdminGroups() { | ||||
|     return new Promise<void>((resolve, reject) => { | ||||
|       this.adminGroupsLoading = true | ||||
|       ;(this.sasViyaService | ||||
|         .getAdminGroups() | ||||
|         .subscribe((res: ViyaApiIdentities) => { | ||||
|           this.adminGroupsLoading = false | ||||
|           // Map admin groups with only needed fields | ||||
|           this.adminGroups = res.items.map((item: Item) => { | ||||
|             return { | ||||
|               id: item.id, | ||||
|               name: item.name | ||||
|             } | ||||
|           }) | ||||
|  | ||||
|           resolve() | ||||
|         }), | ||||
|         (err: any) => { | ||||
|           this.adminGroupsLoading = false | ||||
|           this.loggerService.error('Error while getting admin groups', err) | ||||
|           this.eventService.showAbortModal('admin groups', err) | ||||
|  | ||||
|           reject(err) | ||||
|         }) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   public async onComputeContextChange(computeContextId: string) { | ||||
|     this.sasViyaService | ||||
|       .getComputeContextById(computeContextId) | ||||
|       .subscribe((res: ComputeContextDetails) => { | ||||
|         if (res.attributes && res.attributes.runServerAs) { | ||||
|           this.runningAsUser = res.attributes.runServerAs | ||||
|         } else { | ||||
|           this.runningAsUser = this.currentUserInfo?.id || 'unknown' | ||||
|         } | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   public getComputeContextName(id: string): string | undefined { | ||||
|     return ( | ||||
|       this.computeContexts.find( | ||||
|         (context: ComputeContextItem) => context.id === id | ||||
|       )?.name || undefined | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Executes sas.json file to deploy the backend | ||||
| @@ -63,7 +219,6 @@ export class AutomaticComponent implements OnInit { | ||||
|    * to create database if checkbox is toggled on | ||||
|    */ | ||||
|   public async executeJson() { | ||||
|     this.autodeploying = true | ||||
|     this.isSubmittingJson = true | ||||
|  | ||||
|     try { | ||||
| @@ -98,11 +253,19 @@ export class AutomaticComponent implements OnInit { | ||||
|     } | ||||
|  | ||||
|     this.isSubmittingJson = false | ||||
|   } | ||||
|  | ||||
|   public async runAutoDeploy(executeJson: boolean = false) { | ||||
|     if (!this.deployInNewWindow) this.autodeploying = true | ||||
|  | ||||
|     if (executeJson) { | ||||
|       this.executeJson() | ||||
|     } | ||||
|  | ||||
|     if (this.recreateDatabase) { | ||||
|       this.createDatabase() | ||||
|     } else { | ||||
|       this.autodeployDone = true | ||||
|       if (!this.deployInNewWindow) this.autodeployDone = true | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -119,45 +282,160 @@ 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. | ||||
|      * contextName: null is the MUST field for it. | ||||
|      */ | ||||
|     let overrideConfig = { | ||||
|       useComputeApi: false, | ||||
|       contextName: this.sasJsConfig.contextName, | ||||
|       useComputeApi: null, | ||||
|       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 | ||||
|         } | ||||
|       }) | ||||
|       .catch((err: any) => { | ||||
|         this.autoDeployStatus.runMakeData = false | ||||
|         this.autodeployDone = true | ||||
|           this.autodeployDone = true | ||||
|  | ||||
|         try { | ||||
|           this.makeDataResponse = JSON.stringify(err) | ||||
|         } catch { | ||||
|           this.makeDataResponse = err | ||||
|         } | ||||
|           try { | ||||
|             this.makeDataResponse = JSON.stringify(err) | ||||
|           } catch { | ||||
|             this.makeDataResponse = err | ||||
|           } | ||||
|         }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public runMakedataInNewWindow(params: { | ||||
|     contextName: string | ||||
|     admin: string | ||||
|     dcPath: string | ||||
|   }) { | ||||
|     let serverUrl = this.sasjsConfig.serverUrl | ||||
|     let appLoc = this.sasjsConfig.appLoc | ||||
|     const execPath = this.sasService.getExecutionPath() | ||||
|     let contextname = `&_contextname=${params.contextName}` | ||||
|     let admin = `&admin=${params.admin}` | ||||
|     let dcPath = `&dcpath=${params.dcPath}` | ||||
|     let debug = `&_debug=131` | ||||
|  | ||||
|     let programUrl = | ||||
|       serverUrl + | ||||
|       execPath + | ||||
|       '/?_program=' + | ||||
|       appLoc + | ||||
|       '/services/admin/makedata' + | ||||
|       contextname + | ||||
|       admin + | ||||
|       dcPath + | ||||
|       debug | ||||
|  | ||||
|     window.open(programUrl) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Only when on Viya streamed app, this method will update the `contextname` in the `index.html` on the SAS drive | ||||
|    * This is needed to ensure that the DC will use the same compute context `makedata` service used to run against. | ||||
|    */ | ||||
|   public async updateIndexHtmlComputeContext() { | ||||
|     const filenamePath = location.search.split('/').pop() | ||||
|     const filename = filenamePath?.includes('.') ? filenamePath : undefined | ||||
|  | ||||
|     if (!filename) { | ||||
|       this.eventService.showAbortModal( | ||||
|         null, | ||||
|         'We could not figure out the file name of `index.html` based on the url.' | ||||
|       ) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     const indexHtmlContent = await this.sasService.getFileContent( | ||||
|       `${this.appLoc}/services`, | ||||
|       filename | ||||
|     ) | ||||
|  | ||||
|     if (!indexHtmlContent) { | ||||
|       this.loggerService.error( | ||||
|         `Failed to get ${filename} at ${this.appLoc}/services` | ||||
|       ) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     const computeContextName = this.getComputeContextName( | ||||
|       this.selectedComputeContext | ||||
|     ) | ||||
|  | ||||
|     if (!computeContextName) { | ||||
|       this.loggerService.error( | ||||
|         `Compute context name not found for ID: ${this.selectedComputeContext} | List: ${JSON.stringify(this.computeContexts)}` | ||||
|       ) | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     const updatedContent = indexHtmlContent.replace( | ||||
|       /contextname="[^"]*"/g, | ||||
|       `contextname="${computeContextName}"` | ||||
|     ) | ||||
|  | ||||
|     await this.sasService | ||||
|       .updateFileContent(`${this.appLoc}/services`, filename, updatedContent) | ||||
|       .catch((err: any) => { | ||||
|         this.loggerService.error(`Failed to update DataController.html: ${err}`) | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +0,0 @@ | ||||
| .clear-memory-button { | ||||
|   right: 10px; | ||||
|   top: 2px; | ||||
| } | ||||
| @@ -1,4 +1,11 @@ | ||||
| import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core' | ||||
| import { | ||||
|   Component, | ||||
|   Input, | ||||
|   OnInit, | ||||
|   Output, | ||||
|   EventEmitter, | ||||
|   ViewEncapsulation | ||||
| } from '@angular/core' | ||||
| import SASjs, { SASjsConfig } from '@sasjs/adapter' | ||||
| import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings' | ||||
| import { RequestWrapperResponse } from 'src/app/models/request-wrapper/RequestWrapperResponse' | ||||
| @@ -10,7 +17,8 @@ import { SasService } from 'src/app/services/sas.service' | ||||
| @Component({ | ||||
|   selector: 'app-manual-deploy', | ||||
|   templateUrl: './manual.component.html', | ||||
|   styleUrls: ['./manual.component.scss'] | ||||
|   styleUrls: ['./manual.component.scss'], | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class ManualComponent implements OnInit { | ||||
|   @Input() sasJs!: SASjs | ||||
| @@ -266,7 +274,7 @@ export class ManualComponent implements OnInit { | ||||
|      * contextName: null is the MUST field for it. | ||||
|      */ | ||||
|     let overrideConfig = { | ||||
|       useComputeApi: false, | ||||
|       useComputeApi: null, | ||||
|       contextName: this.sasJsConfig.contextName, | ||||
|       debug: true | ||||
|     } | ||||
|   | ||||
| @@ -10,11 +10,13 @@ | ||||
| </p> | ||||
|  | ||||
| <p class="m-0 mt-10"> | ||||
|   Please specify a physical directory below, to which user | ||||
|   <strong>{{ SYSUSERID }}</strong> can write, on behalf of Data Controller: | ||||
|   Please specify a physical directory (on the | ||||
|   <strong> {{ SYSHOSTNAME }}</strong> | ||||
|   compute server) below, to which user | ||||
|   <strong>{{ SYSUSERID }}</strong> can write, on behalf of Data Controller. | ||||
| </p> | ||||
|  | ||||
| <label class="mt-20 clr-control-label">DC Directory</label> | ||||
| <label class="mt-20 clr-control-label">DC Staging Directory</label> | ||||
| <div class="mb-10 clr-control-container"> | ||||
|   <div class="clr-input-wrapper"> | ||||
|     <input | ||||
|   | ||||
| @@ -1,23 +0,0 @@ | ||||
| .clr-control-container { | ||||
|   width: 50vw; | ||||
| } | ||||
|  | ||||
| .clr-input-wrapper { | ||||
|   width: 100%; | ||||
|  | ||||
|   input { | ||||
|     width: 100%; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .thinProgress { | ||||
|   left: 0px; | ||||
|   right: 0; | ||||
|   width: unset; | ||||
|   height: 1px; | ||||
|   margin-top: 0 !important; | ||||
|  | ||||
|   &::after { | ||||
|     top: 0; | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +1,12 @@ | ||||
| import { Location } from '@angular/common' | ||||
| import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||||
| import { | ||||
|   Component, | ||||
|   EventEmitter, | ||||
|   Input, | ||||
|   OnInit, | ||||
|   Output, | ||||
|   ViewEncapsulation | ||||
| } from '@angular/core' | ||||
| import SASjs, { SASjsConfig } from '@sasjs/adapter' | ||||
| import { ServerType } from '@sasjs/utils/types/serverType' | ||||
| import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings' | ||||
| @@ -12,7 +19,8 @@ import { SasjsService } from 'src/app/services/sasjs.service' | ||||
| @Component({ | ||||
|   selector: 'app-sasjs-configurator', | ||||
|   templateUrl: './sasjs-configurator.component.html', | ||||
|   styleUrls: ['./sasjs-configurator.component.scss'] | ||||
|   styleUrls: ['./sasjs-configurator.component.scss'], | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class SasjsConfiguratorComponent implements OnInit { | ||||
|   @Input() sasJs!: SASjs | ||||
|   | ||||
| @@ -1,238 +0,0 @@ | ||||
| .record-edit-modal { | ||||
|   .column-entry { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     .name-input-row { | ||||
|       width: 100%; | ||||
|       max-width: 260px; | ||||
|  | ||||
|       .cell-desc { | ||||
|         margin-right: 30px; | ||||
|         margin-top: 10px; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .inputs-wrapper { | ||||
|       flex: 1; | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|  | ||||
|       ::ng-deep >*:not(.date-field):not(clr-select-container) { | ||||
|         flex: 1; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     p { | ||||
|       margin-top: 0px; | ||||
|     } | ||||
|  | ||||
|     ::ng-deep { | ||||
|       .clr-textarea-wrapper { | ||||
|         margin-top: 0 !important; | ||||
|       } | ||||
|  | ||||
|       .clr-form-control { | ||||
|         margin-top: 0px !important; | ||||
|       } | ||||
|  | ||||
|       app-soft-select { | ||||
|         display: block; | ||||
|         width: 224px; | ||||
|         border: 1px solid #999; | ||||
|         color: #000; | ||||
|         padding: calc(.25rem + 2px) .5rem; | ||||
|         border-radius: .125rem; | ||||
|         font-size: .541667rem; | ||||
|         margin-right: 6px; | ||||
|  | ||||
|         input { | ||||
|           width: 100%; | ||||
|           border: 0; | ||||
|  | ||||
|           &:focus { | ||||
|             background: none; | ||||
|             border: 0 !important; | ||||
|           } | ||||
|  | ||||
|           &::-webkit-outer-spin-button, | ||||
|           &::-webkit-inner-spin-button { | ||||
|             -webkit-appearance: none; | ||||
|             margin: 0; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &:first-child p:first-child { | ||||
|       margin-top: 0; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .date-field { | ||||
|     position: relative; | ||||
|     display: inline-block; | ||||
|  | ||||
|     textarea { | ||||
|       width: 230px; | ||||
|     } | ||||
|  | ||||
|     .date-picker { | ||||
|       position: absolute; | ||||
|       right: 0; | ||||
|       top: 4px; | ||||
|  | ||||
|       ::ng-deep { | ||||
|         // clr-datepicker-view-manager { | ||||
|         //   transform: unset !important; | ||||
|         //   left: unset !important; | ||||
|         //   right: 70px !important; | ||||
|         // } | ||||
|         .clr-input-group { | ||||
|           border: 0 !important; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .modal-body { | ||||
|     padding-bottom: 10px; | ||||
|   } | ||||
|  | ||||
|   ::ng-deep { | ||||
|     clr-select-container { | ||||
|       border: 1px solid #999; | ||||
|       color: #000; | ||||
|       border-radius: .125rem; | ||||
|       margin-right: 5px; | ||||
|  | ||||
|       .clr-select-wrapper { | ||||
|         max-height: unset; | ||||
|  | ||||
|         &::after { | ||||
|           top: 15px; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       select { | ||||
|         height: auto; | ||||
|         padding: 10px; | ||||
|         padding-right: 20px; | ||||
|         border: 0 !important; | ||||
|  | ||||
|         &:focus { | ||||
|           background: 0 0 !important; | ||||
|         } | ||||
|         &:hover { | ||||
|           background: transparent; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     clr-input-container { | ||||
|       width: 224px; | ||||
|       border: 1px solid #999; | ||||
|       color: #000; | ||||
|       padding: calc(.25rem + 2px) .5rem; | ||||
|       border-radius: .125rem; | ||||
|       font-size: .541667rem; | ||||
|       margin-right: 6px; | ||||
|  | ||||
|       input { | ||||
|         width: 100%; | ||||
|         border: 0; | ||||
|  | ||||
|         &:focus { | ||||
|           background: none; | ||||
|           border: 0 !important; | ||||
|         } | ||||
|  | ||||
|         &::-webkit-outer-spin-button, | ||||
|         &::-webkit-inner-spin-button { | ||||
|           -webkit-appearance: none; | ||||
|           margin: 0; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       &.invalid-data { | ||||
|         border-color: red; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .modal-dialog { | ||||
|       width: 80vw; | ||||
|     } | ||||
|  | ||||
|     .clr-control-container { | ||||
|       width: 100%; | ||||
|  | ||||
|       textarea { | ||||
|         width: 100%; | ||||
|         resize: none; | ||||
|         border-color: #999; | ||||
|  | ||||
|         &.invalid-data { | ||||
|           border-color: red; | ||||
|           outline: 0; | ||||
|         } | ||||
|  | ||||
|         &.not-char { | ||||
|           font-family: "Lucida Console", Monaco, monospace; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .generate-record-url { | ||||
|       right: 40px; | ||||
|       top: 40px; | ||||
|       font-size: 12px; | ||||
|     } | ||||
|  | ||||
|     .generate-record-url-button { | ||||
|       right: 25px; | ||||
|       top: 5px; | ||||
|     } | ||||
|  | ||||
|     .modal-header { | ||||
|       padding: 0 0 1rem 0; | ||||
|     } | ||||
|  | ||||
|     .modal-footer { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       justify-content: space-between; | ||||
|  | ||||
|       // height: 65px; | ||||
|  | ||||
|       .alert { | ||||
|         margin: 0; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .prev-next { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|  | ||||
|   p { | ||||
|     margin: 0; | ||||
|   } | ||||
|  | ||||
|   button { | ||||
|     margin: 0px 10px; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .focusable { | ||||
|   &:focus { | ||||
|     box-shadow: 0 0 3px 0px #5aa220; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .entry-input-left-offset { | ||||
|   left: -30px; | ||||
| } | ||||
|  | ||||
| .validation-info-alert { | ||||
|   width: 310px | ||||
| } | ||||
| @@ -1,5 +1,12 @@ | ||||
| import { KeyValue } from '@angular/common' | ||||
| import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||||
| import { | ||||
|   Component, | ||||
|   EventEmitter, | ||||
|   Input, | ||||
|   OnInit, | ||||
|   Output, | ||||
|   ViewEncapsulation | ||||
| } from '@angular/core' | ||||
| import moment from 'moment' | ||||
| import { ValidateFilterSASResponse } from 'src/app/models/sas/validate-filter.model' | ||||
| import { QueryClause } from 'src/app/models/TableData' | ||||
| @@ -16,7 +23,8 @@ import { EditRecordModal } from '../../models/EditRecordModal' | ||||
| @Component({ | ||||
|   selector: 'app-edit-record', | ||||
|   templateUrl: './edit-record.component.html', | ||||
|   styleUrls: ['./edit-record.component.scss'] | ||||
|   styleUrls: ['./edit-record.component.scss'], | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class EditRecordComponent implements OnInit { | ||||
|   @Input() currentRecord!: EditRecordModal | ||||
| @@ -163,23 +171,8 @@ export class EditRecordComponent implements OnInit { | ||||
|   } | ||||
|  | ||||
|   public copyToClip(text: string) { | ||||
|     const modalElement = document.querySelector('#recordModalRef .modal-title') | ||||
|  | ||||
|     if (modalElement) { | ||||
|       const selBox = document.createElement('textarea') | ||||
|       selBox.style.position = 'fixed' | ||||
|       selBox.style.left = '0' | ||||
|       selBox.style.top = '0' | ||||
|       selBox.style.opacity = '0' | ||||
|       selBox.style.zIndex = '5000' | ||||
|       selBox.value = text | ||||
|       modalElement.appendChild(selBox) | ||||
|       selBox.focus() | ||||
|       selBox.select() | ||||
|       document.execCommand('copy') | ||||
|       modalElement.removeChild(selBox) | ||||
|       this.generatedRecordUrl = text | ||||
|     } | ||||
|     navigator.clipboard.writeText(text) | ||||
|     this.generatedRecordUrl = text | ||||
|   } | ||||
|  | ||||
|   async generateEditRecordUrl() { | ||||
|   | ||||
| @@ -1,8 +0,0 @@ | ||||
| :host { | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| p { | ||||
|     margin: 0; | ||||
|     text-align: center; | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| import { Component, OnInit, ViewEncapsulation } from '@angular/core' | ||||
|  | ||||
| /** | ||||
|  * Goal of this component is to recieve array of strings where every element is one state | ||||
| @@ -10,7 +10,8 @@ import { Component, OnInit } from '@angular/core' | ||||
| @Component({ | ||||
|   selector: 'app-upload-stater', | ||||
|   templateUrl: './upload-stater.component.html', | ||||
|   styleUrls: ['./upload-stater.component.scss'] | ||||
|   styleUrls: ['./upload-stater.component.scss'], | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class UploadStaterComponent implements OnInit { | ||||
|   public statesList: string[] = [] //States appended to be displayed | ||||
|   | ||||
| @@ -36,7 +36,7 @@ | ||||
|         <div class="clr-row card-block mt-15 d-flex justify-content-between"> | ||||
|           <div class="clr-col-md-auto"> | ||||
|             <div class="encoding-block"> | ||||
|               <clr-radio-container class="mt-0-i" clrInline> | ||||
|               <clr-radio-container class="mt-0" clrInline> | ||||
|                 <clr-radio-wrapper> | ||||
|                   <input | ||||
|                     type="radio" | ||||
| @@ -193,13 +193,14 @@ | ||||
|               libName: (libds?.split('.'))![0], | ||||
|               tableName: (libds?.split('.'))![1] | ||||
|             } as libdsParsed" | ||||
|             class="editor-title text-center mt-0-i" | ||||
|             class="editor-title text-center mt-0" | ||||
|           > | ||||
|             <clr-tooltip> | ||||
|               <clr-icon | ||||
|                 clrTooltipTrigger | ||||
|                 (click)="datasetInfo = true" | ||||
|                 shape="info-circle" | ||||
|                 aria-label="View dataset meta info" | ||||
|                 class="is-highlight cursor-pointer" | ||||
|                 size="24" | ||||
|               ></clr-icon> | ||||
| @@ -407,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> | ||||
|   | ||||
| @@ -1,246 +0,0 @@ | ||||
| .card { | ||||
|   margin-top: 0; | ||||
|   border: 0; | ||||
| } | ||||
|  | ||||
| .buttonBar { | ||||
|   padding: 2px 10px 2px 10px; | ||||
|   align-items: center; | ||||
| } | ||||
|  | ||||
| .testRed { | ||||
|   color: white; | ||||
|   background: rgba(255,0,0, 0.8) !important; | ||||
| } | ||||
|  | ||||
| hot-table { | ||||
|   ::ng-deep { | ||||
|     .firstColumnHeaderStyle button.changeType { | ||||
|       display: none; | ||||
|     } | ||||
|  | ||||
|     .handsontable tbody th.ht__highlight, .handsontable thead th.ht__highlight { | ||||
|       &.primaryKeyHeaderStyle { | ||||
|         background-color: #306b00b0 !important; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .primaryKeyHeaderStyle { | ||||
|       background-color: #306b006e !important; | ||||
|     } | ||||
|  | ||||
|     th.readonlyCell { | ||||
|       div { | ||||
|         opacity: 0.4; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     td.readonlyCell { | ||||
|       opacity: 0.5 | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .submit-reason { | ||||
|   min-height: 120px; | ||||
|   max-height: 120px; | ||||
|   height: 120px; | ||||
| } | ||||
|  | ||||
| .infoBar { | ||||
|   margin-top:14px; | ||||
|   background: #495967; | ||||
|   color: white; | ||||
|   text-align:center; | ||||
|   padding: 3px; | ||||
|   font-size: 16px; | ||||
|  | ||||
|   height: 30px; | ||||
|  | ||||
|   text-overflow: ellipsis; | ||||
|   overflow: hidden; | ||||
|   white-space: nowrap; | ||||
|  | ||||
|   span { | ||||
|     width: 80%; | ||||
|   } | ||||
|  | ||||
|   &:hover { | ||||
|     height: unset; | ||||
|     white-space: normal; | ||||
|  | ||||
|     span { | ||||
|       width: unset; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .pkHeader { | ||||
|   background: #687682; | ||||
|   color: #fff; | ||||
|   margin: -1px -1px -1px -1px; | ||||
| } | ||||
|  | ||||
| .headerBar { | ||||
|   //  padding: 13px 0px 14px 0px; | ||||
|   -webkit-box-align: center; | ||||
|   -ms-flex-align: center; | ||||
|   align-items: center; | ||||
|   background: var(--clr-vertical-nav-bg-color); | ||||
| } | ||||
|  | ||||
| .error-icon { | ||||
|   width: 30px; | ||||
|   height: 30px; | ||||
|   color: red; | ||||
| } | ||||
|  | ||||
| .btnCtrl { | ||||
|   display:flex; | ||||
|   justify-content:flex-end; | ||||
| } | ||||
|  | ||||
| .card-header { | ||||
|   border-bottom: 1px solid transparent; | ||||
| } | ||||
|  | ||||
| .hidden { | ||||
|   visibility: hidden; | ||||
| } | ||||
|  | ||||
| .my-drop-zone { | ||||
|   border: solid 1px lightgray; | ||||
|   border-radius: 10px; | ||||
|   background: whitesmoke; | ||||
|   box-shadow: inset 0px 0px 4px 2px #a7a5a52b; | ||||
|   height: 50vh; | ||||
| } | ||||
| .nv-file-over { | ||||
|   border: solid 2px green; | ||||
| } /* Default class applied to drop zones on over */ | ||||
|  | ||||
| .file-drop-text{ | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| .nv-file-over { | ||||
|   border: solid 2px green; | ||||
| } /* Default class applied to drop zones on over */ | ||||
|  | ||||
| .file-drop-text{ | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| @media screen and (max-width: 768px) { | ||||
|  | ||||
|   .progresStatic { | ||||
|     margin-top:9px!important; | ||||
|   } | ||||
|  | ||||
|   .progress, .progress-static { | ||||
|     width: calc(100% - 14px); | ||||
|   } | ||||
| } | ||||
|  | ||||
| .hotEditor { | ||||
|   position: relative; | ||||
| } | ||||
|  | ||||
| .excel-parsing { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|  | ||||
|   position: relative; | ||||
|  | ||||
|   .details { | ||||
|     margin: 0; | ||||
|     position: absolute; | ||||
|     top: -45px; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .edit-record-spinner { | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   background: rgba(255, 255, 255, 0.6); | ||||
|   position: absolute; | ||||
|   top: 0px; | ||||
|   bottom: 0px; | ||||
|   width: 100%; | ||||
|   z-index: 500; | ||||
| } | ||||
|  | ||||
| @media screen and (max-width: 480px) { | ||||
|  | ||||
|    .progresStatic { | ||||
|       margin-top:32px!important; | ||||
|     } | ||||
|  | ||||
|    .card-block, .card-footer { | ||||
|     padding: 10px 0px 0px 0px; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .content-area { | ||||
|   padding: 0 0.8rem 0.8rem 0.8rem !important; | ||||
|   padding-top: 0; | ||||
|  | ||||
|   // .card { | ||||
|   //   min-height: calc(100vh - 160px); | ||||
|   // } | ||||
| } | ||||
|  | ||||
| .drop-area { | ||||
|   position: fixed; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   bottom: 0; | ||||
|   right: 0; | ||||
|  | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: flex-start; | ||||
|  | ||||
|   margin: 1px; | ||||
|  | ||||
|   border: 2px dashed #fff; | ||||
|  | ||||
|   z-index: -1; | ||||
|  | ||||
|   span { | ||||
|     font-size: 20px; | ||||
|     margin-top: 20px; | ||||
|     padding: 10px; | ||||
|     background: #dbdbdb; | ||||
|     border-radius: 5px; | ||||
|     color: black; | ||||
|   } | ||||
| } | ||||
|  | ||||
| #submitBtn, #cancelSubmitBtn { | ||||
|   width: 150px; | ||||
| } | ||||
|  | ||||
| .view-table { | ||||
|   font-size: inherit !important; | ||||
| } | ||||
|  | ||||
| // When width is smaller remove the text from the buttons | ||||
| // keep only the icons | ||||
| @media (max-width: 992px) { | ||||
|   .icon-collapse { | ||||
|     .text { | ||||
|       display: none; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // FIXME | ||||
| // Let's leave it here for a reference if there | ||||
| // is an issue with viewboxes/filter modal overlaying | ||||
| // we will remove it if no issues found | ||||
| // .filter-modal { | ||||
| //   z-index: 1210; | ||||
| // } | ||||
| @@ -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' | ||||
| @@ -69,15 +70,15 @@ import { ParseResult } from '../models/ParseResult.interface' | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.Emulated | ||||
|   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, | ||||
|   | ||||
| @@ -1,85 +0,0 @@ | ||||
| @import '../../colors.scss'; | ||||
|  | ||||
| ::ng-deep body[cds-theme="dark"] { | ||||
|   .group-info { | ||||
|       background-color: $headerBackground; | ||||
|       border-color: $headerBackground; | ||||
|   } | ||||
|  | ||||
|   .group-data { | ||||
|       background-color: $headerBackground; | ||||
|       border-color: $headerBackground; | ||||
|   } | ||||
|  | ||||
|   .member-table tbody{ | ||||
|     tr:hover{ | ||||
|         background-color: #29404b; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| ::ng-deep body[cds-theme="light"] { | ||||
|   .group-info{ | ||||
|       background-color: #f9f9f9; | ||||
|       border-color: #a7a7a7; | ||||
|       box-shadow: 0px 2px 5px #dad7d7; | ||||
|   } | ||||
|  | ||||
|   .group-data { | ||||
|       background-color: #f9f9f9; | ||||
|       border-color: #a7a7a7; | ||||
|       box-shadow: 0px 2px 5px #dad7d7; | ||||
|   } | ||||
|  | ||||
|   .member-table tbody{ | ||||
|     tr:hover{ | ||||
|           background-color: #e6e6e6; | ||||
|       } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .sidebar-height{ | ||||
|     height: 100%; | ||||
| } | ||||
| .group-info-text{ | ||||
|     display: inline; | ||||
|     font-size: 20px; | ||||
| } | ||||
| .group-info{ | ||||
|     border: 1px solid; | ||||
|     border-radius: 3px; | ||||
| } | ||||
| .group-info td{ | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .group-data{ | ||||
|   border: 1px solid; | ||||
|   border-radius: 3px; | ||||
| } | ||||
|  | ||||
| .group-data{ | ||||
|   min-height: auto; | ||||
|   h3, h5{ | ||||
|       text-align: center; | ||||
| } | ||||
|  | ||||
| .member-table{ | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| .member-table tbody{ | ||||
|   tr:hover{ | ||||
|       cursor: pointer; | ||||
|   } | ||||
| } | ||||
| } | ||||
| .table-container{ | ||||
|   overflow: auto; | ||||
| } | ||||
|  | ||||
| @media screen and (max-width: 768px){ | ||||
|   .group-data{ | ||||
|     min-height: unset !important; | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { Location } from '@angular/common' | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| import { Component, OnInit, ViewEncapsulation } from '@angular/core' | ||||
| import { ActivatedRoute, Router } from '@angular/router' | ||||
| import { SASjsConfig } from '@sasjs/adapter' | ||||
| import { ServerType } from '@sasjs/utils/types/serverType' | ||||
| @@ -14,7 +14,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper | ||||
|   styleUrls: ['./group.component.scss'], | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class GroupComponent implements OnInit { | ||||
|   public groups: Array<any> | undefined | ||||
|   | ||||
| @@ -123,11 +123,11 @@ | ||||
|     </div> | ||||
|  | ||||
|     <div *ngIf="!loading" class="no-table-selected"> | ||||
|       <clr-icon | ||||
|         shape="warning-standard" | ||||
|         size="60" | ||||
|         class="is-info icon-dc-fill" | ||||
|       ></clr-icon> | ||||
|       <img | ||||
|         src="images/select-table.png" | ||||
|         class="select-table-icon" | ||||
|         alt="select table icon" | ||||
|       /> | ||||
|       <p | ||||
|         *ngIf="treeNodeLibraries?.length! > 0" | ||||
|         class="text-center color-gray mt-10" | ||||
|   | ||||
| @@ -1,32 +0,0 @@ | ||||
| clr-tree-node button { | ||||
|   white-space: nowrap; | ||||
| } | ||||
|  | ||||
| .card-block { | ||||
|   height: 100%; | ||||
|   padding: 0; | ||||
| } | ||||
|  | ||||
| .no-table-selected { | ||||
|   position: relative; | ||||
|   height: 100%; | ||||
| } | ||||
|  | ||||
| ::ng-deep { | ||||
|   // .badge.badge-info { | ||||
|   //     background: #6a9235!important; | ||||
|   //     color: #fff; | ||||
|   // } | ||||
|   clr-icon.is-blue, clr-icon.is-info { | ||||
|     fill: #6a9235; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .spinner-wrapper-fullpage { | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|  | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| } | ||||
| @@ -3,7 +3,7 @@ | ||||
|  * This software is released under MIT license. | ||||
|  * The full license information can be found in LICENSE in the root directory of this project. | ||||
|  */ | ||||
| import { Component, AfterContentInit } from '@angular/core' | ||||
| import { Component, AfterContentInit, ViewEncapsulation } from '@angular/core' | ||||
| import { Router } from '@angular/router' | ||||
| import { ActivatedRoute } from '@angular/router' | ||||
| import { globals } from '../_globals' | ||||
| @@ -18,7 +18,8 @@ import { LicenceService } from '../services/licence.service' | ||||
|   templateUrl: './home.component.html', | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class HomeComponent implements AfterContentInit { | ||||
|   public treeNodeLibraries: Array<any> | null = null | ||||
|   | ||||
| @@ -1,51 +0,0 @@ | ||||
| :host { | ||||
|   height: calc(100% - 96px); | ||||
|   padding: 20px 20px; | ||||
| } | ||||
|  | ||||
| .card { | ||||
|   margin-top: 0; | ||||
| } | ||||
|  | ||||
| .key-error { | ||||
|   font-size: 16px; | ||||
| } | ||||
|  | ||||
| .misskey { | ||||
|   color: #E74C3C; | ||||
| } | ||||
|  | ||||
| .license-key-form, .activation-key-form { | ||||
|   padding: 0; | ||||
|  | ||||
|   .clr-control-container { | ||||
|     width: 100%; | ||||
|  | ||||
|     textarea { | ||||
|       width: 100%; | ||||
|       height: 170px; | ||||
|       max-height: 170px; | ||||
|       min-height: 170px; | ||||
|       resize: none; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .apply-keys { | ||||
|   height: 40px; | ||||
| } | ||||
|  | ||||
| .drop-area { | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
|   padding: 15px; | ||||
|   border: 2px dashed #b2b2b2; | ||||
|   border-radius: 4px; | ||||
|   cursor: pointer; | ||||
|   margin: 10px 0; | ||||
| } | ||||
|  | ||||
| clr-tabs button { | ||||
|   box-shadow: none !important | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| import { Component, OnInit, ViewEncapsulation } from '@angular/core' | ||||
| import { ActivatedRoute, Router } from '@angular/router' | ||||
| import { AppService, LicenceService, SasService } from '../services' | ||||
| import { LicenseKeyData } from '../models/LicenseKeyData' | ||||
| @@ -14,7 +14,8 @@ enum LicenseActions { | ||||
| @Component({ | ||||
|   selector: 'app-licensing', | ||||
|   templateUrl: './licensing.component.html', | ||||
|   styleUrls: ['./licensing.component.scss'] | ||||
|   styleUrls: ['./licensing.component.scss'], | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class LicensingComponent implements OnInit { | ||||
|   public action: LicenseActions | null = null | ||||
| @@ -51,6 +52,7 @@ export class LicensingComponent implements OnInit { | ||||
|  | ||||
|   constructor( | ||||
|     private route: ActivatedRoute, | ||||
|     private router: Router, | ||||
|     private licenceService: LicenceService, | ||||
|     private sasService: SasService, | ||||
|     private appService: AppService | ||||
| @@ -123,7 +125,9 @@ export class LicensingComponent implements OnInit { | ||||
|           res.adapterResponse.return[0] && | ||||
|           res.adapterResponse.return[0].MSG === 'SUCCESS' | ||||
|         ) { | ||||
|           location.replace(location.href.split('#')[0]) | ||||
|           this.router.navigateByUrl('/').then(() => { | ||||
|             window.location.reload() | ||||
|           }) | ||||
|         } | ||||
|       }) | ||||
|       .finally(() => { | ||||
|   | ||||
| @@ -1,85 +0,0 @@ | ||||
| @import '../../colors.scss'; | ||||
|  | ||||
| .toggle-switch input[type=checkbox]:checked+label:before { | ||||
|     border-color: $headerBackground; | ||||
|     background-color: $headerBackground !important; | ||||
|     transition: .15s ease-in; | ||||
|     transition-property: border-color,background-color; | ||||
| } | ||||
|  | ||||
| #graph{ | ||||
|     height: calc(100vh - 195px); | ||||
|     overflow: hidden; | ||||
|     text-align: center; | ||||
|     display: block; | ||||
|     width: 100%; | ||||
|     border: 1px solid #e4e4e4; | ||||
|     margin-top: 10px; | ||||
| } | ||||
|  | ||||
| .selection-wrapper { | ||||
|     width: 100%; | ||||
|     max-width: 670px; | ||||
| } | ||||
|  | ||||
| .column-active { | ||||
|     background: #d8e3e9; | ||||
|     color: black; | ||||
| } | ||||
|  | ||||
| .content-area { | ||||
|     padding: 0.5rem !important; | ||||
|  | ||||
|     .card { | ||||
|         min-height: calc(100vh - 120px); | ||||
|  | ||||
|         .card-block { | ||||
|             padding: 0.5rem 0.35rem !important; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| clr-tree-node button { | ||||
|     white-space: nowrap; | ||||
| } | ||||
|  | ||||
| .btn-group.direction { | ||||
|     margin-left: var(--cds-global-space-6); | ||||
| } | ||||
|  | ||||
| .graph-render-spinner { | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     width: 100%; | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     margin-top: 10px; | ||||
| } | ||||
|  | ||||
| .biglineage-row { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
|  | ||||
|     margin-bottom: 20px; | ||||
| } | ||||
|  | ||||
| .modal-footer { | ||||
|     p { | ||||
|         margin: 0; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .lineage-title-wrapper { | ||||
|     left: 12px; | ||||
| } | ||||
|  | ||||
| .max-depth-input { | ||||
|     width: 100%; | ||||
| } | ||||
|  | ||||
| @media (max-width: 768px) { | ||||
|     .toggle-switch-container { | ||||
|         margin-bottom: 20px; | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component } from '@angular/core' | ||||
| import { Component, ViewEncapsulation } from '@angular/core' | ||||
| import { Location } from '@angular/common' | ||||
| import { globals } from '../_globals' | ||||
| import * as d3Viz from 'd3-graphviz' | ||||
| @@ -18,7 +18,8 @@ const moment = require('moment') | ||||
|   templateUrl: './lineage.component.html', | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class LineageComponent { | ||||
|   public switchFlag: boolean = false | ||||
| @@ -745,28 +746,13 @@ export class LineageComponent { | ||||
|     return URL.createObjectURL(svg_blob) | ||||
|   } | ||||
|  | ||||
|   private getSVGBlob() { | ||||
|     let svg: any = document.getElementById('graph') | ||||
|     let serializer = new XMLSerializer() | ||||
|     let svg_blob = new Blob([serializer.serializeToString(svg)], { | ||||
|       type: 'image/svg+xml' | ||||
|     }) | ||||
|     return svg_blob | ||||
|   } | ||||
|  | ||||
|   downloadSVG() { | ||||
|     d3Viz.graphviz('#graph').resetZoom() | ||||
|  | ||||
|     if (navigator.appVersion.toString().indexOf('.NET') > 0) { | ||||
|       window.navigator.msSaveBlob(this.getSVGBlob(), this.constructName('svg')) | ||||
|     } else { | ||||
|       let downloadLink = document.createElement('a') | ||||
|       downloadLink.href = this.getSVGURL() | ||||
|       downloadLink.download = this.constructName('svg') | ||||
|       document.body.appendChild(downloadLink) | ||||
|       downloadLink.click() | ||||
|       document.body.removeChild(downloadLink) | ||||
|     } | ||||
|     let downloadLink = document.createElement('a') | ||||
|     downloadLink.href = this.getSVGURL() | ||||
|     downloadLink.download = this.constructName('svg') | ||||
|     downloadLink.click() | ||||
|   } | ||||
|  | ||||
|   async downloadPNG() { | ||||
| @@ -794,16 +780,11 @@ export class LineageComponent { | ||||
|     var a = document.createElement('a') | ||||
|     var blob = new Blob([csvArray], { type: 'text/csv' }) | ||||
|  | ||||
|     if (navigator.appVersion.toString().indexOf('.NET') > 0) { | ||||
|       window.navigator.msSaveBlob(blob, this.constructName('csv')) | ||||
|     } else { | ||||
|       var url = window.URL.createObjectURL(blob) | ||||
|       a.href = url | ||||
|       a.download = this.constructName('csv') | ||||
|       a.click() | ||||
|       window.URL.revokeObjectURL(url) | ||||
|       a.remove() | ||||
|     } | ||||
|     var url = window.URL.createObjectURL(blob) | ||||
|     a.href = url | ||||
|     a.download = this.constructName('csv') | ||||
|     a.click() | ||||
|     window.URL.revokeObjectURL(url) | ||||
|   } | ||||
|  | ||||
|   private getDotUrl() { | ||||
| @@ -812,23 +793,11 @@ export class LineageComponent { | ||||
|     return window.URL.createObjectURL(dot_blob) | ||||
|   } | ||||
|  | ||||
|   private getDotBlob() { | ||||
|     let data = this.vizInput | ||||
|     let dot_blob = new Blob([data], { type: 'text/plain' }) | ||||
|     return dot_blob | ||||
|   } | ||||
|  | ||||
|   downloadDot() { | ||||
|     if (navigator.appVersion.toString().indexOf('.NET') > 0) { | ||||
|       window.navigator.msSaveBlob(this.getDotBlob(), this.constructName('txt')) | ||||
|     } else { | ||||
|       let downloadLink = document.createElement('a') | ||||
|       downloadLink.href = this.getDotUrl() | ||||
|       downloadLink.download = this.constructName('txt') | ||||
|       document.body.appendChild(downloadLink) | ||||
|       downloadLink.click() | ||||
|       document.body.removeChild(downloadLink) | ||||
|     } | ||||
|     let downloadLink = document.createElement('a') | ||||
|     downloadLink.href = this.getDotUrl() | ||||
|     downloadLink.download = this.constructName('txt') | ||||
|     downloadLink.click() | ||||
|   } | ||||
|  | ||||
|   public showSvg() { | ||||
|   | ||||
| @@ -1,82 +0,0 @@ | ||||
| ::ng-deep body[cds-theme="dark"] { | ||||
|   .object-header:hover { | ||||
|     background-color: #405560; | ||||
|   } | ||||
| } | ||||
|  | ||||
| ::ng-deep body[cds-theme="light"] { | ||||
|   .objects-col { | ||||
|     background: white; | ||||
|   } | ||||
|  | ||||
|   .object-header:hover { | ||||
|     background-color: #d8e3e9; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .objects-col{ | ||||
|   height: 75vh; | ||||
|   overflow: scroll; | ||||
|   border: 1px solid #cccccc; | ||||
|   border-radius: 4px; | ||||
| } | ||||
|  | ||||
| .cols-head { | ||||
|   border: 1px solid #cccccc; | ||||
|   padding: 10px; | ||||
|   display: flex; | ||||
| } | ||||
| .object-text { | ||||
|   display: flex; | ||||
|   align-items: flex-start; | ||||
|   justify-content: space-between; | ||||
|   margin-left: 10px; | ||||
|   flex: 1; | ||||
| } | ||||
| .repo-dropdown{ | ||||
|   margin-right: 15px; | ||||
|   margin-left: 15px; | ||||
|   margin-bottom: 10px; | ||||
| } | ||||
| .clr-accordion-title{ | ||||
|   width: 100%; | ||||
| } | ||||
| .float-right{ | ||||
|   margin: 0px; | ||||
|   float: right; | ||||
| } | ||||
| .full-width{ | ||||
|   width: 100%; | ||||
| } | ||||
| .object-uri{ | ||||
|   margin: 0px; | ||||
|   margin-top: 5px; | ||||
| } | ||||
| .object-header{ | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: space-between; | ||||
|   padding-left: 3px; | ||||
|   padding-right: 3px; | ||||
| } | ||||
| .object-header:hover{ | ||||
|   border-radius: 3px; | ||||
| } | ||||
| .datagrid-host{ | ||||
|   display: unset !important; | ||||
| } | ||||
|  | ||||
| .card { | ||||
|   margin-top: 0; | ||||
|  | ||||
|   flex: 1; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| } | ||||
|  | ||||
| .content-area { | ||||
|   padding: 0.5rem !important; | ||||
|  | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| } | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { Location } from '@angular/common' | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| import { Component, OnInit, ViewEncapsulation } from '@angular/core' | ||||
| import { ActivatedRoute, Router } from '@angular/router' | ||||
| import { ClrDatagridStringFilterInterface } from '@clr/angular' | ||||
| import { Observable, of } from 'rxjs' | ||||
| @@ -50,7 +50,8 @@ class ValueFilter implements ClrDatagridStringFilterInterface<MetaData> { | ||||
|   styleUrls: ['./metadata.component.scss'], | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class MetadataComponent implements OnInit { | ||||
|   metaDataList: Array<any> | undefined | ||||
| @@ -98,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 | ||||
| } | ||||
| @@ -127,7 +127,7 @@ | ||||
|       class="no-table-selected pointer-events-none" | ||||
|     > | ||||
|       <clr-icon | ||||
|         shape="warning-standard" | ||||
|         shape="upload-cloud" | ||||
|         size="40" | ||||
|         class="is-info icon-dc-fill" | ||||
|       ></clr-icon> | ||||
| @@ -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> | ||||
|   | ||||
| @@ -1,54 +0,0 @@ | ||||
| .no-table-selected { | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|   position: absolute; | ||||
|   background: var(--clr-vertical-nav-bg-color); | ||||
|   z-index: 10; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   top: 0; | ||||
| } | ||||
|  | ||||
| .header-row { | ||||
|   padding: 15px 0; | ||||
|   border-bottom: 1px solid #d3d3d3; | ||||
| } | ||||
|  | ||||
| .dataset-input-wrapper { | ||||
|   max-width: 500px; | ||||
|   width: 100%; | ||||
|  | ||||
|   textarea { | ||||
|     min-height: 200px; | ||||
|     height: 200px; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .submit-reason { | ||||
|   min-height: 70px; | ||||
|   max-height: 70px; | ||||
|   height: 70px; | ||||
| } | ||||
|  | ||||
| .log-wrapper { | ||||
|   margin: 0 10px; | ||||
|   height: auto; | ||||
| } | ||||
|  | ||||
| ::ng-deep td.not-matched { | ||||
|   background-color: #ff000054; | ||||
| } | ||||
|  | ||||
| .dataset-selection-actions { | ||||
|   border-top: 1px solid #d3d3d3; | ||||
| } | ||||
|  | ||||
| .licence-limit-notice { | ||||
|   color: var(--cds-alias-status-warning-dark); | ||||
| } | ||||
|  | ||||
| .submission-results { | ||||
|   border-bottom: 1px solid #d3d3d3; | ||||
| } | ||||
| @@ -4,7 +4,9 @@ import { | ||||
|   ElementRef, | ||||
|   HostBinding, | ||||
|   OnInit, | ||||
|   ViewChild | ||||
|   AfterViewInit, | ||||
|   ViewChild, | ||||
|   ViewEncapsulation | ||||
| } from '@angular/core' | ||||
| import { | ||||
|   EventService, | ||||
| @@ -21,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' | ||||
| @@ -45,9 +47,10 @@ enum FileLoadingState { | ||||
| @Component({ | ||||
|   selector: 'app-multi-dataset', | ||||
|   templateUrl: './multi-dataset.component.html', | ||||
|   styleUrls: ['./multi-dataset.component.scss'] | ||||
|   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 | ||||
|  | ||||
| @@ -87,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 = '' | ||||
| @@ -134,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 | ||||
| @@ -147,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 { | ||||
| @@ -231,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 { | ||||
| @@ -390,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) => { | ||||
| @@ -481,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) | ||||
| @@ -554,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() | ||||
| @@ -614,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' | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| import { Component, OnInit, ViewEncapsulation } from '@angular/core' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-not-found', | ||||
| @@ -6,7 +6,8 @@ import { Component, OnInit } from '@angular/core' | ||||
|   styleUrls: ['./not-found.component.scss'], | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class NotFoundComponent implements OnInit { | ||||
|   constructor() {} | ||||
|   | ||||
| @@ -1,307 +0,0 @@ | ||||
| ::ng-deep { | ||||
|     body[cds-theme="dark"] { | ||||
|         .clause-logic { | ||||
|             background: #192a30; | ||||
|         } | ||||
|  | ||||
|         .clause-query { | ||||
|             background: #263e48; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     body[cds-theme="light"] { | ||||
|         .clause-logic { | ||||
|             background: #e9e9e9; | ||||
|         } | ||||
|  | ||||
|         .clause-query { | ||||
|             background: #fbf8f8; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .content { | ||||
|     display: flex; | ||||
|  | ||||
|     .clauses-container { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|  | ||||
|         .clause-logic { | ||||
|             display: flex; | ||||
|             justify-content: center; | ||||
|             flex-direction: column; | ||||
|  | ||||
|             padding: 15px; | ||||
|         } | ||||
|  | ||||
|         .clause-query { | ||||
|             padding: 30px 0px 20px 20px; | ||||
|             display: flex; | ||||
|             justify-content: center; | ||||
|             flex-direction: column; | ||||
|             position: relative; | ||||
|  | ||||
|             & > .clr-row { | ||||
|                 justify-content: space-between; | ||||
|  | ||||
|                 &:not(:last-child) { | ||||
|                     padding-bottom: 15px; | ||||
|                     margin-bottom: 15px; | ||||
|                     border-bottom: 1px solid rgba(0, 0, 0, 0.16) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             .remove-group-clause-button { | ||||
|                 position: absolute; | ||||
|                 top: 0px; | ||||
|                 right: 10px; | ||||
|                 color: gray; | ||||
|             } | ||||
|  | ||||
|             .variable-col { | ||||
|                 display: flex; | ||||
|                 align-items: flex-start; | ||||
|                 padding-bottom: 1px; | ||||
|  | ||||
|                 .datalist-wrapper { | ||||
|                     width: 100%; | ||||
|  | ||||
|                     input { | ||||
|                         width: 100%; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             .operator-col { | ||||
|                 display: flex; | ||||
|                 align-items: flex-start; | ||||
|  | ||||
|                 clr-select-container { | ||||
|                     height: 45px; | ||||
|                     margin-top: 0; | ||||
|                     width: 100%; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             .value-col { | ||||
|                 display: flex; | ||||
|                 align-items: flex-start; | ||||
|                 padding-bottom: 1px; | ||||
|  | ||||
|                 .checkbox-vals { | ||||
|                     width: 100%; | ||||
|                     padding: 0px 5px; | ||||
|                     border-bottom: 1px solid rgba(0, 0, 0, 0.3); | ||||
|  | ||||
|                     clr-checkbox-container { | ||||
|                         margin-top: 0; | ||||
|                     } | ||||
|  | ||||
|                     section { | ||||
|                         max-height: 120px; | ||||
|                         overflow-y: scroll; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 .single-field-vals { | ||||
|                     width: 100%; | ||||
|  | ||||
|                     ::ng-deep { | ||||
|                         .clr-control-container { | ||||
|                             width: 100%; | ||||
|  | ||||
|                             .clr-input-wrapper { | ||||
|                                 max-width: none; | ||||
|  | ||||
|                                 .clr-input-group { | ||||
|                                     width: 100%; | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     & > input { | ||||
|                         width: 100%; | ||||
|                     } | ||||
|  | ||||
|                     input[type=time] { | ||||
|                         width: 100%; | ||||
|                         padding-right: 17px; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 .range-vals { | ||||
|                     width: 100%; | ||||
|  | ||||
|                     ::ng-deep { | ||||
|                         .clr-control-container { | ||||
|                             width: 100%; | ||||
|  | ||||
|                             .clr-input-wrapper { | ||||
|                                 max-width: none; | ||||
|  | ||||
|                                 .clr-input-group { | ||||
|                                     width: 100%; | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     .from { | ||||
|                         margin-bottom: 10px; | ||||
|  | ||||
|                         & > input { | ||||
|                             width: 100%; | ||||
|                         } | ||||
|  | ||||
|                         input[type=time] { | ||||
|                             width: 100%; | ||||
|                             padding-right: 17px; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     .from, .to { | ||||
|                         min-width: 100px; | ||||
|  | ||||
|                         & > input { | ||||
|                             width: 100%; | ||||
|                         } | ||||
|  | ||||
|                         input[type=time] { | ||||
|                             width: 100%; | ||||
|                             padding-right: 17px; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 .contains-vals { | ||||
|                     width: 100%; | ||||
|  | ||||
|                     ::ng-deep { | ||||
|                         .clr-control-container { | ||||
|                             width: 100%; | ||||
|  | ||||
|                             .clr-input-wrapper { | ||||
|                                 max-width: none; | ||||
|  | ||||
|                                 .clr-input-group { | ||||
|                                     width: 100%; | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     & > input { | ||||
|                         width: 100%; | ||||
|                     } | ||||
|  | ||||
|                     input[type=time] { | ||||
|                         width: 100%; | ||||
|                         padding-right: 17px; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             .clause-buttons { | ||||
|                 display: flex; | ||||
|                 justify-content: space-around; | ||||
|                 align-items: center; | ||||
|                 flex-direction: row; | ||||
|                 align-items: center; | ||||
|  | ||||
|                 button { | ||||
|                     min-width: auto; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .invalid-clause { | ||||
|     border-left: 2px solid #d94b31; | ||||
| } | ||||
|  | ||||
| .clause-row { | ||||
|     clr-icon { | ||||
|         margin: 0; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .clause-row:after { | ||||
|     position: relative; | ||||
|     content: ""; | ||||
|     height: .41667rem; | ||||
|     width: .41667rem; | ||||
|     top: .29167rem; | ||||
|     right: .25rem; | ||||
|     background-image: url(data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org…%2C9.84%2C3.24a0.68%2C0.68%2C0%2C1%2C1%2C1%2C1Z%22%2F%3E%0A%3C%2Fsvg%3E%0A); | ||||
|     background-repeat: no-repeat; | ||||
|     background-size: contain; | ||||
|     vertical-align: middle; | ||||
|     margin: 0; | ||||
| } | ||||
|  | ||||
| ::ng-deep body[cds-theme="dark"] { | ||||
|     .line-numbers { | ||||
|         border-color: #989797 !important; | ||||
|     } | ||||
| } | ||||
|  | ||||
| pre[class*="language-"] { | ||||
|     padding: 8px; | ||||
|     margin: 0; | ||||
|     border-radius: 1px; | ||||
|  | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     min-height: 66px; | ||||
|  | ||||
|     position: relative; | ||||
|  | ||||
|     span.spinner { | ||||
|         position: absolute; | ||||
|         left: 10px; | ||||
|         top: 10px; | ||||
|     } | ||||
|  | ||||
|     code { | ||||
|         white-space: pre-wrap; | ||||
|         word-break: break-word; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .input-val { | ||||
|     border: 0px; | ||||
|     background: #fbf8f8; | ||||
|     border-bottom: 1px solid #999999; | ||||
| } | ||||
|  | ||||
| clr-date-container { | ||||
|     margin-top: 2px !important; | ||||
| } | ||||
|  | ||||
| input[type="time"] { | ||||
|     border: 0; | ||||
|     background: transparent; | ||||
|     border-bottom: 1px solid #b3b3b3; | ||||
|  | ||||
|     &:focus { | ||||
|         outline: none; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .in-values-modal { | ||||
|     .modal-footer { | ||||
|         border-top: 1px solid #d8d8d8; | ||||
|         margin-top: 10px; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .progress, .progress-static { | ||||
|     background-color: transparent; | ||||
|     width: 100%; | ||||
|     height: 4px; | ||||
|     top: 3px; | ||||
| } | ||||
| @@ -6,7 +6,8 @@ import { | ||||
|   OnDestroy, | ||||
|   ChangeDetectorRef, | ||||
|   LOCALE_ID, | ||||
|   Input | ||||
|   Input, | ||||
|   ViewEncapsulation | ||||
| } from '@angular/core' | ||||
| import { SasStoreService } from '../services/sas-store.service' | ||||
| import { globals } from '../_globals' | ||||
| @@ -27,7 +28,8 @@ registerLocaleData(localeEnGB) | ||||
|   selector: 'app-query', | ||||
|   templateUrl: './query.component.html', | ||||
|   styleUrls: ['./query.component.scss'], | ||||
|   providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }] | ||||
|   providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }], | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class QueryComponent | ||||
|   implements AfterViewInit, AfterContentInit, OnDestroy | ||||
|   | ||||
| @@ -1,210 +0,0 @@ | ||||
| @import '../../../colors.scss'; | ||||
|  | ||||
| .loader { | ||||
|   display:flex; | ||||
|   justify-content: center; | ||||
|   height:75vh; | ||||
|   align-items:center; | ||||
|   flex-direction:column | ||||
| } | ||||
| .modalLarge { | ||||
|     width: 50rem!important; | ||||
| } | ||||
|  | ||||
| .addedRow { | ||||
|   border: 1px solid rgba(9, 77, 117, 0.2); | ||||
|   border-radius: 5px; | ||||
| } | ||||
|  | ||||
| .deletedRow { | ||||
|   border: 1px solid rgba(70, 71, 70, 0.2); | ||||
|   border-radius: 5px; | ||||
| } | ||||
|  | ||||
| ::ng-deep body[cds-theme="dark"] { | ||||
|   table { | ||||
|     .updatedRow { | ||||
|       background: #93971e; | ||||
|     } | ||||
|  | ||||
|     .addedRow { | ||||
|       background:  rgb(86 153 95); | ||||
|     } | ||||
|  | ||||
|     .deletedRow { | ||||
|       background: rgb(138 90 90); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| ::ng-deep body[cds-theme="light"] { | ||||
|   table { | ||||
|     .updatedRow { | ||||
|       background: #fafda8; | ||||
|     } | ||||
|  | ||||
|     .addedRow { | ||||
|       background:  rgb(146, 208, 154); | ||||
|     } | ||||
|  | ||||
|     .deletedRow { | ||||
|       background: rgb(230, 179, 179); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .updatedRow { | ||||
|   border: 1px solid rgba(9, 117, 9, 0.2); | ||||
|   border-radius: 5px; | ||||
| } | ||||
|  | ||||
| .table { | ||||
|   border: 0px solid; | ||||
| } | ||||
|  | ||||
| .ch { | ||||
|   background: rgba(0,0,0,.1); | ||||
|   border: 1px solid rgba(104, 100, 0, 0.4); | ||||
|   border-radius: 5px; | ||||
| } | ||||
|  | ||||
| .ch:hover { | ||||
| background: rgba(252, 135, 120, 0.4); | ||||
| } | ||||
|  | ||||
| .tooltip .tooltip-content.tooltip-top-right, .tooltip.tooltip-top-right>.tooltip-content, .tooltip>.tooltip-content { | ||||
|     font-size: .54167rem; | ||||
|     font-weight: 400; | ||||
|     letter-spacing: normal; | ||||
|     background: $headerBackground; | ||||
|     border-radius: .125rem; | ||||
|     color: #f0f1ec;; | ||||
|     line-height: .75rem; | ||||
|     margin: 0; | ||||
|     padding: .375rem .5rem; | ||||
|     width: 235px; | ||||
|     position: absolute; | ||||
|     top: auto; | ||||
|     bottom: 100%; | ||||
|     left: 12px; | ||||
|     right: auto; | ||||
|     border-bottom-left-radius: 0; | ||||
|     margin-bottom: .66667rem; | ||||
| } | ||||
|  | ||||
| .tooltip .tooltip-content.tooltip-top-right:before, .tooltip.tooltip-top-right>.tooltip-content:before, .tooltip>.tooltip-content:before { | ||||
|     position: absolute; | ||||
|     bottom: -.375rem; | ||||
|     left: 0; | ||||
|     top: auto; | ||||
|     right: auto; | ||||
|     content: ""; | ||||
|     border-left: .25rem solid $headerBackground; | ||||
|     border-top: .20833rem solid $headerBackground; | ||||
|     border-right: .25rem solid transparent; | ||||
|     border-bottom: .20833rem solid transparent; | ||||
| } | ||||
|  | ||||
| .table { | ||||
| border: 0px solid; | ||||
| } | ||||
|  | ||||
| .toggle-switch input[type=checkbox]:checked+label:before { | ||||
|     border-color: $headerBackground; | ||||
|     background-color: $headerBackground !important; | ||||
|     transition: .15s ease-in; | ||||
|     transition-property: border-color,background-color; | ||||
| } | ||||
|  | ||||
| .tableCont { | ||||
|   overflow:auto; | ||||
|   margin: 15px 10px 10px 10px; | ||||
|  | ||||
|   td { | ||||
|     word-break: break-word; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .approvalInfo { | ||||
|   display: flex; | ||||
|   justify-content: flex-end | ||||
| } | ||||
|  | ||||
| .approvalBack { | ||||
|   display: flex; | ||||
|   justify-content: flex-start; | ||||
| } | ||||
|  | ||||
| @media screen and (max-width:768px) { | ||||
|   .approvalInfo { | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     margin-top: 15px; | ||||
|  | ||||
|   } | ||||
|  | ||||
|   .approvalBack { | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     margin-bottom: 15px; | ||||
|   } | ||||
|  | ||||
|   .card { | ||||
|     margin-top:0rem!important; | ||||
|     min-height: calc(100vh - 0px)!important; | ||||
|   } | ||||
|  | ||||
|   .table td.left, .table th.left { | ||||
|     text-align: left; | ||||
|     width: 150px!important; | ||||
|     flex: 0 | ||||
|   } | ||||
| } | ||||
|  | ||||
| .table td.left, .table th.left { | ||||
|   text-align: left; | ||||
|   flex: 1; | ||||
|   width: 300px!important; | ||||
| } | ||||
|  | ||||
|  | ||||
| .tooll { | ||||
|     position: absolute; | ||||
|     background: #e6b3b3; | ||||
|     color: $headerBackground; | ||||
|     top: 0px; | ||||
|     height: 36px; | ||||
|     width: 100%; | ||||
|     left: 0px; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     display: flex; | ||||
| } | ||||
|  | ||||
| #acceptBtn, #rejectBtn { | ||||
|   width: 175px | ||||
| } | ||||
|  | ||||
| .formatted-values-toggle { | ||||
|   min-width: 75px | ||||
| } | ||||
|  | ||||
| clr-modal { | ||||
|   ::ng-deep { | ||||
|     .modal-body-wrapper { | ||||
|       overflow: auto; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .rows-notice { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   margin-right: 10px; | ||||
|   color: #6a6a6a; | ||||
|   font-size: 15px; | ||||
|  | ||||
|   clr-icon { | ||||
|     margin: 0; | ||||
|   } | ||||
| } | ||||
| @@ -1,6 +1,11 @@ | ||||
| import { ActivatedRoute } from '@angular/router' | ||||
| import { SasStoreService } from '../../services/sas-store.service' | ||||
| import { Component, AfterViewInit, OnDestroy } from '@angular/core' | ||||
| import { | ||||
|   Component, | ||||
|   AfterViewInit, | ||||
|   OnDestroy, | ||||
|   ViewEncapsulation | ||||
| } from '@angular/core' | ||||
| import { Subscription } from 'rxjs' | ||||
| import { Router } from '@angular/router' | ||||
| import { EventService } from '../../services/event.service' | ||||
| @@ -8,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 | ||||
| @@ -22,7 +29,8 @@ interface ChangesObj { | ||||
|   styleUrls: ['./approve-details.component.scss'], | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class ApproveDetailsComponent implements AfterViewInit, OnDestroy { | ||||
|   private _detailsSub: Subscription | undefined | ||||
| @@ -70,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 | ||||
| @@ -156,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) => { | ||||
| @@ -170,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] | ||||
|  | ||||
| @@ -183,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 | ||||
| @@ -349,13 +367,12 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy { | ||||
|             this.params = param | ||||
|             this.response = res | ||||
|             this.calcDiff() | ||||
|             this.callChangesInfo(this.tableId) | ||||
|           }) | ||||
|           .catch((err: any) => err) | ||||
|           .finally(() => { | ||||
|             this.loadingTable = true | ||||
|           }) | ||||
|  | ||||
|         this.callChangesInfo(this.tableId) | ||||
|       } | ||||
|     ) | ||||
|     if (typeof this.router.snapshot.params['tableId'] === 'undefined') { | ||||
| @@ -378,6 +395,7 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy { | ||||
|         this.params = param | ||||
|         this.response = res | ||||
|         this.calcDiff() | ||||
|         this.callChangesInfo(this.tableId) | ||||
|       }) | ||||
|       .catch((err: any) => { | ||||
|         this.acceptLoading = false | ||||
| @@ -386,8 +404,6 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy { | ||||
|         this.loadingTable = true | ||||
|         this.setFocus() | ||||
|       }) | ||||
|  | ||||
|     this.callChangesInfo(this.tableId) | ||||
|   } | ||||
|  | ||||
|   ngOnDestroy() { | ||||
|   | ||||
| @@ -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> | ||||
| @@ -89,6 +121,7 @@ | ||||
|             <clr-dg-cell class="p-0 d-flex justify-content-center"> | ||||
|               <button | ||||
|                 class="btn btn-success" | ||||
|                 aria-label="Download audit file" | ||||
|                 [id]="approveItem.tableId" | ||||
|                 (click)=" | ||||
|                   download(approveItem.tableId); $event.stopPropagation() | ||||
|   | ||||
| @@ -1,43 +0,0 @@ | ||||
| @import '../../../colors.scss'; | ||||
|  | ||||
| .column-center { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
| } | ||||
| .datagrid .datagrid-column .datagrid-column-title{ | ||||
|   outline: none!important; | ||||
| } | ||||
|  | ||||
| .links { | ||||
|   font-weight: 700;cursor: pointer; | ||||
| } | ||||
|  | ||||
| .tooltip.tooltip-bottom-left>.tooltip-content, .tooltip .tooltip-content.tooltip-bottom-left { | ||||
|     background: $headerBackground !important; | ||||
| } | ||||
| .tooltip.tooltip-bottom-left>.tooltip-content:before, .tooltip .tooltip-content.tooltip-bottom-left:before { | ||||
|     border-right: .25rem solid $headerBackground; | ||||
|     border-bottom: .20833rem solid $headerBackground; | ||||
| } | ||||
|  | ||||
| .noBorder { | ||||
|   border-bottom: 1px solid transparent!important; | ||||
| } | ||||
|  | ||||
| .approvals-list-wrapper { | ||||
|   height: 70vh; | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
| } | ||||
|  | ||||
| .noapprovals-info-wrapper { | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|   height: calc(100vh - 200px); | ||||
| } | ||||
| @@ -1,8 +1,14 @@ | ||||
| import { Component, OnInit, ChangeDetectorRef } from '@angular/core' | ||||
| import { | ||||
|   Component, | ||||
|   OnInit, | ||||
|   ChangeDetectorRef, | ||||
|   ViewEncapsulation | ||||
| } from '@angular/core' | ||||
| 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 | ||||
| @@ -14,13 +20,40 @@ 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', | ||||
|   styleUrls: ['./approve.component.scss'], | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class ApproveComponent implements OnInit { | ||||
|   public approveList: Array<ApproveData> | undefined | ||||
| @@ -29,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, | ||||
|   | ||||
| @@ -24,19 +24,19 @@ | ||||
|               <a | ||||
|                 *ngIf="ind < 1" | ||||
|                 (click)="getTable(approveData[col])" | ||||
|                 class="cursor-pointer" | ||||
|                 class="cursor-pointer table-link" | ||||
|                 >{{ approveData[col] }}</a | ||||
|               > | ||||
|               <div *ngIf="ind < 2 && ind >= 1"> | ||||
|                 <a | ||||
|                   (click)="getBaseTable(approveData[col])" | ||||
|                   class="cursor-pointer" | ||||
|                   class="cursor-pointer table-link" | ||||
|                   >VIEW</a | ||||
|                 > | ||||
|                 <span> / </span> | ||||
|                 <a | ||||
|                   (click)="getEditTable(approveData[col])" | ||||
|                   class="cursor-pointer" | ||||
|                   class="cursor-pointer table-link" | ||||
|                   >EDIT</a | ||||
|                 > | ||||
|               </div> | ||||
| @@ -47,7 +47,12 @@ | ||||
|       </table> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|       <button type="button" class="btn btn-outline" (click)="openModal = false"> | ||||
|       <button | ||||
|         type="button" | ||||
|         aria-label="Close modal" | ||||
|         class="btn btn-outline" | ||||
|         (click)="openModal = false" | ||||
|       > | ||||
|         OK | ||||
|       </button> | ||||
|     </div> | ||||
| @@ -80,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 | ||||
| @@ -119,6 +156,7 @@ | ||||
|         <clr-dg-cell class="verCenter">{{ historyItem.reviewed }}</clr-dg-cell> | ||||
|         <clr-dg-cell class="verCenter p-0 d-flex justify-content-center"> | ||||
|           <button | ||||
|             aria-label="Download audit file" | ||||
|             class="btn btn-success" | ||||
|             (click)="download(historyItem.tableId); $event.stopPropagation()" | ||||
|           > | ||||
|   | ||||
| @@ -1,37 +0,0 @@ | ||||
| .rejected { | ||||
|   color: #f83126; | ||||
|   font-weight: bold | ||||
| } | ||||
|  | ||||
| .accepted { | ||||
|   color: #3fc424; | ||||
|   font-weight: bold | ||||
| } | ||||
|  | ||||
| .hsCell { | ||||
|   display: flex !important; | ||||
|     flex-direction: column !important; | ||||
|     justify-content: center !important; | ||||
|     align-items: center !important; | ||||
|     padding: 7px; | ||||
| } | ||||
| .btCell { | ||||
|   display: flex !important; | ||||
|   justify-content: center !important; | ||||
| } | ||||
|  | ||||
| .verCenter { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     word-break: break-all; | ||||
| } | ||||
|  | ||||
| .load-more { | ||||
|   input { | ||||
|     width: 90px; | ||||
|   } | ||||
| } | ||||
|  | ||||
| #noDataContainer { | ||||
|   height: calc(100vh - 200px); | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| import { Component, OnInit, ViewEncapsulation } from '@angular/core' | ||||
|  | ||||
| import { Router } from '@angular/router' | ||||
| import { SASjsConfig } from '@sasjs/adapter' | ||||
| @@ -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', | ||||
| @@ -15,7 +64,8 @@ import { | ||||
|   styleUrls: ['./history.component.scss'], | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class HistoryComponent implements OnInit { | ||||
|   public history: Array<any> = [] | ||||
| @@ -28,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 | ||||
|             > | ||||
| @@ -64,7 +74,11 @@ | ||||
|               <!-- <clr-dg-cell>{{sub.approver}}</clr-dg-cell> --> | ||||
|               <clr-dg-cell>{{ sub.submitReason }}</clr-dg-cell> | ||||
|               <clr-dg-cell> | ||||
|                 <div class="row justify-content-around" role="tooltip"> | ||||
|                 <div | ||||
|                   class="row justify-content-around" | ||||
|                   role="tooltip" | ||||
|                   aria-label="Go to staged data screen" | ||||
|                 > | ||||
|                   <a | ||||
|                     class="column-center links tooltip tooltip-md tooltip-bottom-left color-blue" | ||||
|                     (click)="goToStage(sub.tableId)" | ||||
| @@ -79,6 +93,7 @@ | ||||
|               <clr-dg-cell class="p-0 d-flex justify-content-center"> | ||||
|                 <button | ||||
|                   class="btn btn-success" | ||||
|                   aria-label="Download audit file for table record" | ||||
|                   (click)="download(sub.tableId); $event.stopPropagation()" | ||||
|                 > | ||||
|                   <clr-icon shape="download"></clr-icon> | ||||
|   | ||||
| @@ -1,17 +0,0 @@ | ||||
| @import '../../../colors.scss'; | ||||
|  | ||||
| .noBorder { | ||||
|   border-bottom: 1px solid transparent!important; | ||||
| } | ||||
|  | ||||
| .tooltip.tooltip-bottom-left>.tooltip-content, .tooltip .tooltip-content.tooltip-bottom-left { | ||||
|   background: $headerBackground !important; | ||||
| } | ||||
| .tooltip.tooltip-bottom-left>.tooltip-content:before, .tooltip .tooltip-content.tooltip-bottom-left:before { | ||||
|   border-right: .25rem solid $headerBackground; | ||||
|   border-bottom: .20833rem solid $headerBackground; | ||||
| } | ||||
|  | ||||
| .no-submitted-tables { | ||||
|   height: calc(100vh - 200px); | ||||
| } | ||||
| @@ -1,7 +1,13 @@ | ||||
| import { Component, AfterViewInit, OnInit } from '@angular/core' | ||||
| import { | ||||
|   Component, | ||||
|   AfterViewInit, | ||||
|   OnInit, | ||||
|   ViewEncapsulation | ||||
| } from '@angular/core' | ||||
| 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 | ||||
| @@ -11,13 +17,30 @@ 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', | ||||
|   styleUrls: ['./submitter.component.scss'], | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class SubmitterComponent implements OnInit, AfterViewInit { | ||||
|   public remained: number = 0 | ||||
| @@ -31,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,4 +1,4 @@ | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| import { Component, OnInit, ViewEncapsulation } from '@angular/core' | ||||
| import { globals } from '../_globals' | ||||
| import { HelperService } from '../services/helper.service' | ||||
| import { Location } from '@angular/common' | ||||
| @@ -12,7 +12,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper | ||||
|   styleUrls: ['./role.component.scss'], | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class RoleComponent implements OnInit { | ||||
|   public roles: Array<any> | undefined | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component, OnInit, OnDestroy } from '@angular/core' | ||||
| import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-home-route', | ||||
| @@ -6,7 +6,8 @@ import { Component, OnInit, OnDestroy } from '@angular/core' | ||||
|   styleUrls: ['./home-route.component.scss'], | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class HomeRouteComponent implements OnInit, OnDestroy { | ||||
|   constructor() {} | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| import { Component, OnInit, ViewEncapsulation } from '@angular/core' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-review-route', | ||||
| @@ -6,7 +6,8 @@ import { Component, OnInit } from '@angular/core' | ||||
|   styleUrls: ['./review-route.component.scss'], | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class ReviewRouteComponent implements OnInit { | ||||
|   constructor() {} | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| import { Component, OnInit, ViewEncapsulation } from '@angular/core' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-usernav-route', | ||||
| @@ -6,7 +6,8 @@ import { Component, OnInit } from '@angular/core' | ||||
|   styleUrls: ['./usernav-route.component.scss'], | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class UsernavRouteComponent implements OnInit { | ||||
|   constructor() {} | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component, OnInit, OnDestroy } from '@angular/core' | ||||
| import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-view-route', | ||||
| @@ -6,7 +6,8 @@ import { Component, OnInit, OnDestroy } from '@angular/core' | ||||
|   styleUrls: ['./view-route.component.scss'], | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class ViewRouteComponent implements OnInit, OnDestroy { | ||||
|   constructor() {} | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component, OnInit, OnDestroy } from '@angular/core' | ||||
| import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-xlmap-route', | ||||
| @@ -6,7 +6,8 @@ import { Component, OnInit, OnDestroy } from '@angular/core' | ||||
|   styleUrls: ['./xlmap-route.component.scss'], | ||||
|   host: { | ||||
|     class: 'content-container' | ||||
|   } | ||||
|   }, | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class XLMapRouteComponent implements OnInit, OnDestroy { | ||||
|   constructor() {} | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -223,7 +223,7 @@ export class SasStoreService { | ||||
|     tables[tableName] = [tableData] | ||||
|     return ( | ||||
|       await this.sasService.request(program, tables, { | ||||
|         useComputeApi: false | ||||
|         useComputeApi: null // Using WEB APPROACH as a temporary workaround until VIYA JES API is fixed. For other server types then VIYA this is not applicable | ||||
|       }) | ||||
|     ).adapterResponse | ||||
|   } | ||||
| @@ -232,7 +232,7 @@ export class SasStoreService { | ||||
|     tables[tableName] = [tableData] | ||||
|     return ( | ||||
|       await this.sasService.request(program, tables, { | ||||
|         useComputeApi: false | ||||
|         useComputeApi: null // Using WEB APPROACH as a temporary workaround until VIYA JES API is fixed. For other server types then VIYA this is not applicable | ||||
|       }) | ||||
|     ).adapterResponse | ||||
|   } | ||||
|   | ||||
| @@ -1,9 +1,21 @@ | ||||
| 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' | ||||
| import { ViyaApiFolderMembers } from '../viya-api-explorer/models/viya-api-folder-content.model' | ||||
| 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' | ||||
| @@ -22,7 +34,8 @@ export class SasViyaService { | ||||
|     }, | ||||
|     Compute: { | ||||
|       jobs: '/jobDefinitions', | ||||
|       jobExecution: '/jobExecution' | ||||
|       jobExecution: '/jobExecution', | ||||
|       contexts: '/compute/contexts' | ||||
|     }, | ||||
|     Decision_Management: { | ||||
|       modelManagement: '/modelManagement', | ||||
| @@ -57,15 +70,23 @@ export class SasViyaService { | ||||
|   constructor( | ||||
|     private http: HttpClient, | ||||
|     private appStoreService: AppStoreService | ||||
|   ) { | ||||
|   ) {} | ||||
|  | ||||
|   /** | ||||
|    * This function is replacing the constructor. | ||||
|    * The reason for this is timing issues, other services eg. sas.service, app-store.service | ||||
|    * must be initialized before this bit of code is executed. | ||||
|    * This function is being called by `sas.service` | ||||
|    */ | ||||
|   setup() { | ||||
|     const adapterConfig = this.appStoreService.getDcAdapterSettings() | ||||
|  | ||||
|     this.serverUrl = adapterConfig?.serverUrl || '' | ||||
|  | ||||
|     //example | ||||
|     this.getByCollection('jobs').subscribe((res) => { | ||||
|       console.log('res', res) | ||||
|     }) | ||||
|     // example collection request | ||||
|     // this.getByCollection('jobs').subscribe((res) => { | ||||
|     //   console.log('res', res) | ||||
|     // }) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -82,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 | ||||
|     }) | ||||
|   } | ||||
| @@ -93,8 +114,106 @@ 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<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.get<ViyaApiFolder>( | ||||
|       `${this.serverUrl}/folders/folders/@item?path=${path}`, | ||||
|       { | ||||
|         withCredentials: true | ||||
|       } | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   getFolderMembers(folderId: string): Observable<ViyaApiFolderMembers> { | ||||
|     return this.get<ViyaApiFolderMembers>( | ||||
|       `${this.serverUrl}/folders/folders/${folderId}/members`, | ||||
|       { | ||||
|         withCredentials: true | ||||
|       } | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   getAdminGroups(limit: number = 5000): Observable<ViyaApiIdentities> { | ||||
|     return this.get<ViyaApiIdentities>( | ||||
|       `${this.serverUrl}/identities/groups?sortBy=name&limit=${limit}`, | ||||
|       { | ||||
|         withCredentials: true | ||||
|       } | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   getCurrentUser(): Observable<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) | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -16,6 +16,9 @@ import { RequestWrapperOptions } from '../models/request-wrapper/RequestWrapperO | ||||
| import { ErrorBody } from '../models/ErrorBody' | ||||
| import { UploadFileResponse } from '../models/UploadFile' | ||||
| import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse' | ||||
| import { SasViyaService } from './sas-viya.service' | ||||
| import { ViyaApiFolder } from '../viya-api-explorer/models/viya-api-folder.model' | ||||
| import { ViyaApiFolderMembers } from '../viya-api-explorer/models/viya-api-folder-content.model' | ||||
|  | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
| @@ -38,6 +41,7 @@ export class SasService { | ||||
|     private userService: UserService, | ||||
|     private eventService: EventService, | ||||
|     private sasjsService: SasjsService, | ||||
|     private sasViyaService: SasViyaService, | ||||
|     private loggerService: LoggerService, | ||||
|     private router: Router | ||||
|   ) {} | ||||
| @@ -51,6 +55,7 @@ export class SasService { | ||||
|     this.dcAdapterSettings = this.appStoreService.getDcAdapterSettings() | ||||
|  | ||||
|     this.sasjsService.setup() | ||||
|     this.sasViyaService.setup() | ||||
|  | ||||
|     if (!this.dcAdapterSettings) { | ||||
|       this.eventService.showInfoModal( | ||||
| @@ -328,6 +333,10 @@ export class SasService { | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   public reloadStartupData() { | ||||
|     this.loadStartupServiceEmitter.emit() | ||||
|   } | ||||
|  | ||||
|   public getLicenseSiteId(): string[] { | ||||
|     return this.license_site_id.value || [] | ||||
|   } | ||||
| @@ -356,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.' | ||||
|           ) | ||||
| @@ -410,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() | ||||
|             } | ||||
|           } | ||||
| @@ -423,21 +441,93 @@ export class SasService { | ||||
|       typeof this.sasjsAdapter.getFolder !== 'undefined' | ||||
|  | ||||
|     let appLocExists: boolean = false | ||||
|     let errorMessage: string | undefined = undefined | ||||
|  | ||||
|     if (getFolderExistsInAdapter) { | ||||
|       appLocExists = await this.appLocCheck(path) | ||||
|       const results = await this.appLocCheck(path) | ||||
|  | ||||
|       appLocExists = results.found | ||||
|       errorMessage = results.errorMessage | ||||
|     } else { | ||||
|       appLocExists = await this.appLocCheckPreAxiosdAdapter(path) | ||||
|     } | ||||
|  | ||||
|     if (appLocExists) { | ||||
|       this.loadStartupServiceEmitter.emit() | ||||
|       // Check if there is appLoc/services/admin/makedata.sas present | ||||
|       // if yes, it needs to be run, so we redirect to /deploy | ||||
|       // if not, we load the startup service | ||||
|  | ||||
|       this.viyaMakedataSuccessfull().then( | ||||
|         (success: boolean) => { | ||||
|           if (success) { | ||||
|             this.loadStartupServiceEmitter.emit() | ||||
|           } else { | ||||
|             this.eventService.startupDataLoaded() | ||||
|             this.router.navigateByUrl('/deploy') | ||||
|           } | ||||
|         }, | ||||
|         (error: any) => { | ||||
|           console.error('Error while looking for the file: makedata.sas', error) | ||||
|         } | ||||
|       ) | ||||
|     } else { | ||||
|       const errorMessageToShow = | ||||
|         (errorMessage || | ||||
|           'Viya services are not present on the current appLoc, or API not reachable. Check the ADAPTER configuration.') + | ||||
|         `\nAppLoc: ${path}` | ||||
|  | ||||
|       this.eventService.showInfoModal('Error', errorMessageToShow) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public appLocCheck(path: string): Promise<boolean> { | ||||
|   private async viyaMakedataSuccessfull(): Promise<boolean> { | ||||
|     return new Promise((resolve, reject) => { | ||||
|       const sasjsConfig = this.getSasjsConfig() | ||||
|       const configuratorFolder = `${sasjsConfig.appLoc}/services/admin` | ||||
|  | ||||
|       this.sasViyaService.getFolderByPath(configuratorFolder).subscribe( | ||||
|         (folderInfo: ViyaApiFolder) => { | ||||
|           const folderId = folderInfo.id | ||||
|  | ||||
|           if (!folderId) { | ||||
|             console.error( | ||||
|               `Folder ID is not present. ${configuratorFolder}`, | ||||
|               sasjsConfig | ||||
|             ) | ||||
|             resolve(false) | ||||
|           } | ||||
|  | ||||
|           this.sasViyaService.getFolderMembers(folderId).subscribe( | ||||
|             (members: ViyaApiFolderMembers) => { | ||||
|               if ( | ||||
|                 !members.items.some((item: any) => item.name === 'makedata') | ||||
|               ) { | ||||
|                 // Makedata.sas is not present, which means it was run | ||||
|                 resolve(true) | ||||
|               } else { | ||||
|                 // Makedata.sas is present, which means it was not run | ||||
|                 resolve(false) | ||||
|               } | ||||
|             }, | ||||
|             (err: any) => { | ||||
|               console.error('Error getting folder contents', err) | ||||
|               reject() | ||||
|             } | ||||
|           ) | ||||
|         }, | ||||
|         (err: any) => { | ||||
|           console.warn('Error getting folder info', err) | ||||
|           reject(err) | ||||
|         } | ||||
|       ) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   public appLocCheck( | ||||
|     path: string | ||||
|   ): Promise<{ found: boolean; errorMessage?: string }> { | ||||
|     return new Promise(async (resolve, reject) => { | ||||
|       let statusNotFound: boolean = false | ||||
|       let fetchError: string = '' | ||||
|  | ||||
|       let res: any | ||||
|  | ||||
| @@ -448,20 +538,21 @@ export class SasService { | ||||
|           this.appLocCheckPending = true | ||||
|           this.shouldLogin.next(true) | ||||
|  | ||||
|           resolve(false) | ||||
|           resolve({ found: false }) | ||||
|         } else if (err.name === 'NotFoundeError') { | ||||
|           fetchError = err.message | ||||
|         } else { | ||||
|           statusNotFound = true | ||||
|           fetchError = | ||||
|             'Viya services are not present on the current appLoc, or API not reachable. Check the ADAPTER configuration.' | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (statusNotFound) { | ||||
|         console.warn('Viya services are not present on the current appLoc.') | ||||
|         this.eventService.startupDataLoaded() | ||||
|         this.router.navigateByUrl('/deploy') | ||||
|         return resolve(false) | ||||
|       if (fetchError.length) { | ||||
|         console.warn(fetchError) | ||||
|         return resolve({ found: false, errorMessage: fetchError }) | ||||
|       } | ||||
|  | ||||
|       resolve(true) | ||||
|       resolve({ found: true }) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
| @@ -579,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) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,10 @@ | ||||
| > | ||||
|   <h3 class="modal-title"> | ||||
|     {{ data.modalTitle }} | ||||
|     <p *ngIf="data.sasService && data.sasService.length > 0" class="sasService"> | ||||
|     <p | ||||
|       *ngIf="data.sasService && data.sasService.length > 0" | ||||
|       class="sasService mt-0" | ||||
|     > | ||||
|       SAS Service: <strong>{{ data.sasService }}</strong> | ||||
|     </p> | ||||
|   </h3> | ||||
|   | ||||
| @@ -1,43 +0,0 @@ | ||||
| .clr-abort-modal { | ||||
|     ::ng-deep { | ||||
|         .modal { | ||||
|             z-index: 2050; | ||||
|         } | ||||
|          | ||||
|         .modal-title-wrapper { | ||||
|             width: 100%; | ||||
|         } | ||||
|  | ||||
|         .modal { | ||||
|             z-index: 2050; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .modal-title { | ||||
|     position: relative; | ||||
| } | ||||
|  | ||||
| .sasService { | ||||
|     position: absolute; | ||||
|     top: 0px; | ||||
|     right: 10px; | ||||
|     margin: 0; | ||||
| } | ||||
|  | ||||
| .modal-footer { | ||||
|     position: relative; | ||||
|     border-top: 1px solid #dcdcdc; | ||||
| } | ||||
|  | ||||
| .systext { | ||||
|     overflow: auto; | ||||
|     margin-top: 20px; | ||||
|     padding: 10px 0; | ||||
|     border-top: 1px solid #dcdcdc; | ||||
|  | ||||
|     p { | ||||
|         margin-top: 0; | ||||
|         word-wrap: break-word; | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,11 @@ | ||||
| import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||||
| import { | ||||
|   Component, | ||||
|   EventEmitter, | ||||
|   Input, | ||||
|   OnInit, | ||||
|   Output, | ||||
|   ViewEncapsulation | ||||
| } from '@angular/core' | ||||
| import { Router } from '@angular/router' | ||||
| import { ServerType } from '@sasjs/utils/types/serverType' | ||||
| import { EventService } from 'src/app/services/event.service' | ||||
| @@ -9,7 +16,8 @@ import { AbortDetails, InfoModal } from '../../models/InfoModal' | ||||
| @Component({ | ||||
|   selector: 'app-info-modal', | ||||
|   templateUrl: './info-modal.component.html', | ||||
|   styleUrls: ['./info-modal.component.scss'] | ||||
|   styleUrls: ['./info-modal.component.scss'], | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class InfoModalComponent implements OnInit { | ||||
|   @Output() onConfirmModalClick: EventEmitter<any> = new EventEmitter() | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| clr-alerts { | ||||
|   display: block; | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| import { Component, OnInit, ViewEncapsulation } from '@angular/core' | ||||
| import { Subscription } from 'rxjs' | ||||
| import { Alert } from './alert' | ||||
| import { AlertsService } from './alerts.service' | ||||
| @@ -6,7 +6,8 @@ import { AlertsService } from './alerts.service' | ||||
| @Component({ | ||||
|   selector: 'app-alerts', | ||||
|   templateUrl: './alerts.component.html', | ||||
|   styleUrls: ['./alerts.component.scss'] | ||||
|   styleUrls: ['./alerts.component.scss'], | ||||
|   encapsulation: ViewEncapsulation.None | ||||
| }) | ||||
| export class AlertsComponent implements OnInit { | ||||
|   public alerts: Array<Alert> = [] | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user