Compare commits

..

147 Commits

Author SHA1 Message Date
fa2c8eb839 Merge pull request 'Adding cypress videos to the artifacts' (#58) from ci into main
Some checks failed
Release / Build-production-and-ng-test (push) Successful in 7m5s
Release / Build-and-test-development (push) Successful in 11m21s
Release / release (push) Failing after 2m6s
Reviewed-on: #58
2023-10-16 14:24:10 +00:00
Mihajlo Medjedovic
ba67248155 chore: adding cypress videos to the artifacts
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m21s
2023-10-16 16:11:56 +02:00
a6d962bfaa Merge pull request 'Downgrading SheetJS' (#55) from sheetjs-downgrade into main
Some checks reported warnings
Release / Build-production-and-ng-test (push) Has been cancelled
Release / Build-and-test-development (push) Has been cancelled
Release / release (push) Has been cancelled
Reviewed-on: #55
2023-10-16 10:48:02 +00:00
Mihajlo Medjedovic
95cddb52d4 chore: package-lock
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m22s
2023-10-16 12:40:17 +02:00
Mihajlo Medjedovic
5a5118d775 chore: downgrading sheetjs
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 27s
2023-10-16 12:34:30 +02:00
1b1cdd7a4b Merge pull request 'Removing development branch jobs, moving test jobs to the release pipeline' (#54) from discard-development into main
Some checks failed
Release / Build-production-and-ng-test (push) Successful in 7m9s
Release / Build-and-test-development (push) Failing after 14m17s
Release / release (push) Has been skipped
Reviewed-on: #54
2023-10-16 09:46:34 +00:00
Mihajlo Medjedovic
a9ddf7f7dd chore: removing development branch JOBS, moving test jobs to the release pipeline
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m23s
2023-10-13 11:24:42 +02:00
semantic-release-bot
b54b3f1778 chore(release): 6.2.3 [skip ci]
## [6.2.3](https://git.datacontroller.io/dc/dc/compare/v6.2.2...v6.2.3) (2023-10-12)

### Bug Fixes

* bumping core library to avoid non-ascii char in mp_validatecols.sas. [#50](#50) ([11b06f6](11b06f6416))
* removing copyright symbol from mpe_alerts macro. [#50](#50) ([adb7eb7](adb7eb7755))
2023-10-12 09:42:44 +00:00
349a63c591 Merge pull request 'fix: removing non-ascii chars from SAS program headers' (#51) from issue50 into main
All checks were successful
Release / release (push) Successful in 11m21s
Reviewed-on: #51
2023-10-12 09:40:30 +00:00
293d33912f Merge pull request 'development' (#52) from development into issue50
Some checks reported warnings
Build / Build-and-ng-test (pull_request) Has been cancelled
Reviewed-on: #52
2023-10-12 09:38:43 +00:00
357b9849e7 Merge branch 'issue50' into development
Some checks reported warnings
Build / Build-and-ng-test (pull_request) Has been cancelled
Test / Build-and-test-development (push) Has been cancelled
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
Test / Build-production-and-ng-test (push) Has been cancelled
2023-10-12 09:38:19 +00:00
Mihajlo Medjedovic
0c8a9eef32 chore(ci): build and ng test fix
Some checks failed
Test / Build-production-and-ng-test (push) Successful in 7m30s
Test / Build-and-test-development (push) Failing after 14m29s
Test / Build-and-test-development-latest-adapter (push) Failing after 14m45s
Build / Build-and-ng-test (pull_request) Successful in 1m25s
2023-10-12 08:33:31 +02:00
Mihajlo Medjedovic
112b1d0da4 chore(ci): audit check fix
Some checks failed
Test / Build-production-and-ng-test (push) Failing after 2m22s
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
Test / Build-and-test-development (push) Has been cancelled
2023-10-12 08:26:35 +02:00
Mihajlo Medjedovic
a05007416a chore(ci): audit check fix
Some checks failed
Test / Build-production-and-ng-test (push) Failing after 2m25s
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
Test / Build-and-test-development (push) Has been cancelled
2023-10-12 08:21:58 +02:00
9f7dd55583 Merge pull request 'Release fix' (#49) from release-fix into development
Some checks failed
Test / Build-production-and-ng-test (push) Failing after 2m36s
Test / Build-and-test-development (push) Failing after 14m29s
Test / Build-and-test-development-latest-adapter (push) Failing after 14m30s
Reviewed-on: #49
2023-10-11 22:02:20 +00:00
11b06f6416 fix: bumping core library to avoid non-ascii char in mp_validatecols.sas. #50
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m30s
2023-10-11 22:57:07 +01:00
adb7eb7755 fix: removing copyright symbol from mpe_alerts macro. #50 2023-10-11 22:56:14 +01:00
Mihajlo Medjedovic
b776b80728 chore: making semantic-release fail if no release available
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m17s
2023-10-11 16:34:06 +02:00
Mihajlo Medjedovic
73a149ea7b chore: updated contributing.md with release instructions
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m18s
2023-10-09 13:02:13 +02:00
Mihajlo Medjedovic
ef8784093b chore: release is draft fix, release will update package.json version
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m18s
2023-10-09 12:46:17 +02:00
semantic-release-bot
b30c788e3d chore(release): 6.2.2 [skip ci]
## [6.2.2](https://git.datacontroller.io/dc/dc/compare/v6.2.1...v6.2.2) (2023-10-09)

### Bug Fixes

* updated SheetJS (crypto) to the latest ([8bd0dd2](8bd0dd22c2))
2023-10-09 09:52:20 +00:00
23899bdff3 Merge pull request 'fix: updated SheetJS (crypto) to the latest' (#48) from development into main
All checks were successful
Release / release (push) Successful in 11m13s
Reviewed-on: #48
2023-10-09 09:49:16 +00:00
Mihajlo Medjedovic
8bd0dd22c2 fix: updated SheetJS (crypto) to the latest
Some checks reported warnings
Test / Build-production-and-ng-test (push) Has been cancelled
Test / Build-and-test-development (push) Has been cancelled
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
Build / Build-and-ng-test (pull_request) Successful in 1m19s
2023-10-09 11:47:27 +02:00
semantic-release-bot
c55b00c74f chore(release): 6.2.1 [skip ci]
Some checks failed
Test / Build-production-and-ng-test (push) Failing after 2m3s
Test / Build-and-test-development (push) Successful in 11m49s
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
## [6.2.1](https://git.datacontroller.io/dc/dc/compare/v6.2.0...v6.2.1) (2023-10-09)

### Bug Fixes

* approve, history and submit pages grouped in review module ([e056ece](e056ece223))
* closes [#39](#39) upcase issue in MPE_SECURITY ([a00d31c](a00d31caf3))
* handsontable v13 ([6f482ec](6f482ec6d9))
* latest adapter ([5e30dc0](5e30dc0f89))
* sasjs/cli and sasjs/core updated to the latest ([8571e01](8571e01e44))
* updating editors/stagedata to address issues in particular viya configurations as described in issue [#33](#33) ([94ab949](94ab949df8))
* updating logic for REPLACE loadtype ([1f2ce55](1f2ce55f24))
2023-10-09 09:03:58 +00:00
c895f509b0 Merge pull request 'chore: fixed CI, trigger pending release' (#47) from development into main
All checks were successful
Release / release (push) Successful in 11m20s
Reviewed-on: #47
2023-10-09 09:00:52 +00:00
Mihajlo Medjedovic
5968915331 chore: fixed CI, trigger pending release
Some checks reported warnings
Test / Build-and-test-development (push) Has been cancelled
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
Test / Build-production-and-ng-test (push) Has been cancelled
Build / Build-and-ng-test (pull_request) Successful in 1m20s
2023-10-09 11:00:09 +02:00
Mihajlo Medjedovic
44ffc082f6 chore: changelog revert to 6.2.0
Some checks reported warnings
Release / release (push) Has been cancelled
Test / Build-and-test-development (push) Has been cancelled
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
Test / Build-production-and-ng-test (push) Has been cancelled
2023-10-09 10:37:06 +02:00
semantic-release-bot
b716ae5675 chore(release): 6.2.1 [skip ci]
## [6.2.1](https://git.datacontroller.io/dc/dc/compare/v6.2.0...v6.2.1) (2023-10-08)

### Bug Fixes

* approve, history and submit pages grouped in review module ([e056ece](e056ece223))
* closes [#39](#39) upcase issue in MPE_SECURITY ([a00d31c](a00d31caf3))
* handsontable v13 ([6f482ec](6f482ec6d9))
* latest adapter ([5e30dc0](5e30dc0f89))
* sasjs/cli and sasjs/core updated to the latest ([8571e01](8571e01e44))
* updating editors/stagedata to address issues in particular viya configurations as described in issue [#33](#33) ([94ab949](94ab949df8))
* updating logic for REPLACE loadtype ([1f2ce55](1f2ce55f24))
2023-10-08 19:33:42 +00:00
01a0b59494 Merge pull request 'package-lock re-generated for CI release to pass, fixed development branch CI yaml' (#46) from development into main
Some checks failed
Release / release (push) Failing after 6m57s
Reviewed-on: #46
2023-10-08 19:30:05 +00:00
Mihajlo Medjedovic
8ebc3da0bb chore(git): Merge branch 'main' into development
Some checks reported warnings
Build / Build-and-ng-test (pull_request) Has been cancelled
Test / Build-production-and-ng-test (push) Has been cancelled
Test / Build-and-test-development (push) Has been cancelled
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
2023-10-08 21:29:12 +02:00
Mihajlo Medjedovic
133577a4fa ci: fixed development yaml
Some checks failed
Test / Build-production-and-ng-test (push) Failing after 2m3s
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
Test / Build-and-test-development (push) Has been cancelled
Build / Build-and-ng-test (pull_request) Successful in 1m22s
2023-10-08 21:26:55 +02:00
Mihajlo Medjedovic
a19615db41 chore: reverting 6.2.1 release
Some checks reported warnings
Release / release (push) Has been cancelled
2023-10-08 21:22:29 +02:00
Mihajlo Medjedovic
32b212a6bf chore: package-lock fix 2023-10-08 21:21:35 +02:00
semantic-release-bot
00ec4529cd chore(release): 6.2.1 [skip ci]
## [6.2.1](https://git.datacontroller.io/dc/dc/compare/v6.2.0...v6.2.1) (2023-10-08)

### Bug Fixes

* approve, history and submit pages grouped in review module ([e056ece](e056ece223))
* closes [#39](#39) upcase issue in MPE_SECURITY ([a00d31c](a00d31caf3))
* handsontable v13 ([6f482ec](6f482ec6d9))
* latest adapter ([5e30dc0](5e30dc0f89))
* sasjs/cli and sasjs/core updated to the latest ([8571e01](8571e01e44))
* updating editors/stagedata to address issues in particular viya configurations as described in issue [#33](#33) ([94ab949](94ab949df8))
* updating logic for REPLACE loadtype ([1f2ce55](1f2ce55f24))
2023-10-08 19:08:09 +00:00
102d03888f Merge pull request 'Release fix' (#45) from development into main
Some checks failed
Release / release (push) Failing after 7m6s
Reviewed-on: #45
2023-10-08 19:06:03 +00:00
9f8247320e Merge pull request 'Master branch sync, release procedure fixing' (#44) from master-sync into development
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m20s
Reviewed-on: #44
2023-10-08 19:03:01 +00:00
Mihajlo Medjedovic
ef871de30e chore: fixing changelog
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m20s
2023-10-08 20:56:09 +02:00
semantic-release-bot
b3a15ce26b chore(release): 6.2.1 [skip ci]
## [6.2.1](https://git.datacontroller.io/dc/dc/compare/v6.2.0...v6.2.1) (2023-10-08)

### Bug Fixes

* approve, history and submit pages grouped in review module ([e056ece](e056ece223))
* closes [#39](#39) upcase issue in MPE_SECURITY ([a00d31c](a00d31caf3))
* handsontable v13 ([6f482ec](6f482ec6d9))
* latest adapter ([5e30dc0](5e30dc0f89))
* sasjs/cli and sasjs/core updated to the latest ([8571e01](8571e01e44))
* updating editors/stagedata to address issues in particular viya configurations as described in issue [#33](#33) ([94ab949](94ab949df8))
* updating logic for REPLACE loadtype ([1f2ce55](1f2ce55f24))
2023-10-08 16:56:17 +00:00
270695aec2 Merge pull request 'fix: triggering release' (#43) from development into main
Some checks failed
Release / release (push) Failing after 2m9s
Reviewed-on: #43
2023-10-08 16:53:54 +00:00
ad7392a326 Merge pull request 'chore: setting legacy-peer-deps in .npmrc' (#42) from npmrc into development
Some checks reported warnings
Build / Build-and-ng-test (pull_request) Has been cancelled
Reviewed-on: #42
2023-10-08 16:53:03 +00:00
92a50a42e2 chore: setting legacy-peer-deps in .npmrc
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m21s
2023-10-08 17:51:27 +01:00
semantic-release-bot
a3a8856d8c chore(release): 6.2.1 [skip ci]
## [6.2.1](https://git.datacontroller.io/dc/dc/compare/v6.2.0...v6.2.1) (2023-10-08)

### Bug Fixes

* approve, history and submit pages grouped in review module ([e056ece](e056ece223))
* closes [#39](#39) upcase issue in MPE_SECURITY ([a00d31c](a00d31caf3))
* handsontable v13 ([6f482ec](6f482ec6d9))
* latest adapter ([5e30dc0](5e30dc0f89))
* sasjs/cli and sasjs/core updated to the latest ([8571e01](8571e01e44))
* updating editors/stagedata to address issues in particular viya configurations as described in issue [#33](#33) ([94ab949](94ab949df8))
* updating logic for REPLACE loadtype ([1f2ce55](1f2ce55f24))
2023-10-08 16:49:20 +00:00
150c19b1b0 Merge pull request 'development' (#41) from development into main
Some checks failed
Release / release (push) Failing after 2m15s
Reviewed-on: #41
2023-10-08 16:46:19 +00:00
f04c51ee4e Merge pull request 'fix: closes #39 upcase issue in MPE_SECURITY' (#40) from issue39 into development
Some checks reported warnings
Build / Build-and-ng-test (pull_request) Has been cancelled
Reviewed-on: #40
2023-10-08 16:45:23 +00:00
c4338bf957 chore: updating tests, post edit hook, and access check macro. #39
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m22s
2023-10-08 17:42:26 +01:00
5b06f4ede8 chore: improving docs for mpe_accesscheck and adding a test for mpe_accesscheck
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m27s
2023-10-07 22:46:32 +01:00
e7ab2cc956 chore: adding mpe_security_postedit hook link in MPE_TABLES on deploy. #39
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m28s
2023-10-07 18:28:36 +01:00
5ebf8a66f7 chore: error message source file fix
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m19s
2023-10-07 00:16:05 +01:00
3d4e886b9b chore: adding missing dependency
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m22s
2023-10-07 00:13:48 +01:00
a00d31caf3 fix: closes #39 upcase issue in MPE_SECURITY
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m23s
adding frontend validation rule, backend upcase enforcement rule, and modification to service code to ensure values are upcased before comparison
2023-10-07 00:11:38 +01:00
40fe707287 Merge pull request 'maincopy' (#38) from maincopy into development
Reviewed-on: #38
2023-09-26 07:40:05 +00:00
8296be01ba chore: cleanup of items in changelog for v6.2.1
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m19s
2023-09-26 08:38:40 +01:00
semantic-release-bot
dbeb003292 chore(release): 6.2.1 [skip ci]
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m25s
## [6.2.1](https://git.datacontroller.io/dc/dc/compare/v6.2.0...v6.2.1) (2023-09-25)

### Bug Fixes

* approve, history and submit pages grouped in review module ([e056ece](e056ece223))
* handsontable v13 ([6f482ec](6f482ec6d9))
* latest adapter ([5e30dc0](5e30dc0f89))
* sasjs/cli and sasjs/core updated to the latest ([8571e01](8571e01e44))
* updating editors/stagedata to address issues in particular viya configurations as described in issue [#33](#33) ([94ab949](94ab949df8))
* updating logic for REPLACE loadtype ([1f2ce55](1f2ce55f24))
2023-09-25 14:59:38 +00:00
f048501c48 Merge pull request 'development' (#37) from development into main
All checks were successful
Release / release (push) Successful in 11m11s
Reviewed-on: #37
2023-09-25 14:57:44 +00:00
498350b3f3 Merge pull request 'master-sync' (#36) from master-sync into development
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m18s
Reviewed-on: #36
Reviewed-by: allan <allan@4gl.io>
2023-09-25 14:55:33 +00:00
Mihajlo Medjedovic
91e82c9c65 ci: npm ci in build.yaml
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m19s
2023-09-25 16:53:40 +02:00
Mihajlo Medjedovic
24067ea82b chore: calling licence checker script as a part of PR action
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 24s
2023-09-25 16:51:33 +02:00
semantic-release-bot
aa7deddba0 chore(release): 6.2.1 [skip ci]
## [6.2.1](https://git.datacontroller.io/dc/dc/compare/v6.2.0...v6.2.1) (2023-09-25)

### Bug Fixes

* approve, history and submit pages grouped in review module ([e056ece](e056ece223))
* handsontable v13 ([6f482ec](6f482ec6d9))
* latest adapter ([5e30dc0](5e30dc0f89))
* sasjs/cli and sasjs/core updated to the latest ([8571e01](8571e01e44))
* updating editors/stagedata to address issues in particular viya configurations as described in issue [#33](#33) ([94ab949](94ab949df8))
* updating logic for REPLACE loadtype ([1f2ce55](1f2ce55f24))
2023-09-25 14:33:27 +00:00
b2d13203d1 Merge pull request 'development' (#34) from development into main
Some checks failed
Release / release (push) Failing after 2m25s
Reviewed-on: #34
2023-09-25 14:31:25 +00:00
19c1092b5b Merge pull request 'fix: latest adapter' (#32) from adapter-bump into development
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 25s
Reviewed-on: #32
2023-09-25 14:26:49 +00:00
Mihajlo Medjedovic
94ab949df8 fix: updating editors/stagedata to address issues in particular viya configurations as described in issue #33
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 25s
2023-09-25 16:24:50 +02:00
Mihajlo Medjedovic
9eb2451c2f chore(git): Merge branch 'adapter-bump' of ssh://git.datacontroller.io:29419/dc/dc into adapter-bump 2023-09-07 12:52:25 +02:00
zver
6e521bfa3e chore: renaming format names due to changes in how Viya treats the last letter
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 23s
2023-09-07 11:49:29 +01:00
Mihajlo Medjedovic
239720fe0c chore(git): Merge branch 'development' into adapter-bump 2023-09-07 12:13:17 +02:00
Mihajlo Medjedovic
8571e01e44 fix: sasjs/cli and sasjs/core updated to the latest 2023-09-07 12:12:09 +02:00
Mihajlo Medjedovic
6f482ec6d9 fix: handsontable v13
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 23s
2023-09-06 21:57:11 +02:00
Mihajlo Medjedovic
5e30dc0f89 fix: latest adapter
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 24s
2023-09-06 17:14:49 +02:00
semantic-release-bot
3d76d12c86 chore(release): 6.2.1 [skip ci]
## [6.2.1](https://git.datacontroller.io/dc/dc/compare/v6.2.0...v6.2.1) (2023-08-25)

### Bug Fixes

* approve, history and submit pages grouped in review module ([e056ece](e056ece223))
* updating logic for REPLACE loadtype ([1f2ce55](1f2ce55f24))
2023-08-25 11:46:06 +00:00
93f1b81d70 Merge pull request 'Refactoring solo pages into a modules' (#19) from tsdoc into development
Some checks failed
Release / release (push) Failing after 2m2s
Build / Build-and-ng-test (pull_request) Successful in 24s
Reviewed-on: #19
Reviewed-by: allan <allan@4gl.io>
2023-08-25 11:24:10 +00:00
e2b65ddd82 Merge branch 'development' into tsdoc
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 22s
2023-08-25 11:19:38 +00:00
b64bbe91d4 Merge pull request 'fix: updating logic for REPLACE loadtype' (#25) from replace into development
Reviewed-on: #25
2023-08-25 11:19:16 +00:00
1f2ce55f24 fix: updating logic for REPLACE loadtype
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 24s
Staged rows are always considered as NEW, and previous rows are considered DELETED
This is consistent with the fact that REPLACE involves a delete *, followed by an append.
2023-08-25 12:17:43 +01:00
Mihajlo Medjedovic
921157da9e chore(git): Merge branch 'development' into tsdoc
Some checks reported warnings
Build / Build-and-ng-test (pull_request) Successful in 23s
Release / release (push) Has been cancelled
2023-08-24 12:17:01 +02:00
413acf7d05 Merge pull request 'main to dev' (#23) from main into development
Reviewed-on: #23
2023-08-24 10:12:26 +00:00
Mihajlo Medjedovic
725f75aa74 style: lint
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 23s
2023-08-24 11:52:02 +02:00
Mihajlo Medjedovic
c64ab8a577 chore(git): Merge branch 'tsdoc' of ssh://git.datacontroller.io:29419/dc/dc into tsdoc
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 22s
2023-08-24 11:50:53 +02:00
Mihajlo Medjedovic
1154c99e0a ci: surfer install 2023-08-24 11:44:54 +02:00
5bcdef77b8 Merge branch 'development' into tsdoc
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 22s
2023-08-24 09:43:45 +00:00
semantic-release-bot
e4a0089102 chore(release): 6.2.0 [skip ci]
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 23s
# [6.2.0](https://git.datacontroller.io/dc/dc/compare/v6.1.0...v6.2.0) (2023-08-24)

### Bug Fixes

* re-enabling full REPLACE uploads ([08e39c4](08e39c4fca))

### Features

* support for European numeric formats ([e48e47b](e48e47bc63))
2023-08-24 08:53:36 +00:00
Mihajlo Medjedovic
ba022d8a35 ci: surfer install
Some checks failed
Release / release (push) Failing after 11m15s
2023-08-24 10:51:38 +02:00
semantic-release-bot
c8ac859d2e chore(release): 6.2.0 [skip ci]
# [6.2.0](https://git.datacontroller.io/dc/dc/compare/v6.1.0...v6.2.0) (2023-08-24)

### Bug Fixes

* re-enabling full REPLACE uploads ([08e39c4](08e39c4fca))

### Features

* support for European numeric formats ([e48e47b](e48e47bc63))
2023-08-24 08:30:06 +00:00
d6fd72e880 Merge pull request 'Create Release' (#22) from development into main
Some checks failed
Release / release (push) Failing after 11m15s
Reviewed-on: #22
2023-08-23 23:12:38 +00:00
61cc360c85 Merge pull request 'fix: re-enabling full REPLACE uploads' (#21) from issue20 into development
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 23s
Reviewed-on: #21
2023-08-23 23:10:23 +00:00
08e39c4fca fix: re-enabling full REPLACE uploads
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 24s
Implemented by provision of the necessary temp tables
2023-08-24 00:09:25 +01:00
8c2ee441fc Merge pull request 'Added support for European numeric formats' (#16) from issue-1 into development
Reviewed-on: #16
Reviewed-by: allan <allan@4gl.io>
2023-08-23 14:46:37 +00:00
0a7d23c763 Merge branch 'development' into issue-1
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 25s
2023-08-23 14:45:59 +00:00
Mihajlo Medjedovic
7b54fff26e chore: typo fix
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 22s
2023-08-04 12:18:31 +02:00
Mihajlo Medjedovic
cd3e0f614b chore: adding comments part 3
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 23s
2023-08-04 12:05:00 +02:00
Mihajlo Medjedovic
81c0aec202 chore: removed column.ts leftover 2023-08-04 08:52:53 +02:00
Mihajlo Medjedovic
3193bdd720 chore: removing cellValidation leftover 2023-08-03 22:33:16 +02:00
Mihajlo Medjedovic
b9a12454e1 chore: removing table.ts leftover 2023-08-03 22:31:59 +02:00
Mihajlo Medjedovic
01a857f7c6 chore: adding comments part 2 2023-08-03 22:31:17 +02:00
Mihajlo Medjedovic
2bb2eee80e chore: removed commented code about old workaround for excel upload validation 2023-08-03 21:58:50 +02:00
Mihajlo Medjedovic
c054ea500d chore: adding comments part1 2023-08-03 17:56:34 +02:00
Mihajlo Medjedovic
d7f8201246 ci: tsdoc -> webdoc
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 22s
2023-08-02 09:24:16 +02:00
Mihajlo Medjedovic
622cfcc6fe chore: removed edit-route leftover
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 21s
2023-08-01 16:45:18 +02:00
Mihajlo Medjedovic
303240e4d2 style: lint
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 23s
2023-08-01 16:40:03 +02:00
Mihajlo Medjedovic
a8b849aede chore: modularization refactor finish 2023-08-01 16:39:45 +02:00
Mihajlo Medjedovic
ca281b70c9 chore(git): Merge branch 'development' into tsdoc
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 22s
2023-08-01 14:52:53 +02:00
Mihajlo Medjedovic
e056ece223 fix: approve, history and submit pages grouped in review module
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m5s
Using compodoc instead of typedoc because of better angular support.
2023-08-01 14:50:04 +02:00
0ae35214fb Merge pull request 'Removed ng build and ng test from PR action' (#17) from ci into development
Reviewed-on: #17
Reviewed-by: allan <allan@4gl.io>
2023-07-31 12:05:32 +00:00
Mihajlo Medjedovic
34ffac39cb ci: PR will only run lint check
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 23s
2023-07-31 13:13:00 +02:00
Mihajlo Medjedovic
52d4b3eefc chore(git): Merge branch 'issue-1' of ssh://git.datacontroller.io:29419/dc/dc into issue-1
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 7m52s
2023-07-31 10:12:40 +02:00
Mihajlo Medjedovic
908d2761f2 chore: addressing comments 2023-07-31 10:12:22 +02:00
7c98ad8c5b Merge branch 'development' into issue-1
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 7m40s
2023-07-28 18:10:25 +00:00
Mihajlo Medjedovic
5bb55e6484 style: lint
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 7m39s
2023-07-28 20:03:58 +02:00
Mihajlo Medjedovic
e48e47bc63 feat: support for European numeric formats 2023-07-28 20:03:41 +02:00
2a3f4f755c Merge pull request 'Releasing of tsdoc.datacontroller.io' (#15) from ci into development
All checks were successful
Test / Build-and-test-development (push) Successful in 11m20s
Test / Build-and-test-development-latest-adapter (push) Successful in 11m23s
Reviewed-on: #15
Reviewed-by: allan <allan@4gl.io>
2023-07-27 19:34:16 +00:00
Mihajlo Medjedovic
ddfae9227c chore(git): Merge branch 'development' into ci
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 7m31s
2023-07-27 19:26:06 +02:00
c74378423d Merge pull request 'chore: improving the descriptions of the steps in the release.yaml file' (#11) from releasedoc into development
All checks were successful
Test / Build-and-test-development (push) Successful in 11m25s
Test / Build-and-test-development-latest-adapter (push) Successful in 11m25s
Reviewed-on: #11
Reviewed-by: mihajlo <mihajlo@4gl.io>
2023-07-27 17:24:51 +00:00
Mihajlo Medjedovic
0e020623a5 chore(git): Merge branch 'development' into releasedoc
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 7m34s
2023-07-27 19:23:57 +02:00
Mihajlo Medjedovic
eeb4000efe ci: releasing typedoc 2023-07-27 19:20:14 +02:00
2053c858ac Merge pull request 'Added cypress videos to the build artifacts' (#14) from ci into development
All checks were successful
Test / Build-and-test-development (push) Successful in 11m29s
Test / Build-and-test-development-latest-adapter (push) Successful in 11m34s
Reviewed-on: #14
Reviewed-by: allan <allan@4gl.io>
2023-07-27 12:58:08 +00:00
Mihajlo Medjedovic
4c2c5d5526 style: lint
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 7m38s
2023-07-27 13:49:13 +02:00
Mihajlo Medjedovic
7ccc745eb2 chore(git): Merge branch 'development' into ci
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m2s
2023-07-27 13:15:39 +02:00
Mihajlo Medjedovic
e93367fb42 ci: capturing cypress videos
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m3s
2023-07-27 13:09:16 +02:00
250fb1e8cf Merge pull request 'ci: release' (#13) from ci into development
Some checks failed
Test / Build-and-test-development (push) Failing after 12m32s
Test / Build-and-test-development-latest-adapter (push) Successful in 11m15s
Reviewed-on: #13
Reviewed-by: allan <allan@4gl.io>
2023-07-26 19:02:05 +00:00
Mihajlo Medjedovic
1c2b87940d ci: release
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 7m30s
2023-07-26 13:39:04 +02:00
25fed203c6 chore: improving the descriptions of the steps in the release.yaml file
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 7m29s
2023-07-25 21:36:10 +01:00
semantic-release-bot
bba2a6cc9d chore(release): 6.1.0 [skip ci]
All checks were successful
Release / release (push) Successful in 11m2s
# [6.1.0](https://git.datacontroller.io/dc/dc/compare/v6.0.0...v6.1.0) (2023-07-25)

### Bug Fixes

* missing mf_existds dependency in bitemporal_dataloader ([5ce1701](5ce1701657))
* reducing audit data volumes. Closes [#4](#4) ([54fe701](54fe7013b1))
* release script, excel upload duplicate primary keys, cypress fix ([2f79487](2f79487aea))

### Features

* full format deletion, closes [#2](#2) ([8dc40bd](8dc40bdd4e))
2023-07-25 15:14:52 +00:00
b036cc2abe Merge pull request 'adding PDF of non commercial licence to repo' (#10) from development into main
All checks were successful
Release / release (push) Successful in 10m14s
Reviewed-on: #10
2023-07-25 14:41:56 +00:00
218b15918d Merge branch 'main' into development
Some checks reported warnings
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
Test / Build-and-test-development (push) Has been cancelled
Build / Build-and-ng-test (pull_request) Has been cancelled
2023-07-25 14:41:44 +00:00
19489a68a7 chore: merge commit
Some checks reported warnings
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
Test / Build-and-test-development (push) Has been cancelled
Build / Build-and-ng-test (pull_request) Has been cancelled
2023-07-25 15:39:55 +01:00
7d7061a78a chore: adding pdf version of the non commercial licence 2023-07-25 15:38:47 +01:00
1aceb297ba Merge pull request 'Merging Development' (#5) from development into main
Some checks failed
Release / release (push) Failing after 1m53s
Reviewed-on: #5
2023-07-25 14:19:46 +00:00
cdee7480c2 Merge pull request 'CI Integration and Cypress test fixing' (#9) from ci into development
Some checks reported warnings
Test / Build-and-test-development (push) Has been cancelled
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
Build / Build-and-ng-test (pull_request) Successful in 7m38s
Reviewed-on: #9
Reviewed-by: allan <allan@4gl.io>
2023-07-25 13:54:21 +00:00
Mihajlo Medjedovic
bb2fb235aa chore(git): Merge branch 'ci' of ssh://git.datacontroller.io:29419/dc/dc into ci
Some checks reported warnings
Build / Build-and-ng-test (pull_request) Has been cancelled
2023-07-25 15:51:47 +02:00
Mihajlo Medjedovic
b2f88e203a chore: ci and repo links 2023-07-25 15:45:11 +02:00
c2f1af99ef Merge branch 'development' into ci
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 7m32s
2023-07-25 13:36:03 +00:00
Mihajlo Medjedovic
00e9dff1d9 chore: removed woodpecker ci
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m2s
2023-07-25 14:21:32 +02:00
82d74e6fee Merge pull request 'fix: missing mf_existds dependency in bitemporal_dataloader' (#7) from issue4 into development
Some checks failed
Test / Build-and-test-development (push) Failing after 4m54s
Test / Build-and-test-development-latest-adapter (push) Failing after 4m52s
Build / Build-and-ng-test (pull_request) Successful in 7m30s
Reviewed-on: #7
2023-07-25 10:06:41 +00:00
f1e2770574 Merge branch 'development' into issue4
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 7m32s
2023-07-25 10:06:15 +00:00
5ce1701657 fix: missing mf_existds dependency in bitemporal_dataloader
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 7m28s
2023-07-25 11:05:01 +01:00
Mihajlo Medjedovic
2f79487aea fix: release script, excel upload duplicate primary keys, cypress fix 2023-07-25 11:34:58 +02:00
fefdfaf17b chore: readme tidyup
Some checks reported warnings
Test / Build-and-test-development (push) Has been cancelled
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
Build / Build-and-ng-test (pull_request) Successful in 7m32s
2023-07-25 08:56:07 +00:00
c1e6f71430 chore: line spacing
Some checks reported warnings
Test / Build-and-test-development (push) Has been cancelled
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
Build / Build-and-ng-test (pull_request) Has been cancelled
2023-07-25 08:52:27 +00:00
a8310fa63f Merge pull request 'fix: reducing audit data volumes. Closes #4' (#6) from issue4 into development
Some checks failed
Test / Build-and-test-development (push) Failing after 4m55s
Test / Build-and-test-development-latest-adapter (push) Failing after 4m48s
Build / Build-and-ng-test (pull_request) Successful in 7m34s
Reviewed-on: #6
2023-07-25 08:50:32 +00:00
e28ac814b8 chore: readme update
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 7m34s
2023-07-25 09:48:49 +01:00
45fed59a65 chore: readme
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 7m34s
2023-07-25 09:47:27 +01:00
310399112b chore: licence updates 2023-07-25 09:09:57 +01:00
01238fc8a9 chore: newline in _eula.ts
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 7m27s
2023-07-25 09:05:57 +01:00
365e46c568 chore: postdata comment 2023-07-25 09:05:28 +01:00
8d336e5b46 chore: updating tests
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m0s
2023-07-21 14:23:22 +01:00
54fe7013b1 fix: reducing audit data volumes. Closes #4
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m12s
2023-07-20 21:29:34 +01:00
8dc40bdd4e feat: full format deletion, closes #2
Some checks failed
Test / Build-and-test-development (push) Failing after 6m16s
Test / Build-and-test-development-latest-adapter (push) Failing after 6m22s
Build / Build-and-ng-test (pull_request) Failing after 1m12s
2023-07-20 16:12:59 +01:00
Mihajlo Medjedovic
d88ab8bf58 ci: removed legacy-peer-deps flag
Some checks failed
Test / Build-and-test-development (push) Failing after 6m27s
Test / Build-and-test-development-latest-adapter (push) Failing after 6m25s
2023-07-14 12:27:33 +02:00
Mihajlo Medjedovic
67030ab033 chore: contributing
Some checks reported warnings
Test / Build-and-test-development (push) Has started running
Test / Build-and-test-development-latest-adapter (push) Has been cancelled
2023-07-13 14:11:50 +02:00
131 changed files with 15992 additions and 10244 deletions

View File

@@ -1,5 +1,5 @@
name: Build
run-name: Building and testing DC
run-name: Running Lint Check and Licence checker on Pull Request
on: [pull_request]
jobs:
@@ -18,21 +18,8 @@ jobs:
env:
NPMRC: ${{ secrets.NPMRC}}
- run: apt-get update
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- run: apt install -y ./google-chrome*.deb;
- run: export CHROME_BIN=/usr/bin/google-chrome
- run: npm run lint:check
# Install dependencies~
- run: npm ci --legacy-peer-deps
# Audit should fail and stop the CI if critical vulnerability found
- run: npm audit --audit-level=critical
- run: |
cd ./sas
npm audit --audit-level=critical
- run: |
cd ./client
npm audit --audit-level=critical
npm test -- --no-watch --no-progress --browsers=ChromeHeadlessCI
npm run postinstall
npm run build
cd client
npm ci
npm run license-checker

View File

@@ -1,172 +0,0 @@
name: Test
run-name: Building and testing development branch
on:
push:
branches:
- development
jobs:
Build-and-test-development:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Write .npmrc file
run: echo "$NPMRC" > client/.npmrc
shell: bash
env:
NPMRC: ${{ secrets.NPMRC}}
- run: apt-get update
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- run: apt install -y ./google-chrome*.deb;
- run: export CHROME_BIN=/usr/bin/google-chrome
- run: apt-get update -y
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
- run: apt -y install jq
# - run: 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
# - run: eval $(ssh-agent -s)
# - run: echo "$ssh_key" | tr -d '\r' | ssh-add -
- name: Write cypress credentials
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
shell: bash
env:
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
# - run: mkdir -p ~/.ssh
# - run: chmod 700 ~/.ssh
- run: npm ci --legacy-peer-deps
# Install pm2 and prepare SASJS server
- run: npm i -g pm2
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
- run: unzip linux.zip
- run: touch .env
- run: echo RUN_TIMES=js >> .env
- run: echo NODE_PATH=node >> .env
- run: echo CORS=enable >> .env
- run: echo WHITELIST=http://localhost:4200 >> .env
# - run: echo "SERVER_URL=$server_url" >> .env
# - run: echo "SERVER_TYPE=$server_type" >> .env
# - run: echo "CLIENT=$client_sasjs" >> .env
# - run: echo "ACCESS_TOKEN=$access_token_sasjs" >> .env
# - run: echo "REFRESH_TOKEN=$refresh_token_sasjs" >> .env
# - run: cat .env
- run: pm2 start api-linux --wait-ready
- name: Deploy mocked services
run: |
cd ./sas/mocks/sasjs
npm install --legacy-peer-deps -g @sasjs/cli
npm install --legacy-peer-deps -g replace-in-files-cli
sasjs cbd -t server-ci
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
- name: Prepare frontend
run: |
cd ./client
# mv ./cypress.env.example.json ./cypress.env.json
# replace-in-files --regex='"username".*' --replacement='"username":"'$cypress_username_sasjs'",' ./cypress.env.json
# replace-in-files --regex='"password".*' --replacement='"password":"'$cypress_pwd_sasjs'" ' ./cypress.env.json
cat ./cypress.env.json
npm run postinstall
# Prepare index.html to SASJS local
replace-in-files --regex='serverUrl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./src/index.html
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/Public/app/devtest"' ./src/index.html
replace-in-files --regex='serverType=".*?"' --replacement='serverType="SASJS"' ./src/index.html
# Prepare and deploy SASJS version
# replace-in-files --regex='serverurl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./dist/index.html
# replace-in-files --regex='apploc=".*?"' --replacement='appLoc="/30.SASApps/app/devtest"' ./dist/index.html
# replace-in-files --regex='servertype=".*?"' --replacement='serverType="SASJS"' ./dist/index.html
# scp -o stricthostkeychecking=no -r ./dist/* dcgitlab@sas.4gl.io:/var/www/html/dcviya/development/newadapter
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
# replace-in-files --regex='"appLocation".*' --replacement='appLocation:"/dcviya/development/newadapter",' ./cypress.config.ts
cat ./cypress.config.ts
# Start frontend and run cypress
npm start & npx wait-on http://localhost:4200 && ./run-cypress-tests.sh
Build-and-test-development-latest-adapter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Write .npmrc file
run: echo "$NPMRC" > client/.npmrc
shell: bash
env:
NPMRC: ${{ secrets.NPMRC}}
- run: apt-get update
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- run: apt install -y ./google-chrome*.deb;
- run: export CHROME_BIN=/usr/bin/google-chrome
- run: apt-get update -y
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
- run: apt -y install jq
# - run: 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
# - run: eval $(ssh-agent -s)
# - run: echo "$ssh_key" | tr -d '\r' | ssh-add -
- name: Write cypress credentials
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
shell: bash
env:
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
# - run: mkdir -p ~/.ssh
# - run: chmod 700 ~/.ssh
- run: npm ci --legacy-peer-deps
# Install pm2 and prepare SASJS server
- run: npm i -g pm2
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
- run: unzip linux.zip
- run: touch .env
- run: echo RUN_TIMES=js >> .env
- run: echo NODE_PATH=node >> .env
- run: echo CORS=enable >> .env
- run: echo WHITELIST=http://localhost:4200 >> .env
# - run: echo "SERVER_URL=$server_url" >> .env
# - run: echo "SERVER_TYPE=$server_type" >> .env
# - run: echo "CLIENT=$client_sasjs" >> .env
# - run: echo "ACCESS_TOKEN=$access_token_sasjs" >> .env
# - run: echo "REFRESH_TOKEN=$refresh_token_sasjs" >> .env
# - run: cat .env
- run: pm2 start api-linux --wait-ready
- name: Deploy mocked services
run: |
cd ./sas/mocks/sasjs
npm install --legacy-peer-deps -g @sasjs/cli
npm install --legacy-peer-deps -g replace-in-files-cli
sasjs cbd -t server-ci
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
- name: Prepare frontend
run: |
cd ./client
# mv ./cypress.env.example.json ./cypress.env.json
# replace-in-files --regex='"username".*' --replacement='"username":"'$cypress_username_sasjs'",' ./cypress.env.json
# replace-in-files --regex='"password".*' --replacement='"password":"'$cypress_pwd_sasjs'" ' ./cypress.env.json
cat ./cypress.env.json
npm run postinstall
npm install --legacy-peer-deps @sasjs/adapter@latest
# Prepare index.html to SASJS local
replace-in-files --regex='serverUrl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./src/index.html
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/Public/app/devtest"' ./src/index.html
replace-in-files --regex='serverType=".*?"' --replacement='serverType="SASJS"' ./src/index.html
# Prepare and deploy SASJS version
# replace-in-files --regex='serverurl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./dist/index.html
# replace-in-files --regex='apploc=".*?"' --replacement='appLoc="/30.SASApps/app/devtest"' ./dist/index.html
# replace-in-files --regex='servertype=".*?"' --replacement='serverType="SASJS"' ./dist/index.html
# scp -o stricthostkeychecking=no -r ./dist/* dcgitlab@sas.4gl.io:/var/www/html/dcviya/development/newadapter
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
# replace-in-files --regex='"appLocation".*' --replacement='appLocation:"/dcviya/development/newadapter",' ./cypress.config.ts
cat ./cypress.config.ts
# Start frontend and run cypress
npm start & npx wait-on http://localhost:4200 && ./run-cypress-tests.sh

View File

@@ -1,13 +1,151 @@
name: Release
run-name: Releasing DC
run-name: Testing and Releasing DC
on:
push:
branches:
- main
jobs:
Build-production-and-ng-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Write .npmrc file
run: |
touch client/.npmrc
echo '${{ secrets.NPMRC}}' > client/.npmrc
- name: Install Chrome for Angular tests
run: |
apt-get update
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install -y ./google-chrome*.deb;
export CHROME_BIN=/usr/bin/google-chrome
- name: Write cypress credentials
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
shell: bash
env:
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
- name: Install dependencies
run: npm ci
- name: Check audit
# Audit should fail and stop the CI if critical vulnerability found
run: |
npm audit --audit-level=critical --omit=dev
cd ./sas
npm audit --audit-level=critical --omit=dev
cd ../client
npm audit --audit-level=critical --omit=dev
- name: Angular Tests
run: |
cd client
npm test -- --no-watch --no-progress --browsers=ChromeHeadlessCI
- name: Angular Production Build
run: |
cd client
npm run postinstall
npm run build
Build-and-test-development:
runs-on: ubuntu-latest
needs: Build-production-and-ng-test
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Write .npmrc file
run: |
touch client/.npmrc
echo '${{ secrets.NPMRC}}' > client/.npmrc
- run: apt-get update
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- run: apt install -y ./google-chrome*.deb;
- run: export CHROME_BIN=/usr/bin/google-chrome
- run: apt-get update -y
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
- run: apt -y install jq
- name: Write cypress credentials
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
shell: bash
env:
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
- name: Install dependencies
run: npm ci
# Install pm2 and prepare SASJS server
- run: npm i -g pm2
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
- run: unzip linux.zip
- run: touch .env
- run: echo RUN_TIMES=js >> .env
- run: echo NODE_PATH=node >> .env
- run: echo CORS=enable >> .env
- run: echo WHITELIST=http://localhost:4200 >> .env
- run: cat .env
- run: pm2 start api-linux --wait-ready
- name: Deploy mocked services
run: |
cd ./sas/mocks/sasjs
npm install -g @sasjs/cli
npm install -g replace-in-files-cli
sasjs cbd -t server-ci
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
- name: Install ZIP
run: |
apt-get update
apt-get install zip
- name: Prepare and run frontend and cypress
run: |
cd ./client
mv ./cypress.env.example.json ./cypress.env.json
replace-in-files --regex='"username".*' --replacement='"username":"'${{ secrets.CYPRESS_USERNAME_SASJS }}'",' ./cypress.env.json
replace-in-files --regex='"password".*' --replacement='"password":"'${{ secrets.CYPRESS_PWD_SASJS }}'" ' ./cypress.env.json
cat ./cypress.env.json
npm run postinstall
# Prepare index.html to SASJS local
replace-in-files --regex='serverUrl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./src/index.html
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/Public/app/devtest"' ./src/index.html
replace-in-files --regex='serverType=".*?"' --replacement='serverType="SASJS"' ./src/index.html
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
cat ./cypress.config.ts
# Start frontend and run cypress
npm start & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
- name: Zip Cypress videos
if: always()
run: |
zip -r cypress-videos ./client/cypress/videos
- name: Add cypress videos artifacts
if: always()
uses: actions/upload-artifact@v3
with:
name: cypress-videos.zip
path: cypress-videos.zip
release:
runs-on: ubuntu-latest
needs: [Build-production-and-ng-test, Build-and-test-development]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
@@ -22,29 +160,97 @@ jobs:
env:
NPMRC: ${{ secrets.NPMRC}}
- name: Install ZIP
- name: Install packages
run: |
apt-get update
apt-get install zip
apt-get install zip -y
# sasjs cli is used to compile & build the SAS services
npm i -g @sasjs/cli
# jq is used to parse the release JSON
apt-get install jq -y
- name: release-build
- name: Create Empty Release (assets are posted later)
run: |
npm i
npm i -g semantic-release
# We do a semantic-release DRY RUN to make the job fail if there are no changes to release
GITEA_TOKEN=${{ secrets.RELEASE_TOKEN }} GITEA_URL=https://git.datacontroller.io semantic-release --dry-run | grep -q "There are no relevant changes, so no new version is released." && exit 1
GITEA_TOKEN=${{ secrets.RELEASE_TOKEN }} GITEA_URL=https://git.datacontroller.io semantic-release
- name: Frontend Build
description: Must be created AFTER the release as the version (git tag) is used in the interface
run: |
cd client
npm ci
npm run build
zip -r dist.zip ./dist
- name: Install Semantic Release and plugins
- name: Build SAS9 EBI Release
description: Compile SAS 9 services, remove tests & create deployment program
run: |
cd sas
npm i
npm i -g semantic-release
sasjs c -t sas9
rm -rf sasjsbuild/tests
sasjs b -t sas9
cp sasjsbuild/mysas9deploy.sas ./demostream_sas9.sas
#
# remove streamed component and rebuild SAS 9 services
#
rm -rf sasjsbuild/services/web9
rm sasjsbuild/services/clickme.sas
sasjs b -t sas9
cp sasjsbuild/mysas9deploy.sas ./sas9.sas
- name: Release
- name: Build SASjs Server Release
description: Compile Base (SASjs) services, remove tests & create deployment JSON
run: |
GITEA_TOKEN=${{ secrets.RELEASE_TOKEN }} GITEA_URL=https://git.datacontroller.io semantic-release
cd sas
cp sasjs/utils/favicon.ico ../client/dist/favicon.ico
sasjs c -t server
rm -rf sasjsbuild/tests
sasjs b -t server
cp sasjsbuild/server.json.zip ./sasjs_server.json.zip
- name: Build Viya Release
description: compile Viya Streaming Deploy (without tests)
run: |
cd sas
sasjs c -t viya
rm -rf sasjsbuild/tests
sed -i -e 's/servertype="SASJS"/servertype="SASVIYA"/g' sasjsbuild/services/clickme.html
sasjs b -t viya
cp sasjsbuild/viya.sas ./demostream_viya.sas
# compile Viya Full deploy (without web)
rm -rf sasjsbuild/services/web
rm sasjsbuild/services/clickme.html
sasjs b -t viya
cp sasjsbuild/viya.sas ./viya.sas
- name: Zip Frontend (including viya.json for full viya deploy)
run: |
cd sas
cp sasjsbuild/viya.json ../client/dist
cd ..
zip -r frontend.zip ./client/dist
- name: Release Typedoc
run: |
cd client
npm run typedoc
# deploy docs
npm -g install cloudron-surfer
npm run compodoc:build
surfer put --token ${{ secrets.TSDOC_TOKEN }} --server webdoc.datacontroller.io documentation/* /
- name: Upload assets to release
run: |
RELEASE_ID=`curl -k 'https://git.datacontroller.io/api/v1/repos/dc/dc/releases/latest?access_token=${{ secrets.RELEASE_TOKEN }}' | jq -r '.id'`
RELEASE_BODY=`curl -k 'https://git.datacontroller.io/api/v1/repos/dc/dc/releases/latest?access_token=${{ secrets.RELEASE_TOKEN }}' | jq -r '.body'`
# Update body
curl --data '{"draft": false,"body":"'"$RELEASE_BODY\n\nFor installation instructions, please visit https://docs.datacontroller.io/"'"}' -X PATCH --header 'Content-Type: application/json' -k https://git.datacontroller.io/api/v1/repos/dc/dc/releases/$RELEASE_ID?access_token=${{ secrets.RELEASE_TOKEN }}
# Upload assets
URL="https://git.datacontroller.io/api/v1/repos/dc/dc/releases/$RELEASE_ID/assets?access_token=${{ secrets.RELEASE_TOKEN }}"
curl -k $URL -F attachment=@frontend.zip
curl -k $URL -F attachment=@sas/demostream_sas9.sas
curl -k $URL -F attachment=@sas/demostream_viya.sas
curl -k $URL -F attachment=@sas/sasjs_server.json.zip
curl -k $URL -F attachment=@sas/sas9.sas
curl -k $URL -F attachment=@sas/viya.sas

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@ client/src/environments/version.ts
client/cypress/screenshots
client/cypress/results
client/cypress/videos
client/documentation
cypress.env.json
sasjsbuild
sasjsresults

2
.npmrc
View File

@@ -1 +1 @@
legacy-peer-deps=false
legacy-peer-deps=true

View File

@@ -1,22 +1,46 @@
{
"branches": ["main"],
"branches": [
"main"
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
[
"@semantic-release/git",
{
"assets": [
"CHANGELOG.md"
"CHANGELOG.md",
"package.json"
]
}
],
["@saithodev/semantic-release-gitea", {
[
"@saithodev/semantic-release-gitea",
{
"giteaUrl": "https://git.datacontroller.io",
"assets": [
{"path": "client/dist.zip"}
{
"path": "sas/demostream_sas9.sas"
},
{
"path": "sas/demostream_viya.sas"
},
{
"path": "sas/frontend.zip"
},
{
"path": "sas/sasjs_server.json.zip"
},
{
"path": "sas/sas9.sas"
},
{
"path": "sas/viya.sas"
}
]
}
]
}]
]
}

View File

@@ -1,9 +0,0 @@
pipeline:
build:
image: debian
commands:
- echo "This is the build step"
a-test-step:
image: debian
commands:
- echo "Testing.."

View File

@@ -1,3 +1,56 @@
## [6.2.3](https://git.datacontroller.io/dc/dc/compare/v6.2.2...v6.2.3) (2023-10-12)
### Bug Fixes
* bumping core library to avoid non-ascii char in mp_validatecols.sas. [#50](https://git.datacontroller.io/dc/dc/issues/50) ([11b06f6](https://git.datacontroller.io/dc/dc/commit/11b06f6416300b6d70b1570c415d5a5c004976db))
* removing copyright symbol from mpe_alerts macro. [#50](https://git.datacontroller.io/dc/dc/issues/50) ([adb7eb7](https://git.datacontroller.io/dc/dc/commit/adb7eb77550c68a2dab15a6ff358129820e9b612))
## [6.2.2](https://git.datacontroller.io/dc/dc/compare/v6.2.1...v6.2.2) (2023-10-09)
### Bug Fixes
* updated SheetJS (crypto) to the latest ([8bd0dd2](https://git.datacontroller.io/dc/dc/commit/8bd0dd22c258911672303869e4df893a98e93575))
## [6.2.1](https://git.datacontroller.io/dc/dc/compare/v6.2.0...v6.2.1) (2023-10-09)
### Bug Fixes
* approve, history and submit pages grouped in review module ([e056ece](https://git.datacontroller.io/dc/dc/commit/e056ece2234ef6aab050f6a5b1f8de633b163d91))
* closes [#39](https://git.datacontroller.io/dc/dc/issues/39) upcase issue in MPE_SECURITY ([a00d31c](https://git.datacontroller.io/dc/dc/commit/a00d31caf3c5634cd61a4700fb175e76856edbb6))
* handsontable v13 ([6f482ec](https://git.datacontroller.io/dc/dc/commit/6f482ec6d909907a304ef9975262889e2370035f))
* latest adapter ([5e30dc0](https://git.datacontroller.io/dc/dc/commit/5e30dc0f892fab2af41f4ea56e30f27ec3b3912e))
* sasjs/cli and sasjs/core updated to the latest ([8571e01](https://git.datacontroller.io/dc/dc/commit/8571e01e44a8cb6df9d150d271c34bb75bffdf31))
* updating editors/stagedata to address issues in particular viya configurations as described in issue [#33](https://git.datacontroller.io/dc/dc/issues/33) ([94ab949](https://git.datacontroller.io/dc/dc/commit/94ab949df8c75072525751a2156b7a32c2e641dc))
* updating logic for REPLACE loadtype ([1f2ce55](https://git.datacontroller.io/dc/dc/commit/1f2ce55f249f4af56f0cacdec47e69246cd47431))
# [6.2.0](https://git.datacontroller.io/dc/dc/compare/v6.1.0...v6.2.0) (2023-08-24)
### Bug Fixes
* re-enabling full REPLACE uploads ([08e39c4](https://git.datacontroller.io/dc/dc/commit/08e39c4fca570406f9aad3d907cb04596421d074))
### Features
* support for European numeric formats ([e48e47b](https://git.datacontroller.io/dc/dc/commit/e48e47bc635452b59e107b235e597c26e748875e))
# [6.1.0](https://git.datacontroller.io/dc/dc/compare/v6.0.0...v6.1.0) (2023-07-25)
### Bug Fixes
* missing mf_existds dependency in bitemporal_dataloader ([5ce1701](https://git.datacontroller.io/dc/dc/commit/5ce1701657136f2cf792441412230513ff52e7e8))
* reducing audit data volumes. Closes [#4](https://git.datacontroller.io/dc/dc/issues/4) ([54fe701](https://git.datacontroller.io/dc/dc/commit/54fe7013b1a25be228eeb2aba3553f6219952de6))
* release script, excel upload duplicate primary keys, cypress fix ([2f79487](https://git.datacontroller.io/dc/dc/commit/2f79487aeaf6268b027a8fa52fcdaae2b449de3d))
### Features
* full format deletion, closes [#2](https://git.datacontroller.io/dc/dc/issues/2) ([8dc40bd](https://git.datacontroller.io/dc/dc/commit/8dc40bdd4e3a7ad5c1e6582b4130f24bc445eb77))
# Changelog
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.

View File

@@ -1,6 +1,9 @@
# Data Controller
# Contributing
## Workflow guidelines
[Wiki Page](https://git.datacontroller.io/dc/dc/wiki/Git-Workflow)
## Dependencies that requires licences
[SheetJS Pro Version](https://www.npmjs.com/package/sheetjs)
@@ -50,6 +53,17 @@ npm run lint:fix
Typedoc is used for generating typescript documentation based on the code.
That part is automated and beign done as a part of CI job.
# Release
Release is automated as a part of CI job. Workflow file: `.gitea/workflows/release.yaml`.
It will run automatically when branch merged to the `main` branch.
IMPORTANT!
If release job fails, after it has been created empty release and a tag, we must not re-run the relase job until we removed the newly create GIT TAG and RELEASE.
To remove the git tag run:
```
git push -d origin vX.X.X
```
To remove the release, you need to do it with repo administration over at [https://git.datacontroller.io/dc/dc](https://git.datacontroller.io/dc/dc)
# Troubleshooting
## Makedata service "could not create directory" error

View File

@@ -3,25 +3,15 @@ Licence Agreement for Data Controller for SAS®
Copyright (c) Bowe IO Ltd
Data Controller is a software distributed by 4GL Apps, a brand owned by Bowe IO Ltd, a UK Limited Company headquarted in 29 Oldfield Rd, Cumbria, registered by companies house under number 08777171, VAT number: 203914240
Data Controller software is distributed by 4GL Apps, a brand owned by Bowe IO Ltd, a UK Limited Company headquarted in 29 Oldfield Rd, Cumbria, registered at Companies House with company number 08777171, VAT number: 203914240
This software is protected by applicable copyright laws, including international treaties, and dual-
licensed depending on whether your use for commercial purposes, meaning intended for or
resulting in commercial advantage or monetary compensation, or not.
This software is protected by applicable copyright laws, including international treaties, and dual-licensed depending on whether your use for commercial purposes, meaning intended for or resulting in commercial advantage or monetary compensation, or not.
If your use is strictly personal or solely for evaluation purposes, meaning for the purposes of testing
the suitability, performance, and usefulness of this software outside the production environment,
you agree to be bound by the terms included in the "licence-non-commercial-datacontroller.md" file.
If your use is strictly personal or solely for evaluation purposes, meaning for the purposes of testing the suitability, performance, and usefulness of this software outside the production environment, you agree to be bound by the terms included in the "licence-non-commercial-datacontroller.md" file available here: https://git.datacontroller.io/dc/dc/src/branch/main/licence-non-commercial-datacontroller.md
Your use of this software for commercial purposes is subject to the terms included in an applicable
license agreement.
Your use of this software for commercial purposes is subject to the terms included in an applicable license agreement.
In any case, you must not make any such use of this software as to develop software which may be
considered competitive with this software.
In any case, you must not make any such use of this software as to develop software which may be considered competitive with this software.
UNLESS EXPRESSLY AGREED OTHERWISE, 4GL APPS PROVIDES THIS SOFTWARE ON AN "AS IS"
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, AND IN NO EVENT AND UNDER NO
LEGAL THEORY, SHALL 4GL APPS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT,
INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER ARISING FROM
USE OR INABILITY TO USE THIS SOFTWARE.
UNLESS EXPRESSLY AGREED OTHERWISE, 4GL APPS PROVIDES THIS SOFTWARE ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, AND IN NO EVENT AND UNDER NO LEGAL THEORY, SHALL 4GL APPS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER ARISING FROM USE OR INABILITY TO USE THIS SOFTWARE.

View File

@@ -0,0 +1,30 @@
# Data Controller for SAS
**Control your manual data modifications!**
_Alternatives to Data Controller include:_
* 💾 Developing / testing / deploying / scheduling overnight batch jobs to load files from shared drives
* 🔒 Opening (and locking) datasets in Enterprise Guide or SAS® Table Viewer to perform direct updates
* ❓ Asking a #DBA to run validated code after a change management process
* 🌐 Building & maintaining your own custom web application
* 🏃 Running #SAS or #SQL updates in production
_Problems with the above include:_
* Legacy 'black box' solutions with little to no testing, documentation or support
* End users requiring direct write access to critical data sources in production
* Upload routines that must be manually modified when the data model changes
* Breaches due to unnecessary parties having access to the data
* Inability to trace who made a change, when, and why
* Reliance on key individuals to perform updates
* Building bespoke ETL for every new data source
* High risk of manual error / data corruption
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:
* Main site: https://datacontroller.io
* Docs: https://docs.datacontroller.io
* Code: https://code.datacontroller.io

View File

@@ -1,17 +1,20 @@
import { defineConfig } from 'cypress'
import { defineConfig } from "cypress";
export default defineConfig({
reporter: 'mochawesome',
reporter: "mochawesome",
reporterOptions: {
reportDir: 'cypress/results',
reportDir: "cypress/results",
overwrite: false,
html: true,
json: false,
},
chromeWebSecurity: false,
defaultCommandTimeout: 30000,
env: {
hosturl:"http://localhost:4200",
hosturl: "http://localhost:4200",
appLocation: "",
site_id_SAS9: "70221618",
site_id_SASVIYA: "70253615",
@@ -23,6 +26,12 @@ export default defineConfig({
debug: false,
screenshotOnRunFailure: false,
longerCommandTimeout: 50000,
testLicenceUserLimits: false
}
})
testLicenceUserLimits: false,
},
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});

View File

@@ -0,0 +1,250 @@
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 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`)
})
})
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

@@ -0,0 +1,257 @@
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.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
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')
})
})
})
})
})
this.afterEach(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
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', 'Go to approvals screen')
.then((allButtons: any) => {
for (let approvalButton of allButtons) {
if (
approvalButton.innerText
.toLowerCase()
.includes('go to approvals screen')
) {
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

@@ -0,0 +1,537 @@
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/'
context('excel tests: ', function () {
this.beforeAll(() => {
cy.visit(`${hostUrl}/SASLogon/logout`)
cy.loginAndUpdateValidKey(true)
})
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')
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('.duplicate-keys-modal', { 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')
.then((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')
.then((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')
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
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', 'Go to approvals screen')
.then((allButtons: any) => {
for (let approvalButton of allButtons) {
if (
approvalButton.innerText
.toLowerCase()
.includes('go to approvals screen')
) {
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', 'Go to approvals screen')
.then((allButtons: any) => {
for (let approvalButton of allButtons) {
if (
approvalButton.innerText
.toLowerCase()
.includes('go to approvals screen')
) {
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')
.then((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')
.then((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')
// When CI detached browser screen is smaller, below cells are not visible so test fails
// Commenting it out now until we figure out workaround
// 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')
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')
.then((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

@@ -0,0 +1,383 @@
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 })
// cy.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
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('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()
}
)
})
})
})
this.afterEach(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
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('.in-values-modal clr-checkbox-wrapper input').then((inputs: any) => {
inputs[0].click()
cy.get('.in-values-modal .modal-footer button').click()
cy.get('.modal-footer .btn-success-outline').click()
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

@@ -0,0 +1,731 @@
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.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')
})
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()
}
)
})
}
)
})
})
})
})
})
})
}
this.afterEach(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
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', 'Go to approvals screen')
.then((allButtons: any) => {
for (let approvalButton of allButtons) {
if (
approvalButton.innerText
.toLowerCase()
.includes('go to approvals screen')
) {
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

@@ -0,0 +1,157 @@
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)
// cy.get('input.username').type(username)
// cy.get('input.password').type(password)
// cy.get('.login-group button').click()
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', 'Go to approvals screen')
.then((allButtons: any) => {
for (let approvalButton of allButtons) {
if (
approvalButton.innerText
.toLowerCase()
.includes('go to approvals screen')
) {
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

@@ -0,0 +1,61 @@
const username = Cypress.env('username')
const password = Cypress.env('password')
const hostUrl = Cypress.env('hosturl')
const appLocation = Cypress.env('appLocation')
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
const serverType = Cypress.env('serverType')
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
const fixturePath = 'excels_general/'
context('metanav tests: ', function () {
this.beforeAll(() => {
cy.visit(`${hostUrl}/SASLogon/logout`)
cy.loginAndUpdateValidKey()
})
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
cy.get('input.username').type(username)
cy.get('input.password').type(password)
cy.get('.login-group button').click()
visitPage('view/metadata')
})
it('1 | Opens metadata object', (done) => {
openFirstMetadataFromTree(() => {
// BLOCKER
// For unkown reasons, .clr-accordion-header-button always null although it is present on the page.
cy.get('.clr-accordion-header-button').then((panelNodes: any) => {
panelNodes[0].querySelector('button').click()
})
})
})
this.afterEach(() => {
cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
const openFirstMetadataFromTree = (callback?: any) => {
cy.get('.app-loading', { timeout: longerCommandTimeout })
.should('not.exist')
.then(() => {
cy.get('.nav-tree clr-tree > clr-tree-node', {
timeout: longerCommandTimeout
}).then((treeNodes: any) => {
let firstMetaNode
firstMetaNode = treeNodes[1]
cy.get(firstMetaNode).within(() => {
cy.get('.clr-treenode-content').click()
callback()
})
})
})
}
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}

View File

@@ -0,0 +1,655 @@
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}`)
context('editor tests: ', function () {
this.beforeAll(() => {
cy.visit(`${hostUrl}/SASLogon/logout`)
cy.loginAndUpdateValidKey(true)
})
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', { withinSubject: null }).then((viewboxNodes: any) => {
for (let viewboxNode of viewboxNodes) {
cy.get(viewboxNode).within(() => {
cy.get('.table-title').then((tableTitle) => {
const title = tableTitle[0].innerText
const viewbox = viewboxes.find((vb) =>
title.toLowerCase().includes(vb.viewbox_table)
)
if (viewbox) {
cy.get('.ht_master.handsontable .htCore thead tr').then(
(viewboxColNodes: any) => {
let allColsHtml = viewboxColNodes[0].innerHTML
for (let col of viewbox?.columns) {
if (!allColsHtml.includes(col)) return
}
done()
}
)
}
})
})
}
})
})
})
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)
removeAllColumns()
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)
removeAllColumns()
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()
removeAllColumns()
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)
removeAllColumns()
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()
}
)
})
})
})
// We will enable this test when we figure out how to mock filtering
// 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')
// })
// })
// }
// })
// })
// })
// }
// }
// )
// })
this.afterEach(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`)
})
})
const removeAllColumns = () => {
cy.get('.configuration-wrapper clr-icon[shape="trash"]').then(removeNodes => {
for (let removeNode of removeNodes) {
removeNode.click()
}
})
}
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) => {
console.log('viewboxColNode', viewboxColNodes)
console.log('columns', columns)
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(async (innerNodes: any) => {
for (let innerNode of innerNodes) {
for (let tablename of tablenames) {
await pause(300)
if (innerNode.innerText.toLowerCase().includes(tablename)) {
innerNode.click()
}
}
}
})
})
})
})
}
const pause = (ms: number) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(null)
}, ms)
})
}
const visitPage = (url: string) => {
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
}

View File

@@ -0,0 +1,23 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
import 'cypress-plugin-tab'
import "cypress-real-events"

View File

@@ -10,7 +10,7 @@ const check = (cwd) => {
onlyAllow:
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
excludePackages:
'@cds/city@1.1.0;@handsontable/angular@13.0.0;handsontable@13.0.0;hyperformula@2.5.0;jackspeak@2.2.0;path-scurry@1.7.0'
'@cds/city@1.1.0;@handsontable/angular@13.1.0;handsontable@13.1.0;hyperformula@2.5.0;jackspeak@2.2.0;path-scurry@1.7.0'
},
(error, json) => {
if (error) {

6042
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "dc-client",
"description": "dc-client",
"name": "data_controller-client",
"description": "DataController Client",
"angular-cli": {},
"scripts": {
"start": "node --max_old_space_size=4096 node_modules/@angular/cli/bin/ng serve",
@@ -29,7 +29,9 @@
"cy:run": "cypress run",
"audit:prod": "npm audit --omit=dev",
"sasdocs": "sasjs doc && ./sasjs/utils/deploydocs.sh",
"typedoc": "typedoc --options typedoc.json && cd ../dc-devdocs"
"compodoc:build": "compodoc -p tsconfig.doc.json --name 'Data Controller Client'",
"compodoc:build-and-serve": "compodoc -p tsconfig.doc.json -s --name 'Data Controller Client'",
"compodoc:serve": "compodoc -s --name 'Data Controller Client'"
},
"private": true,
"dependencies": {
@@ -46,9 +48,9 @@
"@clr/angular": "^13.17.0",
"@clr/icons": "^13.0.2",
"@clr/ui": "^13.17.0",
"@handsontable/angular": "^13.0.0",
"@sasjs/adapter": "4.3.6",
"@sasjs/utils": "^3.3.0",
"@handsontable/angular": "^13.1.0",
"@sasjs/adapter": "4.10.1",
"@sasjs/utils": "^3.4.0",
"@sheet/crypto": "1.20211122.1",
"@types/d3-graphviz": "^2.6.7",
"@types/text-encoding": "0.0.35",
@@ -58,7 +60,7 @@
"crypto-js": "^3.3.0",
"d3-graphviz": "^5.0.2",
"fs-extra": "^7.0.1",
"handsontable": "^13.0.0",
"handsontable": "^13.1.0",
"https-browserify": "1.0.0",
"hyperformula": "^2.5.0",
"iconv-lite": "^0.5.0",
@@ -69,7 +71,6 @@
"ngx-clipboard": "^16.0.0",
"ngx-json-viewer": "file:libraries/ngx-json-viewer-3.2.1.tgz",
"nodejs": "0.0.0",
"numbro": "^2.1.1",
"os-browserify": "0.3.0",
"rxjs": "^7.8.0",
"save-svg-as-png": "^1.4.17",
@@ -88,7 +89,9 @@
"@angular-eslint/template-parser": "16.0.3",
"@angular/cli": "^16.1.0",
"@angular/compiler-cli": "^16.1.2",
"@cypress/webpack-preprocessor": "^5.11.1",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@compodoc/compodoc": "^1.1.21",
"@cypress/webpack-preprocessor": "^5.17.1",
"@types/core-js": "^2.5.5",
"@types/crypto-js": "^4.0.1",
"@types/es6-shim": "^0.31.39",
@@ -99,10 +102,10 @@
"@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.29.0",
"core-js": "^2.5.4",
"cypress": "^9.5.3",
"cypress": "12.17.1",
"cypress-file-upload": "^5.0.8",
"cypress-plugin-tab": "^1.0.5",
"cypress-real-events": "^1.7.6",
"cypress-real-events": "^1.8.1",
"es6-shim": "^0.35.5",
"eslint": "^8.33.0",
"git-describe": "^4.0.4",
@@ -120,7 +123,8 @@
"rimraf": "3.0.2",
"ts-loader": "^9.2.8",
"ts-node": "^3.3.0",
"typedoc": "^0.23.24",
"typedoc": "^0.24.8",
"typedoc-plugin-external-module-name": "^4.0.6",
"typescript": "~4.9.4",
"wait-on": "^6.0.1",
"watch": "^1.0.2"

View File

@@ -1,7 +0,0 @@
#!/bin/bash
npm run cy:run -- --browser chrome --spec "cypress/integration/liveness.tests.ts"
npm run cy:run -- --browser chrome --spec "cypress/integration/editor.tests.ts"
npm run cy:run -- --browser chrome --spec "cypress/integration/excel.tests.ts"
npm run cy:run -- --browser chrome --spec "cypress/integration/filtering.tests.ts"
npm run cy:run -- --browser chrome --spec "cypress/integration/licensing.tests.ts"

View File

@@ -1,5 +1,8 @@
import { QueryClause } from './models/TableData'
/**
* Filtering cache info, to be reused when filtering modal is re-open
*/
interface FilterCache {
cols: any[]
vals: any[]
@@ -10,12 +13,18 @@ interface FilterCache {
query: QueryClause[]
}
/**
* Filtering cache info in the open viewboxes, to be reused when filtering modal is re-open
*/
interface ViewboxCache {
[key: number]: {
filter: FilterCache
}
}
/**
* Initial values when no cached values stored
*/
export const initFilter: { filter: FilterCache } = {
filter: {
cols: <any[]>[],
@@ -28,6 +37,13 @@ export const initFilter: { filter: FilterCache } = {
}
}
/**
* Cached filtering values across whole app (editor, viewer, viewboxes)
* Cached lineage libraries, tables
* Cached metadata tree
* Cached usernav tree
* Cached viyaApi collections, search and selected endpoint
*/
export const globals: {
rootParam: string
editor: any

View File

@@ -1,40 +0,0 @@
<div class="content-area">
<div class="card">
<div class="card-header d-flex flex-column justify-content-center">
<h3 class="text-center">
You succesfully edited table
<span class="color-blue font-weight-700">{{ libds }}</span>
</h3>
<p class="text-center">
<b>Please choose from the following actions</b>
</p>
<div class="row d-flex justify-content-center mt-20">
<button
class="btn btn-sm btn-outline text-center"
(click)="submittedTableScreen()"
>
Go to submitted table screen
</button>
<button
class="btn btn-sm btn-outline text-center"
(click)="viewerTableScreen()"
>
Go to base table screen
</button>
<button
id="approvalBtn"
class="btn btn-sm btn-success-outline text-center"
(click)="approveTableScreen()"
>
Go to approvals screen
</button>
<button
class="btn btn-sm btn-info-outline text-center"
(click)="goBack()"
>
Go back to editor
</button>
</div>
</div>
</div>
</div>

View File

@@ -1,50 +0,0 @@
import { AfterViewInit, Component, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
@Component({
selector: 'app-actions',
templateUrl: './actions.component.html',
styleUrls: ['./actions.component.scss'],
host: {
class: 'content-container'
}
})
export class ActionsComponent implements OnInit, AfterViewInit {
public dsid: any
public libds: string | undefined
constructor(
private route: ActivatedRoute,
private router: Router
) {}
public submittedTableScreen() {
this.router.navigateByUrl('/stage/' + this.dsid)
}
public approveTableScreen() {
this.router.navigateByUrl('/approve/approveDet/' + this.dsid)
}
public viewerTableScreen() {
this.router.navigateByUrl('/view/data/' + this.libds)
}
public goBack() {
this.router.navigateByUrl('/editor/' + this.libds)
}
async ngOnInit() {
this.dsid = this.route.snapshot.params['dsid']
this.libds = this.route.snapshot.params['libds']
}
ngAfterViewInit() {
setTimeout(() => {
let approvalBtn: any = window.document.getElementById('approvalBtn')
if (!!approvalBtn) {
approvalBtn.focus()
}
}, 700)
}
}

View File

@@ -169,7 +169,7 @@
<clr-dropdown-menu *clrIfOpen clrPosition="bottom-left">
<a [routerLink]="['/view']" clrDropdownItem>VIEW</a>
<a [routerLink]="['/home']" clrDropdownItem>EDIT</a>
<a [routerLink]="['/submitted']" clrDropdownItem>REVIEW</a>
<a [routerLink]="['/review/submitted']" clrDropdownItem>REVIEW</a>
</clr-dropdown-menu>
</clr-dropdown>
</div>
@@ -192,7 +192,7 @@
>EDIT</a
>
<a
[routerLink]="['/submitted']"
[routerLink]="['/review/submitted']"
[class.active]="
router.url.includes('submitted') ||
router.url.includes('approve') ||
@@ -224,7 +224,7 @@
<ul class="nav">
<li class="nav-item">
<a
[routerLink]="['/submitted']"
[routerLink]="['/review/submitted']"
class="nav-link nav-text"
routerLinkActive="active"
>SUBMIT</a
@@ -232,15 +232,16 @@
</li>
<li class="nav-item">
<a
[routerLink]="['/approve']"
[routerLink]="['/review/approve']"
class="nav-link nav-text"
[class.active]="router.url.includes('approve')"
routerLinkActive="active"
>APPROVE</a
>
</li>
<li class="nav-item">
<a
[routerLink]="['/history']"
[routerLink]="['/review/history']"
class="nav-link nav-text"
routerLinkActive="active"
>HISTORY</a

View File

@@ -57,24 +57,15 @@ export class AppComponent {
private elementRef: ElementRef
) {
this.parseDcAdapterSettings()
/**
* Prints app info in the console such as:
* - Adapter versions
* - App version
* - Build timestamp
*
*/
;(window as any).appinfo = () => {
const licenseKeyData = this.licenceService.getLicenseKeyData()
if (licenseKeyData) {
const expiry_date = moment(
licenseKeyData.valid_until,
'YYYY-MM-DD'
).startOf('day')
const current_date = moment().startOf('day')
const daysToExpiry = expiry_date.diff(current_date, 'days')
licenseKeyData.valid_until += ` (${daysToExpiry} ${
daysToExpiry === 1 ? 'day' : 'days'
} remaining)`
if (isNaN(daysToExpiry)) licenseKeyData.valid_until = 'Unlimited'
}
console.table({
'Adapter version': VERSION.adapterVersion || 'n/a',
'App version': (VERSION.tag || '').replace('v', ''),
@@ -87,7 +78,12 @@ export class AppComponent {
this.subscribeToLicenseEvents()
/**
* Fetches git tag ang git hash from `version.ts` file
* It's placed in the user drop down.
*/
this.commitVer = (VERSION.tag || '').replace('v', '') + '.' + VERSION.hash
router.events.subscribe((val) => {
this.routeUrl = this.router.url
@@ -127,8 +123,10 @@ export class AppComponent {
this.subscribeToAppActive()
this.subscribeToDemoLimitModal()
/* In Viya streaming apps, content is served within an iframe. This code
makes that iframe "full screen" so it looks like a regular window. */
/**
* In Viya streaming apps, content is served within an iframe. This code
* makes that iframe "full screen" so it looks like a regular window.
*/
if (window.frameElement) {
window.frameElement.setAttribute(
'style',
@@ -143,6 +141,9 @@ export class AppComponent {
}
}
/**
* Parses adapter settings that are found in the <sasjs> tag inside index.html
*/
private parseDcAdapterSettings() {
const sasjsElement = document.querySelector('sasjs')
@@ -180,9 +181,14 @@ export class AppComponent {
this.appService.sasServiceInit()
}
/**
* Opens licence page with the active licence problem
* Problem details are encoded in the url
*/
public licenceProblemDetails(url: string) {
this.router.navigateByUrl(url)
}
/**
* Based on string provided we return true, false or null
* True -> Compute API
@@ -199,6 +205,12 @@ export class AppComponent {
return value === 'true' || false
}
/**
* Listens for an `demo limit` event that will show the `Feature locked modal`
* For exmaple when in editor upload feature is not enabled
* When user tries to upload the excel, editor component will trgger this event
* And stop the execution of file upload code.
*/
public subscribeToDemoLimitModal() {
this.eventService.onDemoLimitModalShow.subscribe((featureName: string) => {
this.demoLimitNotice = {
@@ -208,6 +220,10 @@ export class AppComponent {
})
}
/**
* Listens for licence events so banner can be displayed.
* App is free tier, licence will expire, is expired or is invalid
*/
public subscribeToLicenseEvents() {
this.licenceService.isAppFreeTier.subscribe((isAppFreeTier: boolean) => {
this.freeTierBanner = isAppFreeTier
@@ -227,6 +243,10 @@ export class AppComponent {
)
}
/**
* Listens for an event that will activate od deactivate full application.
* Based on licence key prcoessing result
*/
public subscribeToAppActive() {
this.licenceService.isAppActivated.subscribe((value: any) => {
this.appActive = value
@@ -248,31 +268,51 @@ export class AppComponent {
})
}
/**
* When startupservice request is finished with valid response, this event will
* make sure loading screen is gone.
*/
public subscribeToStartupData() {
this.eventService.onStartupDataLoaded.subscribe(() => {
this.startupDataLoaded = true
})
}
/**
* Opens requests modal when requested from event service
*/
public subscribeToRequestsModal() {
this.eventService.onRequestsModalOpen.subscribe((value: boolean) => {
this.requestsModal = true
})
}
/**
* Closes abort modal with matching ID (there could be multiple abort modals open)
*/
public closeAbortModal(abortId: number) {
let abortIndex = this.sasjsAborts.findIndex((abort) => abort.id === abortId)
this.sasjsAborts.splice(abortIndex, 1)
}
/**
* Toggles sidebar when requested from event service
*/
public toggleSidebar() {
this.eventService.toggleSidebar()
}
/**
* Whether or not current route includes the route from param
* @param route route to check
*/
public isMainRoute(route: string): boolean {
return this.router.url.includes(route)
}
/**
* Opens a page for updating the licence.
*/
public openLicencingPage() {
this.router.navigateByUrl('/licensing/update')
}

View File

@@ -1,5 +1,5 @@
declare module 'save-svg-as-png'
declare module 'numbro/dist/languages.min'
declare interface Navigator {
msSaveBlob: (blob: any, defaultName?: string) => boolean
}

View File

@@ -11,33 +11,16 @@ import { NotFoundComponent } from './not-found/not-found.component'
import { SasStoreService } from './services/sas-store.service'
import { SharedModule } from './shared/shared.module'
// import { EditorComponent } from './editor/editor.component'
import { ActionsComponent } from './actions/actions.component'
import { AppSharedModule } from './app-shared.module'
import { ApproveDetailsComponent } from './approve-details/approve-details.component'
import { ApproveComponent } from './approve/approve.component'
import { DeployComponent } from './deploy/deploy.component'
import { AutomaticComponent } from './deploy/sections/automatic/automatic.component'
import { ManualComponent } from './deploy/sections/manual/manual.component'
import { SasjsConfiguratorComponent } from './deploy/sections/sasjs-configurator/sasjs-configurator.component'
import { GroupComponent } from './group/group.component'
import { HistoryComponent } from './history/history.component'
import { LicensingComponent } from './licensing/licensing.component'
import { LineageComponent } from './lineage/lineage.component'
import { MetadataComponent } from './metadata/metadata.component'
import { PipesModule } from './pipes/pipes.module'
import { RoleComponent } from './role/role.component'
import { ApproveRouteComponent } from './routes/approve-route/approve-route.component'
import { HistoryRouteComponent } from './routes/history-route/history-route.component'
import { ReviewRouteComponent } from './routes/review-route/review-route.component'
import { LicensingGuard } from './routes/licensing.guard'
import { UsernavRouteComponent } from './routes/usernav-route/usernav-route.component'
import { AppService } from './services/app.service'
import { InfoModalComponent } from './shared/abort-modal/info-modal.component'
import { RequestsModalComponent } from './shared/requests-modal/requests-modal.component'
import { SubmitterComponent } from './submitter/submitter.component'
import { UserComponent } from './user/user.component'
import { HomeModule } from './home/home.module'
import { SystemComponent } from './system/system.component'
import { DirectivesModule } from './directives/directives.module'
import { ViyaApiExplorerComponent } from './viya-api-explorer/viya-api-explorer.component'
import { NgxJsonViewerModule } from 'ngx-json-viewer'
@@ -46,27 +29,11 @@ import { NgxJsonViewerModule } from 'ngx-json-viewer'
declarations: [
AppComponent,
NotFoundComponent,
ApproveComponent,
ApproveDetailsComponent,
ActionsComponent,
HistoryComponent,
LineageComponent,
SubmitterComponent,
ApproveRouteComponent,
HistoryRouteComponent,
MetadataComponent,
ReviewRouteComponent,
ReviewRouteComponent,
UsernavRouteComponent,
UserComponent,
GroupComponent,
RoleComponent,
RequestsModalComponent,
DeployComponent,
InfoModalComponent,
LicensingComponent,
ManualComponent,
AutomaticComponent,
SasjsConfiguratorComponent,
SystemComponent,
ViyaApiExplorerComponent
],
imports: [
@@ -84,7 +51,7 @@ import { NgxJsonViewerModule } from 'ngx-json-viewer'
DirectivesModule,
NgxJsonViewerModule
],
providers: [AppService, SasStoreService, ApproveComponent, LicensingGuard],
providers: [AppService, SasStoreService, LicensingGuard],
bootstrap: [AppComponent]
})
export class AppModule {}

View File

@@ -7,22 +7,20 @@ import { ModuleWithProviders } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
import { HomeComponent } from './home/home.component'
import { ApproveComponent } from './approve/approve.component'
import { ApproveDetailsComponent } from './approve-details/approve-details.component'
import { ActionsComponent } from './actions/actions.component'
import { HistoryComponent } from './history/history.component'
import { NotFoundComponent } from './not-found/not-found.component'
import { SubmitterComponent } from './submitter/submitter.component'
import { ApproveRouteComponent } from './routes/approve-route/approve-route.component'
import { DeployComponent } from './deploy/deploy.component'
import { LicensingComponent } from './licensing/licensing.component'
import { LicensingGuard } from './routes/licensing.guard'
import { ReviewRouteComponent } from './routes/review-route/review-route.component'
import { StageModule } from './stage/stage.module'
import { EditorModule } from './editor/editor.module'
import { ViewerModule } from './viewer/viewer.module'
import { SystemComponent } from './system/system.component'
import { ReviewModule } from './review/review.module'
import { DeployModule } from './deploy/deploy.module'
import { LicensingModule } from './licensing/licensing.module'
import { SystemModule } from './system/system.module'
/**
* Defining routes
*/
export const ROUTES: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{
@@ -30,23 +28,28 @@ export const ROUTES: Routes = [
loadChildren: () => ViewerModule
},
{
path: 'approve',
component: ApproveRouteComponent,
/**
* Load review module (approve, history, submitted)
*/
path: 'review',
component: ReviewRouteComponent,
children: [
{ path: '', pathMatch: 'full', redirectTo: 'toapprove' },
{ path: 'toapprove', component: ApproveComponent },
{ path: 'approveDet/:tableId', component: ApproveDetailsComponent },
{ path: 'submitted', component: SubmitterComponent }
{
path: '',
loadChildren: () => ReviewModule
}
]
},
{
path: 'licensing/:action',
component: LicensingComponent,
canActivate: [LicensingGuard],
canDeactivate: [LicensingGuard]
path: 'licensing',
loadChildren: () => LicensingModule
},
{ path: 'home', component: HomeComponent },
{
/**
* Load editor module with subroutes
*/
path: 'editor',
loadChildren: () => EditorModule
},
@@ -54,16 +57,20 @@ export const ROUTES: Routes = [
path: 'stage',
loadChildren: () => StageModule
},
{ path: 'system', component: SystemComponent },
{ path: 'actions/:libds/:dsid', component: ActionsComponent },
{ path: 'history', component: HistoryComponent },
{ path: 'submitted', component: SubmitterComponent },
{ path: 'submitted/:tableId', component: SubmitterComponent },
{ path: 'deploy', component: DeployComponent },
{ path: 'deploy/manualdeploy', component: DeployComponent },
{
path: 'system',
loadChildren: () => SystemModule
},
{
path: 'deploy',
loadChildren: () => DeployModule
},
{ path: '**', component: NotFoundComponent }
]
/**
* Exporting routes
*/
export const ROUTING: ModuleWithProviders<RouterModule> = RouterModule.forRoot(
ROUTES,
{ useHash: true }

View File

@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { DeployComponent } from './deploy.component'
const routes: Routes = [
{ path: '', component: DeployComponent },
{ path: 'manualdeploy', component: DeployComponent }
]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DeployRoutingModule {}

View File

@@ -78,6 +78,9 @@ export class DeployComponent implements OnInit {
this.setDeployDefaults()
}
/**
* Setting default values used for deploy request
*/
public setDeployDefaults() {
this.dcPath = this.dcAdapterSettings?.dcPath || ''
this.selectedAdminGroup = this.dcAdapterSettings?.adminGroup || ''
@@ -86,6 +89,9 @@ export class DeployComponent implements OnInit {
}
}
/**
* Accepting terms of service shows next screen
*/
public termsAgreeChange() {
if (!this.autodeploy) {
this.getAdminGroups()
@@ -94,6 +100,9 @@ export class DeployComponent implements OnInit {
this.step++
}
/**
* Fetches admin groups from VIYA to be selected for a backend deploy
*/
public getAdminGroups() {
fetch(
this.sasJsConfig.serverUrl + '/identities/groups?sortBy=name&limit=5000',

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { DeployComponent } from './deploy.component'
import { AutomaticComponent } from './sections/automatic/automatic.component'
import { ManualComponent } from './sections/manual/manual.component'
import { SasjsConfiguratorComponent } from './sections/sasjs-configurator/sasjs-configurator.component'
import { ClarityModule } from '@clr/angular'
import { FormsModule } from '@angular/forms'
import { DeployRoutingModule } from './deploy-routing.module'
@NgModule({
declarations: [
DeployComponent,
AutomaticComponent,
ManualComponent,
SasjsConfiguratorComponent
],
imports: [CommonModule, FormsModule, ClarityModule, DeployRoutingModule]
})
export class DeployModule {}

View File

@@ -55,6 +55,13 @@ export class AutomaticComponent implements OnInit {
ngOnInit(): void {}
/**
* Executes sas.json file to deploy the backend
* Method will first try to run the `auto deploy`
* If that fails the rest of the code is ignored.
* If request is successfull, method will continue to try
* to create database if checkbox is toggled on
*/
public async executeJson() {
this.autodeploying = true
this.isSubmittingJson = true
@@ -99,6 +106,9 @@ export class AutomaticComponent implements OnInit {
}
}
/**
* Runs the `makedata` request sending the ADMIN and DCPATH values
*/
public createDatabase() {
let data = {
fromjs: [

View File

@@ -52,6 +52,9 @@ export class ManualComponent implements OnInit {
ngOnInit(): void {}
/**
* FIXME: Remove
*/
public async executableContext() {
// getExecutableContexts now need AuthConfig parameter which we don't have on web
// this.contextsLoading = true
@@ -65,10 +68,16 @@ export class ManualComponent implements OnInit {
// this.contextsLoading = false
}
/**
* Removes sas.json file attached to the input
*/
public clearUploadInput(event: Event) {
this.deployService.clearUploadInput(event)
}
/**
* Reads attached SAS file to be sent to sas for execution (backend deploy)
*/
public onSasFileChange(event: any) {
this.preloadedFile = false
@@ -93,12 +102,18 @@ export class ManualComponent implements OnInit {
fileReader.readAsText(file)
}
/**
* Reads attached JSON file to be sent to sas for execution (backend deploy)
*/
public async onJsonFileChange(event: any) {
let file = event.target.files[0]
this.jsonFile = await this.deployService.readFile(file)
}
/**
* Appending precode lines to the attached sas or json file for backend deploy
*/
public addPrecodeLines() {
let headerLines = [
`%let context=${this.selectedContext};`,
@@ -110,6 +125,9 @@ export class ManualComponent implements OnInit {
this.linesOfCode.unshift(...headerLines)
}
/**
* Downloadng file with precode included
*/
public downloadSasPrecodeFile() {
let linesAsText = this.linesOfCode.join('\n')
let filename = this.fileName.split('.')[0]
@@ -117,6 +135,9 @@ export class ManualComponent implements OnInit {
this.downloadFile(linesAsText, filename, 'sas')
}
/**
* Used for downloading log and repsonse as a file
*/
public downloadFile(
content: any,
filename: string,
@@ -125,10 +146,17 @@ export class ManualComponent implements OnInit {
this.deployService.downloadFile(content, filename, extension)
}
/**
* Saving dcpath to localstorage
* FIXME: maybe it'snot necessary
*/
public saveDcPath() {
localStorage.setItem('deploy_dc_loc', this.dcPath)
}
/**
* Send sas.json to be executed (deploying backend)
*/
public async executeJson() {
this.isSubmittingJson = true
@@ -162,6 +190,9 @@ export class ManualComponent implements OnInit {
this.isSubmittingJson = false
}
/**
* Send sas file to be executed (deploying backend)
*/
public async executeSAS() {
this.executingScript = true
this.jobLog = ''
@@ -194,6 +225,10 @@ export class ManualComponent implements OnInit {
}
}
/**
* Running makedata service
* @param newTab open and run in new tab
*/
public createDatabase(newTab: boolean = true) {
if (newTab) {
let url =

View File

@@ -5,7 +5,6 @@ import { ServerType } from '@sasjs/utils/types/serverType'
import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings'
import { SASGroup } from 'src/app/models/sas/public-getgroups.model'
import { SASjsApiServerInfo } from 'src/app/models/sasjs-api/SASjsApiServerInfo.model'
import { HelperService } from 'src/app/services/helper.service'
import { SasService } from 'src/app/services/sas.service'
import { SasjsService } from 'src/app/services/sasjs.service'
@@ -51,6 +50,9 @@ export class SasjsConfiguratorComponent implements OnInit {
this.getServerInfo()
}
/**
* Fethes the sasjs server instance info
*/
getServerInfo() {
this.sasjsService
.getServerInfo()
@@ -59,6 +61,9 @@ export class SasjsConfiguratorComponent implements OnInit {
})
}
/**
* Fetches user groups from the `usernav/usergroupsbymember` service
*/
getUserGroups() {
this.loading = true
@@ -99,6 +104,9 @@ export class SasjsConfiguratorComponent implements OnInit {
)
}
/**
* Creating database
*/
makeData() {
// const _debug = "&_debug=131"; //debug on
const _debug = ' ' //debug off

View File

@@ -14,7 +14,9 @@ export class DragNdropDirective {
@Output() fileDropped = new EventEmitter<any>()
@Output() fileDraggedOver = new EventEmitter<any>()
// Dragover listener
/**
* Dragover listener
*/
@HostListener('dragover', ['$event'])
onDragOver(event: any) {
event.preventDefault()
@@ -26,7 +28,9 @@ export class DragNdropDirective {
}
}
// Dragleave listener
/**
* Dragleave listener
*/
@HostListener('dragleave', ['$event'])
public onDragLeave(event: any) {
event.preventDefault()
@@ -34,7 +38,9 @@ export class DragNdropDirective {
this.fileOver = false
}
// Drop listener
/**
* Drop listener
*/
@HostListener('drop', ['$event'])
public ondrop(event: any) {
event.preventDefault()
@@ -48,6 +54,9 @@ export class DragNdropDirective {
}
}
/**
* Checks wether dragging element contain files
*/
private containsFiles(event: any) {
if (event && event.dataTransfer && event.dataTransfer.types) {
for (let i = 0; i < event.dataTransfer.types.length; i++) {

View File

@@ -22,6 +22,9 @@ export class FileDropDirective {
this.element = element
}
/**
* Dragging element drop event
*/
@HostListener('drop', ['$event'])
onDrop(event: DragEvent): void {
this._preventAndStop(event)
@@ -39,6 +42,9 @@ export class FileDropDirective {
this.fileDrop.emit(fileList)
}
/**
* Dragging element drag over event
*/
@HostListener('dragover', ['$event'])
onDragOver(event: DragEvent): void {
this._preventAndStop(event)
@@ -59,6 +65,10 @@ export class FileDropDirective {
this.fileOver.emit(false)
}
/**
* Prevent propagation trough elements and stop default behavior
* For particular event
*/
protected _preventAndStop(event: MouseEvent): void {
event.preventDefault()
event.stopPropagation()

View File

@@ -21,6 +21,9 @@ export class FileSelectDirective {
this.element = element
}
/**
* Checks if files exist in the input after input change
*/
isEmptyAfterSelection(): boolean {
return !!this.element.nativeElement.attributes.multiple
}

View File

@@ -1,7 +0,0 @@
export interface RowValidation {
valid: boolean
invalidError: string
rowNumber?: number
colName?: string
value?: string
}

View File

@@ -59,6 +59,12 @@ export class EditRecordComponent implements OnInit {
ngOnInit(): void {}
/**
* Runs native HOT validator against cell value
* @param cellValidation column rules
* @param cellValue value in the cell that is beign validated
* @returns Promise boolean - wether valid or invalid
*/
async validateRecordCol(
cellValidation: any,
cellValue: any
@@ -74,6 +80,12 @@ export class EditRecordComponent implements OnInit {
})
}
/**
* Fired when date field in the record change
* Function will parse date and format to string
* @param date picker value
* @param colKey column name (key)
*/
recordDateChange(date: Date, colKey: string) {
let cellValidation = this.currentRecordValidator?.getRule(colKey)
let format = cellValidation ? cellValidation.dateFormat : ''
@@ -82,24 +94,38 @@ export class EditRecordComponent implements OnInit {
this.currentRecord[colKey] = moment(date).format(format)
}
isRecordModalInvalid(): boolean {
return this.currentRecordInvalidCols.length > 0
}
/**
* Close edit record modal and apply changes by emitting output event
*/
confirmRecordEdit() {
if (this.currentRecordInvalidCols.length < 1) {
this.onRecordChange.emit(this.currentRecord)
}
}
/**
* Close edit record modal without applying the changes
*/
closeRecordEdit() {
this.onRecordEditClose.emit()
}
/**
* Emitting output event when dropdown (autocomplete) input in any col change
* @param colName column name (key)
* @param col column index
*/
onRecordDropdownChange(colName: string, col: number) {
this.onRecordDropdownChanged.emit({ colName, col })
}
/**
* Emitting output event when input is focused (clicked on) so we can run a `dynamic cell validation`
* Since that bit must be run from the parent component (editor.component)
* Result is then applied in the `cellValidation` variable and automatically updated in this component.
* @param event input event
* @param colName column name (key)
*/
onRecordInputFocus(event: any, colName: number) {
this.onRecordInputFocused.emit({ event, colName })
}

View File

@@ -385,7 +385,6 @@
[class.hidden]="hotTable.hidden"
[licenseKey]="hotTable.licenseKey"
>
<!--[licenseKey]=""-->
</hot-table>
</div>
@@ -597,7 +596,7 @@
</button>
</div>
</clr-modal>
<clr-modal [(clrModalOpen)]="pkDups">
<clr-modal class="duplicate-keys-modal" [(clrModalOpen)]="pkDups">
<h3 class="modal-title">Error</h3>
<div class="modal-body">
<p>

View File

@@ -15,7 +15,15 @@ import { Subject, Subscription } from 'rxjs'
import { SasStoreService } from '../services/sas-store.service'
import * as XLSX from '@sheet/crypto'
/**
* Used in combination with buffer
*/
const iconv = require('iconv-lite')
/**
* In combination with `iconv` is used for encoding json data captured with sheet js from excel file into a file again
* Which will be send to backend
*/
const Buffer = require('buffer/').Buffer
type AOA = any[][]
@@ -63,6 +71,8 @@ import {
} from './utils/renderers.utils'
import { isStringDecimal, isStringNumber } from './utils/types.utils'
import { LicenceService } from '../services/licence.service'
import * as numbro from 'numbro'
import * as languages from 'numbro/dist/languages.min'
@Component({
selector: 'app-editor',
@@ -362,12 +372,19 @@ export class EditorComponent implements OnInit, AfterViewInit {
private cdf: ChangeDetectorRef,
private hotRegisterer: HotTableRegisterer
) {
const lang = languages[window.navigator.language]
if (lang)
numbro.default.registerLanguage(languages[window.navigator.language])
this.hotRegisterer = new HotTableRegisterer()
this.parseRestrictions()
this.setRestrictions()
}
/**
* Prepare feature restrictions based on licence key
*/
private parseRestrictions() {
this.restrictions.restrictAddRecord =
this.licenceState.value.addRecord === false
@@ -377,6 +394,10 @@ export class EditorComponent implements OnInit, AfterViewInit {
this.licenceState.value.fileUpload === false
}
/**
* Applying prepared restrictions
* @param overrideRestrictions can be used to apply and override specific restrictions
*/
private setRestrictions(overrideRestrictions?: EditorRestrictions) {
if (overrideRestrictions) {
this.restrictions = {
@@ -396,6 +417,9 @@ export class EditorComponent implements OnInit, AfterViewInit {
}
}
/**
* Disabling add row button based on wether rows limit is present
*/
private checkRowLimit() {
if (this.columnLevelSecurityFlag) return
@@ -410,12 +434,19 @@ export class EditorComponent implements OnInit, AfterViewInit {
}
}
/**
* Resetting filter variables
*/
public resetFilter() {
if (this.queryFilterCompList.first) {
this.queryFilterCompList.first.resetFilter()
}
}
/**
* Openning file upload modal
* If feature is locked, `feature locked` modal will be shown
*/
public onShowUploadModal() {
if (this.restrictions.restrictFileUpload) {
this.eventService.showDemoLimitModal('File Upload')
@@ -433,6 +464,10 @@ export class EditorComponent implements OnInit, AfterViewInit {
if (!this.uploadPreview) this.showUploadModal = true
}
/**
* Called by FileDropDirective
* @param e true if file is dragged over the drop zone
*/
public fileOverBase(e: boolean): void {
this.hasBaseDropZoneOver = e
}
@@ -614,6 +649,10 @@ export class EditorComponent implements OnInit, AfterViewInit {
return returnObj
}
/**
* When excel is password protected we will display the password promppt for user to type password in.
* @returns Password user input or undefined if discarded by user
*/
public promptExcelPassword(): Promise<string | undefined> {
return new Promise((resolve, reject) => {
this.filePasswordModal = true
@@ -639,6 +678,13 @@ export class EditorComponent implements OnInit, AfterViewInit {
})
}
/**
* Parses attached file, to be uploaded
* If attached file is CSV it will be send to backend straight away
* If attached file is EXCEL it will be displayed in the table, in preview mode
* @param event file drop event
* @param dropped whether it's dropped or added by browse button
*/
public getFileDesc(event: any, dropped: boolean = false) {
this.excelUploadState = 'Loading'
this.excelFileParsing = true
@@ -1004,6 +1050,9 @@ export class EditorComponent implements OnInit, AfterViewInit {
}
}
/**
* Submits attached excel file that is in preview mode
*/
public submitExcel() {
if (this.licenceState.value.submit_rows_limit !== Infinity) {
this.submitLimitNotice = true
@@ -1013,12 +1062,25 @@ export class EditorComponent implements OnInit, AfterViewInit {
this.getFile()
}
/**
* This method will run validations and upload all of the pending files that are in the uploader queue
*/
public getFile() {
if (this.checkInvalid()) {
this.eventService.showAbortModal(null, 'Invalid values are present.')
return
}
this.validatePrimaryKeys()
if (this.duplicatePkIndexes.length !== 0) {
this.pkDups = true
this.submit = false
return
} else {
this.pkDups = false
}
this.uploadLoading = true
let filesToUpload: UploadFile[] = []
@@ -1075,6 +1137,9 @@ export class EditorComponent implements OnInit, AfterViewInit {
)
}
/**
* After excel file is attached and parsed, this function will display it's content in the HOT table in read only mode
*/
public getPendingExcelPreview() {
this.queryTextSaved = this.queryText
this.queryText = ''
@@ -1126,46 +1191,12 @@ export class EditorComponent implements OnInit, AfterViewInit {
this.excelFileParsing = false
this.excelUploadState = null
})
/**
* This is half validation feature to speed up file upload
* Currently disabled but will leave it here in case it needs to be re-enabled
*/
// this.excelUploadState = 'Validating-DQ'
// this.validateRowsOnPendingExcel(
// async (rowValidation: RowValidation | undefined) => {
// if (rowValidation) {
// this.eventService.showAbortModal(
// 'Excel validation',
// `Please fix the data and re-submit the file. Invalid data details: <br><br> Row: ${rowValidation.rowNumber} <br> Column: ${rowValidation.colName} <br> Reason: <strong>${rowValidation.invalidError}</strong> <br> Invalid value: ${rowValidation.value}`
// )
// this.excelFileParsing = false
// this.excelUploadState = null
// } else {
// this.excelUploadState = 'Validating-HOT'
// hot.updateSettings(
// {
// data: this.dataSource
// },
// false
// )
// hot.render()
// hot.validateCells(() => {
// this.showUploadModal = false
// this.uploadPreview = true
// this.excelFileParsing = false
// this.excelUploadState = null
// })
// }
// }
// )
}
/**
* Drops the attached excel file
* @param discardData wheter to discard data parsed from the file or to keep it in the table after dropping a attached excel file
*/
public discardPendingExcel(discardData?: boolean) {
this.hotInstance.updateSettings({
maxRows: this.licenceState.value.editor_rows_allowed
@@ -1189,6 +1220,10 @@ export class EditorComponent implements OnInit, AfterViewInit {
}
}
/**
* Drops attached excel file, keeps it's data in the DC table
* User can now edit the table and submit. Witout the file present.
*/
public previewTableEditConfirm() {
this.discardPendingExcel()
this.convertToCorrectTypes(this.dataSource)

View File

@@ -1,5 +1,10 @@
import { DcValidation } from 'src/app/shared/dc-validator/models/dc-validation.model'
/**
* Wrapper for DC Validation because we need `noLinkOption` property
* to be used as a flag to show/hide button that generates link for the
* edit record modal
*/
export interface EditRecordModal extends DcValidation {
noLinkOption: boolean
[key: string]: any

View File

@@ -1,19 +0,0 @@
export interface CellValidation {
data: string
length: number
type?: string
source: string[]
format?: number
validator?: any
valid?: boolean
renderer?: any
dateFormat?: string
readOnly?: boolean
desc?: string
correctFormat?: boolean
/**
* Key for accessing object fields, any type because it can be
* any of the types interface have
*/
[key: string]: any
}

View File

@@ -1,36 +0,0 @@
export enum ColumnType {
string = 'string',
number = 'number'
}
export interface ColumnInterface {
id: number | undefined
name: string | undefined
type: ColumnType | undefined
length: number | undefined
}
export class Column implements ColumnInterface {
public id: number | undefined
public name: string | undefined
public type: ColumnType | undefined
public length: number | undefined
public static fromPlainObject(obj: object) {
return Object.assign(new Column(), obj)
}
constructor(id?: number, name?: string, type?: ColumnType, length?: number) {
this.id = id
this.name = name
this.type = type
this.length = length
}
get hsType() {
return (
(this.type === ColumnType.string && 'text') ||
(this.type === ColumnType.number && 'numeric') ||
null
)
}
}

View File

@@ -1,3 +1,7 @@
/**
* Model for the dynamic cell validation in the editor
* (sending whole row to the backend service and recieving data for the cell dropdown)
*/
export interface DynamicExtendedCellValidation {
DISPLAY_INDEX: number
DISPLAY_TYPE: string

View File

@@ -1,8 +1,14 @@
/**
* Edit record modal - input has been focused event
*/
export interface EditRecordInputFocusedEvent {
event: any
colName: number
}
/**
* Edit record modal - dropdown has been changed event
*/
export interface EditRecordDropdownChangeEvent {
colName: string
col: number

View File

@@ -1,3 +1,6 @@
/**
* Editor restrictions model (based on the licencing)
*/
export interface EditorRestrictions {
restrictEditRecord?: boolean // Feature is locked but edit/add record buttons are visible so when user clicks he gets the `locked feature modal`
restrictAddRecord?: boolean // Same as editRecord, but for addRecord

View File

@@ -1,71 +0,0 @@
import { Column, ColumnType } from './models/column'
export enum TableType {
INPUT = 'In',
OUTPUT = 'Out'
}
export interface TableInterface {
id: number | undefined
name: string | undefined
data: Array<Object>
columns: Array<Column>
type: TableType | undefined
}
export class Table implements TableInterface {
public id: number | undefined
public name: string | undefined
public data: Array<any>
public columns: Array<Column> = []
public type: TableType | undefined
public static fromPlainObject(obj: any) {
obj.columns = obj.columns.map((column: object) => {
return Column.fromPlainObject(column)
})
return Object.assign(new Table(), obj)
}
constructor(
id?: number,
name?: string,
type?: TableType,
data?: Array<Object>,
columns?: Array<Column>
) {
this.id = id
this.name = name
this.type = type
this.data = data || [{}]
this.columns = columns || []
}
public getNextColumnId(): number {
let highestIdColumn: any = this.columns.sort(
(cA: any, cB: any) => cA.id - cB.id
)[this.columns.length - 1]
return (highestIdColumn && highestIdColumn.id + 1) || 0
}
public addColumn(column: Column) {
this.columns.push(column)
this.data.forEach((row: any) => {
if (column.name) {
row[column.name] = column.type === ColumnType.string ? '' : null
}
})
return column
}
public removeColumn(colInd: any) {
this.data.forEach((row) => {
delete row[this.columns[colInd].name!]
})
this.columns.splice(colInd, 1)
}
}

View File

@@ -1,3 +1,6 @@
/**
* Converting date object to the UTC time string
*/
export const dateToUtcTime = (date: Date) => {
let timeStr = ('0' + date.getUTCHours()).slice(-2) + ':'
timeStr = timeStr + ('0' + date.getUTCMinutes()).slice(-2) + ':'
@@ -5,6 +8,9 @@ export const dateToUtcTime = (date: Date) => {
return timeStr
}
/**
* Converts date object to the time string
*/
export const dateToTime = (date: Date) => {
let timeStr = ('0' + date.getHours()).slice(-2) + ':'
timeStr = timeStr + ('0' + date.getMinutes()).slice(-2) + ':'
@@ -12,6 +18,9 @@ export const dateToTime = (date: Date) => {
return timeStr
}
/**
* Converts date object to the YYYY-MM-DD
*/
export const dateFormat = (date: Date) => {
return (
date.getFullYear() +

View File

@@ -1,9 +1,17 @@
import { Col } from 'src/app/shared/dc-validator/models/col.model'
/**
* Converts excel date serial number to JS date
*/
export const excelDateToJSDate = (serial: number) => {
return new Date(Math.round((serial - 25569) * 86400 * 1000))
}
/**
* Parsing table columns for the HOT in editor
* Converts array of objects into array of strings, every string is column name (key)
* @param data array of objects (columns data)
*/
export const parseTableColumns = (data: Col[]): string[] => {
const columns: string[] = []
@@ -16,6 +24,12 @@ export const parseTableColumns = (data: Col[]): string[] => {
return columns
}
/**
* Captures headers that are not found in the current table but is found in the uploaded file data
* @param data
* @param headers
* @returns string array of missing headers
*/
export const getMissingHeaders = (data: any, headers: any) => {
const missingHeaders: string[] = []
const remainingHeaders: string[] = []

View File

@@ -1,3 +1,7 @@
/**
* Custom renderer for HOT cell
* Used to show error icon
*/
export const errorRenderer = (
instance: any,
td: any,
@@ -14,6 +18,10 @@ export const errorRenderer = (
return td
}
/**
* Custom renderer for HOT cell
* Used to revert cell back to original state (no spinner, no error)
*/
export const noSpinnerRenderer = (
instance: any,
td: any,
@@ -28,7 +36,11 @@ export const noSpinnerRenderer = (
return td
}
// Spinner shown whilst waiting for SAS to respond
/**
* Custom renderer for HOT cell
* Used to show loading spinner in the cell
* (Spinner shown whilst waiting for SAS to respond)
*/
export const spinnerRenderer = (
instance: any,
td: any,

View File

@@ -0,0 +1,19 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { LicensingGuard } from '../routes/licensing.guard'
import { LicensingComponent } from './licensing.component'
const routes: Routes = [
{
path: ':action',
component: LicensingComponent,
canActivate: [LicensingGuard],
canDeactivate: [LicensingGuard]
}
]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class LicensingRoutingModule {}

View File

@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { LicensingRoutingModule } from './licensing-routing.module'
import { ClarityModule } from '@clr/angular'
import { SharedModule } from '../shared/shared.module'
import { LicensingComponent } from './licensing.component'
import { FormsModule } from '@angular/forms'
@NgModule({
declarations: [LicensingComponent],
imports: [
CommonModule,
FormsModule,
ClarityModule,
LicensingRoutingModule,
SharedModule
]
})
export class LicensingModule {}

View File

@@ -0,0 +1,5 @@
export interface ErrorBody {
message: string
details: any
raw: any
}

View File

@@ -13,6 +13,7 @@
[(ngModel)]="usePickers"
(change)="usePickersChange()"
type="checkbox"
id="usePickers"
/>
<label class="clr-control-label"> Use pickers </label>
</clr-checkbox-wrapper>

View File

@@ -137,6 +137,11 @@ export class QueryComponent
public whereClause: string | undefined
public logicOperators: Array<string> = ['AND', 'OR']
/**
* Temporary values array used in pickers
* because they need particular format to work
* before sending values to backend, values are parsed
*/
public queryDateTime: QueryDateTime[] = []
public currentClauseIndex: number = -1
@@ -158,6 +163,11 @@ export class QueryComponent
}
}
/**
* Gets and sets temporary values selected with DATETIME or TIME picker
* Those values are used for picker to work with format it lieks
* Later before sending values to backend, values are parsed
*/
getQueryDateTime(clauseIndex: number, queryIndex: number): QueryDateTime {
let existingQueryDateTime = this.queryDateTime.find(
(x) => x.clauseIndex === clauseIndex && x.queryIndex === queryIndex
@@ -178,10 +188,16 @@ export class QueryComponent
return existingQueryDateTime
}
/**
* When toggling pickers feature we reset the temp picker values array
*/
usePickersChange() {
this.queryDateTime = []
}
/**
* Resets all variables used for filtering
*/
public resetFilter() {
this.whereString = undefined
this.whereClause = undefined
@@ -210,6 +226,10 @@ export class QueryComponent
this.whereClauseFn(true)
}
/**
* `Globals` are used to store filtering state (variables) as a caching feature
* until browser reloads
*/
public setToGlobals() {
if (!this.caching) return
@@ -237,6 +257,10 @@ export class QueryComponent
console.log('globals', globals)
}
/**
* `Globals` are used to store filtering state (variables) as a caching feature
* until browser reloads
*/
public getFromGlobals() {
if (!this.caching) return
@@ -269,6 +293,11 @@ export class QueryComponent
}
}
/**
* Sets filtering multiple caluses group logic (and / or)
*
* @param groupLogic to set
*/
public setGroupLogic(groupLogic: any) {
this.groupLogic = groupLogic
this.clauses.groupLogic = groupLogic

View File

@@ -1,13 +1,13 @@
import { ActivatedRoute } from '@angular/router'
import { SasStoreService } from '../services/sas-store.service'
import { SasStoreService } from '../../services/sas-store.service'
import { Component, AfterViewInit, OnDestroy } from '@angular/core'
import { Subscription } from 'rxjs'
import { Router } from '@angular/router'
import { EventService } from '../services/event.service'
import { EventService } from '../../services/event.service'
import {
AuditorsPostdataSASResponse,
Param
} from '../models/sas/auditors-postdata.model'
} from '../../models/sas/auditors-postdata.model'
interface ChangesObj {
ind: any
@@ -89,7 +89,7 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
}
public goToApprovalsList() {
this.route.navigateByUrl('/approve')
this.route.navigateByUrl('/review/approve')
}
public getTable(tableId: any) {
@@ -136,7 +136,7 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
await this.sasStoreService
.rejecting(rejParams, 'BrowserParams', 'approvers/rejection')
.then((res: any) => {
this.route.navigateByUrl('/history')
this.route.navigateByUrl('/review/history')
})
.catch((err: any) => {
this.acceptLoading = false
@@ -156,7 +156,7 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
await this.sasStoreService
.approveTable(approveParams, 'SASControlTable', 'auditors/postdata')
.then((res: any) => {
this.route.navigateByUrl('/history')
this.route.navigateByUrl('/review/history')
})
.catch((err: any) => {
this.acceptLoading = false
@@ -164,7 +164,7 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
}
public goToSubmitList() {
this.route.navigateByUrl('/submitted')
this.route.navigateByUrl('/review/submitted')
}
public async callChangesInfo(tableId: any) {

View File

@@ -1,8 +1,8 @@
import { Component, OnInit, ChangeDetectorRef } from '@angular/core'
import { SasStoreService } from '../services/sas-store.service'
import { SasStoreService } from '../../services/sas-store.service'
import { Router } from '@angular/router'
import { SasService } from '../services/sas.service'
import { EventService } from '../services/event.service'
import { SasService } from '../../services/sas.service'
import { EventService } from '../../services/event.service'
interface ApproveData {
tableId: string
@@ -45,7 +45,7 @@ export class ApproveComponent implements OnInit {
if (this.approveList !== undefined) {
this.tableId = this.approveList[ind].tableId
this.route.navigateByUrl(
'approve/approveDet/' + this.approveList[ind].tableId
'review/approveDet/' + this.approveList[ind].tableId
)
}
}

View File

@@ -1,11 +1,13 @@
import { Component, OnInit } from '@angular/core'
import { SasStoreService } from '../services/sas-store.service'
import { Router } from '@angular/router'
import { SasService } from '../services/sas.service'
import { EventService } from '../services/event.service'
import { SASjsConfig } from '@sasjs/adapter'
import { LicenceService } from '../services/licence.service'
import {
LicenceService,
SasStoreService,
EventService,
SasService
} from 'src/app/services'
@Component({
selector: 'app-history',

View File

@@ -0,0 +1,22 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { ApproveDetailsComponent } from './approve-details/approve-details.component'
import { ApproveComponent } from './approve/approve.component'
import { SubmitterComponent } from './submitter/submitter.component'
import { HistoryComponent } from './history/history.component'
const ROUTES: Routes = [
{ path: 'approve', component: ApproveComponent },
{ path: 'approveDet/:tableId', component: ApproveDetailsComponent },
{ path: 'submitted', component: SubmitterComponent },
{ path: 'submitted/:tableId', component: SubmitterComponent },
{ path: 'history', component: HistoryComponent }
]
@NgModule({
declarations: [],
imports: [CommonModule, RouterModule.forChild(ROUTES)],
exports: [RouterModule]
})
export class ReviewRoutingModule {}

View File

@@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { ClarityModule } from '@clr/angular'
import { HotTableModule } from '@handsontable/angular'
import { DirectivesModule } from '../directives/directives.module'
import { SharedModule } from '../shared/shared.module'
import { ApproveDetailsComponent } from './approve-details/approve-details.component'
import { ApproveComponent } from './approve/approve.component'
import { ReviewRoutingModule } from './review-routing.module'
import { SubmitterComponent } from './submitter/submitter.component'
import { HistoryComponent } from './history/history.component'
@NgModule({
declarations: [
ApproveComponent,
ApproveDetailsComponent,
SubmitterComponent,
HistoryComponent
],
imports: [
CommonModule,
FormsModule,
ReviewRoutingModule,
ClarityModule,
HotTableModule.forRoot(),
DirectivesModule,
SharedModule
]
})
export class ReviewModule {}

View File

@@ -1,9 +1,7 @@
import { Component, AfterViewInit, OnInit } from '@angular/core'
import { Subscription } from 'rxjs'
import { SasStoreService } from '../services/sas-store.service'
import { ActivatedRoute, Router } from '@angular/router'
import { SasService } from '../services/sas.service'
import { EventService } from '../services/event.service'
import { SasStoreService, EventService, SasService } from '../../services'
interface SubmitterData {
tableId: string
@@ -46,7 +44,7 @@ export class SubmitterComponent implements OnInit, AfterViewInit {
}
public goToDetails(table_id: any) {
this.router.navigateByUrl('/submitted/' + table_id)
this.router.navigateByUrl('/review/submitted/' + table_id)
}
public getDetails(sub: any, index: any) {

View File

@@ -1,15 +0,0 @@
import { Component, OnInit } from '@angular/core'
@Component({
selector: 'app-approve-route',
templateUrl: './approve-route.component.html',
styleUrls: ['./approve-route.component.scss'],
host: {
class: 'content-container'
}
})
export class ApproveRouteComponent implements OnInit {
constructor() {}
ngOnInit() {}
}

View File

@@ -1 +0,0 @@
<router-outlet></router-outlet>

View File

@@ -1,12 +0,0 @@
import { Component, OnInit } from '@angular/core'
@Component({
selector: 'app-edit-route',
templateUrl: './edit-route.component.html',
styleUrls: ['./edit-route.component.scss']
})
export class EditRouteComponent implements OnInit {
constructor() {}
ngOnInit() {}
}

View File

@@ -1 +0,0 @@
<router-outlet></router-outlet>

View File

@@ -1,12 +0,0 @@
import { Component, OnInit } from '@angular/core'
@Component({
selector: 'app-history-route',
templateUrl: './history-route.component.html',
styleUrls: ['./history-route.component.scss']
})
export class HistoryRouteComponent implements OnInit {
constructor() {}
ngOnInit() {}
}

View File

@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core'
@Component({
selector: 'app-review-route',
templateUrl: './review-route.component.html',
styleUrls: ['./review-route.component.scss'],
host: {
class: 'content-container'
}
})
export class ReviewRouteComponent implements OnInit {
constructor() {}
ngOnInit() {}
}

View File

@@ -17,6 +17,17 @@ export class HelperService {
console.log('Is IE or Edge?', this.isMicrosoft)
}
/**
* Converts a JavaScript date object to a SAS Date or Datetime, given the logic below:
*
* A JS Date contains the number of _milliseconds_ since 01/01/1970
* A SAS Date contains the number of _days_ since 01/01/1960
* A SAS Datetime contains the number of _seconds_ since 01/01/1960
*
* @param jsDate JS Date to be converted. The type is instance of `Date`
* @param unit Unit in which to return the SAS Date / datetime, eg `sasdate | sasdatetime`
* @returns SAS Date value based on `unit` param
*/
public convertJsDateToSasDate(
jsDate: string | Date,
unit: string = 'days'
@@ -63,6 +74,17 @@ export class HelperService {
return 0
}
/**
* Converts a SAS Date or Datetime to a JavaScript date object, given the logic below:
*
* A JS Date contains the number of _milliseconds_ since 01/01/1970
* A SAS Date contains the number of _days_ since 01/01/1960
* A SAS Datetime contains the number of _seconds_ since 01/01/1960
*
* @param sasValue SAS Date or Datetime to be converted. The type could be `number` or `string.
* @param unit Unit from which to convert the SAS Date / Datetime, eg `sasdate | sasdatetime`
* @returns JavaScript Date object
*/
public convertSasDaysToJsDate(
sasValue: number | string,
unit: string = 'days'
@@ -87,6 +109,11 @@ export class HelperService {
return new Date(msNegativeTenYears + sasValue * msInDay)
}
/**
*
* @param array all elements in the clarity tree
* @param arrToFilter sub array in the tree to be filtered for example `tables`
*/
public treeOnFilter(array: any, arrToFilter: string) {
let search = array['searchString'] ? array['searchString'] : ''
let arrToFilterArray = arrToFilter.split('.')[0]

View File

@@ -40,6 +40,16 @@ export class SasStoreService {
private loggerService: LoggerService
) {}
/**
* Wrapper for making request to service
* Should be removed, as it's redundant now
* TODO: Refactor to call editors/getdata directly
* @param tableData
* @param tableName
* @param program
* @param libds
* @returns
*/
public async callService(
tableData: Array<any>,
tableName: string,
@@ -60,6 +70,15 @@ export class SasStoreService {
return response
}
/**
* Calling editors/stagedata - saving table data, sending request to backend
* @param tableParams params to send to backend
* @param tableData data to be updated
* @param tableName name of the table to be updated
* @param program service against which we send request
* @param $dataFormats column data formats recieved from backend, sending it back
* @returns adapter.request() response
*/
public async updateTable(
tableParams: any,
tableData: any,
@@ -86,6 +105,13 @@ export class SasStoreService {
return res
}
/**
* Sending request to 'approvers/getapprovals' to fetch approvals list
* @param tableData sending to backend table data
* @param tableName sending to backend table name
* @param program service to run request on
* @returns HTTP Response
*/
public async getApprovals(
tableData: any,
tableName: string,
@@ -96,6 +122,13 @@ export class SasStoreService {
let res: any = await this.sasService.request(program, tables)
return res
}
/**
* Interceptor for loading of the submitted details
* @param detail submitter
* @param index submitter index
* @param data submit data
*/
public async sendDetails(detail: any, index: any, data: any) {
let details = Object.assign({ sub: true }, detail)
let subData = data[index]
@@ -105,11 +138,20 @@ export class SasStoreService {
}
this.submittDetail.next(allData)
}
/**
*
* @returns All submits
*/
public async getSubmitts() {
let res: any = await this.sasService.request('editors/getsubmits', null)
return res
}
/**
*
* @returns All libraries
*/
public async viewLibs() {
return this.sasService.request('public/viewlibs', null)
}

View File

@@ -13,6 +13,7 @@ import { DcAdapterSettings } from '../models/DcAdapterSettings'
import { AppStoreService } from './app-store.service'
import { LoggerService } from './logger.service'
import { RequestWrapperOptions } from '../models/RequestWrapperOptions'
import { ErrorBody } from '../models/ErrorBody'
@Injectable({
providedIn: 'root'
@@ -39,6 +40,11 @@ export class SasService {
private router: Router
) {}
/**
* Same as `setup` function in the sasjs.service, this is the constructor replacement.
* This function is being called by `app.service`.
* Because of timing and dependency issues
*/
public sasServiceInit() {
this.dcAdapterSettings = this.appStoreService.getDcAdapterSettings()
@@ -80,6 +86,16 @@ export class SasService {
}
}
/**
* Runing a backend request against a service.
* Function also handles the displaying of success or error modals.
*
* @param url service to run reuqest against
* @param data to be sent to backend service
* @param config additional parameters to force eg. { debug: false }
* @param wrapperOptions used to suppress error or success abort modals after request is finished
* @returns
*/
public request(
url: string,
data: any,
@@ -197,6 +213,14 @@ export class SasService {
})
}
/**
* Uploads a file to the backend, using the adapter upload function.
*
* @param sasService Service to which the file will be sent
* @param files Files to be sent
* @param params Aditional parameters eg. { debug: false }
* @returns HTTP Response
*/
public uploadFile(sasService: string, files: UploadFile[], params: any) {
return this.sasjsAdapter.uploadFile(sasService, files, params)
}
@@ -487,9 +511,3 @@ export class SasService {
}
}
}
interface ErrorBody {
message: string
details: any
raw: any
}

View File

@@ -25,6 +25,12 @@ export class SasjsService {
private appStoreService: AppStoreService
) {}
/**
* This function is replacing the constructor.
* The reason for this is timing issues, other services eg. sas.service, app-store.service
* must be initialized before this bit of code is executed.
* This function is being called by `sas.service`
*/
setup() {
const adapterConfig = this.appStoreService.getDcAdapterSettings()
@@ -32,10 +38,18 @@ export class SasjsService {
this.driveUrl = `${this.url}/drive`
}
/**
*
* @returns Sasjs/server information
*/
getServerInfo(): Observable<SASjsApiServerInfo> {
return this.http.get<SASjsApiServerInfo>(`${this.url}/info`)
}
/**
* Gets file contents on a given path
* @param filePath path to the file
*/
getFileFromDrive(filePath: string) {
return this.http.get(
`${this.driveUrl}/file/?_filePath=${filePath}`,
@@ -43,6 +57,11 @@ export class SasjsService {
)
}
/**
* Gets folder contents on a given path
* @param folderPath path to the folder
* @returns
*/
getFolderContentsFromDrive(
folderPath: string
): Observable<SASjsApiDriveFolderContents> {

View File

@@ -41,6 +41,14 @@ export class InfoModalComponent implements OnInit {
this.data = newData
}
/**
* Wheter or not to show the `Open configurator button`
* Button used for navigating to the `configuration` page
* Only for SAS9
* @param sasService backend service that caused this info modal to be shown
* Decision is made based on that service path
* @returns
*/
showConfiguratorButton(sasService: string | null) {
const sasjsConfig = this.sasService.getSasjsConfig()
@@ -54,6 +62,9 @@ export class InfoModalComponent implements OnInit {
this.onConfirmModalClick.emit()
}
/**
* Only on SAS9, opening a backend configurator/deploy page
*/
openConfigurator() {
this.eventService.startupDataLoaded()
this.router.navigateByUrl('/deploy')

View File

@@ -19,6 +19,7 @@ import { mergeColsRules } from './utils/mergeColsRules'
import { parseColType } from './utils/parseColType'
import { dqValidate } from './validations/dq-validation'
import { specialMissingNumericValidator } from './validations/hot-custom-validators'
import { applyNumericFormats } from './utils/applyNumericFormats'
export class DcValidator {
private rules: DcValidation[] = []
@@ -41,6 +42,7 @@ export class DcValidator {
this.hotInstance = hotInstance
this.rules = parseColType(sasparams.COLTYPE)
this.rules = mergeColsRules(cols, this.rules, $dataFormats)
this.rules = applyNumericFormats(this.rules)
this.dqrules = dqRules
this.dqdata = dqData
this.primaryKeys = sasparams.PK.split(' ')

View File

@@ -0,0 +1,26 @@
import { DcValidation } from '../models/dc-validation.model'
import * as languages from 'numbro/dist/languages.min'
/**
* Applying the numeric formats based on the browser locale/language
* So that correct decimal separators are applied.
* For example european format (thousand dot, decimal comma): 1.000,00
*
* @param rules Cell Validation rules to be updated
* Those rules are passed in the `columns` property Of handsontable settings.
*/
export const applyNumericFormats = (rules: DcValidation[]): DcValidation[] => {
const lang = languages[window.navigator.language]
if (!lang) return rules
for (let rule of rules) {
if (rule.type === 'numeric')
rule.numericFormat = {
pattern: '0,0',
culture: window.navigator.language // use this for EUR (German),
// more cultures available on http://numbrojs.com/languages.html
}
}
return rules
}

View File

@@ -6,7 +6,8 @@ import { DcValidation } from '../models/dc-validation.model'
* Merging old validation params from sasparams with cols params
* @param sasparams sasparams coming from SAS
* @param cols cols coming from SAS
* @param rules rules to be updated
* @param rules Cell Validation rules to be updated
* Those rules are passed in the `columns` property Of handsontable settings.
* @returns
*/
export const mergeColsRules = (

View File

@@ -55,14 +55,14 @@
<a
*ngIf="isMainRoute('approve')"
clrVerticalNavLink
routerLink="/approve/submitted"
routerLink="/review/approve/submitted"
routerLinkActive="active"
>Submitted</a
>
<a
*ngIf="isMainRoute('approve')"
clrVerticalNavLink
routerLink="/approve/toapprove"
routerLink="/review/approve/toapprove"
routerLinkActive="active"
>To Approve</a
>

View File

@@ -47,7 +47,7 @@ export class StageComponent implements OnInit {
}
public approveTableScreen() {
this.route.navigateByUrl('/approve/approveDet/' + this.table_id)
this.route.navigateByUrl('/review/approveDet/' + this.table_id)
}
public viewerTableScreen() {

View File

@@ -0,0 +1,11 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { SystemComponent } from './system.component'
const routes: Routes = [{ path: '', component: SystemComponent }]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class SystemRoutingModule {}

View File

@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { SystemRoutingModule } from './system-routing.module'
import { SystemComponent } from './system.component'
import { ClarityModule } from '@clr/angular'
@NgModule({
declarations: [SystemComponent],
imports: [CommonModule, SystemRoutingModule, ClarityModule]
})
export class SystemModule {}

View File

@@ -201,16 +201,28 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
)
}
/**
* Open viewboxes modal
*/
public newViewbox() {
this.viewboxOpen = true
}
/**
* Resetting filter variables
*/
public resetFilter() {
if (this.queryFilterCompList.first) {
this.queryFilterCompList.first.resetFilter()
}
}
/**
* Searching table against particular string, data is comming from backend.
* There is also a toggle that will search for a numeric values
*
* @param inputElement input from which search string is captured
*/
public async searchTable(inputElement: any) {
this.searchLoading = true
@@ -249,6 +261,9 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
this.searchLoading = false
}
/**
* Re sending request to backend and re-setting data in the HOT
*/
public reloadTableData() {
this.viewData(this.urlFilterPk || 0)
}
@@ -278,6 +293,9 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
)
}
/**
* FIXME: Should be removed, not used
*/
public filterFn(input: string) {
let libraries = this.libraries
this.libraries = libraries.filter(
@@ -286,6 +304,9 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
)
}
/**
* Downloads file from backend, against `getrawdata` service, link is created and open in new tab
*/
public downloadData() {
let storage = this.sasjsConfig.serverUrl
let metaData = this.sasjsConfig.appLoc
@@ -320,6 +341,9 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
this.openDownload = false
}
/**
* Downloads file from backend, against `getddl` service, link is created and open in new tab
*/
public downloadDDL() {
let libref = this.lib
let ds = this.table
@@ -346,15 +370,27 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
this.openDownload = false
}
/**
* When clicked on textarea in the Web Query Modal, this function will
* select all text inside.
* @param evt textarea which contains the web query text
*/
public onCliCommandFocus(evt: any): void {
evt.preventDefault()
evt.target.select()
}
/**
* Navigate to the edit page of a viewing table
*/
public editTable() {
this.router.navigateByUrl('/editor/' + this.libTab)
}
/**
* Used to show/hide the edit table button
* @returns Wheter currently viewed table is edtiable
*/
public tableEditExists() {
let editTables: any = {}
editTables = globals.editor.libsAndTables
@@ -367,12 +403,18 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
return editTables[currentLibrary].includes(currentTable)
}
/**
* Navigate to the lineage of a viewing table
*/
public goToLineage() {
let routeUri = this.tableuri!.split('\\')[1]
let lineageUrl = `/view/lineage/${routeUri}/REVERSE`
this.router.navigateByUrl(lineageUrl)
}
/**
* Displays web query modal
*/
public showWebQuery() {
this.webQuery = true
let filter_pk: number

View File

@@ -13,9 +13,22 @@ import { SharedModule } from '../shared/shared.module'
import { ViewboxesModule } from '../shared/viewboxes/viewboxes.module'
import { QueryModule } from '../query/query.module'
import { DirectivesModule } from '../directives/directives.module'
import { UserComponent } from '../user/user.component'
import { RoleComponent } from '../role/role.component'
import { GroupComponent } from '../group/group.component'
import { LineageComponent } from '../lineage/lineage.component'
import { MetadataComponent } from '../metadata/metadata.component'
@NgModule({
declarations: [ViewerComponent, ViewRouteComponent],
declarations: [
ViewerComponent,
ViewRouteComponent,
UserComponent,
RoleComponent,
GroupComponent,
LineageComponent,
MetadataComponent
],
imports: [
ViewboxesModule,
CommonModule,

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