Compare commits

..

75 Commits

Author SHA1 Message Date
semantic-release-bot
ef1015f33b chore(release): 7.2.1 [skip ci]
## [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](225e693d1f))
2025-08-08 17:35:02 +00:00
b43dfb5cf4 Merge pull request 'fix: removing localhost from index.html' (#187) from localhostfix into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m54s
Release / Build-and-test-development (push) Successful in 8m25s
Release / release (push) Successful in 7m50s
Reviewed-on: #187
2025-08-08 17:19:17 +00:00
allan
225e693d1f fix: removing localhost from index.html
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m47s
Build / Build-and-test-development (pull_request) Successful in 8m27s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 18m41s
2025-08-08 18:08:49 +01:00
semantic-release-bot
fda91770be chore(release): 7.2.0 [skip ci]
# [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](26cdd73331))
* **hot v16 migration:** multi dataset fixed issues, and cypress tests adapted ([712b384](712b384848))
* obsolete cypress deps ([2ba4b53](2ba4b5383e))
* remaining hot migrations - handsontable/angular-wrapper ([b419cd5](b419cd5078))

### Features

* lighthouse accessibility check pipeline ([670ec2c](670ec2c71c))
2025-08-08 11:51:38 +00:00
d512876e0b Merge pull request 'fix: obsolete cypress deps' (#186) from cypress-deps into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m42s
Release / Build-and-test-development (push) Successful in 8m24s
Release / release (push) Successful in 7m50s
Reviewed-on: #186
2025-08-08 11:36:06 +00:00
M
2ba4b5383e fix: obsolete cypress deps
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m46s
Build / Build-and-test-development (pull_request) Successful in 8m17s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 18m35s
2025-08-08 13:25:11 +02:00
ecc3184609 Merge pull request 'fix: remaining hot migrations - handsontable/angular-wrapper' (#185) from remaining-hot-migration into main
Some checks failed
Release / Build-production-and-ng-test (push) Successful in 3m32s
Release / Build-and-test-development (push) Failing after 38s
Release / release (push) Has been skipped
Reviewed-on: #185
Reviewed-by: allan <allan@4gl.io>
2025-08-08 08:28:16 +00:00
M
712b384848 fix(hot v16 migration): multi dataset fixed issues, and cypress tests adapted
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m44s
Build / Build-and-test-development (pull_request) Successful in 8m24s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 18m5s
2025-08-07 16:58:53 +02:00
M
26cdd73331 fix(ci): cypress dependency package not available anymore
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 3m46s
Build / Build-and-test-development (pull_request) Failing after 11m42s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 18m1s
2025-08-07 11:15:00 +02:00
M
919aa6dcfe ci: lighthouse
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 3m46s
Build / Build-and-test-development (pull_request) Failing after 33s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 18m1s
2025-08-07 10:22:10 +02:00
M
3bb3093b49 ci: lighthouse
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 3m54s
Build / Build-and-test-development (pull_request) Failing after 33s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Has been cancelled
2025-08-07 10:14:24 +02:00
M
9c12250558 ci: lighthouse
Some checks failed
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 3m32s
Build / Build-and-ng-test (pull_request) Successful in 3m49s
Build / Build-and-test-development (pull_request) Failing after 35s
2025-08-07 10:09:19 +02:00
M
6c843f64fb chore: install wait-on
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 3m45s
Build / Build-and-test-development (pull_request) Failing after 33s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Has been cancelled
2025-08-07 09:54:27 +02:00
M
3fda7dc5b0 chore: installed wait-on
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 3m49s
Build / Build-and-test-development (pull_request) Failing after 37s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Has been cancelled
2025-08-07 09:48:31 +02:00
M
22ec7f0340 ci: lighthouse
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 4m30s
Build / Build-and-test-development (pull_request) Failing after 38s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 3h12m40s
2025-08-06 16:15:45 +02:00
M
378461dcbb chore: licence checker
Some checks failed
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 3m31s
Build / Build-and-ng-test (pull_request) Successful in 3m49s
Build / Build-and-test-development (pull_request) Failing after 54s
2025-08-06 16:08:50 +02:00
M
905c7b9d3c chore: remove doxy
Some checks failed
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 1m26s
Build / Build-and-ng-test (pull_request) Failing after 2m6s
Build / Build-and-test-development (pull_request) Failing after 53s
2025-08-06 16:05:07 +02:00
M
670ec2c71c feat: lighthouse accessibility check pipeline
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m20s
Build / Build-and-test-development (pull_request) Failing after 57s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 2m24s
2025-08-06 15:52:58 +02:00
M
b419cd5078 fix: remaining hot migrations - handsontable/angular-wrapper
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 2m3s
Build / Build-and-test-development (pull_request) Failing after 1m34s
2025-08-06 14:06:07 +02:00
semantic-release-bot
b1db4ea590 chore(release): 7.1.1 [skip ci]
## [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](672dd6d4f1))
2025-07-24 11:46:09 +00:00
822ddb1274 Merge pull request 'fix(viewboxes): hot v16 fails to load because of relative height 100%' (#183) from hot16-viewboxes into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m50s
Release / Build-and-test-development (push) Successful in 9m17s
Release / release (push) Successful in 8m4s
Reviewed-on: #183
2025-07-24 11:29:33 +00:00
M
672dd6d4f1 fix(viewboxes): hot v16 fails to load because of relative height 100%
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m55s
Build / Build-and-test-development (pull_request) Successful in 8m27s
2025-07-24 13:12:15 +02:00
semantic-release-bot
b3ac73d903 chore(release): 7.1.0 [skip ci]
# [7.1.0](https://git.datacontroller.io/dc/dc/compare/v7.0.3...v7.1.0) (2025-07-23)

### Bug Fixes

* adapter bump ([b495c41](b495c41626))
* bumping CLI to 4.12.10 ([a08a717](a08a717ca8))
* bumping sasjs/core and sasjs/cli ([63e9af4](63e9af402e))

### Features

* improving accessibility score up to 100, hot update to v16.0.1 ([71c308d](71c308d052))
2025-07-23 19:46:56 +00:00
M
f8554dd5e7 ci: cypress tests, ng server 'wait on' fix
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m36s
Release / Build-and-test-development (push) Successful in 8m12s
Release / release (push) Successful in 7m58s
2025-07-23 21:31:29 +02:00
88679c0c9a Merge pull request 'fix: bumping CLI to 4.12.10' (#181) from clibump into main
Some checks failed
Release / Build-production-and-ng-test (push) Successful in 3m35s
Release / Build-and-test-development (push) Failing after 3h10m30s
Release / release (push) Has been cancelled
Reviewed-on: #181
2025-07-23 15:04:30 +00:00
allan
a08a717ca8 fix: bumping CLI to 4.12.10
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m43s
Build / Build-and-test-development (pull_request) Successful in 8m11s
2025-07-23 16:03:57 +01:00
c8b6fdbfdb Merge pull request 'Improving accessibility score up to 100, hot update to v16.0.1' (#180) from accessibility-maxing into main
Some checks failed
Release / Build-production-and-ng-test (push) Failing after 1m13s
Release / Build-and-test-development (push) Has been skipped
Release / release (push) Has been skipped
Reviewed-on: #180
2025-07-23 13:21:40 +00:00
M
b495c41626 fix: adapter bump
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m39s
Build / Build-and-test-development (pull_request) Successful in 8m9s
2025-07-23 14:59:45 +02:00
M
7f4be474c6 chore(git): Merge branch 'main' into accessibility-maxing
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m48s
Build / Build-and-test-development (pull_request) Successful in 8m12s
2025-07-23 13:23:53 +02:00
7f6f68fcbb Merge pull request 'fix: bumping sasjs/core and sasjs/cli' (#177) from issue157 into main
Some checks failed
Release / Build-production-and-ng-test (push) Failing after 1m13s
Release / Build-and-test-development (push) Has been skipped
Release / release (push) Has been skipped
Reviewed-on: #177
2025-07-23 11:14:37 +00:00
M
03fd7db033 chore: licence checker
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m48s
Build / Build-and-test-development (pull_request) Successful in 8m13s
2025-07-23 12:22:04 +02:00
M
9dc5c66f7b chore: package-lock
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m18s
Build / Build-and-test-development (pull_request) Successful in 8m12s
2025-07-23 12:16:25 +02:00
M
aa1b08632e chore: package-lock
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m42s
Build / Build-and-test-development (pull_request) Successful in 7m59s
2025-07-23 11:52:56 +02:00
M
6bbe354c9e ci: fix
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 45s
Build / Build-and-test-development (pull_request) Failing after 46s
2025-07-23 11:51:40 +02:00
M
8ff429793b style: lint
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 45s
Build / Build-and-test-development (pull_request) Failing after 46s
2025-07-23 11:50:35 +02:00
M
70d010127a style: lint
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 43s
Build / Build-and-test-development (pull_request) Failing after 44s
2025-07-23 11:49:34 +02:00
M
696717c509 ci: script
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 41s
Build / Build-and-test-development (pull_request) Successful in 8m1s
2025-07-23 11:38:24 +02:00
M
71c308d052 feat: improving accessibility score up to 100, hot update to v16.0.1
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 41s
Build / Build-and-test-development (pull_request) Has been cancelled
2025-07-23 11:17:57 +02:00
allan
bed5b320ad chore: lint fix
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 3m48s
Build / Build-and-test-development (pull_request) Has been cancelled
2025-07-07 14:51:04 +01:00
b0e827412e Merge branch 'main' into issue157
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 45s
Build / Build-and-test-development (pull_request) Failing after 3h1m39s
2025-07-07 13:34:38 +00:00
allan
63e9af402e fix: bumping sasjs/core and sasjs/cli
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 41s
Build / Build-and-test-development (pull_request) Failing after 3h11m34s
2025-07-07 14:34:03 +01:00
fd55105f62 chore(git): adding size check in precommit hook
Some checks failed
Release / Build-production-and-ng-test (push) Successful in 4m20s
Release / release (push) Has been cancelled
Release / Build-and-test-development (push) Has been cancelled
2025-06-27 16:42:13 +01:00
semantic-release-bot
c2e3b362e7 chore(release): 7.0.3 [skip ci]
## [7.0.3](https://git.datacontroller.io/dc/dc/compare/v7.0.2...v7.0.3) (2025-06-26)

### Bug Fixes

* makedata vars ([e7cb471](e7cb471c0b))
* viya deploy makedata missing params ([7a82316](7a8231615c))
2025-06-26 16:29:21 +00:00
Mihajlo Medjedovic
5d2d60d040 chore: npm audit fix for client folder, lint fix
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 4m5s
Release / Build-and-test-development (push) Successful in 9m9s
Release / release (push) Successful in 9m18s
2025-06-26 17:53:22 +02:00
0e59f5406f Merge pull request 'fix: viya deploy makedata missing params' (#176) from viya-deploy-params into main
Some checks failed
Release / Build-production-and-ng-test (push) Failing after 1m18s
Release / Build-and-test-development (push) Has been skipped
Release / release (push) Has been skipped
Reviewed-on: #176
2025-06-25 21:51:31 +00:00
Medjedovic
e7cb471c0b fix: makedata vars
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m5s
Build / Build-and-test-development (pull_request) Successful in 8m39s
2025-06-25 23:29:42 +02:00
Medjedovic
0465089207 style: lint
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m4s
Build / Build-and-test-development (pull_request) Successful in 8m36s
2025-06-25 23:17:48 +02:00
Medjedovic
4f2f59907c Merge branch 'viya-deploy-params' of ssh://git.datacontroller.io:29419/dc/dc into viya-deploy-params
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 47s
Build / Build-and-test-development (pull_request) Successful in 8m37s
2025-06-25 23:04:38 +02:00
Medjedovic
7d85328d41 style: lint 2025-06-25 23:04:19 +02:00
f2a9329196 chore: bumping node v
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 49s
Build / Build-and-test-development (pull_request) Successful in 8m39s
2025-06-25 20:20:19 +01:00
Medjedovic
7a8231615c fix: viya deploy makedata missing params
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 49s
Build / Build-and-test-development (pull_request) Successful in 9m5s
2025-06-25 15:41:43 +02:00
semantic-release-bot
0db6b25327 chore(release): 7.0.2 [skip ci]
## [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](0b4042af60))
2025-06-21 09:30:41 +00:00
e91f6f01a6 Merge pull request 'fix(viya deploy): run makedata in new window to ensure logs are available for the user' (#175) from issue154 into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 4m4s
Release / Build-and-test-development (push) Successful in 8m40s
Release / release (push) Successful in 8m30s
Reviewed-on: #175
2025-06-21 09:14:11 +00:00
Mihajlo Medjedovic
0b4042af60 fix(viya deploy): run makedata in new window to ensure logs are available for the user
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m12s
Build / Build-and-test-development (pull_request) Successful in 8m42s
2025-06-17 15:33:11 +02:00
semantic-release-bot
519d8953b5 chore(release): 7.0.1 [skip ci]
## [7.0.1](https://git.datacontroller.io/dc/dc/compare/v7.0.0...v7.0.1) (2025-06-11)

### Bug Fixes

* refresh process ([4ecd186](4ecd186e5c))
2025-06-11 18:47:43 +00:00
14a616fc1b Merge pull request 'fix: refresh process' (#173) from issue157 into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 4m9s
Release / Build-and-test-development (push) Successful in 8m45s
Release / release (push) Successful in 8m47s
Reviewed-on: #173
2025-06-11 18:05:06 +00:00
bfe5a8626f Merge branch 'main' into issue157
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m18s
Build / Build-and-test-development (pull_request) Successful in 8m43s
2025-06-11 18:04:48 +00:00
allan
4ecd186e5c fix: refresh process
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m15s
Build / Build-and-test-development (pull_request) Successful in 8m44s
2025-06-11 19:04:21 +01:00
semantic-release-bot
8c60473c15 chore(release): 7.0.0 [skip ci]
# [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](e874143a95))
* commit git hooks checking lint ([69f687a](69f687a85f))
* ensuring apploc is not case sensitive. Closes [#171](#171) ([24545f2](24545f2acd))
* export unregistered formats ([f6d7d6f](f6d7d6f90c)), closes [#158](#158)
* reload startupservice after user approves the MPE_TABLES page ([e5f8e50](e5f8e500c1))
* showing catalog_cnt in libinfo ([e44a25d](e44a25dcc3)), closes [#160](#160)

### Features

* adding 4 new tables for catalogs ([e4dbab8](e4dbab8b16))
* capturing catalog specific information, closes [#159](#159) ([b4c586a](b4c586a859))
* viewer added catalog_cnt ([2aa19d1](2aa19d1dca))

### 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)
2025-06-11 13:46:10 +00:00
bb126eba5b Merge pull request 'Reload startupservice after user approves the MPE_TABLES changes' (#170) from issue157 into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 4m8s
Release / Build-and-test-development (push) Successful in 8m47s
Release / release (push) Successful in 8m50s
Reviewed-on: #170
2025-06-11 13:16:22 +00:00
allan
d1998422d2 chore: fix for mpe_datastatus_libs
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m19s
Build / Build-and-test-development (pull_request) Successful in 8m45s
2025-06-11 14:15:50 +01:00
Mihajlo Medjedovic
69f687a85f fix: commit git hooks checking lint
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m11s
Build / Build-and-test-development (pull_request) Successful in 8m34s
2025-06-11 13:11:30 +02:00
Mihajlo Medjedovic
2aa19d1dca feat: viewer added catalog_cnt
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 48s
Build / Build-and-test-development (pull_request) Successful in 8m49s
2025-06-11 12:57:10 +02:00
allan
e44a25dcc3 fix: showing catalog_cnt in libinfo
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m19s
Build / Build-and-test-development (pull_request) Successful in 8m53s
Closes #160
2025-06-11 10:06:52 +01:00
allan
efb5ffa906 chore: reverting accidental change
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m4s
Build / Build-and-test-development (pull_request) Successful in 8m33s
2025-06-10 22:45:39 +01:00
allan
b4c586a859 feat: capturing catalog specific information, closes #159
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m12s
Build / Build-and-test-development (pull_request) Successful in 8m35s
BREAKING CHANGE:  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)
2025-06-10 22:40:09 +01:00
allan
e874143a95 fix: bumping adapter to re-enable JES API method 2025-06-10 16:18:56 +01:00
allan
e4dbab8b16 feat: adding 4 new tables for catalogs 2025-06-10 16:18:30 +01:00
allan
f6d7d6f90c fix: export unregistered formats
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m14s
Build / Build-and-test-development (pull_request) Successful in 9m0s
Closes #158
2025-06-10 09:41:18 +01:00
allan
063c90caf4 chore(docs): readme fix
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m3s
Build / Build-and-test-development (pull_request) Successful in 8m38s
2025-06-06 23:24:35 +01:00
allan
2011c2eee7 chore(docs): updating README with viya deploy details
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m0s
Build / Build-and-test-development (pull_request) Successful in 8m28s
2025-06-06 23:23:47 +01:00
allan
24545f2acd fix: ensuring apploc is not case sensitive. Closes #171
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m6s
Build / Build-and-test-development (pull_request) Successful in 8m38s
2025-06-06 21:05:08 +01:00
a7c81245ff Merge branch 'main' into issue157
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m6s
Build / Build-and-test-development (pull_request) Successful in 8m45s
2025-06-06 13:17:17 +00:00
Mihajlo Medjedovic
4f2c993b2d style: lint
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m3s
Build / Build-and-test-development (pull_request) Successful in 8m35s
2025-06-06 15:03:51 +02:00
Mihajlo Medjedovic
e5f8e500c1 fix: reload startupservice after user approves the MPE_TABLES page 2025-06-06 15:03:36 +02:00
95 changed files with 5683 additions and 4434 deletions

View File

@@ -1,11 +1,23 @@
#!/bin/sh #!/bin/sh
# Avoid commits to the master branch # Using `--silent` helps for showing any errs in the first line of the response
BRANCH=`git rev-parse --abbrev-ref HEAD` # The first line is picked up by the VS Code GIT UI popup when rc is not 0
REGEX="^(master|development)$"
if [[ "$BRANCH" =~ $REGEX ]]; then if npm run --silent lint:check:silent ; then
echo "You are on branch $BRANCH. Are you sure you want to commit to this branch?" exit 0
echo "If so, commit with -n to bypass the pre-commit hook." else
exit 1 npm run --silent lint:fix:silent
echo "❌ Prettier check failed! We ran lint:fix for you. Please add & commit again."
exit 1
fi 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
)

View File

@@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.14.0 node-version: 20.15.1
- name: Install Google Chrome - name: Install Google Chrome
run: | run: |
@@ -58,7 +58,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.14.0 node-version: 20.15.1
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
@@ -70,7 +70,7 @@ jobs:
- run: apt install -y ./google-chrome*.deb; - run: apt install -y ./google-chrome*.deb;
- run: export CHROME_BIN=/usr/bin/google-chrome - run: export CHROME_BIN=/usr/bin/google-chrome
- run: apt-get update -y - run: apt-get update -y
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev 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 - run: apt -y install jq
- name: Write cypress credentials - name: Write cypress credentials
@@ -126,7 +126,7 @@ jobs:
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
cat ./cypress.config.ts cat ./cypress.config.ts
# Start frontend and run cypress # Start frontend and run cypress
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 - name: Zip Cypress videos
if: always() if: always()

View 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

View File

@@ -80,7 +80,7 @@ jobs:
- run: apt install -y ./google-chrome*.deb; - run: apt install -y ./google-chrome*.deb;
- run: export CHROME_BIN=/usr/bin/google-chrome - run: export CHROME_BIN=/usr/bin/google-chrome
- run: apt-get update -y - run: apt-get update -y
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev 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 - run: apt -y install jq
- name: Write cypress credentials - name: Write cypress credentials
@@ -136,7 +136,7 @@ jobs:
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
cat ./cypress.config.ts cat ./cypress.config.ts
# Start frontend and run cypress # Start frontend and run cypress
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 - name: Zip Cypress videos
if: always() if: always()

1
.gitignore vendored
View File

@@ -21,3 +21,4 @@ sasjsresults
.sasjsrc .sasjsrc
client/.npmrc client/.npmrc
*~ *~
.lighthouseci

View File

@@ -1,3 +1,92 @@
## [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) ## [6.16.2](https://git.datacontroller.io/dc/dc/compare/v6.16.1...v6.16.2) (2025-06-06)

View File

@@ -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). 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 * Main site: https://datacontroller.io
* Docs: https://docs.datacontroller.io * Docs: https://docs.datacontroller.io
* Code: https://code.datacontroller.io * Code: https://code.datacontroller.io
For support, contact support@4gl.io or reach out on [Matrix](https://matrix.to/#/#dc:4gl.io)! For support, contact support@4gl.io or reach out on [Matrix](https://matrix.to/#/#dc:4gl.io)!
## Development
### Lighthouse CI
This project includes automated Lighthouse performance and accessibility checks that run on pull requests. The checks ensure:
- **Accessibility Score**: Minimum 1.0 (100%) median score across all tested pages
The Lighthouse CI workflow:
1. Sets up the development environment with SASjs server and mocked services
2. Builds and serves the Angular frontend
3. Runs Lighthouse CI against key application pages
4. Uploads results as artifacts for review
To run Lighthouse checks locally:
```bash
cd client
npm install
npm run lighthouse
```
Configuration is in `client/lighthouserc.js`.

View File

@@ -32,27 +32,26 @@ context('excel tests: ', function () {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
attachExcelFile('regular.csv', () => { attachExcelFile('regular.csv', () => {
cy.get('#approval-btn', { timeout: 60000 }) cy.get('#approval-btn', { timeout: 60000 }).should('be.visible')
.should('be.visible') // .then(() => {
// .then(() => { // cy.get('#hotInstance', { timeout: 30000 })
// cy.get('#hotInstance', { timeout: 30000 }) // .find('div.ht_master.handsontable')
// .find('div.ht_master.handsontable') // .find('div.wtHolder')
// .find('div.wtHolder') // .find('div.wtHider')
// .find('div.wtHider') // .find('div.wtSpreader')
// .find('div.wtSpreader') // .find('table.htCore')
// .find('table.htCore') // .find('tbody')
// .find('tbody') // .then((data) => {
// .then((data) => { // let cell: any = data[0].children[0].children[1]
// let cell: any = data[0].children[0].children[1] // expect(cell.innerText).to.equal('0')
// expect(cell.innerText).to.equal('0') // cell = data[0].children[0].children[2]
// cell = data[0].children[0].children[2] // expect(cell.innerText).to.equal('44')
// expect(cell.innerText).to.equal('44') // cell = data[0].children[0].children[3]
// cell = data[0].children[0].children[3] // expect(cell.innerText).to.equal('abc')
// expect(cell.innerText).to.equal('abc') // cell = data[0].children[0].children[6]
// cell = data[0].children[0].children[6] // expect(cell.innerText).to.equal('Option abc')
// expect(cell.innerText).to.equal('Option abc') // })
// }) // })
// })
}) })
}) })

View File

@@ -217,11 +217,7 @@ const rejectExcel = (callback?: any) => {
.should('contain', 'Approve') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.innerText
.toLowerCase()
.includes('approve')
) {
approvalButton.click() approvalButton.click()
break break
} }

View File

@@ -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) => { it('1 | Uploads Excel file with multiple sheets, 3 sheets including data, 2 sheets matched with dataset', (done) => {
attachExcelFile('multi_load_test_2.xlsx', () => { attachExcelFile('multi_load_test_2.xlsx', () => {
checkHotUserDatasetTable('hotTableUserDataset', [ checkHotUserDatasetTable(
[library, mpeXTestTable], 'hotTableUserDataset',
[library, mpeTablesTable] [
], () => { [library, mpeXTestTable],
cy.get('#continue-btn').trigger('click').then(() => { [library, mpeTablesTable]
checkIfTreeHasTables([`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], undefined, (includes: boolean) => { ],
if (includes) { () => {
// MPE_TABLES sheet does not have data so 1 error image must be shown cy.get('#continue-btn')
hasErrorTables(1, (valid: boolean) => { .trigger('click')
if (valid) done() .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) => { it('2 | Uploads Excel file with multiple sheets, 2 sheets matched with dataset, 1 matched sheet does not have data', (done) => {
attachExcelFile('multi_load_test_1.xlsx', () => { attachExcelFile('multi_load_test_1.xlsx', () => {
checkHotUserDatasetTable('hotTableUserDataset', [ checkHotUserDatasetTable(
[library, mpeXTestTable], 'hotTableUserDataset',
[library, mpeTablesTable] [
], () => { [library, mpeXTestTable],
cy.get('#continue-btn').trigger('click').then(() => { [library, mpeTablesTable]
checkIfTreeHasTables([`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], `${library}.${mpeXTestTable}`, (includes: boolean) => { ],
if (includes) { () => {
cy.get('#hotTable').should('be.visible').then(() => { cy.get('#continue-btn')
checkHotUserDatasetTable('hotTable', [ .trigger('click')
['No', '1', 'more dummy data'], .then(() => {
['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:'], checkIfTreeHasTables(
['No', '1', 'if you can fill the unforgiving minute'] [`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`],
], () => { `${library}.${mpeXTestTable}`,
submitTables() (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) => { hasSuccessSubmits(2, (valid: boolean) => {
if (valid) done() if (valid) done()
}) })
}
}) )
}) })
} }
}) }
}) )
}) })
}
)
}) })
}) })
it('3 | Uploads Excel file with multiple sheets, 1 sheets has 2 tables', (done) => { it('3 | Uploads Excel file with multiple sheets, 1 sheets has 2 tables', (done) => {
attachExcelFile('multi_load_test_1.xlsx', () => { attachExcelFile('multi_load_test_1.xlsx', () => {
checkHotUserDatasetTable('hotTableUserDataset', [ checkHotUserDatasetTable(
[library, mpeXTestTable], 'hotTableUserDataset',
[library, mpeTablesTable] [
], () => { [library, mpeXTestTable],
cy.get('#continue-btn').trigger('click').then(() => { [library, mpeTablesTable]
checkIfTreeHasTables([`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`], `${library}.${mpeXTestTable}`, (includes: boolean) => { ],
if (includes) { () => {
cy.get('#hotTable').should('be.visible').then(() => { cy.get('#continue-btn')
checkHotUserDatasetTable('hotTable', [ .trigger('click')
['No', '1', 'more dummy data'], .then(() => {
['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:'], checkIfTreeHasTables(
['No', '1', 'if you can fill the unforgiving minute'] [`${library}.${mpeXTestTable}`, `${library}.${mpeTablesTable}`],
], () => { `${library}.${mpeXTestTable}`,
clickOnTreeNode('DC996664.MPE_TABLES', () => { (includes: boolean) => {
cy.wait(1000).then(() => { if (includes) {
cy.get('#hotTable').should('be.visible').then(() => { cy.get('#hotTable')
checkHotUserDatasetTable('hotTable', [ .should('be.visible')
['No', 'DC914286', 'MPE_COLUMN_LEVEL_SECURITY'], .then(() => {
['No', 'DC914286', 'MPE_XLMAP_INFO'], checkHotUserDatasetTable(
['No', 'DC914286', 'MPE_XLMAP_RULES'] 'hotTable',
], () => { [
submitTables() ['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) => { hasSuccessSubmits(
if (valid) done() 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 }) cy.get(`#${hotId}`, { timeout: longerCommandTimeout })
.find('div.ht_master.handsontable') .find('div.ht_master.handsontable')
.find('div.wtHolder') .find('div.wtHolder')
.find('div.wtHider') .find('div.wtHider')
.find('div.wtSpreader') .find('div.wtSpreader')
.find('table.htCore') .find('table.htCore')
.find('tbody') .find('tbody')
.then((data) => { .then((data) => {
cy.wait(2000).then(() => { cy.wait(2000).then(() => {
for (let rowI = 0; rowI < dataToContain.length; rowI++) { for (let rowI = 0; rowI < dataToContain.length; rowI++) {
for (let colI = 0; colI < dataToContain[rowI].length; colI++) { for (let colI = 0; colI < dataToContain[rowI].length; colI++) {
expect(data[0].children[rowI].children[colI]).to.contain(dataToContain[rowI][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) => { 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) => { cy.get('.nav-tree clr-tree > clr-tree-node').then((treeNodes: any) => {
let datasets = tables let datasets = tables
let nodesCorrect = true let nodesCorrect = true
@@ -207,16 +286,26 @@ const submitTables = () => {
cy.wait(1000) cy.wait(1000)
} }
const hasSuccessSubmits = (expectedNoOfSubmits: number, callback: (valid: boolean) => void) => { const hasSuccessSubmits = (
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="success"]').should('be.visible').then(($nodes) => { expectedNoOfSubmits: number,
callback(expectedNoOfSubmits === $nodes.length) 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) => { const hasErrorTables = (
cy.get('.nav-tree clr-tree > clr-tree-node cds-icon[status="danger"]').should('be.visible').then(($nodes) => { expectedNoOfErrors: number,
callback(expectedNoOfErrors === $nodes.length) 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) => { const visitPage = (url: string) => {

View File

@@ -234,7 +234,7 @@ context('excel tests: ', function () {
cy.get('.btn-upload-preview', { timeout: 60000 }) cy.get('.btn-upload-preview', { timeout: 60000 })
.should('be.visible') .should('be.visible')
.then(() => { .then(() => {
cy.get('#hotInstance', { timeout: 30000 }) cy.get('#hotTable', { timeout: 30000 })
.find('div.ht_master.handsontable') .find('div.ht_master.handsontable')
.find('div.wtHolder') .find('div.wtHolder')
.find('div.wtHider') .find('div.wtHider')
@@ -283,7 +283,7 @@ context('excel tests: ', function () {
cy.get('.btn-upload-preview', { timeout: 60000 }) cy.get('.btn-upload-preview', { timeout: 60000 })
.should('be.visible') .should('be.visible')
.then(() => { .then(() => {
cy.get('#hotInstance', { timeout: 30000 }) cy.get('#hotTable', { timeout: 30000 })
.find('div.ht_master.handsontable') .find('div.ht_master.handsontable')
.find('div.wtHolder') .find('div.wtHolder')
.find('div.wtHider') .find('div.wtHider')
@@ -399,11 +399,7 @@ const rejectExcel = (callback?: any) => {
.should('contain', 'Approve') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.innerText
.toLowerCase()
.includes('approve')
) {
approvalButton.click() approvalButton.click()
break break
} }
@@ -432,11 +428,7 @@ const acceptExcel = (callback?: any) => {
.should('contain', 'Approve') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.innerText
.toLowerCase()
.includes('approve')
) {
approvalButton.click() approvalButton.click()
break break
} }
@@ -455,7 +447,7 @@ const acceptExcel = (callback?: any) => {
} }
const checkResultOfFormulaUpload = (callback?: any) => { const checkResultOfFormulaUpload = (callback?: any) => {
cy.get('#hotInstance', { timeout: longerCommandTimeout }) cy.get('#hotTable', { timeout: longerCommandTimeout })
.find('div.ht_master.handsontable') .find('div.ht_master.handsontable')
.find('div.wtHolder') .find('div.wtHolder')
.find('div.wtHider') .find('div.wtHider')
@@ -471,7 +463,7 @@ const checkResultOfFormulaUpload = (callback?: any) => {
const checkResultOfXLSUpload = (callback?: any) => { const checkResultOfXLSUpload = (callback?: any) => {
cy.viewport(1280, 720) cy.viewport(1280, 720)
cy.get('#hotInstance', { timeout: 30000 }) cy.get('#hotTable', { timeout: 30000 })
.find('div.ht_master.handsontable') .find('div.ht_master.handsontable')
.find('div.wtHolder') .find('div.wtHolder')
.find('div.wtHider') .find('div.wtHider')
@@ -500,7 +492,7 @@ const checkResultOfXLSUpload = (callback?: any) => {
if (callback) callback() if (callback) callback()
}) })
cy.get('#hotInstance', { timeout: 30000 }) cy.get('#hotTable', { timeout: 30000 })
.find('div.ht_master.handsontable') .find('div.ht_master.handsontable')
.find('div.wtHolder') .find('div.wtHolder')
.scrollTo('right') .scrollTo('right')

View File

@@ -16,7 +16,6 @@ context('filtering tests: ', function () {
this.beforeEach(() => { this.beforeEach(() => {
cy.visit(hostUrl + appLocation, { timeout: longerCommandTimeout }) cy.visit(hostUrl + appLocation, { timeout: longerCommandTimeout })
visitPage('home') visitPage('home')
}) })
@@ -299,14 +298,16 @@ const setFilterWithValue = (
cy.get('.no-values') cy.get('.no-values')
.should('not.exist') .should('not.exist')
.then(() => { .then(() => {
cy.get('.in-values-modal clr-checkbox-wrapper input').then((inputs: any) => { cy.get('.in-values-modal clr-checkbox-wrapper input').then(
inputs[0].click() (inputs: any) => {
cy.get('.in-values-modal .modal-footer button').click() 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()
}) }
)
}) })
}) })

View File

@@ -23,7 +23,6 @@ interface EditConfigTableCells {
context('licensing tests: ', function () { context('licensing tests: ', function () {
this.beforeAll(() => { this.beforeAll(() => {
cy.loginAndUpdateValidKey() cy.loginAndUpdateValidKey()
}) })
@@ -371,8 +370,6 @@ context('licensing tests: ', function () {
}) })
}) })
} }
}) })
const logout = (callback?: any) => { const logout = (callback?: any) => {
@@ -697,11 +694,7 @@ const approveTable = (callback?: any) => {
.should('contain', 'Approve') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.innerText
.toLowerCase()
.includes('approve')
) {
approvalButton.click() approvalButton.click()
break break
} }

View File

@@ -18,7 +18,6 @@ context('liveness tests: ', function () {
this.beforeEach(() => { this.beforeEach(() => {
cy.visit(hostUrl + appLocation) cy.visit(hostUrl + appLocation)
visitPage('home') visitPage('home')
}) })
@@ -125,11 +124,7 @@ const rejectExcel = (callback?: any) => {
.should('contain', 'Approve') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.innerText
.toLowerCase()
.includes('approve')
) {
approvalButton.click() approvalButton.click()
break break
} }

View File

@@ -76,7 +76,8 @@ context('editor tests: ', function () {
cy.get('.viewbox-open').click() cy.get('.viewbox-open').click()
openTableFromViewboxTree( openTableFromViewboxTree(
libraryToOpenIncludes, libraryToOpenIncludes,
viewboxes.map((viewbox) => viewbox.viewbox_table)) viewboxes.map((viewbox) => viewbox.viewbox_table)
)
cy.get('.open-viewbox').then((viewboxNodes: any) => { cy.get('.open-viewbox').then((viewboxNodes: any) => {
let found = 0 let found = 0
@@ -91,32 +92,34 @@ context('editor tests: ', function () {
if (found < viewboxes.length) return if (found < viewboxes.length) return
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then((viewboxNodes: any) => { cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
for (let viewboxNode of viewboxNodes) { (viewboxNodes: any) => {
cy.get(viewboxNode).within(() => { for (let viewboxNode of viewboxNodes) {
cy.get('.table-title').then((tableTitle) => { cy.get(viewboxNode).within(() => {
const title = tableTitle[0].innerText cy.get('.table-title').then((tableTitle) => {
const viewbox = viewboxes.find((vb) => const title = tableTitle[0].innerText
title.toLowerCase().includes(vb.viewbox_table) 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()
}
) )
}
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 = () => { const removeAllColumns = () => {
cy.get('.configuration-wrapper clr-icon[shape="trash"]').then(removeNodes => { cy.get('.configuration-wrapper clr-icon[shape="trash"]').then(
for (let removeNode of removeNodes) { (removeNodes) => {
removeNode.click() for (let removeNode of removeNodes) {
removeNode.click()
}
} }
}) )
} }
const checkColumns = (columns: string[], callback: () => void) => { const checkColumns = (columns: string[], callback: () => void) => {
@@ -412,7 +417,7 @@ const checkColumns = (columns: string[], callback: () => void) => {
console.log('viewboxColNode', viewboxColNodes) console.log('viewboxColNode', viewboxColNodes)
console.log('columns', columns) console.log('columns', columns)
for (let i = 0; i < viewboxColNodes.length; i++) { for (let i = 0; i < viewboxColNodes.length; i++) {
const col = columns[i]|| '' const col = columns[i] || ''
const colNode = viewboxColNodes[i] const colNode = viewboxColNodes[i]
if ( if (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ const check = (cwd) => {
onlyAllow: onlyAllow:
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;', 'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
excludePackages: excludePackages:
'@cds/city@1.1.0;@handsontable/angular@15.3.0;handsontable@15.3.0;hyperformula@2.7.1;hyperformula@3.0.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1' '@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;handsontable@16.0.1;hyperformula@2.7.1;hyperformula@3.0.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1'
}, },
(error, json) => { (error, json) => {
if (error) { if (error) {

42
client/lighthouserc.js Normal file
View File

@@ -0,0 +1,42 @@
module.exports = {
ci: {
collect: {
settings: {
preset: "desktop",
chromeFlags: "--no-sandbox --disable-dev-shm-usage"
},
url: [
'http://localhost:5000/AppStream/clickme/#/home/tables',
'http://localhost:5000/AppStream/clickme/#/editor/DC996664.MPE_X_TEST',
'http://localhost:5000/AppStream/clickme/#/view/data',
'http://localhost:5000/AppStream/clickme/#/view/data/DC996664',
'http://localhost:5000/AppStream/clickme/#/view/data/DC996664.MPE_X_TEST',
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups',
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups',
'http://localhost:5000/AppStream/clickme/#/view/usernav/groups/1',
'http://localhost:5000/AppStream/clickme/#/view/usernav/users/1',
'http://localhost:5000/AppStream/clickme/#/home/excel-maps',
'http://localhost:5000/AppStream/clickme/#/home/excel-maps/BASEL-CR2',
'http://localhost:5000/AppStream/clickme/#/home/multi-load',
'http://localhost:5000/AppStream/clickme/#/review/submitted',
'http://localhost:5000/AppStream/clickme/#/review/approve',
'http://localhost:5000/AppStream/clickme/#/review/history',
'http://localhost:5000/AppStream/clickme/#/stage/DC20221006T142649516_059582_7169',
'http://localhost:5000/AppStream/clickme/#/review/submitted/DC20221006T142649516_059582_7169',
'http://localhost:5000/AppStream/clickme/#/system'
]
},
assert: {
assertions: {
'categories:accessibility': [
'error',
{ minScore: 1, aggregationMethod: 'median' }
],
'categories:performance': [
'error',
{ minScore: 0.4, aggregationMethod: 'median' }
]
}
}
}
}

3140
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,8 @@
"sasdocs": "sasjs doc && ./sasjs/utils/deploydocs.sh", "sasdocs": "sasjs doc && ./sasjs/utils/deploydocs.sh",
"compodoc:build": "compodoc -p tsconfig.doc.json --name 'Data Controller Client'", "compodoc:build": "compodoc -p tsconfig.doc.json --name 'Data Controller Client'",
"compodoc:build-and-serve": "compodoc -p tsconfig.doc.json -s --name 'Data Controller Client'", "compodoc:build-and-serve": "compodoc -p tsconfig.doc.json -s --name 'Data Controller Client'",
"compodoc:serve": "compodoc -s --name 'Data Controller Client'" "compodoc:serve": "compodoc -s --name 'Data Controller Client'",
"lighthouse": "lhci autorun"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
@@ -48,8 +49,8 @@
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz", "@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
"@clr/icons": "^13.0.2", "@clr/icons": "^13.0.2",
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz", "@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
"@handsontable/angular": "^15.3.0", "@handsontable/angular-wrapper": "16.0.1",
"@sasjs/adapter": "^4.12.0", "@sasjs/adapter": "^4.12.2",
"@sasjs/utils": "^3.4.0", "@sasjs/utils": "^3.4.0",
"@sheet/crypto": "file:libraries/sheet-crypto.tgz", "@sheet/crypto": "file:libraries/sheet-crypto.tgz",
"@types/d3-graphviz": "^2.6.7", "@types/d3-graphviz": "^2.6.7",
@@ -60,7 +61,7 @@
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"d3-graphviz": "^5.0.2", "d3-graphviz": "^5.0.2",
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
"handsontable": "^15.3.0", "handsontable": "^16.0.1",
"https-browserify": "1.0.0", "https-browserify": "1.0.0",
"hyperformula": "^2.5.0", "hyperformula": "^2.5.0",
"iconv-lite": "^0.5.0", "iconv-lite": "^0.5.0",
@@ -95,6 +96,7 @@
"@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-methods": "^7.18.6",
"@compodoc/compodoc": "^1.1.21", "@compodoc/compodoc": "^1.1.21",
"@cypress/webpack-preprocessor": "^5.17.1", "@cypress/webpack-preprocessor": "^5.17.1",
"@lhci/cli": "^0.12.0",
"@types/core-js": "^2.5.5", "@types/core-js": "^2.5.5",
"@types/crypto-js": "^4.2.1", "@types/crypto-js": "^4.2.1",
"@types/es6-shim": "^0.31.39", "@types/es6-shim": "^0.31.39",

View File

@@ -78,6 +78,17 @@ export class AutomaticComponent implements OnInit {
runMakeData: null runMakeData: null
} }
public sasjsConfig = this.sasService.getSasjsConfig()
/**
* makedata service will be run in a new window
* This is needed to ensure that the user can see the logs
* and the progress of the service execution.
* If this is set to `false`, the service will be run in the same window
* using the adapter request method.
*/
public deployInNewWindow: boolean = true
constructor( constructor(
private eventService: EventService, private eventService: EventService,
private deployService: DeployService, private deployService: DeployService,
@@ -156,8 +167,7 @@ export class AutomaticComponent implements OnInit {
public async getAdminGroups() { public async getAdminGroups() {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
this.adminGroupsLoading = true this.adminGroupsLoading = true
;(this.sasViyaService
this.sasViyaService
.getAdminGroups() .getAdminGroups()
.subscribe((res: ViyaApiIdentities) => { .subscribe((res: ViyaApiIdentities) => {
this.adminGroupsLoading = false this.adminGroupsLoading = false
@@ -177,7 +187,7 @@ export class AutomaticComponent implements OnInit {
this.eventService.showAbortModal('admin groups', err) this.eventService.showAbortModal('admin groups', err)
reject(err) reject(err)
} })
}) })
} }
@@ -246,7 +256,7 @@ export class AutomaticComponent implements OnInit {
} }
public async runAutoDeploy(executeJson: boolean = false) { public async runAutoDeploy(executeJson: boolean = false) {
this.autodeploying = true if (!this.deployInNewWindow) this.autodeploying = true
if (executeJson) { if (executeJson) {
this.executeJson() this.executeJson()
@@ -255,7 +265,7 @@ export class AutomaticComponent implements OnInit {
if (this.recreateDatabase) { if (this.recreateDatabase) {
this.createDatabase() this.createDatabase()
} else { } else {
this.autodeployDone = true if (!this.deployInNewWindow) this.autodeployDone = true
} }
} }
@@ -296,51 +306,86 @@ export class AutomaticComponent implements OnInit {
debug: true debug: true
} }
this.sasJs if (this.deployInNewWindow) {
.request(`services/admin/makedata`, data, overrideConfig, () => { this.runMakedataInNewWindow({
this.sasService.shouldLogin.next(true) contextName: selectedComputeContextName,
admin: this.selectedAdminGroup,
dcPath: this.dcPath
}) })
.then((res: any) => { } else {
this.autodeployDone = true this.sasJs
.request(`services/admin/makedata`, data, overrideConfig, () => {
this.sasService.shouldLogin.next(true)
})
.then((res: any) => {
this.autodeployDone = true
try { try {
this.makeDataResponse = JSON.stringify(res) this.makeDataResponse = JSON.stringify(res)
} catch { } catch {
this.makeDataResponse = res this.makeDataResponse = res
} }
if (res.result && res.result.length > 0) { if (res.result && res.result.length > 0) {
this.autoDeployStatus.runMakeData = true this.autoDeployStatus.runMakeData = true
} else { } else {
this.autoDeployStatus.runMakeData = false
}
if (typeof res.sasjsAbort !== 'undefined') {
const abortRes = res
const abortMsg = abortRes.sasjsAbort[0].MSG
const macMsg = abortRes.sasjsAbort[0].MAC
this.eventService.showAbortModal('makedata', abortMsg, {
SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT,
SYSERRORTEXT: abortRes.SYSERRORTEXT,
MAC: macMsg
})
}
if (this.helperService.isStreamingViya())
this.updateIndexHtmlComputeContext()
})
.catch((err: any) => {
this.eventService.showAbortModal('makedata', JSON.stringify(err))
this.autoDeployStatus.runMakeData = false this.autoDeployStatus.runMakeData = false
} this.autodeployDone = true
if (typeof res.sasjsAbort !== 'undefined') { try {
const abortRes = res this.makeDataResponse = JSON.stringify(err)
const abortMsg = abortRes.sasjsAbort[0].MSG } catch {
const macMsg = abortRes.sasjsAbort[0].MAC this.makeDataResponse = err
}
})
}
}
this.eventService.showAbortModal('makedata', abortMsg, { public runMakedataInNewWindow(params: {
SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT, contextName: string
SYSERRORTEXT: abortRes.SYSERRORTEXT, admin: string
MAC: macMsg 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`
if (this.helperService.isStreamingViya()) let programUrl =
this.updateIndexHtmlComputeContext() serverUrl +
}) execPath +
.catch((err: any) => { '/?_program=' +
this.eventService.showAbortModal('makedata', JSON.stringify(err)) appLoc +
this.autoDeployStatus.runMakeData = false '/services/admin/makedata' +
this.autodeployDone = true contextname +
admin +
dcPath +
debug
try { window.open(programUrl)
this.makeDataResponse = JSON.stringify(err)
} catch {
this.makeDataResponse = err
}
})
} }
/** /**

View File

@@ -408,12 +408,11 @@
<div class="hot-wrapper clr-flex-1"> <div class="hot-wrapper clr-flex-1">
<hot-table <hot-table
#hotInstance #hotInstance
hotId="hotInstance"
id="hotTable" id="hotTable"
class="edit-hot" class="edit-hot"
className="htDark"
[class.hidden]="hotTable.hidden" [class.hidden]="hotTable.hidden"
[licenseKey]="hotTable.licenseKey" [data]="hotTable.data"
[settings]="hotTableSettings"
> >
</hot-table> </hot-table>
</div> </div>

View File

@@ -3,6 +3,7 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
OnDestroy,
OnInit, OnInit,
QueryList, QueryList,
ViewChild, ViewChild,
@@ -16,7 +17,7 @@ import { SasStoreService } from '../services/sas-store.service'
type AOA = any[][] type AOA = any[][]
import { HotTableRegisterer } from '@handsontable/angular' import { HotTableComponent } from '@handsontable/angular-wrapper'
import { UploadFile } from '@sasjs/adapter' import { UploadFile } from '@sasjs/adapter'
import { isSpecialMissing } from '@sasjs/utils/input/validators' import { isSpecialMissing } from '@sasjs/utils/input/validators'
import CellRange from 'handsontable/3rdparty/walkontable/src/cell/range' import CellRange from 'handsontable/3rdparty/walkontable/src/cell/range'
@@ -71,13 +72,13 @@ import { ParseResult } from '../models/ParseResult.interface'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class EditorComponent implements OnInit, AfterViewInit { export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChildren('uploadStater') @ViewChildren('uploadStater')
uploadStaterCompList: QueryList<UploadStaterComponent> = new QueryList() uploadStaterCompList: QueryList<UploadStaterComponent> = new QueryList()
@ViewChildren('queryFilter') @ViewChildren('queryFilter')
queryFilterCompList: QueryList<QueryComponent> = new QueryList() queryFilterCompList: QueryList<QueryComponent> = new QueryList()
@ViewChildren('hotInstance') @ViewChild(HotTableComponent, { static: false })
hotInstanceCompList: QueryList<Handsontable> = new QueryList() hotTableComponent!: HotTableComponent
@ViewChildren('fileUploadInput') @ViewChildren('fileUploadInput')
fileUploadInputCompList: QueryList<ElementRef> = new QueryList() fileUploadInputCompList: QueryList<ElementRef> = new QueryList()
@@ -119,13 +120,26 @@ export class EditorComponent implements OnInit, AfterViewInit {
public hotInstance!: Handsontable public hotInstance!: Handsontable
public dcValidator: DcValidator | undefined public dcValidator: DcValidator | undefined
public hotTableSettings: Handsontable.GridSettings = {}
private updateHotTableSettings(): void {
this.hotTableSettings = {
colHeaders: this.hotTable.colHeaders,
columns: this.hotTable.columns,
height: this.hotTable.height,
licenseKey: this.hotTable.licenseKey,
readOnly: this.hotTable.readOnly,
copyPaste: this.hotTable.copyPaste,
contextMenu: true
}
}
public hotTable: HotTableInterface = { public hotTable: HotTableInterface = {
data: [], data: [],
colHeaders: [], colHeaders: [],
hidden: true, hidden: true,
columns: [], columns: [],
height: '100%', height: 'calc(100vh - 160px)',
minSpareRows: 1,
licenseKey: undefined, licenseKey: undefined,
readOnly: true, readOnly: true,
copyPaste: { copyPaste: {
@@ -162,10 +176,30 @@ export class EditorComponent implements OnInit, AfterViewInit {
} }
}, },
row_above: { row_above: {
name: 'Insert Row above' name: 'Insert Row above',
callback: (
key: string,
selection: any[],
clickEvent: MouseEvent
) => {
const firstSelection = selection[0]
const targetRow = firstSelection.start.row
this.insertRowAtPosition(targetRow)
}
}, },
row_below: { row_below: {
name: 'Insert Row below' name: 'Insert Row below',
callback: (
key: string,
selection: any[],
clickEvent: MouseEvent
) => {
const firstSelection = selection[0]
const targetRow = firstSelection.start.row + 1
this.insertRowAtPosition(targetRow)
}
}, },
remove_row: { remove_row: {
name: 'Ignore row' name: 'Ignore row'
@@ -350,6 +384,9 @@ export class EditorComponent implements OnInit, AfterViewInit {
public licenceState = this.licenceService.licenceState public licenceState = this.licenceService.licenceState
private ariaObserver: MutationObserver | undefined
private ariaCheckInterval: any | undefined
constructor( constructor(
private licenceService: LicenceService, private licenceService: LicenceService,
private eventService: EventService, private eventService: EventService,
@@ -360,15 +397,12 @@ export class EditorComponent implements OnInit, AfterViewInit {
private route: ActivatedRoute, private route: ActivatedRoute,
private sasService: SasService, private sasService: SasService,
private cdf: ChangeDetectorRef, private cdf: ChangeDetectorRef,
private hotRegisterer: HotTableRegisterer,
private spreadsheetService: SpreadsheetService private spreadsheetService: SpreadsheetService
) { ) {
const lang = languages[window.navigator.language] const lang = languages[window.navigator.language]
if (lang) if (lang)
numbro.default.registerLanguage(languages[window.navigator.language]) numbro.default.registerLanguage(languages[window.navigator.language])
this.hotRegisterer = new HotTableRegisterer()
this.parseRestrictions() this.parseRestrictions()
this.setRestrictions() this.setRestrictions()
} }
@@ -896,6 +930,11 @@ export class EditorComponent implements OnInit, AfterViewInit {
} }
this.reSetCellValidationValues() this.reSetCellValidationValues()
// Fix ARIA accessibility issues after table edit
setTimeout(() => {
this.fixAriaAccessibility()
}, 100)
}, 0) }, 0)
} }
@@ -922,6 +961,9 @@ export class EditorComponent implements OnInit, AfterViewInit {
this.cellValidationSource = [] this.cellValidationSource = []
// Clear custom validation styling
this.clearDuplicateValidation()
const hot = this.hotInstance const hot = this.hotInstance
const columnSorting = hot.getPlugin('multiColumnSorting') const columnSorting = hot.getPlugin('multiColumnSorting')
const columnSortConfig = columnSorting.getSortConfig() const columnSortConfig = columnSorting.getSortConfig()
@@ -982,22 +1024,54 @@ export class EditorComponent implements OnInit, AfterViewInit {
setTimeout(() => { setTimeout(() => {
const hot = this.hotInstance const hot = this.hotInstance
const dsInsertIndex = this.dataSource.length // Create a new empty row object with proper structure
hot.alter('insert_row_below', dsInsertIndex, 1) 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) hot.updateSettings({ data: this.dataSource }, false)
// Select the newly added row
hot.selectCell(this.dataSource.length - 1, 0) hot.selectCell(this.dataSource.length - 1, 0)
hot.render() hot.render()
if (this.dataSource[dsInsertIndex]) {
this.dataSource[dsInsertIndex]['noLinkOption'] = true
}
this.addingNewRow = false this.addingNewRow = false
this.reSetCellValidationValues() this.reSetCellValidationValues()
}) })
} }
/**
* Creates a new empty row object with proper structure
*/
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() { public cancelSubmit() {
this.dataSource = this.helperService.deepClone(this.dataSourceBeforeSubmit) this.dataSource = this.helperService.deepClone(this.dataSourceBeforeSubmit)
this.dataSourceBeforeSubmit = [] this.dataSourceBeforeSubmit = []
@@ -1077,51 +1151,96 @@ export class EditorComponent implements OnInit, AfterViewInit {
} }
} }
public validatePrimaryKeys() { private clearDuplicateValidation() {
const hot = this.hotInstance const hot = this.hotInstance
const myTable = hot.getData() // Clear previous duplicate validation styling
this.pkFields = [] for (const rowIndex of this.duplicatePkIndexes) {
for (let index = 0; index < myTable.length; index++) { for (let col = 1; col <= this.readOnlyFields; col++) {
let pkRow = '' hot.removeCellMeta(rowIndex, col, 'valid')
for (let ind = 1; ind < this.readOnlyFields + 1; ind++) { hot.removeCellMeta(rowIndex, col, 'dupKey')
pkRow = pkRow + '|' + myTable[index][ind] // Remove our custom class from cell metadata
} const cellMeta = hot.getCellMeta(rowIndex, col)
this.pkFields.push(pkRow) if (cellMeta.className) {
} let cleanedClassName: string
if (Array.isArray(cellMeta.className)) {
const results = [] cleanedClassName = cellMeta.className
const rows = this.dataSource.length .filter((c) => c !== 'dc-invalid-cell')
.join(' ')
for (let j = 0; j < this.pkFields.length; j++) { } else {
for (let i = 0; i < this.pkFields.length; i++) { cleanedClassName = cellMeta.className
if (this.pkFields[j] === this.pkFields[i] && i !== j) { .replace('dc-invalid-cell', '')
results.push(i) .trim()
}
hot.setCellMeta(rowIndex, col, 'className', cleanedClassName)
} }
} }
} }
if (this.pkFields.length > rows) { this.duplicatePkIndexes = []
for (let n = rows; n < this.pkFields.length; n++) { hot.render()
for (let p = rows; p < this.pkFields.length; p++) { }
if (n < p && this.pkFields[n] === this.pkFields[p]) {
results.push(p) 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 k = 0; k < results.length; k++) {
for (let index = 1; index < this.readOnlyFields + 1; index++) { for (let index = 1; index < this.readOnlyFields + 1; index++) {
cellMeta = hot.getCellMeta(results[k], index) hot.setCellMeta(results[k], index, 'valid', false)
cellMeta.valid = false hot.setCellMeta(results[k], index, 'dupKey', true)
cellMeta.dupKey = true hot.setCellMeta(results[k], index, 'className', 'dc-invalid-cell')
hot.render()
} }
} }
this.duplicatePkIndexes = [...new Set(results.sort())] 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) this.dataSourceBeforeSubmit = this.helperService.deepClone(this.dataSource)
// Clean up the data source by removing noLinkOption property
for (let i = 0; i < this.dataSource.length; i++) { for (let i = 0; i < this.dataSource.length; i++) {
delete this.dataSource[i].noLinkOption delete this.dataSource[i].noLinkOption
} }
// Remove any completely empty rows from the end
while (this.dataSource.length > 0) {
const lastRow = this.dataSource[this.dataSource.length - 1]
const isEmpty = Object.keys(lastRow).every((key) => {
if (key === '_____DELETE__THIS__RECORD_____') return true
return !lastRow[key] || lastRow[key] === ''
})
if (isEmpty) {
this.dataSource.pop()
} else {
break
}
}
hot.updateSettings( hot.updateSettings(
{ {
data: this.dataSource, data: this.dataSource,
@@ -1437,17 +1572,6 @@ export class EditorComponent implements OnInit, AfterViewInit {
EditorComponent.cnt = 0 EditorComponent.cnt = 0
EditorComponent.nonPkCnt = 0 EditorComponent.nonPkCnt = 0
// this.saveLoading = true;
/**
* Below code should be analized, not sure what is the purpose of exceedCells
*/
const myTableData = hot.getData()
// If the last row is empty, remove it before validation
if (myTableData.length > 1 && hot.isEmptyRow(myTableData.length - 1)) {
hot.alter('remove_row', myTableData.length - 1)
}
this.validatePrimaryKeys() this.validatePrimaryKeys()
@@ -1477,15 +1601,6 @@ export class EditorComponent implements OnInit, AfterViewInit {
if (txt) txt.focus() if (txt) txt.focus()
}, 200) }, 200)
}) })
// let cnt = 0;
// hot.addHook("afterValidate", () => {
// this.updateSoftSelectColumns(true);
// cnt++;
// if (cnt === long) {
// this.validationDone = 1;
// }
// });
} }
public async saveTable(data: any) { public async saveTable(data: any) {
@@ -1639,11 +1754,20 @@ export class EditorComponent implements OnInit, AfterViewInit {
} }
public checkInvalid() { public checkInvalid() {
const hotElement = (this.hotInstanceCompList.first.container as any) // Use Angular wrapper to access Handsontable element instead of DOM queries
.nativeElement if (!this.hotTableComponent || !this.hotTableComponent.hotInstance)
const invalidCells = hotElement.querySelectorAll('.htInvalid') 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() { public goToEditor() {
@@ -2183,6 +2307,7 @@ export class EditorComponent implements OnInit, AfterViewInit {
*/ */
private setCellFilter(filter: boolean) { private setCellFilter(filter: boolean) {
const hotSelected = this.hotInstance.getSelected() const hotSelected = this.hotInstance.getSelected()
if (!hotSelected) return
const selection = hotSelected ? hotSelected[0] : hotSelected const selection = hotSelected ? hotSelected[0] : hotSelected
// When we open a dropdown we want filter disabled so value in cell // When we open a dropdown we want filter disabled so value in cell
@@ -2207,9 +2332,13 @@ export class EditorComponent implements OnInit, AfterViewInit {
} }
async ngOnInit() { async ngOnInit() {
// Initialize hot table settings
this.updateHotTableSettings()
this.licenceService.hot_license_key.subscribe( this.licenceService.hot_license_key.subscribe(
(hot_license_key: string | undefined) => { (hot_license_key: string | undefined) => {
this.hotTable.licenseKey = hot_license_key this.hotTable.licenseKey = hot_license_key
this.updateHotTableSettings() // Update settings when license key changes
} }
) )
@@ -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) { initSetup(response: EditorsGetDataServiceResponse) {
this.hotInstance = this.hotRegisterer.getInstance('hotInstance') this.hotInstance = this.hotTableComponent!.hotInstance!
if (this.getdataError) return if (this.getdataError) return
if (!response) return if (!response) return
if (!response.data) return if (!response.data) return
if (!this.hotInstance) return
this.cols = response.data.cols this.cols = response.data.cols
this.dsmeta = response.data.dsmeta this.dsmeta = response.data.dsmeta
@@ -2289,7 +2606,7 @@ export class EditorComponent implements OnInit, AfterViewInit {
this.dsNote = '' this.dsNote = ''
} }
const hot: Handsontable = this.hotInstance const hot = this.hotInstance
const approvers: Approver[] = response.data.approvers const approvers: Approver[] = response.data.approvers
@@ -2410,6 +2727,11 @@ export class EditorComponent implements OnInit, AfterViewInit {
rowHeights: 24, rowHeights: 24,
maxRows: this.licenceState.value.editor_rows_allowed || Infinity, maxRows: this.licenceState.value.editor_rows_allowed || Infinity,
invalidCellClassName: 'htInvalid', invalidCellClassName: 'htInvalid',
// Prevent automatic row creation
autoWrapRow: false,
autoWrapCol: false,
// Ensure proper data binding
bindRowsWithHeaders: false,
dropdownMenu: { dropdownMenu: {
items: { items: {
make_read_only: { make_read_only: {
@@ -2484,7 +2806,51 @@ export class EditorComponent implements OnInit, AfterViewInit {
cellProperties: Handsontable.CellProperties cellProperties: Handsontable.CellProperties
) => { ) => {
const isReadonlyCol = col && this.isReadonlyCol(col) const isReadonlyCol = col && this.isReadonlyCol(col)
if (isReadonlyCol) cellProperties.className = 'readonlyCell'
// Check if this cell should be marked as invalid due to duplicate primary key values
// Only applies to primary key columns (col 1 through readOnlyFields)
const isDuplicateCell =
this.duplicatePkIndexes.includes(row) &&
col >= 1 &&
col <= this.readOnlyFields
// Handle existing CSS classes - Handsontable can provide className as string or array
const existingClasses = cellProperties.className || ''
let classes: string[]
if (Array.isArray(existingClasses)) {
// If already an array, create a copy
classes = [...existingClasses]
} else {
// If string, split by spaces and filter out empty strings
classes = existingClasses
.split(' ')
.filter((c: string) => c.length > 0)
}
// Add readonlyCell class for readonly columns to maintain original styling
if (isReadonlyCol && !classes.includes('readonlyCell')) {
classes.push('readonlyCell')
}
// Apply custom validation styling for duplicate primary key cells
// Note: Uses 'dc-invalid-cell' instead of Handsontable's 'htInvalid' class
// because Handsontable's internal validation system was removing 'htInvalid'
// causing flickering. Our custom class persists reliably.
if (isDuplicateCell) {
if (!classes.includes('dc-invalid-cell')) {
classes.push('dc-invalid-cell')
}
// Mark cell as invalid to prevent form submission
cellProperties.valid = false
// Custom flag to identify this as a duplicate key cell for cleanup
cellProperties.dupKey = true
}
// Apply the combined CSS classes back to the cell
if (classes.length > 0) {
cellProperties.className = classes.join(' ')
}
} }
}, },
false false
@@ -2503,22 +2869,6 @@ export class EditorComponent implements OnInit, AfterViewInit {
this.columnHeader[0] = 'Delete?' this.columnHeader[0] = 'Delete?'
this.readOnlyFields = response.data.sasparams[0].PKCNT this.readOnlyFields = response.data.sasparams[0].PKCNT
const hotInstaceEl = document.getElementById('hotInstance')
if (hotInstaceEl) {
hotInstaceEl.addEventListener('mousedown', (event) => {
if (!this.uploadPreview) {
this.hotClicked()
}
setTimeout(() => {
const menuDebugItem: any =
document.querySelector('.debug-switch-item') || undefined
if (menuDebugItem) menuDebugItem.click()
}, 100)
})
}
hot.addHook( hot.addHook(
'afterSelection', 'afterSelection',
( (
@@ -2597,6 +2947,17 @@ export class EditorComponent implements OnInit, AfterViewInit {
hot.addHook('afterRender', (isForced: boolean) => { hot.addHook('afterRender', (isForced: boolean) => {
this.eventService.dispatchEvent('resize') this.eventService.dispatchEvent('resize')
// Fix ARIA accessibility issues after each render
this.fixAriaAccessibility()
})
// Add a more frequent accessibility fix hook
hot.addHook('afterChange', () => {
// Fix ARIA accessibility issues after any data change
setTimeout(() => {
this.fixAriaAccessibility()
}, 50)
}) })
hot.addHook('afterCreateRow', (source: any, change: any) => { hot.addHook('afterCreateRow', (source: any, change: any) => {
@@ -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) => { hot.addHook('beforePaste', (data: any, cords: any) => {
const startCol = cords[0].startCol const startCol = cords[0].startCol
@@ -2660,5 +3036,10 @@ export class EditorComponent implements OnInit, AfterViewInit {
} }
hot.render() hot.render()
// Fix ARIA accessibility issues after table initialization
setTimeout(() => {
this.fixAriaAccessibility()
}, 500)
} }
} }

View File

@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { ClarityModule } from '@clr/angular' import { ClarityModule } from '@clr/angular'
import { HotTableModule } from '@handsontable/angular' import { HotTableModule } from '@handsontable/angular-wrapper'
import { registerAllModules } from 'handsontable/registry' import { registerAllModules } from 'handsontable/registry'
import { AppSharedModule } from '../app-shared.module' import { AppSharedModule } from '../app-shared.module'
import { DirectivesModule } from '../directives/directives.module' import { DirectivesModule } from '../directives/directives.module'
@@ -28,7 +28,7 @@ registerAllModules()
FormsModule, FormsModule,
EditorRoutingModule, EditorRoutingModule,
ClarityModule, ClarityModule,
HotTableModule.forRoot(), HotTableModule,
AppSharedModule, AppSharedModule,
DirectivesModule, DirectivesModule,
SharedModule, SharedModule,

View File

@@ -99,6 +99,11 @@ export class MetadataComponent implements OnInit {
} }
this.pageSize = 5 this.pageSize = 5
// Initialize filters for accessibility
this.typeFilter = new TypeFilter()
this.nameFilter = new NameFilter()
this.valueFilter = new ValueFilter()
if ( if (
globals.metadata.metaDataList && globals.metadata.metaDataList &&
globals.metadata.metaRepositories && globals.metadata.metaRepositories &&

View File

@@ -8,4 +8,5 @@ export interface Libinfo {
LIBID: string LIBID: string
LIBSIZE: number LIBSIZE: number
TABLE_CNT: number TABLE_CNT: number
CATALOG_CNT: number
} }

View File

@@ -1,30 +1,14 @@
import { BaseSASResponse } from './common/BaseSASResponse' import { BaseSASResponse } from './common/BaseSASResponse'
export interface EditorsStageDataSASResponse extends BaseSASResponse { export interface EditorsStageDataSASResponse extends BaseSASResponse {
SYSDATE: string
SYSTIME: string
sasparams: Sasparam[] sasparams: Sasparam[]
_DEBUG: string
_PROGRAM: string
AUTOEXEC: string AUTOEXEC: string
MF_GETUSER: string
SYSCC: string
SYSENCODING: string SYSENCODING: string
SYSERRORTEXT: string
SYSHOSTINFOLONG: string SYSHOSTINFOLONG: string
SYSHOSTNAME: string
SYSPROCESSID: string SYSPROCESSID: string
SYSPROCESSMODE: string SYSPROCESSMODE: string
SYSPROCESSNAME: string SYSPROCESSNAME: string
SYSJOBID: string
SYSSCPL: string
SYSSITE: string
SYSTCPIPHOSTNAME: string SYSTCPIPHOSTNAME: string
SYSUSERID: string
SYSVLONG: string
SYSWARNINGTEXT: string
END_DTTM: string
MEMSIZE: string
} }
export interface Sasparam { export interface Sasparam {

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

View File

@@ -166,13 +166,10 @@
> >
<hot-table <hot-table
hotId="hotInstanceUserDataset" #hotInstanceUserDataset
id="hotTableUserDataset" id="hotTableUserDataset"
class="mt-15" class="mt-15"
[afterGetColHeader]="afterGetColHeader" [settings]="hotUserDatasetsSettings"
[settings]="hotUserDatasets"
[licenseKey]="hotTableLicenseKey"
stretchH="all"
> >
</hot-table> </hot-table>
@@ -360,17 +357,10 @@
</div> </div>
<hot-table <hot-table
hotId="hotInstance" #hotInstanceMain
id="hotTable" id="hotTable"
class="mt-15" class="mt-15"
[afterGetColHeader]="afterGetColHeader" [settings]="hotMainTableSettings"
[className]="['htDark', 'htCustomHidden']"
[licenseKey]="hotTableLicenseKey"
[multiColumnSorting]="true"
[viewportRowRenderingOffset]="50"
[manualColumnResize]="true"
[filters]="true"
stretchH="all"
> >
</hot-table> </hot-table>
</ng-container> </ng-container>

View File

@@ -4,6 +4,7 @@ import {
ElementRef, ElementRef,
HostBinding, HostBinding,
OnInit, OnInit,
AfterViewInit,
ViewChild, ViewChild,
ViewEncapsulation ViewEncapsulation
} from '@angular/core' } from '@angular/core'
@@ -22,7 +23,7 @@ import { HotTableInterface } from '../models/HotTable.interface'
import { Col } from '../shared/dc-validator/models/col.model' import { Col } from '../shared/dc-validator/models/col.model'
import { SpreadsheetService } from '../services/spreadsheet.service' import { SpreadsheetService } from '../services/spreadsheet.service'
import Handsontable from 'handsontable' import Handsontable from 'handsontable'
import { HotTableRegisterer } from '@handsontable/angular' import { HotTableComponent } from '@handsontable/angular-wrapper'
import { EditorsStageDataSASResponse } from '../models/sas/editors-stagedata.model' import { EditorsStageDataSASResponse } from '../models/sas/editors-stagedata.model'
import { CellChange, ChangeSource } from 'handsontable/common' import { CellChange, ChangeSource } from 'handsontable/common'
import { baseAfterGetColHeader } from '../shared/utils/hot.utils' import { baseAfterGetColHeader } from '../shared/utils/hot.utils'
@@ -49,7 +50,7 @@ enum FileLoadingState {
styleUrls: ['./multi-dataset.component.scss'], styleUrls: ['./multi-dataset.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class MultiDatasetComponent implements OnInit { export class MultiDatasetComponent implements OnInit, AfterViewInit {
@HostBinding('class.content-container') contentContainerClass = true @HostBinding('class.content-container') contentContainerClass = true
@ViewChild('contentArea', { static: true }) contentAreaRef!: ElementRef @ViewChild('contentArea', { static: true }) contentAreaRef!: ElementRef
@@ -89,7 +90,13 @@ export class MultiDatasetComponent implements OnInit {
public hotInstance!: Handsontable public hotInstance!: Handsontable
public hotInstanceUserDataset!: 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 showSubmitReasonModal: boolean = false
public submitReasonMessage: string = '' public submitReasonMessage: string = ''
@@ -136,7 +143,36 @@ export class MultiDatasetComponent implements OnInit {
} }
}, },
manualRowMove: true, manualRowMove: true,
columnSorting: true columnSorting: true,
afterGetColHeader: baseAfterGetColHeader,
stretchH: 'all'
}
private initializeHotSettings() {
this.hotMainTableSettings = {
className: ['htDark'],
licenseKey: this.hotTableLicenseKey,
multiColumnSorting: true,
viewportRowRenderingOffset: 50,
manualColumnResize: true,
autoColumnSize: true,
filters: true,
stretchH: 'all',
afterGetColHeader: baseAfterGetColHeader,
modifyColWidth: this.maxWidthCheker
}
// Exclude data from settings for HOT v16 - it will be loaded manually
const { data, ...settingsWithoutData } = this.hotUserDatasets
this.hotUserDatasetsSettings = {
...settingsWithoutData,
licenseKey: this.hotTableLicenseKey
}
}
public maxWidthCheker(width: any, col: any) {
if (width > 200) return 200
else return width
} }
public afterGetColHeader = baseAfterGetColHeader public afterGetColHeader = baseAfterGetColHeader
@@ -149,16 +185,28 @@ export class MultiDatasetComponent implements OnInit {
private spreadsheetService: SpreadsheetService, private spreadsheetService: SpreadsheetService,
private sasService: SasService, private sasService: SasService,
private cdr: ChangeDetectorRef private cdr: ChangeDetectorRef
) { ) {}
this.hotRegisterer = new HotTableRegisterer()
}
ngOnInit() { ngOnInit() {
this.licenceService.hot_license_key.subscribe( this.licenceService.hot_license_key.subscribe(
(hot_license_key: string | undefined) => { (hot_license_key: string | undefined) => {
this.hotTableLicenseKey = hot_license_key this.hotTableLicenseKey = hot_license_key
this.initializeHotSettings()
} }
) )
this.initializeHotSettings()
}
ngAfterViewInit() {
// Ensure HOT instances are properly initialized after view is ready
setTimeout(() => {
if (this.hotTableUserDatasetComponent && !this.hotInstanceUserDataset) {
this.initUserInputHot()
}
if (this.hotTableMainComponent && !this.hotInstance) {
this.initHot()
}
}, 50)
} }
ngAfterContentInit(): void { ngAfterContentInit(): void {
@@ -233,7 +281,10 @@ export class MultiDatasetComponent implements OnInit {
} }
this.initUserInputHot() this.initUserInputHot()
this.onAutoDetectColumns() // Call onAutoDetectColumns after HOT is initialized
setTimeout(() => {
this.onAutoDetectColumns()
}, 100)
} else if (matchedExtension === 'csv') { } else if (matchedExtension === 'csv') {
this.onMultiCsvFiles(event.target.files) this.onMultiCsvFiles(event.target.files)
} else { } else {
@@ -392,84 +443,112 @@ export class MultiDatasetComponent implements OnInit {
initHot() { initHot() {
setTimeout(() => { 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 // Set height of parsed data to full height of the page content area
const contentAreaHeight = this.contentAreaRef.nativeElement.clientHeight const contentAreaHeight = this.contentAreaRef.nativeElement.clientHeight
const hotHeight = `${contentAreaHeight - 160}px` const hotHeight = `${contentAreaHeight - 160}px`
if (this.activeParsedDataset) { if (this.activeParsedDataset) {
this.hotInstance.updateSettings({ // Update settings without data - data will be loaded manually
data: this.activeParsedDataset.datasource || [], this.hotInstance.updateSettings({
colHeaders: this.activeParsedDataset.datasetInfo.headerColumns, colHeaders: this.activeParsedDataset.datasetInfo.headerColumns,
columns: this.activeParsedDataset.datasetInfo.dcValidator?.getRules(), columns:
readOnly: true, this.activeParsedDataset.datasetInfo.dcValidator?.getRules(),
height: hotHeight || '300px', readOnly: true,
className: 'htDark' 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() { initUserInputHot() {
setTimeout(() => { setTimeout(() => {
this.hotInstanceUserDataset = this.hotRegisterer.getInstance( if (this.hotTableUserDatasetComponent?.hotInstance) {
'hotInstanceUserDataset' this.hotInstanceUserDataset =
) this.hotTableUserDatasetComponent.hotInstance
this.hotInstanceUserDataset.addHook( // Load initial data manually after instance is ready
'beforeChange', setTimeout(() => {
(changes: (CellChange | null)[], source: ChangeSource) => { if (this.hotUserDatasets.data) {
if (changes) { this.hotInstanceUserDataset.loadData(this.hotUserDatasets.data)
for (let change of changes) { this.hotInstanceUserDataset.render()
if (change && change[3]) { }
change[3] = change[3].toUpperCase() }, 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( this.hotInstanceUserDataset.addHook(
'afterChange', 'afterChange',
async (changes: CellChange[] | null, source: ChangeSource) => { async (changes: CellChange[] | null, source: ChangeSource) => {
if (changes) { if (changes) {
if (source === 'edit') { if (source === 'edit') {
await this.onUserInputDatasetsChange() await this.onUserInputDatasetsChange()
}
for (let change of changes) {
const row = change[0] as number
this.markUnmatchedRows(row)
}
this.dynamicCellValidations()
this.hotInstanceUserDataset.render()
} }
}
)
for (let change of changes) { this.hotInstanceUserDataset.addHook(
const row = change[0] as number 'afterRemoveRow',
async (
index: number,
amount: number,
physicalRows: number[],
source?: Handsontable.ChangeSource | undefined
) => {
await this.onUserInputDatasetsChange()
for (let row of physicalRows) {
this.markUnmatchedRows(row) this.markUnmatchedRows(row)
} }
this.dynamicCellValidations()
this.hotInstanceUserDataset.render()
} }
} )
) }
}, 100)
this.hotInstanceUserDataset.addHook(
'afterRemoveRow',
async (
index: number,
amount: number,
physicalRows: number[],
source?: Handsontable.ChangeSource | undefined
) => {
await this.onUserInputDatasetsChange()
for (let row of physicalRows) {
this.markUnmatchedRows(row)
}
}
)
})
} }
dynamicCellValidations() { dynamicCellValidations() {
if (!this.hotInstanceUserDataset) return
const hotData = this.hotInstanceUserDataset.getData() const hotData = this.hotInstanceUserDataset.getData()
hotData.forEach((row, rowIndex) => { hotData.forEach((row, rowIndex) => {
@@ -483,6 +562,8 @@ export class MultiDatasetComponent implements OnInit {
} }
markUnmatchedRows(row: number) { markUnmatchedRows(row: number) {
if (!this.hotInstanceUserDataset) return
const dataAtRow = this.hotInstanceUserDataset.getDataAtRow(row) as number[] const dataAtRow = this.hotInstanceUserDataset.getDataAtRow(row) as number[]
const dataset = `${dataAtRow[0]}.${dataAtRow[1]}` const dataset = `${dataAtRow[0]}.${dataAtRow[1]}`
const cellMetaAtRow = this.hotInstanceUserDataset.getCellMetaAtRow(row) const cellMetaAtRow = this.hotInstanceUserDataset.getCellMetaAtRow(row)
@@ -556,6 +637,20 @@ export class MultiDatasetComponent implements OnInit {
* convention. {@link isValidDatasetFormat} * convention. {@link isValidDatasetFormat}
*/ */
async onAutoDetectColumns() { async onAutoDetectColumns() {
// Wait for hotInstanceUserDataset to be ready
if (!this.hotInstanceUserDataset) {
let attempts = 0
const maxAttempts = 20
while (!this.hotInstanceUserDataset && attempts < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, 100))
attempts++
}
if (!this.hotInstanceUserDataset) {
console.warn('hotInstanceUserDataset not ready after waiting')
return
}
}
let passwordError = false let passwordError = false
await this.parseExcelSheetNames() await this.parseExcelSheetNames()
@@ -616,7 +711,13 @@ export class MultiDatasetComponent implements OnInit {
} }
} }
this.hotInstanceUserDataset.updateData(hotReadyData) if (this.hotInstanceUserDataset) {
// Load data manually - this is required for HOT v16 Angular wrapper
setTimeout(() => {
this.hotInstanceUserDataset.loadData(hotReadyData)
this.hotInstanceUserDataset.render()
}, 100)
}
this.dynamicCellValidations() this.dynamicCellValidations()
} }

View File

@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { ClarityModule } from '@clr/angular' import { ClarityModule } from '@clr/angular'
import { HotTableModule } from '@handsontable/angular' import { HotTableModule } from '@handsontable/angular-wrapper'
import { registerAllModules } from 'handsontable/registry' import { registerAllModules } from 'handsontable/registry'
import { AppSharedModule } from '../app-shared.module' import { AppSharedModule } from '../app-shared.module'
import { DirectivesModule } from '../directives/directives.module' import { DirectivesModule } from '../directives/directives.module'

View File

@@ -13,6 +13,8 @@ import {
AuditorsPostdataSASResponse, AuditorsPostdataSASResponse,
Param Param
} from '../../models/sas/auditors-postdata.model' } from '../../models/sas/auditors-postdata.model'
import { PublicGetChangeinfo } from 'src/app/models/sas/public-getchangeinfo.model'
import { SasService } from 'src/app/services'
interface ChangesObj { interface ChangesObj {
ind: any ind: any
@@ -76,9 +78,11 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
public diffsLimit: boolean = false public diffsLimit: boolean = false
public recordsLimit: number = 100 public recordsLimit: number = 100
public refreshStartupserviceAfterApprove: boolean = false
constructor( constructor(
private sasStoreService: SasStoreService, private sasStoreService: SasStoreService,
private sasService: SasService,
private eventService: EventService, private eventService: EventService,
private router: ActivatedRoute, private router: ActivatedRoute,
private route: Router private route: Router
@@ -162,6 +166,9 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
await this.sasStoreService await this.sasStoreService
.approveTable(approveParams, 'SASControlTable', 'auditors/postdata') .approveTable(approveParams, 'SASControlTable', 'auditors/postdata')
.then((res: any) => { .then((res: any) => {
// If we are approving MPE_TABLES we will arm the trigger for the reload of startup data to se the updated tables
if (this.refreshStartupserviceAfterApprove)
this.sasService.reloadStartupData()
this.route.navigateByUrl('/review/history') this.route.navigateByUrl('/review/history')
}) })
.catch((err: any) => { .catch((err: any) => {
@@ -176,7 +183,7 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
public async callChangesInfo(tableId: any) { public async callChangesInfo(tableId: any) {
await this.sasStoreService await this.sasStoreService
.getChangeInfo(tableId) .getChangeInfo(tableId)
.then((res: any) => { .then((res: PublicGetChangeinfo) => {
this.tableDetails = res.jsparams[0] this.tableDetails = res.jsparams[0]
this.jsParams = res.jsparams[0] this.jsParams = res.jsparams[0]
@@ -189,6 +196,11 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
} }
this.keysArray = keysArray this.keysArray = keysArray
// If we are approving MPE_TABLES we will arm the trigger for the reload of startup data to se the updated tables
// After user approved if armed, reload will be triggered
if (res.jsparams[0].BASE_DS === 'MPE_TABLES')
this.refreshStartupserviceAfterApprove = true
}) })
.catch((err: any) => { .catch((err: any) => {
this.acceptLoading = false this.acceptLoading = false

View File

@@ -33,12 +33,34 @@
<div class="clr-col-md-12" ng-if="loaded"> <div class="clr-col-md-12" ng-if="loaded">
<div *ngIf="approveList && remained !== 0"> <div *ngIf="approveList && remained !== 0">
<clr-datagrid class="datagrid-compact datagrid-custom-footer"> <clr-datagrid class="datagrid-compact datagrid-custom-footer">
<clr-dg-column [clrDgField]="'submitter'">SUBMITTER</clr-dg-column> <clr-dg-column [clrDgField]="'submitter'">
<clr-dg-column [clrDgField]="'baseTable'">BASE TABLE</clr-dg-column> SUBMITTER
<clr-dg-column [clrDgField]="'submitted'">SUBMITTED</clr-dg-column> <clr-dg-string-filter
<clr-dg-column [clrDgField]="'submitReason'" [clrDgStringFilter]="submitterFilter"
>SUBMIT REASON</clr-dg-column 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>ACTION</clr-dg-column>
<clr-dg-column>DOWNLOAD</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>{{ approveItem.submitReason }}</clr-dg-cell>
<clr-dg-cell> <clr-dg-cell>
<div <div
class="clr-row" class="clr-row d-flex justify-content-around"
role="tooltip" role="toolbar"
class="d-flex justify-content-around" aria-label="Table actions"
> >
<a <a
class="column-center links tooltip tooltip-md tooltip-bottom-left color-green" class="column-center links tooltip tooltip-md tooltip-bottom-left color-green"
(click)="getClicked(i)" (click)="getClicked(i)"
> >
<clr-icon 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> <span class="tooltip-content">Go to review page screen</span>
</a> </a>
<a <a
@@ -70,10 +96,12 @@
*ngIf="!approveItem.rejectLoading" *ngIf="!approveItem.rejectLoading"
shape="ban" shape="ban"
size="22" size="22"
aria-hidden="true"
></clr-icon> ></clr-icon>
<clr-spinner <clr-spinner
*ngIf="approveItem.rejectLoading" *ngIf="approveItem.rejectLoading"
[clrSmall]="true" [clrSmall]="true"
aria-hidden="true"
></clr-spinner> ></clr-spinner>
<span class="tooltip-content">Reject</span> <span class="tooltip-content">Reject</span>
</a> </a>
@@ -81,7 +109,11 @@
class="column-center links tooltip tooltip-md tooltip-bottom-left color-blue" class="column-center links tooltip tooltip-md tooltip-bottom-left color-blue"
(click)="getTable(approveItem.tableId)" (click)="getTable(approveItem.tableId)"
> >
<clr-icon 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> <span class="tooltip-content">Go to staged data screen</span>
</a> </a>
</div> </div>

View File

@@ -8,6 +8,7 @@ import { SasStoreService } from '../../services/sas-store.service'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { SasService } from '../../services/sas.service' import { SasService } from '../../services/sas.service'
import { EventService } from '../../services/event.service' import { EventService } from '../../services/event.service'
import { ClrDatagridStringFilterInterface } from '@clr/angular'
interface ApproveData { interface ApproveData {
tableId: string tableId: string
@@ -19,6 +20,32 @@ interface ApproveData {
rejectLoading?: boolean rejectLoading?: boolean
} }
class SubmitterFilter implements ClrDatagridStringFilterInterface<ApproveData> {
accepts(data: ApproveData, search: string): boolean {
return data.submitter.toLowerCase().indexOf(search.toLowerCase()) >= 0
}
}
class BaseTableFilter implements ClrDatagridStringFilterInterface<ApproveData> {
accepts(data: ApproveData, search: string): boolean {
return data.baseTable.toLowerCase().indexOf(search.toLowerCase()) >= 0
}
}
class SubmittedFilter implements ClrDatagridStringFilterInterface<ApproveData> {
accepts(data: ApproveData, search: string): boolean {
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
}
}
class SubmitReasonFilter
implements ClrDatagridStringFilterInterface<ApproveData>
{
accepts(data: ApproveData, search: string): boolean {
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
}
}
@Component({ @Component({
selector: 'app-approve', selector: 'app-approve',
templateUrl: './approve.component.html', templateUrl: './approve.component.html',
@@ -35,6 +62,12 @@ export class ApproveComponent implements OnInit {
public loaded: boolean = false public loaded: boolean = false
public itemsNum: number = 10 public itemsNum: number = 10
// Filter instances for datagrid accessibility
public submitterFilter = new SubmitterFilter()
public baseTableFilter = new BaseTableFilter()
public submittedFilter = new SubmittedFilter()
public submitReasonFilter = new SubmitReasonFilter()
constructor( constructor(
private sasStoreService: SasStoreService, private sasStoreService: SasStoreService,
private eventService: EventService, private eventService: EventService,

View File

@@ -85,16 +85,48 @@
class="datagrid-history datagrid-custom-footer" class="datagrid-history datagrid-custom-footer"
*ngIf="loaded" *ngIf="loaded"
> >
<clr-dg-column [clrDgField]="'basetable'">BASE_TABLE</clr-dg-column> <clr-dg-column [clrDgField]="'basetable'">
<clr-dg-column [clrDgField]="'status'">STATUS</clr-dg-column> BASE_TABLE
<clr-dg-column [clrDgField]="'submitter'">SUBMITTER</clr-dg-column> <clr-dg-string-filter
<clr-dg-column [clrDgField]="'submittedReason'" [clrDgStringFilter]="baseTableFilter"
>SUBMIT REASON</clr-dg-column aria-label="Filter base table"
> ></clr-dg-string-filter>
<clr-dg-column [clrDgField]="'submitted'">SUBMITTED</clr-dg-column> </clr-dg-column>
<clr-dg-column [clrDgField]="'reviewed'" <clr-dg-column [clrDgField]="'status'">
>APPROVED / REJECTED</clr-dg-column 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-column>DOWNLOAD</clr-dg-column>
<clr-dg-row <clr-dg-row

View File

@@ -8,6 +8,55 @@ import {
EventService, EventService,
SasService SasService
} from 'src/app/services' } 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({ @Component({
selector: 'app-history', selector: 'app-history',
@@ -29,6 +78,14 @@ export class HistoryComponent implements OnInit {
public approveData: any = {} public approveData: any = {}
public sasjsConfig: SASjsConfig = new SASjsConfig() 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 } = { public histParams: { HIST: number; STARTROW: number; NOBS: number } = {
HIST: 0, HIST: 0,
STARTROW: 1, STARTROW: 1,

View File

@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { ClarityModule } from '@clr/angular' import { ClarityModule } from '@clr/angular'
import { HotTableModule } from '@handsontable/angular' import { HotTableModule } from '@handsontable/angular-wrapper'
import { DirectivesModule } from '../directives/directives.module' import { DirectivesModule } from '../directives/directives.module'
import { SharedModule } from '../shared/shared.module' import { SharedModule } from '../shared/shared.module'
import { ApproveDetailsComponent } from './approve-details/approve-details.component' import { ApproveDetailsComponent } from './approve-details/approve-details.component'
@@ -23,7 +23,7 @@ import { HistoryComponent } from './history/history.component'
FormsModule, FormsModule,
ReviewRoutingModule, ReviewRoutingModule,
ClarityModule, ClarityModule,
HotTableModule.forRoot(), HotTableModule,
DirectivesModule, DirectivesModule,
SharedModule SharedModule
] ]

View File

@@ -44,10 +44,20 @@
<div *ngIf="submitterList && remained !== 0"> <div *ngIf="submitterList && remained !== 0">
<clr-datagrid class="datagrid-compact datagrid-custom-footer"> <clr-datagrid class="datagrid-compact datagrid-custom-footer">
<clr-dg-column>BASE TABLE</clr-dg-column> <clr-dg-column>BASE TABLE</clr-dg-column>
<clr-dg-column [clrDgField]="'submitted'">SUBMITTED</clr-dg-column> <clr-dg-column [clrDgField]="'submitted'">
<clr-dg-column [clrDgField]="'submitReason'" SUBMITTED
>SUBMIT REASON</clr-dg-column <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" <clr-dg-column class="d-flex justify-content-center"
>ACTION</clr-dg-column >ACTION</clr-dg-column
> >

View File

@@ -7,6 +7,7 @@ import {
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { SasStoreService, EventService, SasService } from '../../services' import { SasStoreService, EventService, SasService } from '../../services'
import { ClrDatagridStringFilterInterface } from '@clr/angular'
interface SubmitterData { interface SubmitterData {
tableId: string tableId: string
@@ -16,6 +17,22 @@ interface SubmitterData {
approver: string 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({ @Component({
selector: 'app-submitter', selector: 'app-submitter',
templateUrl: './submitter.component.html', templateUrl: './submitter.component.html',
@@ -37,6 +54,10 @@ export class SubmitterComponent implements OnInit, AfterViewInit {
private _readySub!: Subscription private _readySub!: Subscription
private _backToSub!: Subscription private _backToSub!: Subscription
// Filter instances for datagrid accessibility
public submittedFilter = new SubmittedFilter()
public submitReasonFilter = new SubmitReasonFilter()
constructor( constructor(
private sasStoreService: SasStoreService, private sasStoreService: SasStoreService,
private eventService: EventService, private eventService: EventService,

View File

@@ -333,6 +333,10 @@ export class SasService {
}) })
} }
public reloadStartupData() {
this.loadStartupServiceEmitter.emit()
}
public getLicenseSiteId(): string[] { public getLicenseSiteId(): string[] {
return this.license_site_id.value || [] return this.license_site_id.value || []
} }
@@ -361,13 +365,18 @@ export class SasService {
} }
}, },
(err: any) => { (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.next(true)
this.shouldLogin.subscribe((res: boolean) => { this.shouldLogin.subscribe((res: boolean) => {
if (res === false) location.reload() if (res === false) location.reload()
}) })
} else if (err.error.includes(`Folder doesn't exist.`)) { } else if (errorMessage.includes(`Folder doesn't exist.`)) {
console.warn( console.warn(
'SASjs SAS services are not present on the current appLoc.' 'SASjs SAS services are not present on the current appLoc.'
) )
@@ -415,7 +424,11 @@ export class SasService {
} }
}, },
(err: any) => { (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() reject()
} }
} }

View File

@@ -386,27 +386,9 @@
</div> </div>
<hot-table <hot-table
*ngIf="viewboxTableIndex > -1" *ngIf="viewboxTableIndex > -1 && viewboxHotSettings.get(viewbox.id)"
[hotId]="'hotInstance_viewbox_' + viewbox.id" [settings]="viewboxHotSettings.get(viewbox.id) || {}"
id="hotTable" [id]="'hotTable_' + viewbox.id"
className="htDark"
[readOnly]="true"
[modifyColWidth]="maxWidthCheker"
[copyPaste]="viewboxTables[viewboxTableIndex].hotTable.copyPaste"
[contextMenu]="viewboxTables[viewboxTableIndex].hotTable.contextMenu"
[multiColumnSorting]="true"
[viewportRowRenderingOffset]="50"
[data]="viewboxTables[viewboxTableIndex].hotTable.data"
[colHeaders]="viewboxTables[viewboxTableIndex].hotTable.colHeaders"
[columns]="viewboxTables[viewboxTableIndex].hotTable.columns"
[filters]="true"
[dropdownMenu]="viewboxTables[viewboxTableIndex].hotTable.dropdownMenu"
[height]="viewboxTables[viewboxTableIndex].hotTable.height"
stretchH="all"
[cells]="viewboxTables[viewboxTableIndex].hotTable.cells"
[maxRows]="viewboxTables[viewboxTableIndex].hotTable.maxRows"
[manualColumnResize]="true"
[licenseKey]="viewboxTables[viewboxTableIndex].hotTable.licenseKey"
></hot-table> ></hot-table>
</div> </div>
</div> </div>

View File

@@ -21,9 +21,9 @@ import {
ViewEncapsulation ViewEncapsulation
} from '@angular/core' } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { HotTableRegisterer } from '@handsontable/angular'
import { SASjsConfig } from '@sasjs/adapter' import { SASjsConfig } from '@sasjs/adapter'
import Handsontable from 'handsontable' import Handsontable from 'handsontable'
import { HotTableComponent } from '@handsontable/angular-wrapper'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { FilterQuery, FilterGroup } from 'src/app/models/FilterQuery' import { FilterQuery, FilterGroup } from 'src/app/models/FilterQuery'
@@ -54,6 +54,8 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChildren('resizeBox') resizeBoxQuery!: QueryList<ElementRef> //make query list, handle multiple @ViewChildren('resizeBox') resizeBoxQuery!: QueryList<ElementRef> //make query list, handle multiple
@ViewChildren('dragHandleCorner') @ViewChildren('dragHandleCorner')
dragHandleCornerQuery!: QueryList<ElementRef> dragHandleCornerQuery!: QueryList<ElementRef>
@ViewChildren(HotTableComponent)
hotTableComponents!: QueryList<HotTableComponent>
private _viewboxModal: boolean = false private _viewboxModal: boolean = false
get viewboxModal(): boolean { get viewboxModal(): boolean {
@@ -111,7 +113,7 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
}, },
columns: [], columns: [],
cols: [], cols: [],
height: '100%', height: 200, //WORKAROUND: Changed from '100%' to fixed pixel value because otherwize hot does not load
settings: {}, settings: {},
hiddenColumns: true, hiddenColumns: true,
manualColumnMove: false, manualColumnMove: false,
@@ -119,8 +121,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
licenseKey: undefined, licenseKey: undefined,
dropdownMenu: undefined dropdownMenu: undefined
} }
public viewboxHotSettings: Map<number, Handsontable.GridSettings> = new Map()
public viewboxTables: ViewboxTable[] = [] public viewboxTables: ViewboxTable[] = []
private hotTableRegisterer: HotTableRegisterer
public filteringViewbox: Viewbox | undefined public filteringViewbox: Viewbox | undefined
@@ -150,9 +153,7 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
private router: Router, private router: Router,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private cdf: ChangeDetectorRef private cdf: ChangeDetectorRef
) { ) {}
this.hotTableRegisterer = new HotTableRegisterer()
}
ngOnInit(): void { ngOnInit(): void {
// Load libraries // Load libraries
@@ -207,7 +208,17 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
//set handles for box resize // Set handles for box resize and ensure HOT components are properly initialized
setTimeout(() => {
this.setAllHandleTransform()
// Force refresh of any existing HOT instances after view init
this.viewboxes.forEach((viewbox) => {
if (this.getViewboxTableIndex(viewbox) > -1) {
this.refreshTableAfterResize(viewbox)
}
})
}, 1000)
} }
// Maximum number of open viewboxes reached // Maximum number of open viewboxes reached
@@ -304,6 +315,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
if (viewboxTable) { if (viewboxTable) {
viewboxTable.hotTable.data = res.viewdata viewboxTable.hotTable.data = res.viewdata
// Update settings with new data
this.createViewboxTableSettings(viewbox)
resolve(null) resolve(null)
} else { } else {
resolve(null) resolve(null)
@@ -413,6 +427,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
viewbox.query = this.helperService.deepClone(res.query) viewbox.query = this.helperService.deepClone(res.query)
viewbox.filterText = res.sasparams[0].FILTER_TEXT viewbox.filterText = res.sasparams[0].FILTER_TEXT
// Create settings for this viewbox
this.createViewboxTableSettings(viewbox)
setTimeout(() => { setTimeout(() => {
this.updateHotColumns( this.updateHotColumns(
viewboxTable!.hotTable.colHeadersHidden || [], viewboxTable!.hotTable.colHeadersHidden || [],
@@ -421,30 +438,34 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
// HOT Settings are bound in HTML but some settings due to timing issues // HOT Settings are bound in HTML but some settings due to timing issues
// requires to be updated after the HOT is instanced // requires to be updated after the HOT is instanced
// after the update `render` method is called // Use a longer timeout to ensure the HOT component is fully initialized
const hotInstance = this.getViewboxHotInstance(viewbox.id) setTimeout(() => {
const hotInstance = this.getViewboxHotInstance(viewbox.id)
hotInstance?.updateSettings({ if (hotInstance) {
manualColumnMove: viewboxTable!.hotTable.manualColumnMove, hotInstance.updateSettings({
afterGetColHeader: (col: number, th: any) => { manualColumnMove: viewboxTable!.hotTable.manualColumnMove,
const column = hotInstance?.colToProp(col) as string afterGetColHeader: (col: number, th: any) => {
const column = hotInstance?.colToProp(col) as string
// header columns styling - primary keys // header columns styling - primary keys
const isPKCol = const isPKCol =
column && column &&
viewboxTable!.hotTable.headerPks.indexOf(column) > -1 viewboxTable!.hotTable.headerPks.indexOf(column) > -1
if (isPKCol) th.classList.add('primaryKeyHeaderStyle') if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
// Dark mode // Dark mode
th.classList.add(globals.handsontable.darkTableHeaderClass) th.classList.add(globals.handsontable.darkTableHeaderClass)
}
})
hotInstance.render()
} }
})
hotInstance?.render()
if (this.selectedViewbox) { if (this.selectedViewbox) {
this.resetSelectedViewbox(viewbox) this.resetSelectedViewbox(viewbox)
} }
}) }, 500)
}, 100)
resolve() resolve()
}) })
@@ -490,6 +511,68 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
return index return index
} }
/**
* Create and store Handsontable settings for a specific viewbox
* @param viewbox
*/
private createViewboxTableSettings(viewbox: Viewbox): void {
const viewboxTableIndex = this.getViewboxTableIndex(viewbox)
if (viewboxTableIndex === -1) {
this.viewboxHotSettings.set(viewbox.id, {})
return
}
const viewboxTable = this.viewboxTables[viewboxTableIndex]
const calculatedHeight = this.calculateTableHeight(viewbox)
// HOT v16 settings - data will be loaded manually after initialization
const settings: Handsontable.GridSettings = {
colHeaders: viewboxTable.hotTable.colHeaders,
columns: viewboxTable.hotTable.columns,
height: calculatedHeight,
readOnly: true,
modifyColWidth: this.maxWidthCheker,
copyPaste: viewboxTable.hotTable.copyPaste,
contextMenu: viewboxTable.hotTable.contextMenu,
multiColumnSorting: true,
viewportRowRenderingOffset: 50,
filters: true,
dropdownMenu: viewboxTable.hotTable.dropdownMenu,
stretchH: 'all',
cells: viewboxTable.hotTable.cells,
maxRows: viewboxTable.hotTable.maxRows || Infinity,
manualColumnResize: true,
rowHeaders: true,
licenseKey: viewboxTable.hotTable.licenseKey
}
// Force a new object reference to trigger change detection
this.viewboxHotSettings.set(viewbox.id, { ...settings })
// Force change detection to ensure the template updates
setTimeout(() => {
this.cdf.detectChanges()
// Try to get the HOT instance and force a render
setTimeout(() => {
const hotInstance = this.getViewboxHotInstance(viewbox.id)
if (hotInstance) {
// Load data manually - this is required for HOT v16 Angular wrapper
hotInstance.loadData(viewboxTable.hotTable.data)
hotInstance.render()
}
}, 500)
})
}
/**
* Get stored Handsontable settings for a specific viewbox
* @param viewbox
*/
getViewboxTableSettings(viewbox: Viewbox): Handsontable.GridSettings {
return this.viewboxHotSettings.get(viewbox.id) || {}
}
/** /**
* Viewbox resize * Viewbox resize
* @param dragHandle * @param dragHandle
@@ -513,6 +596,12 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
this.helperService.debounceCall(1000, () => { this.helperService.debounceCall(1000, () => {
this.viewboxChanged() this.viewboxChanged()
this.eventService.dispatchEvent('resize') this.eventService.dispatchEvent('resize')
// Refresh all viewbox tables after resize and update their settings
this.viewboxes.forEach((viewbox) => {
// Settings will include updated heights when accessed
this.refreshTableAfterResize(viewbox)
})
}) })
return { return {
@@ -672,6 +761,12 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
setTimeout(() => { setTimeout(() => {
this.setAllHandleTransform() this.setAllHandleTransform()
// Refresh all tables after snap to grid
this.viewboxes.forEach((viewbox) => {
// Settings will include correct height when accessed
this.refreshTableAfterResize(viewbox)
})
}) })
} }
@@ -713,6 +808,12 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
viewbox.minimized = false viewbox.minimized = false
this.viewboxChanged() this.viewboxChanged()
// Refresh table after restoring
setTimeout(() => {
// Settings will include correct height when accessed
this.refreshTableAfterResize(viewbox)
}, 100)
} }
collapse(viewbox: Viewbox) { collapse(viewbox: Viewbox) {
@@ -723,6 +824,12 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
expand(viewbox: Viewbox) { expand(viewbox: Viewbox) {
viewbox.collapsed = false viewbox.collapsed = false
this.viewboxChanged() this.viewboxChanged()
// Refresh table after expanding
setTimeout(() => {
// Settings will include correct height when accessed
this.refreshTableAfterResize(viewbox)
}, 100)
} }
/** /**
@@ -739,6 +846,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
if (index > -1) this.viewboxes.splice(index, 1) if (index > -1) this.viewboxes.splice(index, 1)
if (viewtableIndex > -1) this.viewboxTables.splice(viewtableIndex, 1) if (viewtableIndex > -1) this.viewboxTables.splice(viewtableIndex, 1)
// Clean up settings for this viewbox
this.viewboxHotSettings.delete(viewbox.id)
if (this.selectedViewbox?.id === viewbox.id) { if (this.selectedViewbox?.id === viewbox.id) {
this.unsetSelectedViewbox() this.unsetSelectedViewbox()
} }
@@ -1036,6 +1146,9 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
} }
viewboxTable.hotTable.data = res.viewdata viewboxTable.hotTable.data = res.viewdata
// Update settings with new data
this.createViewboxTableSettings(viewbox)
}) })
.catch((err: any) => { .catch((err: any) => {
this.loggerService.error(err) this.loggerService.error(err)
@@ -1064,6 +1177,8 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
this.updateHiddenColumnsHot(hiddenColProps, viewboxId) this.updateHiddenColumnsHot(hiddenColProps, viewboxId)
this.setColumnOrder(viewboxId) this.setColumnOrder(viewboxId)
// Settings will be regenerated when accessed
} }
/** /**
@@ -1158,19 +1273,87 @@ export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
} }
} }
/**
* Calculate available height for Handsontable
* @param viewbox The viewbox to calculate height for
* @returns Available height in pixels
*/
calculateTableHeight(viewbox: Viewbox): number {
// Calculate the exact height of the content div
const dragHandleHeight = 20
const searchFormHeight = 36
const padding = 2
// Return the exact remaining height for the table with minimum height
const calculatedHeight =
viewbox.height - dragHandleHeight - searchFormHeight - padding
return calculatedHeight
}
/**
* Refresh Handsontable instance after resize
* @param viewbox The viewbox to refresh
*/
refreshTableAfterResize(viewbox: Viewbox): void {
const hotInstance = this.getViewboxHotInstance(viewbox.id)
if (hotInstance) {
// Force the table to recalculate its dimensions
setTimeout(() => {
try {
// Update height setting and refresh
hotInstance.updateSettings({
height: this.calculateTableHeight(viewbox)
})
hotInstance.refreshDimensions()
hotInstance.render()
} catch (error) {
// If refresh fails, try again later
setTimeout(() => {
try {
hotInstance.updateSettings({
height: this.calculateTableHeight(viewbox)
})
hotInstance.refreshDimensions()
} catch (e) {
console.warn(
'Failed to refresh HOT dimensions for viewbox',
viewbox.id,
e
)
}
}, 500)
}
}, 100)
}
}
/** /**
* *
* @param viewboxId * @param viewboxId
* @returns HOT Instance from the given Viewbox * @returns HOT Instance from the given Viewbox
*/ */
private getViewboxHotInstance(viewboxId?: number): Handsontable | undefined { private getViewboxHotInstance(viewboxId?: number): Handsontable | undefined {
if (!viewboxId) return if (!viewboxId || !this.hotTableComponents) return
const hotInstance = this.hotTableRegisterer.getInstance( // Find the component corresponding to this viewbox
`hotInstance_viewbox_${viewboxId}` // Since we only show one table per viewbox and they're rendered in order,
) // we can match by the viewbox's position in the array
const viewboxIndex = this.viewboxes.findIndex((vb) => vb.id === viewboxId)
if (viewboxIndex === -1) return
return hotInstance // Get the HOT component at this index
const hotComponents = this.hotTableComponents.toArray()
let hotComponentIndex = 0
// Count how many viewboxes before this one have loaded tables
for (let i = 0; i < viewboxIndex; i++) {
if (this.getViewboxTableIndex(this.viewboxes[i]) > -1) {
hotComponentIndex++
}
}
const hotTableComponent = hotComponents[hotComponentIndex]
return hotTableComponent?.hotInstance || undefined
} }
/** /**

View File

@@ -4,7 +4,11 @@ import { ClarityModule } from '@clr/angular'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { ViewboxesComponent } from './viewboxes.component' import { ViewboxesComponent } from './viewboxes.component'
import { QueryModule } from 'src/app/query/query.module' import { QueryModule } from 'src/app/query/query.module'
import { HotTableModule } from '@handsontable/angular' import { HotTableModule } from '@handsontable/angular-wrapper'
import { registerAllModules } from 'handsontable/registry'
// register Handsontable's modules
registerAllModules()
import { DragDropModule } from '@angular/cdk/drag-drop' import { DragDropModule } from '@angular/cdk/drag-drop'
import { AutocompleteModule } from '../autocomplete/autocomplete.module' import { AutocompleteModule } from '../autocomplete/autocomplete.module'
import { DcTreeModule } from '../dc-tree/dc-tree.module' import { DcTreeModule } from '../dc-tree/dc-tree.module'

View File

@@ -46,6 +46,10 @@
rejected: tableDetails?.REVIEW_STATUS_ID === 'REJECTED', rejected: tableDetails?.REVIEW_STATUS_ID === 'REJECTED',
accepted: tableDetails?.REVIEW_STATUS_ID === 'APPROVED' accepted: tableDetails?.REVIEW_STATUS_ID === 'APPROVED'
}" }"
[attr.aria-label]="
'Review status: ' + tableDetails?.REVIEW_STATUS_ID
"
role="status"
> >
{{ tableDetails?.REVIEW_STATUS_ID }} {{ tableDetails?.REVIEW_STATUS_ID }}
</span> </span>
@@ -61,6 +65,7 @@
class="btn btn-sm btn-outline text-center mr-5i" class="btn btn-sm btn-outline text-center mr-5i"
(click)="viewerTableScreen()" (click)="viewerTableScreen()"
[disabled]="revertingChanges" [disabled]="revertingChanges"
aria-label="View base table"
> >
View base table View base table
</button> </button>
@@ -74,6 +79,7 @@
" "
(click)="approveTableScreen()" (click)="approveTableScreen()"
[disabled]="revertingChanges" [disabled]="revertingChanges"
aria-label="Approve table"
> >
Approve Approve
</button> </button>
@@ -81,14 +87,16 @@
class="btn btn-sm btn-info-outline text-center mr-5i" class="btn btn-sm btn-info-outline text-center mr-5i"
(click)="goBack()" (click)="goBack()"
[disabled]="revertingChanges" [disabled]="revertingChanges"
aria-label="Edit base table"
> >
Edit base table Edit base table
</button> </button>
<button <button
class="btn btn-sm btn-success text-center mr-5i min-w-0" class="btn btn-sm btn-success text-center mr-5i min-w-0"
(click)="download(tableDetails?.TABLE_ID)" (click)="download(tableDetails?.TABLE_ID)"
aria-label="Download audit file"
> >
<clr-icon shape="download"></clr-icon> <clr-icon shape="download" aria-hidden="true"></clr-icon>
</button> </button>
<clr-tooltip> <clr-tooltip>
@@ -98,6 +106,7 @@
clrTooltipTrigger clrTooltipTrigger
[clrLoading]="revertingChanges" [clrLoading]="revertingChanges"
class="btn btn-sm btn-danger text-center mt-20" class="btn btn-sm btn-danger text-center mt-20"
aria-label="Revert this and all subsequent changes"
> >
REVERT REVERT
@@ -116,18 +125,10 @@
</div> </div>
<div class="card-block"> <div class="card-block">
<hot-table <hot-table
hotId="hotInstance"
id="hotTable" id="hotTable"
className="htDark"
[data]="hotTable.data" [data]="hotTable.data"
[colHeaders]="hotTable.colHeaders" [settings]="hotTableSettings"
[columns]="hotTable.columns" aria-label="Staged data table"
[maxRows]="hotTable.maxRows"
[height]="hotTable.height"
[licenseKey]="hotTable.licenseKey"
[afterGetColHeader]="hotTable.afterGetColHeader"
stretchH="all"
[cells]="hotTable.cells"
> >
<!--[licenseKey]=null--> <!--[licenseKey]=null-->
</hot-table> </hot-table>

View File

@@ -1,10 +1,17 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core' import {
Component,
OnInit,
ViewEncapsulation,
ViewChild,
AfterViewInit
} from '@angular/core'
import { SasStoreService } from '../services/sas-store.service' import { SasStoreService } from '../services/sas-store.service'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { SasService } from '../services/sas.service' import { SasService } from '../services/sas.service'
import { EventService } from '../services/event.service' import { EventService } from '../services/event.service'
import { HotTableInterface } from '../models/HotTable.interface' import { HotTableInterface } from '../models/HotTable.interface'
import Handsontable from 'handsontable'
import { LicenceService } from '../services/licence.service' import { LicenceService } from '../services/licence.service'
import { globals } from '../_globals' import { globals } from '../_globals'
import { EditorsRestoreServiceResponse } from '../models/sas/editors-restore.model' import { EditorsRestoreServiceResponse } from '../models/sas/editors-restore.model'
@@ -19,7 +26,7 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class StageComponent implements OnInit { export class StageComponent implements OnInit, AfterViewInit {
public table_id: any public table_id: any
public jsParams: any public jsParams: any
public keysArray: any public keysArray: any
@@ -32,12 +39,42 @@ export class StageComponent implements OnInit {
colHeaders: [], colHeaders: [],
columns: [], columns: [],
height: 500, height: 500,
settings: {}, settings: {
// Disable problematic ARIA attributes that cause accessibility issues
ariaTags: false,
// Use grid role instead of treegrid for better accessibility
tableClassName: 'htCenter',
// Disable focus management to avoid focus catcher issues
outsideClickDeselects: false,
// Use simpler accessibility mode
autoWrapRow: false,
autoWrapCol: false
},
licenseKey: undefined, licenseKey: undefined,
maxRows: this.licenceState.value.stage_rows_allowed || Infinity, maxRows: this.licenceState.value.stage_rows_allowed || Infinity,
afterGetColHeader: (column, th, headerLevel) => { afterGetColHeader: (column, th, headerLevel) => {
// Dark mode // Dark mode
th.classList.add(globals.handsontable.darkTableHeaderClass) th.classList.add(globals.handsontable.darkTableHeaderClass)
},
afterInit: () => {
// Fix accessibility issues with focus catcher inputs
this.fixFocusCatcherAccessibility()
}
}
get hotTableSettings(): Handsontable.GridSettings {
return {
...this.hotTable.settings,
colHeaders: this.hotTable.colHeaders,
columns: this.hotTable.columns,
maxRows: this.hotTable.maxRows,
height: this.hotTable.height,
licenseKey: this.hotTable.licenseKey,
afterGetColHeader: this.hotTable.afterGetColHeader,
afterInit: this.hotTable.afterInit,
stretchH: 'all',
cells: this.hotTable.cells,
className: 'htDark'
} }
} }
@@ -169,6 +206,13 @@ export class StageComponent implements OnInit {
} }
} }
ngAfterViewInit() {
// Additional accessibility fixes after view is initialized
setTimeout(() => {
this.fixFocusCatcherAccessibility()
}, 500)
}
revertChanges() { revertChanges() {
this.revertingChanges = true this.revertingChanges = true
@@ -204,4 +248,27 @@ export class StageComponent implements OnInit {
} }
}, 200) }, 200)
} }
private fixFocusCatcherAccessibility() {
// Add labels to focus catcher inputs to fix accessibility issues
setTimeout(() => {
const focusCatchers = document.querySelectorAll('.htFocusCatcher')
focusCatchers.forEach((input: any, index: number) => {
if (input) {
// Add proper accessibility attributes
input.setAttribute('aria-label', `Table focus catcher ${index + 1}`)
input.setAttribute('aria-hidden', 'true')
input.setAttribute('tabindex', '-1')
input.setAttribute('role', 'presentation')
// Add a hidden label element
const label = document.createElement('label')
label.setAttribute('for', input.id || `htFocusCatcher${index}`)
label.setAttribute('aria-hidden', 'true')
label.style.display = 'none'
label.textContent = `Table focus catcher ${index + 1}`
input.parentNode?.insertBefore(label, input)
}
})
}, 100)
}
} }

View File

@@ -1,7 +1,7 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { StageComponent } from './stage.component' import { StageComponent } from './stage.component'
import { HotTableModule } from '@handsontable/angular' import { HotTableModule } from '@handsontable/angular-wrapper'
import { ClarityModule } from '@clr/angular' import { ClarityModule } from '@clr/angular'
import { RouterModule, Routes } from '@angular/router' import { RouterModule, Routes } from '@angular/router'

View File

@@ -598,6 +598,12 @@
{{ libinfo[0] ? libinfo[0].TABLE_CNT : '' }} {{ libinfo[0] ? libinfo[0].TABLE_CNT : '' }}
</td> </td>
</tr> </tr>
<tr *ngIf="libinfo[0].CATALOG_CNT !== null">
<td class="m-0">CATALOG_CNT:</td>
<td class="m-0 font-bold">
{{ libinfo[0] ? libinfo[0].CATALOG_CNT : '' }}
</td>
</tr>
</table> </table>
</ng-container> </ng-container>
</div> </div>
@@ -615,32 +621,16 @@
</div> </div>
<div *ngIf="!noData && !noDataReqErr && table" class="clr-flex-1"> <div *ngIf="!noData && !noDataReqErr && table" class="clr-flex-1">
<hot-table <div class="hot-wrapper clr-flex-1">
hotId="hotInstance" <hot-table
id="hotTable" #hotInstance
className="htDark" id="hotTable"
[multiColumnSorting]="true" class="view-hot"
[viewportRowRenderingOffset]="50" [data]="hotTable.data"
[data]="hotTable.data" [settings]="hotTableSettings"
[colHeaders]="hotTable.colHeaders" >
[columns]="hotTable.columns" </hot-table>
[copyPaste]="hotTable.copyPaste" </div>
[contextMenu]="hotTable.contextMenu"
[filters]="true"
[dropdownMenu]="hotTable.dropdownMenu"
[height]="hotTable.height"
stretchH="all"
[modifyColWidth]="maxWidthCheker"
[cells]="hotTable.cells"
[maxRows]="hotTable.maxRows"
[manualColumnResize]="true"
[afterGetColHeader]="hotTable.afterGetColHeader"
[rowHeaders]="hotTable.rowHeaders"
[rowHeaderWidth]="hotTable.rowHeaderWidth"
[rowHeights]="hotTable.rowHeights"
[licenseKey]="hotTable.licenseKey"
>
</hot-table>
</div> </div>
<div> <div>

View File

@@ -3,9 +3,11 @@ import {
AfterContentInit, AfterContentInit,
ChangeDetectorRef, ChangeDetectorRef,
AfterViewInit, AfterViewInit,
OnDestroy,
ViewChildren, ViewChildren,
QueryList, QueryList,
ViewEncapsulation ViewEncapsulation,
ViewChild
} from '@angular/core' } from '@angular/core'
import { SasStoreService } from '../services/sas-store.service' import { SasStoreService } from '../services/sas-store.service'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
@@ -16,7 +18,7 @@ import { globals } from '../_globals'
import { EventService } from '../services/event.service' import { EventService } from '../services/event.service'
import { HelperService } from '../services/helper.service' import { HelperService } from '../services/helper.service'
import { HotTableRegisterer } from '@handsontable/angular' import { HotTableComponent } from '@handsontable/angular-wrapper'
import { SasService } from '../services/sas.service' import { SasService } from '../services/sas.service'
import { SASjsConfig } from '@sasjs/adapter' import { SASjsConfig } from '@sasjs/adapter'
import { QueryComponent } from '../query/query.component' import { QueryComponent } from '../query/query.component'
@@ -49,10 +51,15 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class ViewerComponent implements AfterContentInit, AfterViewInit { export class ViewerComponent
implements AfterContentInit, AfterViewInit, OnDestroy
{
@ViewChildren('queryFilter') @ViewChildren('queryFilter')
queryFilterCompList: QueryList<QueryComponent> = new QueryList() queryFilterCompList: QueryList<QueryComponent> = new QueryList()
@ViewChild('hotInstance', { static: false })
hotInstanceViewChild!: Handsontable
public libraries!: Array<any> public libraries!: Array<any>
public librariesPaging: boolean = false public librariesPaging: boolean = false
public librariesSearch: string = '' public librariesSearch: string = ''
@@ -95,7 +102,35 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
public sasjsConfig: SASjsConfig = new SASjsConfig() public sasjsConfig: SASjsConfig = new SASjsConfig()
public searchLoading: boolean = false public searchLoading: boolean = false
public searchNumeric: boolean = false public searchNumeric: boolean = false
private hotTableRegisterer: HotTableRegisterer @ViewChild(HotTableComponent, { static: false })
hotTableComponent!: HotTableComponent
public hotTableSettings: Handsontable.GridSettings = {}
private updateHotTableSettings(): void {
this.hotTableSettings = {
multiColumnSorting: true,
viewportRowRenderingOffset: 30,
colHeaders: this.hotTable.colHeaders,
columns: this.hotTable.columns,
copyPaste: this.hotTable.copyPaste,
contextMenu: this.hotTable.contextMenu,
filters: true,
dropdownMenu: this.hotTable.dropdownMenu,
height: this.hotTable.height,
stretchH: 'all',
modifyColWidth: this.maxWidthCheker,
cells: this.hotTable.cells,
maxRows: this.hotTable.maxRows,
manualColumnResize: true,
afterGetColHeader: this.hotTable.afterGetColHeader,
rowHeaders: this.hotTable.rowHeaders,
rowHeaderWidth: this.hotTable.rowHeaderWidth,
rowHeights: this.hotTable.rowHeights,
licenseKey: this.hotTable.licenseKey,
className: 'htDark'
}
}
public numberOfRows: number | null = null public numberOfRows: number | null = null
public headerPks: string[] = [] public headerPks: string[] = []
public $dataFormats: $DataFormats | null = null public $dataFormats: $DataFormats | null = null
@@ -107,11 +142,14 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
public licenceState = this.licenceService.licenceState public licenceState = this.licenceService.licenceState
public Infinity = Infinity public Infinity = Infinity
private ariaObserver: MutationObserver | undefined
private ariaCheckInterval: any | undefined
public hotTable: HotTableInterface = { public hotTable: HotTableInterface = {
data: [], data: [],
colHeaders: [], colHeaders: [],
columns: [], columns: [],
height: '100%', height: 'calc(100vh - 182px)',
maxRows: this.licenceState.value.viewer_rows_allowed || Infinity, maxRows: this.licenceState.value.viewer_rows_allowed || Infinity,
settings: {}, settings: {},
licenseKey: undefined, licenseKey: undefined,
@@ -119,6 +157,13 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
return ' ' return ' '
}, },
afterGetColHeader: (col: number, th: any, headerLevel: number) => { afterGetColHeader: (col: number, th: any, headerLevel: number) => {
const column = this.hotInstance?.colToProp(col) as string
// header columns styling - primary keys
const isPKCol = column && this.headerPks.indexOf(column) > -1
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
// Dark mode // Dark mode
th.classList.add(globals.handsontable.darkTableHeaderClass) th.classList.add(globals.handsontable.darkTableHeaderClass)
}, },
@@ -193,7 +238,6 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
private location: Location, private location: Location,
private cdf: ChangeDetectorRef private cdf: ChangeDetectorRef
) { ) {
this.hotTableRegisterer = new HotTableRegisterer()
this.sasjsConfig = this.sasService.getSasjsConfig() this.sasjsConfig = this.sasService.getSasjsConfig()
} }
@@ -213,6 +257,7 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
this.licenceService.hot_license_key.subscribe( this.licenceService.hot_license_key.subscribe(
(hot_license_key: string | undefined) => { (hot_license_key: string | undefined) => {
this.hotTable.licenseKey = hot_license_key this.hotTable.licenseKey = hot_license_key
this.updateHotTableSettings() // Update settings when license key changes
} }
) )
} }
@@ -530,7 +575,6 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
`#search_${library.LIBRARYREF}` `#search_${library.LIBRARYREF}`
) )
this.loggerService.log('[libTreeSearchInput]', libTreeSearchInput)
if (libTreeSearchInput) libTreeSearchInput.focus() if (libTreeSearchInput) libTreeSearchInput.focus()
if (library && library.libinfo) this.libinfo = library.libinfo if (library && library.libinfo) this.libinfo = library.libinfo
@@ -819,27 +863,37 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
this.versions = res.versions || [] this.versions = res.versions || []
this.setDSNote() this.setDSNote()
this.queryText = res.sasparams[0].FILTER_TEXT this.queryText = res.sasparams[0].FILTER_TEXT
let columns: any[] = []
let colArr = []
for (let key in res.viewdata[0]) { // Initialize columns only if we have data
if (key) { if (res.viewdata && res.viewdata.length > 0) {
colArr.push(key) let columns: any[] = []
let colArr = []
for (let key in res.viewdata[0]) {
if (key) {
colArr.push(key)
}
} }
for (let index = 0; index < colArr.length; index++) {
columns.push({ data: colArr[index] })
}
this.hotTable.colHeaders = colArr
this.hotTable.columns = columns
} else {
// Set empty arrays if no data
this.hotTable.colHeaders = []
this.hotTable.columns = []
} }
for (let index = 0; index < colArr.length; index++) { // Set cells function
columns.push({ data: colArr[index] }) this.hotTable.cells = () => {
return { readOnly: true }
} }
let cells = function () { // Update hot table settings after data is loaded
let cellProperties = { readOnly: true } this.updateHotTableSettings()
return cellProperties
}
this.hotTable.colHeaders = colArr
this.hotTable.columns = columns
this.hotTable.cells = cells
this.tableFlag = false this.tableFlag = false
let ds = [] let ds = []
@@ -911,6 +965,11 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
//That is intorduced by HOT update //That is intorduced by HOT update
if (!this.noData && !this.noDataReqErr && libDataset) this.setupHot() if (!this.noData && !this.noDataReqErr && libDataset) this.setupHot()
// Fix ARIA accessibility issues after data loading
setTimeout(() => {
this.fixAriaAccessibility()
}, 1500)
/** /**
* This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area. * This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area.
* Without it, hot table does not steal focus, so it doesn't close the nav. * Without it, hot table does not steal focus, so it doesn't close the nav.
@@ -1060,12 +1119,12 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
private setupHot() { private setupHot() {
setTimeout(() => { setTimeout(() => {
if (!this.loadingTableView && this.libDataset) { if (!this.loadingTableView && this.libDataset) {
this.hotInstance = this.hotTableRegisterer.getInstance('hotInstance') this.hotInstance = this.hotTableComponent?.hotInstance
if (this.hotInstance) { if (this.hotInstance) {
this.hotInstance.updateSettings({ this.hotInstance.updateSettings({
height: this.hotTable.height, height: this.hotTable.height,
modifyColWidth: function (width: any, col: any) { modifyColWidth: (width: any, col: any) => {
if (width > 500) return 500 if (width > 500) return 500
else return width else return width
}, },
@@ -1081,8 +1140,26 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
th.classList.add(globals.handsontable.darkTableHeaderClass) th.classList.add(globals.handsontable.darkTableHeaderClass)
} }
}) })
// Add hooks for accessibility fixes
this.hotInstance.addHook('afterRender', () => {
// Fix ARIA accessibility issues after each render
this.fixAriaAccessibility()
})
this.hotInstance.addHook('afterChange', () => {
// Fix ARIA accessibility issues after any data change
setTimeout(() => {
this.fixAriaAccessibility()
}, 50)
})
} }
} }
// Fix ARIA accessibility issues after table setup
setTimeout(() => {
this.fixAriaAccessibility()
}, 500)
}, 1000) }, 1000)
} }
@@ -1148,7 +1225,173 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
} }
} }
ngAfterViewInit() {} ngAfterViewInit() {
// Fix ARIA accessibility issues after table initialization
setTimeout(() => {
this.fixAriaAccessibility()
}, 1000)
}
ngOnDestroy() {
// Clean up the MutationObserver
if (this.ariaObserver) {
this.ariaObserver.disconnect()
this.ariaObserver = undefined
}
// Clean up the interval
if (this.ariaCheckInterval) {
clearInterval(this.ariaCheckInterval)
this.ariaCheckInterval = undefined
}
}
/**
* Fixes ARIA accessibility issues in the Handsontable component
* This addresses the accessibility report issues with treegrid and presentation roles
*/
private fixAriaAccessibility() {
// Use a more aggressive approach to find and fix all ARIA issues
const fixAriaIssues = () => {
// Specifically target Handsontable wrapper elements that are causing issues
const hotWrappers = document.querySelectorAll(
'.ht-wrapper, .wtHolder, [id^="ht_"]'
)
hotWrappers.forEach((wrapper) => {
// Remove problematic ARIA attributes from Handsontable wrappers
wrapper.removeAttribute('role')
wrapper.removeAttribute('aria-rowcount')
wrapper.removeAttribute('aria-colcount')
wrapper.removeAttribute('aria-multiselectable')
})
// Find all elements with problematic ARIA roles in the entire document
const allTreegridElements = document.querySelectorAll('[role="treegrid"]')
const allPresentationElements = document.querySelectorAll(
'[role="presentation"]'
)
// Fix treegrid role issues - remove them completely as they're causing problems
allTreegridElements.forEach((element) => {
element.removeAttribute('role')
element.removeAttribute('aria-rowcount')
element.removeAttribute('aria-colcount')
element.removeAttribute('aria-multiselectable')
})
// Fix presentation role issues - remove them if they contain interactive elements
allPresentationElements.forEach((element) => {
const hasInteractiveChildren =
element.querySelectorAll(
'button, input, select, textarea, [tabindex], [onclick], [contenteditable]'
).length > 0
if (hasInteractiveChildren) {
element.removeAttribute('role')
}
})
// Also fix any elements with aria-rowcount="-1" which is problematic
const negativeRowCountElements = document.querySelectorAll(
'[aria-rowcount="-1"]'
)
negativeRowCountElements.forEach((element) => {
element.removeAttribute('aria-rowcount')
})
// Ensure proper table structure
const tableElements = document.querySelectorAll('table')
tableElements.forEach((table) => {
if (!table.getAttribute('role')) {
table.setAttribute('role', 'table')
}
// Ensure table headers have proper scope
const headerCells = table.querySelectorAll('th')
headerCells.forEach((th) => {
if (!th.getAttribute('scope')) {
th.setAttribute('scope', 'col')
}
})
})
// Add proper ARIA labels to interactive elements
const interactiveElements = document.querySelectorAll(
'button, input, select, textarea, [contenteditable]'
)
interactiveElements.forEach((element) => {
if (
!element.getAttribute('aria-label') &&
!element.getAttribute('aria-labelledby')
) {
const textContent = element.textContent?.trim()
if (textContent) {
element.setAttribute('aria-label', textContent)
}
}
})
}
// Run the fix immediately
fixAriaIssues()
// Run it again after a short delay to catch any dynamically created elements
setTimeout(fixAriaIssues, 100)
setTimeout(fixAriaIssues, 500)
setTimeout(fixAriaIssues, 1000)
setTimeout(fixAriaIssues, 2000)
// Set up a periodic check to ensure accessibility fixes are maintained
if (!this.ariaCheckInterval) {
this.ariaCheckInterval = setInterval(fixAriaIssues, 3000)
}
// Set up a MutationObserver to continuously monitor for new problematic elements
if (!this.ariaObserver) {
this.ariaObserver = new MutationObserver((mutations) => {
let shouldFix = false
mutations.forEach((mutation) => {
if (
mutation.type === 'attributes' &&
(mutation.attributeName === 'role' ||
mutation.attributeName === 'aria-rowcount')
) {
shouldFix = true
}
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element
if (
element.hasAttribute('role') ||
element.hasAttribute('aria-rowcount')
) {
shouldFix = true
}
}
})
}
})
if (shouldFix) {
setTimeout(fixAriaIssues, 50)
}
})
// Start observing the entire document for changes
this.ariaObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: [
'role',
'aria-rowcount',
'aria-colcount',
'aria-multiselectable'
]
})
}
}
async ngAfterContentInit() { async ngAfterContentInit() {
if (this.hotTable.data.length > 0) { if (this.hotTable.data.length > 0) {

View File

@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { ViewerComponent } from './viewer.component' import { ViewerComponent } from './viewer.component'
import { ViewRouteComponent } from '../routes/view-route/view-route.component' import { ViewRouteComponent } from '../routes/view-route/view-route.component'
import { HotTableModule } from '@handsontable/angular' import { HotTableModule } from '@handsontable/angular-wrapper'
import { ViewerRoutingModule } from './viewer-routing.module' import { ViewerRoutingModule } from './viewer-routing.module'
import { ClarityModule } from '@clr/angular' import { ClarityModule } from '@clr/angular'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
@@ -36,7 +36,7 @@ import { MetadataComponent } from '../metadata/metadata.component'
ClipboardModule, ClipboardModule,
FormsModule, FormsModule,
ClarityModule, ClarityModule,
HotTableModule.forRoot(), HotTableModule,
AppSharedModule, AppSharedModule,
SharedModule, SharedModule,
PipesModule, PipesModule,

View File

@@ -125,30 +125,9 @@
<div class="clr-flex-1"> <div class="clr-flex-1">
<hot-table <hot-table
hotId="hotInstance"
id="hot-table" id="hot-table"
className="htDark"
[multiColumnSorting]="true"
[viewportRowRenderingOffset]="50"
[data]="selectedTab === TabsEnum.Rules ? xlmapRules : xlData" [data]="selectedTab === TabsEnum.Rules ? xlmapRules : xlData"
[colHeaders]=" [settings]="hotTableSettings"
selectedTab === TabsEnum.Rules ? xlmapRulesHeaders : xlUploadHeader
"
[columns]="
selectedTab === TabsEnum.Rules ? xlmapRulesColumns : xlUploadColumns
"
[filters]="true"
[height]="'100%'"
stretchH="all"
[afterGetColHeader]="afterGetColHeader"
[modifyColWidth]="maxWidthChecker"
[cells]="getCellConfiguration"
[maxRows]="hotTableMaxRows"
[manualColumnResize]="true"
[rowHeaders]="rowHeaders"
[rowHeaderWidth]="15"
[rowHeights]="20"
[licenseKey]="hotTableLicenseKey"
> >
</hot-table> </hot-table>
</div> </div>

View File

@@ -23,6 +23,7 @@ import {
} from '../services' } from '../services'
import { getCellAddress, getFinishingCell } from './utils/xl.utils' import { getCellAddress, getFinishingCell } from './utils/xl.utils'
import { blobToFile, byteArrayToBinaryString } from './utils/file.utils' import { blobToFile, byteArrayToBinaryString } from './utils/file.utils'
import Handsontable from 'handsontable'
import { UploadFileResponse } from '../models/UploadFile' import { UploadFileResponse } from '../models/UploadFile'
interface XLMapRule { interface XLMapRule {
@@ -136,6 +137,34 @@ export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
public hotTableMaxRows = public hotTableMaxRows =
this.licenceState.value.viewer_rows_allowed || Infinity this.licenceState.value.viewer_rows_allowed || Infinity
get hotTableSettings(): Handsontable.GridSettings {
return {
multiColumnSorting: true,
viewportRowRenderingOffset: 50,
colHeaders:
this.selectedTab === this.TabsEnum.Rules
? this.xlmapRulesHeaders
: this.xlUploadHeader,
columns:
this.selectedTab === this.TabsEnum.Rules
? this.xlmapRulesColumns
: this.xlUploadColumns,
filters: true,
height: '100%',
stretchH: 'all',
afterGetColHeader: this.afterGetColHeader,
modifyColWidth: this.maxWidthChecker,
cells: this.getCellConfiguration,
maxRows: this.hotTableMaxRows,
manualColumnResize: true,
rowHeaders: this.rowHeaders,
rowHeaderWidth: 15,
rowHeights: 20,
licenseKey: this.hotTableLicenseKey,
className: 'htDark'
}
}
constructor( constructor(
private eventService: EventService, private eventService: EventService,
private licenceService: LicenceService, private licenceService: LicenceService,

View File

@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { ClarityModule } from '@clr/angular' import { ClarityModule } from '@clr/angular'
import { HotTableModule } from '@handsontable/angular' import { HotTableModule } from '@handsontable/angular-wrapper'
import { registerAllModules } from 'handsontable/registry' import { registerAllModules } from 'handsontable/registry'
import { AppSharedModule } from '../app-shared.module' import { AppSharedModule } from '../app-shared.module'
import { DirectivesModule } from '../directives/directives.module' import { DirectivesModule } from '../directives/directives.module'

View File

@@ -45,7 +45,7 @@
<sasjs <sasjs
serverUrl="" serverUrl=""
appLoc="/Public/app/dc" appLoc="/Public/app/devtest"
serverType="SASJS" serverType="SASJS"
loginMechanism="Redirected" loginMechanism="Redirected"
debug="false" debug="false"

View File

@@ -937,11 +937,6 @@ app-multi-dataset {
.dataset-input-wrapper { .dataset-input-wrapper {
max-width: 500px; max-width: 500px;
width: 100%; width: 100%;
textarea {
min-height: 200px;
height: 200px;
}
} }
.submit-reason { .submit-reason {
@@ -1042,12 +1037,12 @@ app-approve {
// HISTORY.COMPONENT // HISTORY.COMPONENT
app-history { app-history {
.rejected { .rejected {
color: #f83126; color: #92201a;
font-weight: bold font-weight: bold
} }
.accepted { .accepted {
color: #3fc424; color: #105c26;
font-weight: bold font-weight: bold
} }
@@ -1206,6 +1201,8 @@ app-viewer {
} }
hot-table { hot-table {
height: calc(100vh - 200px);
.handsontable tbody th.ht__highlight, .handsontable thead th.ht__highlight { .handsontable tbody th.ht__highlight, .handsontable thead th.ht__highlight {
&.primaryKeyHeaderStyle { &.primaryKeyHeaderStyle {
background-color: #306b00b0 !important; background-color: #306b00b0 !important;
@@ -3442,6 +3439,10 @@ app-approve-details {
width: 175px width: 175px
} }
#rejectBtn {
background-color: #a62f16 !important;
}
.formatted-values-toggle { .formatted-values-toggle {
min-width: 75px min-width: 75px
} }
@@ -3630,12 +3631,12 @@ app-excel-password-modal {
// STAGE.COMPONENT // STAGE.COMPONENT
app-stage { app-stage {
.rejected { .rejected {
color: #f83126; color: #92201a;
font-weight: bold font-weight: bold
} }
.accepted { .accepted {
color: #3fc424; color: #105c26;
font-weight: bold font-weight: bold
} }
@@ -3644,6 +3645,25 @@ app-stage {
margin-top:10px; margin-top:10px;
color: #007cbb; color: #007cbb;
} }
// Accessibility fixes for handsontable focus catchers
.htFocusCatcher {
// Hide from screen readers but maintain functionality
position: absolute !important;
left: -9999px !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
border: 0 !important;
margin: -1px !important;
padding: 0 !important;
// Ensure it's not focusable by screen readers
&:focus {
outline: none !important;
}
}
} }
body[cds-theme="dark"] { body[cds-theme="dark"] {
@@ -4746,7 +4766,6 @@ body[cds-theme="dark"] {
} }
.handsontable.listbox { .handsontable.listbox {
padding: 5px 0px 5px 5px;
box-shadow: 0px 4px 20px 0px #00000070; box-shadow: 0px 4px 20px 0px #00000070;
} }
@@ -4756,6 +4775,12 @@ body[cds-theme="dark"] {
color: #ffffff !important; color: #ffffff !important;
} }
.handsontable td.dc-invalid-cell {
background: #e62700ad !important;
border: 1px solid red !important;
color: #ffffff !important;
}
.handsontable .numericListbox { .handsontable .numericListbox {
text-align: right; text-align: right;
} }

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "dcfrontend", "name": "dcfrontend",
"version": "6.14.7", "version": "7.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "dcfrontend", "name": "dcfrontend",
"version": "6.14.7", "version": "7.1.1",
"hasInstallScript": true, "hasInstallScript": true,
"devDependencies": { "devDependencies": {
"@saithodev/semantic-release-gitea": "^2.1.0", "@saithodev/semantic-release-gitea": "^2.1.0",
@@ -16,7 +16,7 @@
"@semantic-release/npm": "11.0.0", "@semantic-release/npm": "11.0.0",
"@semantic-release/release-notes-generator": "^11.0.4", "@semantic-release/release-notes-generator": "^11.0.4",
"commit-and-tag-version": "^11.2.2", "commit-and-tag-version": "^11.2.2",
"prettier": "3.2.5" "prettier": "3.6.2"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@@ -6690,9 +6690,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.2.5", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {

View File

@@ -1,25 +1,28 @@
{ {
"name": "dcfrontend", "name": "dcfrontend",
"version": "6.16.2", "version": "7.2.1",
"description": "Data Controller", "description": "Data Controller",
"devDependencies": { "devDependencies": {
"@saithodev/semantic-release-gitea": "^2.1.0", "@saithodev/semantic-release-gitea": "^2.1.0",
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^10.0.1", "@semantic-release/commit-analyzer": "^10.0.1",
"@semantic-release/npm": "11.0.0",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"@semantic-release/npm": "11.0.0",
"@semantic-release/release-notes-generator": "^11.0.4", "@semantic-release/release-notes-generator": "^11.0.4",
"commit-and-tag-version": "^11.2.2", "commit-and-tag-version": "^11.2.2",
"prettier": "3.2.5" "prettier": "3.6.2"
}, },
"scripts": { "scripts": {
"install": "cd client && npm i && cd ../sas && npm i", "install": "cd client && npm i && cd ../sas && npm i",
"build-frontend": "cd client && npm run build", "build-frontend": "cd client && npm run build",
"release": "commit-and-tag-version", "release": "commit-and-tag-version",
"lint": "npm run lint:fix", "lint": "npm run lint:fix",
"lint:fix": "npx prettier --write \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/integration/*.tests.ts\"", "lint:fix": "npx prettier --write \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/e2e/*.cy.ts\"",
"lint:check": "npx prettier --check \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/integration/*.tests.ts\"", "lint:fix:silent": "npx prettier --log-level silent --write \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/e2e/*.cy.ts\"",
"jo": "echo" "lint:check": "npx prettier --check \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/e2e/*.cy.ts\"",
"lint:check:silent": "npx prettier --log-level silent --check \"client/{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\" \"client/cypress/e2e/*.cy.ts\"",
"jo": "echo",
"prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks && git config core.autocrlf false || true"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -29,5 +32,6 @@
"//": [ "//": [
"Readme", "Readme",
"We must set private: true so that semantic-release/npm plugin will update the package.json version but not try to release it as NPM package" "We must set private: true so that semantic-release/npm plugin will update the package.json version but not try to release it as NPM package"
] ],
"dependencies": {}
} }

View File

@@ -13,6 +13,12 @@ if (fs.existsSync(sessionStoragePath)){
} catch (err) {} } catch (err) {}
} }
let controlTableText = ''
if (_WEBIN_FILENAME1.includes('SASControlTable')) controlTableText = _WEBIN_FILEREF1.toString()
let webouts = { let webouts = {
MPE_X_TEST: `{"SYSDATE" : "26SEP22" MPE_X_TEST: `{"SYSDATE" : "26SEP22"
,"SYSTIME" : "08:48" ,"SYSTIME" : "08:48"

213
sas/package-lock.json generated
View File

@@ -6,8 +6,8 @@
"": { "": {
"name": "dc-sas", "name": "dc-sas",
"dependencies": { "dependencies": {
"@sasjs/cli": "^4.12.7", "@sasjs/cli": "^4.12.10",
"@sasjs/core": "^4.58.2" "@sasjs/core": "^4.59.5"
} }
}, },
"node_modules/@coolaj86/urequest": { "node_modules/@coolaj86/urequest": {
@@ -30,29 +30,29 @@
} }
}, },
"node_modules/@sasjs/adapter": { "node_modules/@sasjs/adapter": {
"version": "4.11.3", "version": "4.12.2",
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.11.3.tgz", "resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.12.2.tgz",
"integrity": "sha512-KF6G4vzs4l4efjpCD02og3kB44uFfJ1u2UWu749VdHtLKNN9l+PO26/moR+YAmRmmz2I9sC3X09fZE1nlN6zgw==", "integrity": "sha512-OV5mx3N2Hywhp0M+SBLTuG42x/LDnMxrb2/pwG4RQbhfzvAwdHVEoXRouDJ49RMSY9s6TJcwUPh+Xzafl5sG/g==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@sasjs/utils": "3.5.2", "@sasjs/utils": "3.5.2",
"axios": "1.8.2", "axios": "1.8.2",
"axios-cookiejar-support": "5.0.5", "axios-cookiejar-support": "5.0.5",
"form-data": "4.0.0", "form-data": "4.0.4",
"https": "1.0.0", "https": "1.0.0",
"tough-cookie": "4.1.3" "tough-cookie": "4.1.3"
} }
}, },
"node_modules/@sasjs/cli": { "node_modules/@sasjs/cli": {
"version": "4.12.7", "version": "4.12.10",
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.12.7.tgz", "resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.12.10.tgz",
"integrity": "sha512-KcXSR+3dRgINOLiN+7oJbzWsNQu7qm1YQ7eaVqiHTZI429BSgZez9+7p1bq09R4otHN8IzMAgLP9se/r9p9yJA==", "integrity": "sha512-eWh7cTtIEH9PnKdooRWZ8yWVle4jPb0D7k+QKaPSBIW+ZbtyS87yNVU4MwkonSKCHneuhvrMMo8YV8QrqoUo7Q==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@sasjs/adapter": "4.11.3", "@sasjs/adapter": "4.12.2",
"@sasjs/core": "4.58.1", "@sasjs/core": "4.59.1",
"@sasjs/lint": "2.4.3", "@sasjs/lint": "2.4.3",
"@sasjs/utils": "3.5.2", "@sasjs/utils": "3.5.2",
"adm-zip": "0.5.10", "adm-zip": "0.5.10",
@@ -77,15 +77,15 @@
} }
}, },
"node_modules/@sasjs/cli/node_modules/@sasjs/core": { "node_modules/@sasjs/cli/node_modules/@sasjs/core": {
"version": "4.58.1", "version": "4.59.1",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.58.1.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.59.1.tgz",
"integrity": "sha512-Qp6KAtp1VZcmN5HLGSIUE9H41qpFuihWLbjNygOYp+NRs/Y8VagpHrYeyIQbh3cSgchiJEMXudLql8hoU06wpg==", "integrity": "sha512-52GNI4nIll5YivI8uobWrucE6TkHcTjcbKTr/YPC9eTauC4sh0V0MptebfAJ5E6vE5P2WevNZGr42KdDpckLpg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@sasjs/core": { "node_modules/@sasjs/core": {
"version": "4.58.2", "version": "4.59.5",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.58.2.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.59.5.tgz",
"integrity": "sha512-P/DMCHfFrZT+50DIT7CiYBSjxOo5m0AHBLNKHGFOMfQnEymOKekQPk2Xzw5wkQyg8gjp2yBKhRwhpni5rvJFgQ==", "integrity": "sha512-PAPinHjuFi3P/MxkZHGSF0iLWoBFnpxon7/SshHPcBRmSQwmFOvh9X8xdE8ZClqY9XDOwFZ6sqeOQ7GYZliLfw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@sasjs/lint": { "node_modules/@sasjs/lint": {
@@ -308,9 +308,10 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -347,6 +348,19 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -561,6 +575,20 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -577,6 +605,51 @@
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://github.com/fb55/entities?sponsor=1"
} }
}, },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
@@ -620,12 +693,15 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.0", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
}, },
"engines": { "engines": {
@@ -667,6 +743,43 @@
"node": "6.* || 8.* || >= 10.*" "node": "6.* || 8.* || >= 10.*"
} }
}, },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -687,6 +800,18 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -701,6 +826,33 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -748,9 +900,9 @@
} }
}, },
"node_modules/http-cookie-agent/node_modules/agent-base": { "node_modules/http-cookie-agent/node_modules/agent-base": {
"version": "7.1.3", "version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 14" "node": ">= 14"
@@ -1054,6 +1206,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": { "node_modules/mime-db": {
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",

View File

@@ -22,13 +22,13 @@
"serverdata": "sasjs request services/admin/makedata -d deploy/makeDataServer.json -l sasjsresults/makedata_server.log -o sasjsresults/makedata_server.json -t server", "serverdata": "sasjs request services/admin/makedata -d deploy/makeDataServer.json -l sasjsresults/makedata_server.log -o sasjsresults/makedata_server.json -t server",
"viya": "npm run viyacbd && sasjs test", "viya": "npm run viyacbd && sasjs test",
"viyacbd": "sasjs folder delete /Public/app/viya && sasjs cbd -t viya && npm run viyadata", "viyacbd": "sasjs folder delete /Public/app/viya && sasjs cbd -t viya && npm run viyadata",
"viyadata": "sasjs request services/admin/makedata -d deploy/makeDataViya.json -l sasjsresults/makedata.log -o sasjsresults/output.json", "viyadata": "sasjs request services/admin/makedata -d deploy/makeDataViya.json -l sasjsresults/makedata.log -o sasjsresults/output.json -t viya",
"v4data": "sasjs request services/admin/makedata -d deploy/makeDataV4.json -l sasjsresults/makedatav4.log -o sasjsresults/outputv4.json -t v4", "v4data": "sasjs request services/admin/makedata -d deploy/makeDataV4.json -l sasjsresults/makedatav4.log -o sasjsresults/outputv4.json -t v4",
"sasdocs": "sasjs doc && ./sasjs/utils/deploydocs.sh" "sasdocs": "sasjs doc && ./sasjs/utils/deploydocs.sh"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@sasjs/cli": "^4.12.7", "@sasjs/cli": "^4.12.10",
"@sasjs/core": "^4.58.2" "@sasjs/core": "^4.59.5"
} }
} }

View File

@@ -0,0 +1,17 @@
/**
@file mpe_datacatalog_CATS.ddl
@brief ddl file
@details
@version 9.3
@author 4GL Apps Ltd
@copyright 4GL Apps Ltd
**/
create table &curlib..mpe_datacatalog_CATS(
TX_FROM float format=datetime19.,
TX_TO float format=datetime19.,
libref char(8) label='Library Name',
memname char(64) label='Member Name',
constraint pk_mpe_datacatalog_CATS
primary key(libref,memname,tx_to));

View File

@@ -0,0 +1,21 @@
/**
@file mpe_datacatalog_CATS.ddl
@brief ddl file
@details
@version 9.3
@author 4GL Apps Ltd
@copyright 4GL Apps Ltd
**/
create table &curlib..mpe_datacatalog_OBJS(
TX_FROM float format=datetime19.,
TX_TO float format=datetime19.,
libref char(8) label='Library Name',
memname char(64) label='Member Name',
objname char(32) label='Object Name',
objtype char(8) label='Object Type',
objdesc char(256) label='Object Description',
alias char(32) label='Object Alias',
constraint pk_mpe_datacatalog_OBJS
primary key(libref,memname,objname,objtype,tx_to));

View File

@@ -0,0 +1,20 @@
/**
@file mpe_datacatalog_CATS.ddl
@brief ddl file
@details
@version 9.3
@author 4GL Apps Ltd
@copyright 4GL Apps Ltd
**/
create table &curlib..mpe_datastatus_CATS(
TX_FROM float format=datetime19.,
TX_TO float format=datetime19.,
libref char(8) label='Library Name',
memname char(64) label='Member Name',
nobjs num label='Number of objects',
created num format=DATETIME. label='Date Created',
modified num format=DATETIME. label='Date Modified',
constraint pk_mpe_datastatus_CATS
primary key(libref,memname,tx_to));

View File

@@ -14,5 +14,6 @@ create table &curlib..mpe_datastatus_libs(
libref char(8) label='Library Name', libref char(8) label='Library Name',
libsize num format=SIZEKMG. label='Size of file', libsize num format=SIZEKMG. label='Size of file',
table_cnt num label='Number of Tables', table_cnt num label='Number of Tables',
catalog_cnt num label='Number of Catalogs',
constraint pk_mpe_datastatus_libs constraint pk_mpe_datastatus_libs
primary key(libref,tx_to)); primary key(libref,tx_to));

View File

@@ -0,0 +1,22 @@
/**
@file mpe_datacatalog_CATS.ddl
@brief ddl file
@details
@version 9.3
@author 4GL Apps Ltd
@copyright 4GL Apps Ltd
**/
create table &curlib..mpe_datastatus_OBJS(
TX_FROM float format=datetime19.,
TX_TO float format=datetime19.,
libref char(8) label='Library Name',
memname char(64) label='Member Name',
objname char(32) label='Object Name',
objtype char(8) label='Object Type',
created num format=DATETIME. label='Date Created',
modified num format=DATETIME. label='Date Modified',
level num label='Library Concatenation Level',
constraint pk_mpe_datastatus_OBJS
primary key(libref,memname,objname,objtype,tx_to));

View File

@@ -14,8 +14,8 @@ create table &curlib..mpe_datastatus_tabs(
libref char(8) label='Library Name', libref char(8) label='Library Name',
dsn char(64) label='Member Name', dsn char(64) label='Member Name',
filesize num format=SIZEKMG. label='Size of file', filesize num format=SIZEKMG. label='Size of file',
crdate num format=DATETIME. informat=DATETIME. label='Date Created', crdate num format=DATETIME. label='Date Created',
modate num format=DATETIME. informat=DATETIME. label='Date Modified', modate num format=DATETIME. label='Date Modified',
nobs num label='Number of Physical (Actual, inc. deleted) Observations', nobs num label='Number of Physical (Actual, inc. deleted) Observations',
constraint pk_mpe_datastatus_tabs constraint pk_mpe_datastatus_tabs
primary key(libref,dsn,tx_to)); primary key(libref,dsn,tx_to));

View File

@@ -29,7 +29,7 @@ create table dc.mpe_datacatalog_TABS(
nvar num label='Number of Variables', nvar num label='Number of Variables',
compress char(8) label='Compression Routine', compress char(8) label='Compression Routine',
pk_fields char(512) pk_fields char(512)
label='Primary Key Fields (identified by being in a constraint that is both Unique and Not Null)', label='Primary Key Fields (in a constraint that is both Unique and Not Null)',
constraint pk constraint pk
primary key(libref,dsn,tx_to)); primary key(libref,dsn,tx_to));
@@ -67,8 +67,8 @@ create table dc.mpe_datacatalog_vars(
libref char(8) label='Library Name', libref char(8) label='Library Name',
dsn char(64) label='Member Name', dsn char(64) label='Member Name',
filesize num format=SIZEKMG. label='Size of file', filesize num format=SIZEKMG. label='Size of file',
crdate num format=DATETIME. informat=DATETIME. label='Date Created', crdate num format=DATETIME. label='Date Created',
modate num format=DATETIME. informat=DATETIME. label='Date Modified', modate num format=DATETIME. label='Date Modified',
nobs num label='Number of Physical (Actual, inc. deleted) Observations', nobs num label='Number of Physical (Actual, inc. deleted) Observations',
constraint pk constraint pk
primary key(libref,dsn,tx_to)); primary key(libref,dsn,tx_to));

View File

@@ -0,0 +1,71 @@
/**
@file
@brief migration script to move from v6.8.2 to v7.0 of data controller
BREAKING CHANGE - 1 new column and 4 additional tables for capturing catalogs
Be sure to run this using the correct system account
(eg the regular DC account)
On SAS 9 you may wish to run proc metalib or refresh in DI Studio afterwards
to see the new tables in the VIEW page
proc metalib;
omr (library="&DC_LIBNAME");
folder="&root/data";
update_rule=(delete);
run;
**/
%let dclib=YOURDCLIB;
libname &dclib "/YOUR/DATACONTROLLER/LIBRARY/PATH";
proc sql;
create table work.BACKUP as select * from &dclib..mpe_datastatus_libs;
alter table &dclib..mpe_datastatus_libs add catalog_cnt num;
create table &dclib..mpe_datacatalog_CATS(
TX_FROM float format=datetime19.,
TX_TO float format=datetime19.,
libref char(8) label='Library Name',
memname char(64) label='Member Name',
constraint pk_mpe_datacatalog_CATS
primary key(libref,memname,tx_to));
create table &dclib..mpe_datacatalog_OBJS(
TX_FROM float format=datetime19.,
TX_TO float format=datetime19.,
libref char(8) label='Library Name',
memname char(64) label='Member Name',
objname char(32) label='Object Name',
objtype char(8) label='Object Type',
objdesc char(256) label='Object Description',
alias char(32) label='Object Alias',
constraint pk_mpe_datacatalog_OBJS
primary key(libref,memname,objname,objtype,tx_to));
create table &dclib..mpe_datastatus_CATS(
TX_FROM float format=datetime19.,
TX_TO float format=datetime19.,
libref char(8) label='Library Name',
memname char(64) label='Member Name',
nobjs num label='Number of objects',
created num format=DATETIME. label='Date Created',
modified num format=DATETIME. label='Date Modified',
constraint pk_mpe_datastatus_CATS
primary key(libref,memname,tx_to));
create table &dclib..mpe_datastatus_OBJS(
TX_FROM float format=datetime19.,
TX_TO float format=datetime19.,
libref char(8) label='Library Name',
memname char(64) label='Member Name',
objname char(32) label='Object Name',
objtype char(8) label='Object Type',
created num format=DATETIME. label='Date Created',
modified num format=DATETIME. label='Date Modified',
level num label='Library Concatenation Level',
constraint pk_mpe_datastatus_OBJS
primary key(libref,memname,objname,objtype,tx_to));

View File

@@ -14,14 +14,31 @@
**/ **/
%macro mpe_dsmeta(libds, outds=dsmeta); %macro mpe_dsmeta(libds, outds=dsmeta);
%local ddsd ddld notes lenstmt; %local ddsd ddld notes lenstmt memname;
%let lenstmt=length ods_table $18 name $100 value $1000; %let lenstmt=length ods_table $18 name $100 value $1000;
%let libds=%upcase(&libds); %let libds=%upcase(&libds);
%mp_dsmeta(&libds, outds=&outds)
%if "%scan(&libds,2,-)" ne "FC" %then %do;
%let memname=%scan(&libds,2,.);
%mp_dsmeta(&libds, outds=&outds)
%end;
%else %do;
%let memname=%scan(&libds,2,.-);
data &outds;
&lenstmt;
set sashelp.vcatalg;
ods_table=cats(objtype);
name=cats(objname);
value=catx(' ',objdesc,'(modified:',put(modified,datetime19.),')');
where libname="%scan(&libds,1,.)" and memname="&memname";
keep ods_table name value;
run;
proc sort; by ods_table name;run;
%end;
data _null_; data _null_;
set &mpelib..mpe_datadictionary; set &mpelib..mpe_datadictionary;
where &dc_dttmtfmt < tx_to & dd_source=%upcase("&libds") & dd_type='TABLE'; where &dc_dttmtfmt < tx_to & dd_source="&memname" & dd_type='TABLE';
call symputx('ddsd',dd_shortdesc,'l'); call symputx('ddsd',dd_shortdesc,'l');
call symputx('ddld',dd_longdesc,'l'); call symputx('ddld',dd_longdesc,'l');
run; run;

View File

@@ -34,4 +34,15 @@ run;
outds=work.test_results outds=work.test_results
) )
%mpe_dsmeta(FMTONLY.DCFMTS-FC,outds=test2)
data work.test2;
set work.test2;
putlog (_all_)(=);
run;
%mp_assertdsobs(work.test2,
desc=Test 2 - records returned,
test=EQUALS 5,
outds=work.test_results
)

View File

@@ -106,6 +106,19 @@ proc datasets lib=&lib noprint;
/nomiss unique; /nomiss unique;
quit; quit;
proc sql; proc sql;
create table &lib..mpe_datacatalog_CATS(
TX_FROM float &notnull format=datetime19.,
TX_TO float format=datetime19.,
libref char(8) label='Library Name',
memname char(64) label='Member Name'
);quit;
proc datasets lib=&lib noprint;
modify mpe_datacatalog_CATS;
index create
pk_mpe_datacatalog_CATS=(tx_to libref memname)
/nomiss unique;
quit;
proc sql;
create table &lib..mpe_datacatalog_libs( create table &lib..mpe_datacatalog_libs(
TX_FROM num &notnull format=datetime19.3, TX_FROM num &notnull format=datetime19.3,
TX_TO num &notnull format=datetime19.3, TX_TO num &notnull format=datetime19.3,
@@ -125,6 +138,23 @@ proc datasets lib=&lib noprint;
/nomiss unique; /nomiss unique;
quit; quit;
proc sql; proc sql;
create table &lib..mpe_datacatalog_OBJS(
TX_FROM num &notnull format=datetime19.,
TX_TO num &notnull format=datetime19.,
libref char(8) &notnull label='Library Name',
memname char(64) &notnull label='Member Name',
objname char(32) &notnull label='Object Name',
objtype char(8) &notnull label='Object Type',
objdesc char(256) label='Object Description',
alias char(32) label='Object Alias'
);quit;
proc datasets lib=&lib noprint;
modify mpe_datacatalog_OBJS;
index create
pk_mpe_datacatalog_OBJS=(libref memname objname objtype tx_to)
/nomiss unique;
quit;
proc sql;
create table &lib..mpe_datacatalog_TABS( create table &lib..mpe_datacatalog_TABS(
TX_FROM num &notnull format=datetime19.3, TX_FROM num &notnull format=datetime19.3,
TX_TO num &notnull format=datetime19.3, TX_TO num &notnull format=datetime19.3,
@@ -137,7 +167,7 @@ create table &lib..mpe_datacatalog_TABS(
nvar num label='Number of Variables', nvar num label='Number of Variables',
compress char(8) label='Compression Routine', compress char(8) label='Compression Routine',
pk_fields char(512) pk_fields char(512)
label='Primary Key Fields (identified by being in a constraint that is both Unique and Not Null)' label='Primary Key Fields - in a constraint being both Unique and Not Null'
);quit; );quit;
proc datasets lib=&lib noprint; proc datasets lib=&lib noprint;
modify mpe_datacatalog_TABS; modify mpe_datacatalog_TABS;
@@ -169,12 +199,29 @@ proc datasets lib=&lib noprint;
/nomiss unique; /nomiss unique;
quit; quit;
proc sql; proc sql;
create table &lib..mpe_datastatus_CATS(
TX_FROM float format=datetime19.,
TX_TO float format=datetime19.,
libref char(8) label='Library Name',
memname char(64) label='Member Name',
nobjs num &notnull label='Number of objects',
created num &notnull format=DATETIME. label='Date Created',
modified num format=DATETIME. label='Date Modified'
);quit;
proc datasets lib=&lib noprint;
modify mpe_datastatus_CATS;
index create
pk_mpe_datastatus_cats=(libref memname tx_to)
/nomiss unique;
quit;
proc sql;
create table &lib..mpe_datastatus_libs( create table &lib..mpe_datastatus_libs(
TX_FROM num &notnull format=datetime19.3, TX_FROM num &notnull format=datetime19.3,
TX_TO num &notnull format=datetime19.3, TX_TO num &notnull format=datetime19.3,
libref char(8) label='Library Name', libref char(8) label='Library Name',
libsize num format=SIZEKMG. label='Size of library', libsize num format=SIZEKMG. label='Size of library',
table_cnt num label='Number of Tables' table_cnt num label='Number of Tables',
catalog_cnt num label='Number of Catalogs'
);quit; );quit;
proc datasets lib=&lib noprint; proc datasets lib=&lib noprint;
modify mpe_datastatus_libs; modify mpe_datastatus_libs;
@@ -183,14 +230,32 @@ proc datasets lib=&lib noprint;
/nomiss unique; /nomiss unique;
quit; quit;
proc sql; proc sql;
create table &lib..mpe_datastatus_OBJS(
TX_FROM float &notnull format=datetime19.,
TX_TO float &notnull format=datetime19.,
libref char(8) label='Library Name',
memname char(64) label='Member Name',
objname char(32) label='Object Name',
objtype char(8) label='Object Type',
created num &notnull format=DATETIME. label='Date Created',
modified num format=DATETIME. label='Date Modified',
level num label='Library Concatenation Level'
);quit;
proc datasets lib=&lib noprint;
modify mpe_datastatus_OBJS;
index create
pk_mpe_datastatus_OBJS=(libref memname objname objtype tx_to)
/nomiss unique;
quit;
proc sql;
create table &lib..mpe_datastatus_tabs( create table &lib..mpe_datastatus_tabs(
TX_FROM num &notnull format=datetime19.3, TX_FROM num &notnull format=datetime19.3,
TX_TO num &notnull format=datetime19.3, TX_TO num &notnull format=datetime19.3,
libref char(8) label='Library Name', libref char(8) label='Library Name',
dsn char(64) label='Member Name', dsn char(64) label='Member Name',
filesize num format=SIZEKMG. label='Size of file', filesize num format=SIZEKMG. label='Size of file',
crdate num format=DATETIME. informat=DATETIME. label='Date Created', crdate num format=DATETIME. label='Date Created',
modate num format=DATETIME. informat=DATETIME. label='Date Modified', modate num format=DATETIME. label='Date Modified',
nobs num label='Number of Physical (Actual, inc. deleted) Observations' nobs num label='Number of Physical (Actual, inc. deleted) Observations'
);quit; );quit;
proc datasets lib=&lib noprint; proc datasets lib=&lib noprint;

View File

@@ -0,0 +1,26 @@
/**
@file
@brief Testing mpe_refreshtables macro
<h4> SAS Macros </h4>
@li mpe_makedatamodel.sas
@li mp_assert.sas
@li mp_assertscope.sas
@author 4GL Apps Ltd
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
and may not be re-distributed or re-sold without the express permission of
4GL Apps Ltd.
**/
%mp_assertscope(SNAPSHOT)
%mpe_makedatamodel(lib=WORK)
%mp_assertscope(COMPARE,
desc=Checking macro variables against previous snapshot
)
%mp_assert(
iftrue=(&syscc = 0),
desc=Checking error condition
)

View File

@@ -0,0 +1,140 @@
/**
@file mpe_refreshtables.sas
@brief Refreshes the data catalog
@details Assumes library is already assigned.
Usage:
%mpe_refreshcatalogs(sashelp)
<h4> SAS Macros </h4>
@li bitemporal_dataloader.sas
@version 9.3
@author 4GL Apps Ltd
**/
%macro mpe_refreshcatalogs(lib,cat=#all);
%let lib=%upcase(&lib);
%let cat=%upcase(&cat);
%put running &sysmacroname &lib for &cat;
proc sql;
create table work.catdata as
select libname as libref,
memname,
objname,
objtype,
objdesc,
created,
modified,
alias,
level
from dictionary.catalogs
where upcase(libname)="&lib"
%if &cat ne #ALL %then %do;
and upcase(memname)="&cat"
%end;
;
%mp_abort(iftrue= (&syscc ne 0)
,mac=&_program
,msg=%str(syscc=&syscc afer &lib objects extraction)
)
/* load mpe_datacatalog_CATS */
proc sql;
create table datacats as select distinct libref,memname from catdata;
%bitemporal_dataloader(base_lib=&mpelib
,base_dsn=mpe_datacatalog_CATS
,append_dsn=datacats
,PK=LIBREF MEMNAME
,etlsource=&sysmacroname
,loadtype=TXTEMPORAL
,tech_from=TX_FROM
,tech_to=TX_TO
%if &cat = #ALL %then %do;
,close_vars=LIBREF
%end;
,dclib=&mpelib
)
/* load mpe_datacatalog_objsS */
proc sql;
create table dataobjs as
select distinct libref,
memname,
objname,
objtype,
objdesc,
alias
from catdata;
quit;
%bitemporal_dataloader(base_lib=&mpelib
,base_dsn=mpe_datacatalog_OBJS
,append_dsn=dataobjs
,PK=LIBREF MEMNAME OBJNAME OBJTYPE
,etlsource=&sysmacroname
,loadtype=TXTEMPORAL
,tech_from=TX_FROM
,tech_to=TX_TO
%if &cat = #ALL %then %do;
,close_vars=LIBREF MEMNAME
%end;
,dclib=&mpelib
)
%put load mpe_datastatus_OBJS;
proc sql;
create table statusobjs as
select distinct libref,
memname,
objname,
objtype,
created,
modified,
level
from catdata;
%bitemporal_dataloader(base_lib=&mpelib
,base_dsn=mpe_datastatus_OBJS
,append_dsn=statusobjs
,PK=LIBREF MEMNAME OBJNAME OBJTYPE
,etlsource=&sysmacroname
,loadtype=TXTEMPORAL
,tech_from=TX_FROM
,tech_to=TX_TO
%if &cat = #ALL %then %do;
,close_vars=LIBREF MEMNAME
%end;
,dclib=&mpelib
)
%put load mpe_datastatus_cats;
proc sql;
create table statuscats as
select libref,
memname,
count(*) as nobjs,
min(created) as created,
max(modified) as modified
from catdata
group by 1,2;
%bitemporal_dataloader(base_lib=&mpelib
,base_dsn=mpe_datastatus_cats
,append_dsn=statuscats
,PK=LIBREF MEMNAME
,etlsource=&sysmacroname
,loadtype=TXTEMPORAL
,tech_from=TX_FROM
,tech_to=TX_TO
%if &cat = #ALL %then %do;
,close_vars=LIBREF
%end;
,dclib=&mpelib
)
%mend mpe_refreshcatalogs;

View File

@@ -0,0 +1,38 @@
/**
@file
@brief Testing mpe_refreshcatalogs macro
<h4> SAS Macros </h4>
@li mpe_refreshcatalogs.sas
@li mp_assert.sas
@li mp_assertscope.sas
@li mp_ds2md.sas
@author 4GL Apps Ltd
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
and may not be re-distributed or re-sold without the express permission of
4GL Apps Ltd.
**/
%mp_assertscope(SNAPSHOT)
%mpe_refreshcatalogs(FMTONLY)
%mp_assertscope(COMPARE,
desc=Checking macro variables against previous snapshot
)
/* make sure that the process picks up the catalog */
proc sql noprint;
create table work.test1 as
select *
from &mpelib..mpe_datacatalog_cats(where=(&dc_dttmtfmt. lt tx_to))
where libref="FMTONLY";
%let test1=0;
select count(*) into: test1 from work.test1;
%mp_assert(
iftrue=(&test1>0),
desc=Checking fmtonly.dcfmts was picked up
)
%mp_ds2md(work.test1)

View File

@@ -48,11 +48,6 @@ create table cols as
,msg=%str(syscc=&syscc afer &lib cols extraction) ,msg=%str(syscc=&syscc afer &lib cols extraction)
) )
%mp_abort(iftrue= (&syscc ne 0)
,mac=&_program
,msg=%str(syscc=&syscc afer &lib indexes extraction)
)
%if &engine=SQLSVR %then %do; %if &engine=SQLSVR %then %do;
proc sql; proc sql;
connect using &lib; connect using &lib;
@@ -175,6 +170,11 @@ create table cols as
%end; %end;
%mp_abort(iftrue= (&syscc ne 0)
,mac=&_program
,msg=%str(syscc=&syscc afer &lib indexes extraction)
)
/* load columns */ /* load columns */
%bitemporal_dataloader(base_lib=&mpelib %bitemporal_dataloader(base_lib=&mpelib
,base_dsn=mpe_datacatalog_vars ,base_dsn=mpe_datacatalog_vars
@@ -295,7 +295,7 @@ proc sql;
%if &ds ne #ALL %then %do; %if &ds ne #ALL %then %do;
and upcase(memname)="&ds" and upcase(memname)="&ds"
%end; %end;
; ;
%end; %end;
%bitemporal_dataloader(base_lib=&mpelib %bitemporal_dataloader(base_lib=&mpelib
@@ -314,12 +314,32 @@ proc sql;
%if &ds = #ALL %then %do; %if &ds = #ALL %then %do;
proc sql; proc sql;
create table statuslibs as select create table work.sumcat as
select libname as libref,
count(distinct memname) as catalog_cnt
from dictionary.catalogs
where upcase(libname)="&lib"
group by 1;
create table work.sumdsn as select
libref libref
,sum(filesize) as libsize ,sum(filesize) as libsize
,count(*) as table_cnt ,count(*) as table_cnt
from statustabs from statustabs
group by 1; group by 1;
create table work.libs as
select libref from work.sumcat
union
select libref from work.sumdsn;
create table work.statuslibs as
select a.libref,
b.libsize,
b.table_cnt,
c.catalog_cnt
from work.libs a
left join work.sumdsn b
on a.libref=b.libref
left join work.sumcat c
on a.libref=c.libref;
%bitemporal_dataloader(base_lib=&mpelib %bitemporal_dataloader(base_lib=&mpelib
,base_dsn=mpe_datastatus_libs ,base_dsn=mpe_datastatus_libs

View File

@@ -0,0 +1,50 @@
/**
@file
@brief Testing mpe_refreshtables macro
<h4> SAS Macros </h4>
@li mpe_refreshtables.sas
@li mp_assert.sas
@li mp_assertscope.sas
@author 4GL Apps Ltd
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
and may not be re-distributed or re-sold without the express permission of
4GL Apps Ltd.
**/
%mp_assertscope(SNAPSHOT)
%mpe_refreshtables(FMTONLY)
%mp_assertscope(COMPARE,
desc=Checking macro variables against previous snapshot
)
/* make sure that the process picks up a library that contains only a single
catalog */
proc sql;
create table work.libinfo as
select a.engine,
a.libname,
a.paths,
a.perms,
a.owners,
a.schemas,
a.libid,
b.libsize,
b.table_cnt,
b.catalog_cnt
from &mpelib..mpe_datacatalog_libs(where=(&dc_dttmtfmt. lt tx_to)) a
left join &mpelib..mpe_datastatus_libs(where=(&dc_dttmtfmt. lt tx_to)) b
on a.libref=b.libref
where a.libref="&libref";
%let test1=0;
data _null_;
set work.libinfo;
call symputx('test1',table_cnt);
run;
%mp_assert(
iftrue=(&test1>0),
desc=Checking fmtonly.dcfmts was picked up
)

View File

@@ -94,38 +94,41 @@
}, },
{ {
"name": "viya", "name": "viya",
"serverUrl": "https://sas.4gl.io", "serverUrl": "https://viya-f0g8ht62vq.engage.sas.com",
"serverType": "SASVIYA", "serverType": "SASVIYA",
"httpsAgentOptions": { "httpsAgentOptions": {
"allowInsecureRequests": false "allowInsecureRequests": false
}, },
"appLoc": "/Public/app/dcplaceholder", "appLoc": "/Public/app/dcplaceholder",
"deployConfig": {
"deployServicePack": true,
"deployScripts": [
"sasjs/utils/viyadeploy.sh"
]
},
"macroFolders": [ "macroFolders": [
"sasjs/targets/viya/macros_viya" "sasjs/targets/viya/macros_viya"
], ],
"programFolders": [ "programFolders": [
"sasjs/db/datactrl" "sasjs/db/datactrl"
], ],
"binaryFolders": [],
"buildConfig": { "buildConfig": {
"initProgram": "sasjs/utils/buildinitviya.sas", "initProgram": "sasjs/utils/buildinitviya.sas",
"buildResultsFolder": "sasjsresults", "buildResultsFolder": "sasjsresults",
"buildOutputFolder": "sasjsbuild", "buildOutputFolder": "sasjsbuild",
"buildOutputFileName": "viya.sas" "buildOutputFileName": "viya.sas",
"termProgram": "",
"macroVars": {}
}, },
"serviceConfig": { "serviceConfig": {
"initProgram": "sasjs/utils/serviceinitviya.sas",
"serviceFolders": [ "serviceFolders": [
"sasjs/targets/viya/services_viya/viya_users", "sasjs/targets/viya/services_viya/viya_users",
"sasjs/targets/viya/services_viya/admin", "sasjs/targets/viya/services_viya/admin",
"sasjs/targets/viya/services_viya/public" "sasjs/targets/viya/services_viya/public"
] ],
}, "initProgram": "sasjs/utils/serviceinitviya.sas",
"deployConfig": { "termProgram": "",
"deployServicePack": true, "macroVars": {}
"deployScripts": [
"sasjs/utils/viyadeploy.sh"
]
}, },
"streamConfig": { "streamConfig": {
"streamWeb": true, "streamWeb": true,
@@ -137,54 +140,6 @@
}, },
"contextName": "SAS Job Execution compute context" "contextName": "SAS Job Execution compute context"
}, },
{
"name": "viyacloud",
"serverUrl": "https://4gl.viyacloud.sas.com",
"serverType": "SASVIYA",
"httpsAgentOptions": {
"rejectUnauthorized": false,
"allowInsecureRequests": true
},
"appLoc": "/30.SASApps/app/dc",
"macroFolders": [
"sasjs/targets/viya/macros_viya"
],
"programFolders": [
"sasjs/db/datactrl"
],
"buildConfig": {
"initProgram": "sasjs/utils/buildinitviya.sas",
"termProgram": "sasjs/utils/buildtermviya.sas",
"macroVars": {
"dcpath": "/opt/sas/viya/config/var/tmp/dc",
"adminGroup": "DataBuilders"
},
"buildResultsFolder": "sasjsresults",
"buildOutputFolder": "sasjsbuild",
"buildOutputFileName": "viya.sas"
},
"serviceConfig": {
"initProgram": "sasjs/utils/serviceinitviya.sas",
"serviceFolders": [
"sasjs/targets/viya/services_viya/viya_users",
"sasjs/targets/viya/services_viya/admin",
"sasjs/targets/viya/services_viya/public"
],
"termProgram": "",
"macroVars": {}
},
"streamConfig": {
"streamWeb": true,
"streamWebFolder": "webv",
"webSourcePath": "../client/dist",
"streamServiceName": "clickme",
"assetPaths": []
},
"deployConfig": {
"deployServicePack": true
},
"contextName": "Datacontroller compute context"
},
{ {
"name": "vtest", "name": "vtest",
"appLoc": "/30.SASApps/app/vtest", "appLoc": "/30.SASApps/app/vtest",

View File

@@ -675,7 +675,7 @@ data xl_rules;
keep xl_column xl_rule; keep xl_column xl_rule;
run; run;
%mpe_dsmeta(&libds, outds=dsmeta) %mpe_dsmeta(&orig_libds, outds=dsmeta)
%mpe_getversions(&mpelib, %mpe_getversions(&mpelib,
%scan(&orig_libds,1,.), %scan(&orig_libds,1,.),

View File

@@ -54,7 +54,10 @@ data _null_;
/* if a TXTEMPORAL table then filter as such */ /* if a TXTEMPORAL table then filter as such */
call symputx('txfrom',var_txfrom); call symputx('txfrom',var_txfrom);
call symputx('txto',var_txto); call symputx('txto',var_txto);
run;
/* if a format, extract relevant info */
data _null_;
ds=symget('ds'); ds=symget('ds');
is_fmt=0; is_fmt=0;
if subpad(cats(reverse(ds)),1,3)=:'CF-' then do; if subpad(cats(reverse(ds)),1,3)=:'CF-' then do;

View File

@@ -76,3 +76,37 @@ run;
test=EQUALS 3, test=EQUALS 3,
outds=work.test_results outds=work.test_results
) )
/* test 3 - when there is a format catalog / nothing else in the library */
data work.params3;
length name $32 value $1000;
name='type';value='SAS';output;
name='table';value="FMTONLY.DCFMTS-FC";output;
name='filter';value='0';output;
run;
%mx_testservice(&_program,
viyacontext=&defaultcontext,
inputparams=work.params3,
outref=web3,
viyaresult=WEBOUT_TXT
)
data work.results3;
infile web3;
input;
putlog _infile_;
if _infile_=:'datalines4;' then do;
output;
output;
output;
stop;
end;
if _n_>100 then stop;
run;
%mp_assertdsobs(work.results3,
desc=datalines file is successfully returned for LONE format catalog,
test=EQUALS 3,
outds=work.test_results
)

View File

@@ -1,85 +1,86 @@
/** /**
@file refreshlibinfo.sas @file refreshlibinfo.sas
@brief Refresh the Data Catalog for a particular library @brief Refresh the Data Catalog for a particular library
@details When showing library info in the VIEW menu, the data is taken from @details When showing library info in the VIEW menu, the data is taken from
the Data Catalog tables. These may be empty or outdated, and so this service the Data Catalog tables. These may be empty or outdated, and so this service
allows end users to run a refresh of the data. allows end users to run a refresh of the data.
<h4> Service Inputs </h4> <h4> Service Inputs </h4>
<h5> lib2refresh </h5> <h5> lib2refresh </h5>
Should contain the libref to be refreshed. Should contain the libref to be refreshed.
|libref:$8.| |libref:$8.|
|---| |---|
|SOMELIB| |SOMELIB|
<h4> Service Outputs </h4> <h4> Service Outputs </h4>
<h5> libinfo </h5> <h5> libinfo </h5>
|engine $|libname $|paths $|perms $|owners $|schemas $ |libid $|libsize $|table_cnt | |engine $|libname $|paths $|perms $|owners $|schemas $ |libid $|libsize $|table_cnt |
|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|
|V9|SOMELIB|"some/path"|rwxrwxr-x|sassrv|` `|` `|636MB|33| |V9|SOMELIB|"some/path"|rwxrwxr-x|sassrv|` `|` `|636MB|33|
<h4> SAS Macros </h4> <h4> SAS Macros </h4>
@li dc_assignlib.sas @li dc_assignlib.sas
@li dc_refreshcatalog.sas @li dc_refreshcatalog.sas
@li mp_abort.sas @li mp_abort.sas
@version 9.3 @version 9.3
@author 4GL Apps Ltd @author 4GL Apps Ltd
@copyright 4GL Apps Ltd. This code may only be used within Data Controller @copyright 4GL Apps Ltd. This code may only be used within Data Controller
and may not be re-distributed or re-sold without the express permission of and may not be re-distributed or re-sold without the express permission of
4GL Apps Ltd. 4GL Apps Ltd.
**/ **/
%mpeinit() %mpeinit()
%webout(FETCH) %webout(FETCH)
%mp_abort(iftrue= (&syscc ne 0) %mp_abort(iftrue= (&syscc ne 0)
,msg=%str(syscc=&syscc Problem on startup) ,msg=%str(syscc=&syscc Problem on startup)
) )
%let libref=; %let libref=;
data _null_; data _null_;
set work.lib2refresh; set work.lib2refresh;
call symputx('libref',libref); call symputx('libref',libref);
run; run;
%mp_abort(iftrue= (&syscc ne 0) %mp_abort(iftrue= (&syscc ne 0)
,msg=%str(syscc=&syscc Problem with inputs - was lib2refresh object sent?) ,msg=%str(syscc=&syscc Problem with inputs - was lib2refresh object sent?)
) )
%dc_assignlib(WRITE,&libref) %dc_assignlib(WRITE,&libref)
%mp_abort(iftrue= (&syscc ne 0) %mp_abort(iftrue= (&syscc ne 0)
,msg=%str(syscc=&syscc after lib assignment) ,msg=%str(syscc=&syscc after lib assignment)
) )
%dc_refreshcatalog(&libref) %dc_refreshcatalog(&libref)
%mp_abort(iftrue= (&syscc ne 0) %mp_abort(iftrue= (&syscc ne 0)
,msg=%str(syscc=&syscc Problem when running the catalog refresh) ,msg=%str(syscc=&syscc Problem when running the catalog refresh)
) )
/* get libinfo */ /* get libinfo */
proc sql; proc sql;
create table work.libinfo as create table work.libinfo as
select a.engine, select a.engine,
a.libname, a.libname,
a.paths, a.paths,
a.perms, a.perms,
a.owners, a.owners,
a.schemas, a.schemas,
a.libid, a.libid,
b.libsize, b.libsize,
b.table_cnt b.table_cnt,
from &mpelib..mpe_datacatalog_libs(where=(&dc_dttmtfmt. lt tx_to)) a b.catalog_cnt
inner join &mpelib..mpe_datastatus_libs(where=(&dc_dttmtfmt. lt tx_to)) b from &mpelib..mpe_datacatalog_libs(where=(&dc_dttmtfmt. lt tx_to)) a
on a.libref=b.libref inner join &mpelib..mpe_datastatus_libs(where=(&dc_dttmtfmt. lt tx_to)) b
where a.libref="&libref"; on a.libref=b.libref
where a.libref="&libref";
%webout(OPEN)
%webout(OBJ,libinfo) %webout(OPEN)
%webout(CLOSE) %webout(OBJ,libinfo)
%webout(CLOSE)

View File

@@ -355,7 +355,7 @@ run;
%mp_getcols(&libds, outds=cols) %mp_getcols(&libds, outds=cols)
%mpe_dsmeta(&libds, outds=dsmeta) %mpe_dsmeta(&orig_libds, outds=dsmeta)
%mpe_getversions(&mpelib, %mpe_getversions(&mpelib,
%scan(&orig_libds,1,.), %scan(&orig_libds,1,.),

View File

@@ -167,7 +167,8 @@ create table work.libinfo as
a.schemas, a.schemas,
a.libid, a.libid,
coalesce(b.libsize,0) as libsize, coalesce(b.libsize,0) as libsize,
coalesce(b.table_cnt,0) as table_cnt coalesce(b.table_cnt,0) as table_cnt,
coalesce(b.catalog_cnt,0) as catalog_cnt
from &mpelib..mpe_datacatalog_libs(where=(&dc_dttmtfmt. lt tx_to)) a from &mpelib..mpe_datacatalog_libs(where=(&dc_dttmtfmt. lt tx_to)) a
left join &mpelib..mpe_datastatus_libs(where=(&dc_dttmtfmt. lt tx_to)) b left join &mpelib..mpe_datastatus_libs(where=(&dc_dttmtfmt. lt tx_to)) b
on a.libref=b.libref on a.libref=b.libref

View File

@@ -9,6 +9,7 @@
<h4> SAS Macros </h4> <h4> SAS Macros </h4>
@li mpe_refreshlibs.sas @li mpe_refreshlibs.sas
@li dc_assignlib.sas @li dc_assignlib.sas
@li mpe_refreshcatalogs.sas
@li mpe_refreshtables.sas @li mpe_refreshtables.sas
@li mm_getrepos.sas @li mm_getrepos.sas
@@ -44,6 +45,7 @@ run;
%dc_assignlib(WRITE,&libref) /* write just in order to assign direct lib */ %dc_assignlib(WRITE,&libref) /* write just in order to assign direct lib */
%mpe_refreshlibs(lib=&libref) %mpe_refreshlibs(lib=&libref)
%mpe_refreshtables(&libref) %mpe_refreshtables(&libref)
%mpe_refreshcatalogs(&libref)
%end; %end;
%else %do xx=1 %to &repocnt; %else %do xx=1 %to &repocnt;
options metarepository=&&repo&xx; options metarepository=&&repo&xx;
@@ -73,6 +75,7 @@ run;
%do i=1 %to &libcnt; %do i=1 %to &libcnt;
%dc_assignlib(WRITE,&&lib&i) %dc_assignlib(WRITE,&&lib&i)
%mpe_refreshtables(&&lib&i) %mpe_refreshtables(&&lib&i)
%mpe_refreshcatalogs(&&lib&i)
%end; %end;
%end; %end;

View File

@@ -9,6 +9,7 @@
<h4> SAS Macros </h4> <h4> SAS Macros </h4>
@li mpe_refreshlibs.sas @li mpe_refreshlibs.sas
@li dc_assignlib.sas @li dc_assignlib.sas
@li mpe_refreshcatalogs.sas
@li mpe_refreshtables.sas @li mpe_refreshtables.sas
@@ -34,6 +35,8 @@ data libraries;
str=cats('%mpe_refreshtables(',libref,')'); str=cats('%mpe_refreshtables(',libref,')');
put str; put str;
putlog str; putlog str;
str=cats('%mpe_refreshcatalogs(',libref,')');
put str;
run; run;
%inc executor/source2; %inc executor/source2;

View File

@@ -10,6 +10,7 @@
@li mpe_refreshlibs.sas @li mpe_refreshlibs.sas
@li dc_assignlib.sas @li dc_assignlib.sas
@li mpe_refreshtables.sas @li mpe_refreshtables.sas
@li mpe_refreshcatalogs.sas
@version 3.4 @version 3.4
@@ -19,19 +20,29 @@
4GL Apps Ltd. 4GL Apps Ltd.
**/ **/
%macro dc_refreshcatalog(); %macro dc_refreshcatalog(libref);
%mpe_refreshlibs() %if #&libref# ne ## %then %do;
%put &sysmacroname: assigning specific libref, &libref;
filename executor catalog 'work.code.code.source'; %dc_assignlib(WRITE,&libref) /* write just in order to assign direct lib */
data libraries; %mpe_refreshlibs(lib=&libref)
set &mpelib..mpe_datacatalog_libs; %mpe_refreshtables(&libref)
where &dc_dttmtfmt. le TX_TO; %mpe_refreshcatalogs(&libref)
file executor; %end;
str=cats('%mpe_refreshtables(',libref,')'); %else %do;
put str; %mpe_refreshlibs()
putlog str; filename executor catalog 'work.code.code.source';
run; data libraries;
%inc executor; set &mpelib..mpe_datacatalog_libs;
where &dc_dttmtfmt. le TX_TO;
file executor;
str=cats('%mpe_refreshtables(',libref,')');
put str;
str=cats('%mpe_refreshcatalogs(',libref,')');
put str;
putlog str;
run;
%inc executor;
%end;
%mend dc_refreshcatalog; %mend dc_refreshcatalog;

View File

@@ -21,7 +21,7 @@ REMOVE THAT LAST MACRO
%let syscc=0; %let syscc=0;
%global apploc _program dclib defaultcontext _debug sasjs_mdebug dc_dttmtfmt; %global apploc _program dclib defaultcontext _debug sasjs_mdebug dc_dttmtfmt;
%let defaultcontext=Datacontroller compute context; %let defaultcontext=SAS Job Execution compute context;
%let sasjs_mdebug=0; %let sasjs_mdebug=0;
options mprint mprintnest nobomfile lrecl=32767; options mprint mprintnest nobomfile lrecl=32767;

View File

@@ -28,7 +28,7 @@
%global apploc _program; %global apploc _program;
%let defaultcontext=Datacontroller compute context; %let defaultcontext=SAS Job Execution compute context;
data _null_; data _null_;
length _pgm $1000; length _pgm $1000;
@@ -60,6 +60,9 @@ run;
%let testloc=%sysfunc(pathname(&DC_LIBREF))/fmt%mf_getuniquefileref(); %let testloc=%sysfunc(pathname(&DC_LIBREF))/fmt%mf_getuniquefileref();
%mf_mkdir(&testloc) %mf_mkdir(&testloc)
libname dctest "&testloc"; libname dctest "&testloc";
/* test library with only one format catalog */
%mf_mkdir(&testloc/fmtonly)
libname fmtonly "&testloc/fmtonly";
/* add formats */ /* add formats */
PROC FORMAT library=dctest.dcfmts; PROC FORMAT library=dctest.dcfmts;
@@ -99,6 +102,10 @@ run;
proc append base=&dc_libref..mpe_tables data=work.append; proc append base=&dc_libref..mpe_tables data=work.append;
run; run;
/* lone format catalog */
proc format cntlin=work.fmts library=fmtonly.dcfmts;
run;
/* add some other tables */ /* add some other tables */
%mp_coretable(LOCKTABLE,libds=dctest.locktable) %mp_coretable(LOCKTABLE,libds=dctest.locktable)
%mp_coretable(DIFFTABLE,libds=dctest.difftable) %mp_coretable(DIFFTABLE,libds=dctest.difftable)
@@ -130,6 +137,7 @@ options mprint;
put _infile_; put _infile_;
if last then do; if last then do;
put "libname dctest '&testloc';"; put "libname dctest '&testloc';";
put "libname fmtonly '&testloc/fmtonly';";
end; end;
run; run;
data _null_; data _null_;
@@ -151,6 +159,7 @@ options mprint;
put _infile_; put _infile_;
if last then do; if last then do;
put "libname dctest '&testloc';"; put "libname dctest '&testloc';";
put "libname fmtonly '&testloc/fmtonly';";
end; end;
run; run;
%ms_deletefile(&root/services/public/settings.sas) %ms_deletefile(&root/services/public/settings.sas)
@@ -182,6 +191,7 @@ options mprint;
put _infile_; put _infile_;
if last then do; if last then do;
put "libname dctest '&testloc';"; put "libname dctest '&testloc';";
put "libname fmtonly '&testloc/fmtonly';";
end; end;
run; run;
%mm_deletestp(target=&root/services/public/Data_Controller_Settings) %mm_deletestp(target=&root/services/public/Data_Controller_Settings)

View File

@@ -2,6 +2,11 @@
@file buildinitviya.sas @file buildinitviya.sas
@brief initialisation for viya build program @brief initialisation for viya build program
<h4> SAS Macros </h4>
@li mfv_getfolderpath.sas
@li mfv_getpathuri.sas
@li mv_createfolder.sas
**/ **/
options nonotes nomprint; options nonotes nomprint;
@@ -9,4 +14,8 @@ options nonotes nomprint;
/* update apploc to default to user home area if not set */ /* update apploc to default to user home area if not set */
%let apploc=%sysfunc(ifc("&apploc"="/Public/app/dcplaceholder" %let apploc=%sysfunc(ifc("&apploc"="/Public/app/dcplaceholder"
,/Users/&sysuserid/My Folder/Data Controller ,/Users/&sysuserid/My Folder/Data Controller
,&apploc)); ,&apploc));
/* ensure the correct casing of appLoc */
%mv_createfolder(path=&apploc)
%let apploc=%mfv_getfolderpath(%mfv_getpathuri(&apploc));