Compare commits

..

88 Commits

Author SHA1 Message Date
semantic-release-bot
d8010d4c0c chore(release): 7.4.0 [skip ci]
# [7.4.0](https://git.datacontroller.io/dc/dc/compare/v7.3.0...v7.4.0) (2026-02-20)

### Bug Fixes

* cli bump for mf_getscheme support ([a84ba41](a84ba41ea9))
* missing upcase on SNOW section, plus local sasjs target ([dc20064](dc200646f7))

### Features

* SAS code changes for snowflake support ([e273e87](e273e870ef))
2026-02-20 18:53:31 +00:00
a57b49c936 Merge pull request 'feat: SAS code changes for snowflake support' (#208) from sf into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 4m0s
Release / Build-and-test-development (push) Successful in 9m57s
Release / release (push) Successful in 7m57s
Reviewed-on: #208
2026-02-20 18:36:00 +00:00
a84ba41ea9 fix: cli bump for mf_getscheme support
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m10s
Build / Build-and-test-development (pull_request) Successful in 10m4s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m58s
2026-02-20 18:15:31 +00:00
dc200646f7 fix: missing upcase on SNOW section, plus local sasjs target
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m11s
Build / Build-and-test-development (pull_request) Successful in 9m55s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m25s
2026-02-20 01:08:55 +00:00
e273e870ef feat: SAS code changes for snowflake support
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m9s
Build / Build-and-test-development (pull_request) Successful in 10m1s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m57s
2026-02-20 00:15:23 +00:00
semantic-release-bot
6fc34aca00 chore(release): 7.3.0 [skip ci]
# [7.3.0](https://git.datacontroller.io/dc/dc/compare/v7.2.8...v7.3.0) (2026-02-10)

### Bug Fixes

* bump xlsx, add crypto-shim ([8dc18b1](8dc18b155a))
* correctly applying deletes on viya, also ([46cdeb0](46cdeb0bab))
* crypto module requirement for sheetjs/crypto package ([505d0af](505d0af2b3))
* disable parsing excel in web worker beacuse it breaks in the stream apps ([280bdee](280bdeeb1b))
* Display all contexts when installing DC on Viya ([d41f88f](d41f88f8bf))
* **edit:** use cellValidation keys and hotDataSchema to fill in defaults on add row ([4957548](495754816c))
* enabling closeouts for UPDATE in CAS tables ([8b8e8ae](8b8e8aec15))
* enabling rollback when the table has formatted values ([815d6e9](815d6e97a8))
* improvements to validations ([6ceb681](6ceb681463))
* remove IE checks and conditions ([ece6bd1](ece6bd1d78))
* updates to demodata to enable auto CAS promote ([7740d2a](7740d2ac86))
* upgrade angular core and compiler ([aecd597](aecd597687))
* using fcopy instead of binary copy for file upload, for Viya 2026 compatibility ([716ee6e](716ee6eba0))
* **viewer:** search causing blank Handsontable ([338c7a2](338c7a2e41)), closes [#206](#206)

### Features

* adding demo data job ([8c2aeac](8c2aeacc85))
* **dq rules:** notnull validation when invalid cell, will auto populate a default value ([96f2518](96f2518af9))
2026-02-10 19:17:29 +00:00
f97ac70678 Merge pull request 'demodata' (#203) from demodata into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m27s
Release / Build-and-test-development (push) Successful in 9m22s
Release / release (push) Successful in 7m53s
Reviewed-on: #203
2026-02-10 19:01:17 +00:00
6ceb681463 fix: improvements to validations
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m47s
Build / Build-and-test-development (pull_request) Successful in 9m38s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m21s
2026-02-10 18:26:02 +00:00
716ee6eba0 fix: using fcopy instead of binary copy for file upload, for Viya 2026 compatibility
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m42s
Build / Build-and-test-development (pull_request) Successful in 9m33s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m44s
2026-02-10 16:34:09 +00:00
f6b0f6b0cd Merge pull request 'fix(viewer): search causing blank Handsontable' (#207) from fix/206-search-issue into demodata
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m39s
Build / Build-and-test-development (pull_request) Successful in 9m37s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m31s
Reviewed-on: #207
2026-02-10 15:50:42 +00:00
s
737a652ff0 refactor(viewer): use drop instead of debounce
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m40s
Build / Build-and-test-development (pull_request) Successful in 9m31s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m32s
2026-02-10 15:36:20 +01:00
s
2995e5c9dc chore: restore comments and condition
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m52s
Build / Build-and-test-development (pull_request) Successful in 9m39s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m8s
2026-02-10 14:37:56 +01:00
s
338c7a2e41 fix(viewer): search causing blank Handsontable
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m46s
Build / Build-and-test-development (pull_request) Successful in 9m26s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m52s
Closes #206
2026-02-10 14:24:06 +01:00
ad27358deb Merge pull request 'fix(edit): use cellValidation keys and hotDataSchema to fill in defaults on add row' (#205) from fix/204-default-value into demodata
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m15s
Build / Build-and-test-development (pull_request) Successful in 10m18s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m50s
Reviewed-on: #205
2026-02-10 12:58:15 +00:00
s
495754816c fix(edit): use cellValidation keys and hotDataSchema to fill in defaults on add row
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m40s
Build / Build-and-test-development (pull_request) Successful in 10m33s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 26m51s
2026-02-10 13:53:29 +01:00
M
96f2518af9 feat(dq rules): notnull validation when invalid cell, will auto populate a default value
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m41s
Build / Build-and-test-development (pull_request) Successful in 9m30s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m50s
2026-02-10 12:29:15 +01:00
M
280bdeeb1b fix: disable parsing excel in web worker beacuse it breaks in the stream apps
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m41s
Build / Build-and-test-development (pull_request) Successful in 9m41s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m0s
2026-02-10 11:58:00 +01:00
46cdeb0bab fix: correctly applying deletes on viya, also
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m32s
Build / Build-and-test-development (pull_request) Successful in 9m14s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m18s
adding more info to the staged directory in relation to deletes.  Also
refactoring the demo data.
2026-02-09 23:23:08 +00:00
d41f88f8bf fix: Display all contexts when installing DC on Viya
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m58s
Build / Build-and-test-development (pull_request) Successful in 9m48s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m29s
2026-02-09 18:22:06 +00:00
815d6e97a8 fix: enabling rollback when the table has formatted values
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m35s
Build / Build-and-test-development (pull_request) Successful in 9m16s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m4s
2026-02-08 02:40:07 +00:00
4e35aefe41 Merge pull request 'Upgrade angular core and compiler' (#199) from fix/audit-20260112 into demodata
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m59s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m16s
Build / Build-and-test-development (pull_request) Successful in 9m48s
Reviewed-on: #199
Reviewed-by: mihajlo <mihajlo@4gl.io>
2026-02-07 23:46:19 +00:00
ca84915e43 Merge branch 'demodata' into fix/audit-20260112
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m15s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m43s
Build / Build-and-test-development (pull_request) Successful in 9m56s
2026-02-07 23:46:10 +00:00
zver
31cc7e9e4d chore: server release and more demodata
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m3s
Build / Build-and-test-development (pull_request) Successful in 9m46s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m52s
2026-02-07 23:44:37 +00:00
4ec107705e Merge branch 'main' into demodata
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 3m57s
Build / Build-and-test-development (pull_request) Successful in 9m9s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Failing after 3h13m38s
2026-02-07 20:04:16 +00:00
zver
7740d2ac86 fix: updates to demodata to enable auto CAS promote
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 3m36s
Build / Build-and-test-development (pull_request) Successful in 9m2s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Failing after 3h13m50s
2026-02-07 20:02:14 +00:00
zver
8c2aeacc85 feat: adding demo data job 2026-02-06 20:50:55 +00:00
zver
8b8e8aec15 fix: enabling closeouts for UPDATE in CAS tables 2026-02-06 20:50:38 +00:00
semantic-release-bot
6ac3f660e9 chore(release): 7.2.8 [skip ci]
## [7.2.8](https://git.datacontroller.io/dc/dc/compare/v7.2.7...v7.2.8) (2026-02-06)

### Bug Fixes

* bump adapter version ([f4c8699](f4c8699aaf))
2026-02-06 12:51:50 +00:00
7ee576a9c1 Merge pull request 'Update @sasjs/adapter version' (#202) from fix/bump-adapter-20260206 into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m32s
Release / Build-and-test-development (push) Successful in 9m7s
Release / release (push) Successful in 7m39s
Reviewed-on: #202
2026-02-06 12:35:55 +00:00
s
f4c8699aaf fix: bump adapter version
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m48s
Build / Build-and-test-development (pull_request) Successful in 9m12s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m14s
2026-02-06 13:12:51 +01:00
zver
4273ca6e5c chore: demo data job 2026-02-06 02:13:23 +00:00
semantic-release-bot
4ba043b77e chore(release): 7.2.7 [skip ci]
## [7.2.7](https://git.datacontroller.io/dc/dc/compare/v7.2.6...v7.2.7) (2026-02-05)

### Bug Fixes

* dclib not found error in getchangeinfo job ([86791db](86791dbaca))
2026-02-05 19:15:36 +00:00
0169415ea2 Merge pull request 'fix: dclib not found error in getchangeinfo job' (#201) from dclibfix into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m32s
Release / Build-and-test-development (push) Successful in 9m15s
Release / release (push) Successful in 7m56s
Reviewed-on: #201
2026-02-05 18:59:19 +00:00
86791dbaca fix: dclib not found error in getchangeinfo job
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m48s
Build / Build-and-test-development (pull_request) Successful in 9m21s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m34s
2026-02-05 16:53:23 +00:00
s
d5b58a3cbd test(excel): password tests - click away overlay modal
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m39s
Build / Build-and-test-development (pull_request) Successful in 9m28s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 17m38s
2026-01-14 17:38:36 +01:00
s
3d8281d27e test(excel): fix attachFile upload for password protected tests
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 3m43s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m3s
Build / Build-and-test-development (pull_request) Failing after 11m3s
2026-01-14 15:47:10 +01:00
s
b1a014c7bc test(excel): add password protected excels tests
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 3m36s
Build / Build-and-test-development (pull_request) Failing after 10m17s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m22s
2026-01-13 18:50:17 +01:00
s
505d0af2b3 fix: crypto module requirement for sheetjs/crypto package
Undid the crypto-shim changes from 8dc18b155a
2026-01-13 18:41:56 +01:00
s
ece6bd1d78 fix: remove IE checks and conditions 2026-01-13 17:00:56 +01:00
s
8dc18b155a fix: bump xlsx, add crypto-shim
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m29s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m24s
Build / Build-and-test-development (pull_request) Successful in 9m33s
crypto-shim fixes vulnerable crypto-browserify package used by sheetjs/crypto, shim is based on crypto-js
2026-01-13 15:04:17 +01:00
s
aecd597687 fix: upgrade angular core and compiler
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m50s
Build / Build-and-test-development (pull_request) Successful in 9m8s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m27s
2026-01-13 08:38:29 +01:00
c99f106bae Merge pull request 'Fix lighthouse ci save path' (#198) from fix/lighthouse-20251230 into main
Some checks failed
Release / Build-production-and-ng-test (push) Successful in 3m31s
Release / Build-and-test-development (push) Successful in 9m14s
Release / release (push) Failing after 3m16s
Reviewed-on: #198
Reviewed-by: mihajlo <mihajlo@4gl.io>
2026-01-08 11:38:32 +00:00
d2fc7ae6fe Merge branch 'main' into fix/lighthouse-20251230
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m15s
Build / Build-and-test-development (pull_request) Successful in 9m42s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m16s
2026-01-08 11:38:25 +00:00
semantic-release-bot
c1c1d0055a chore(release): 7.2.6 [skip ci]
## [7.2.6](https://git.datacontroller.io/dc/dc/compare/v7.2.5...v7.2.6) (2026-01-05)

### Bug Fixes

* **deps:** update angular and moment ([8c5b357](8c5b357dd2))
2026-01-05 13:56:25 +00:00
f37ec82d39 Merge branch 'main' into fix/lighthouse-20251230
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m4s
Build / Build-and-test-development (pull_request) Successful in 10m9s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 19m4s
2026-01-05 13:39:20 +00:00
6e9e30e0f0 Merge pull request 'npm audit vulnerabilities' (#197) from fix/audit-20251229 into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m54s
Release / Build-and-test-development (push) Successful in 10m5s
Release / release (push) Successful in 7m57s
Reviewed-on: #197
Reviewed-by: mihajlo <mihajlo@4gl.io>
2026-01-05 13:39:02 +00:00
s
990ddb5cd3 chore: update .gitignore
Some checks failed
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Has been cancelled
Build / Build-and-test-development (pull_request) Has been cancelled
Build / Build-and-ng-test (pull_request) Has been cancelled
2025-12-30 14:55:33 +01:00
s
c6ebbb48bb ci: fix lighthouse artifacts save path
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m55s
Build / Build-and-test-development (pull_request) Successful in 8m9s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m30s
2025-12-30 14:49:01 +01:00
s
95cc0b1c91 chore: undo unnecessary changes
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m49s
Build / Build-and-test-development (pull_request) Successful in 9m19s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m34s
2025-12-30 13:42:20 +01:00
s
81b282f1f1 chore: revert from esbuild back to webpack
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m41s
Build / Build-and-test-development (pull_request) Successful in 9m3s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m31s
2025-12-29 23:14:51 +01:00
s
8c5b357dd2 fix(deps): update angular and moment
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 2m28s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Has been cancelled
Build / Build-and-test-development (pull_request) Has been cancelled
2025-12-29 22:15:47 +01:00
s
a13b2cbfd2 chore: angular update from v18 to v19 2025-12-29 18:23:21 +01:00
s
19617c2285 chore: ng update cdk 2025-12-29 17:01:48 +01:00
s
f9794a973f chore: ng update cli, schematics, and core 2025-12-29 16:53:33 +01:00
s
65efe62d19 chore: prettier 2025-12-29 16:46:40 +01:00
s
683ddcaf53 chore: add ng command 2025-12-29 16:46:11 +01:00
semantic-release-bot
113e0bbc3c chore(release): 7.2.5 [skip ci]
## [7.2.5](https://git.datacontroller.io/dc/dc/compare/v7.2.4...v7.2.5) (2025-12-09)

### Bug Fixes

* (build) rebuilt package-lock files ([bfbfd55](bfbfd55fe7))
* (deps) bump @sasjs/cli and @sasjs/core ([d7c7302](d7c7302c12))
* (deps) bumped @sasjs/core, @sasjs/cli, @sasjs/utils and @sasjs/adapter ([af1657e](af1657e226))
* configurable audit table on restore check ([26ce95f](26ce95f7c1)), closes [#193](#193)
* improved testing ([fb3c49a](fb3c49aa8b))
* output values to intended macro variables ([43ae73c](43ae73c5f3))
2025-12-09 12:27:01 +00:00
2af97e40bf Merge pull request 'fix: (build) bump node version for pipelines' (#196) from node_version_bump_for_pipelines into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m23s
Release / Build-and-test-development (push) Successful in 7m59s
Release / release (push) Successful in 7m3s
Reviewed-on: #196
2025-12-09 12:12:31 +00:00
Trevor Moody
83cbe3aece chore: (build) bump node version for pipelines
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m36s
Build / Build-and-test-development (pull_request) Successful in 8m10s
Lighthouse Checks / lighthouse (24.5.0) (pull_request) Successful in 18m11s
2025-12-09 11:49:56 +00:00
ceac1ba614 Merge pull request 'bumpCoreAndCli_20251125' (#195) from bumpCoreAndCli_20251125 into main
Some checks failed
Release / Build-production-and-ng-test (push) Successful in 3m39s
Release / Build-and-test-development (push) Successful in 8m11s
Release / release (push) Failing after 3m19s
Reviewed-on: #195
2025-12-08 19:02:14 +00:00
allan
765fdbdf9d chore: merge
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m33s
Build / Build-and-test-development (pull_request) Successful in 8m16s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 18m46s
2025-12-08 17:41:07 +00:00
Trevor Moody
43ae73c5f3 fix: output values to intended macro variables
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m33s
Build / Build-and-test-development (pull_request) Successful in 8m19s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 19m13s
2025-12-08 16:55:22 +00:00
Trevor Moody
e57a0de8a9 chore: Re-basing file due to merge conflict 2025-12-08 16:53:03 +00:00
Trevor Moody
3de491105b chore: (build) amended licenseChecker.js exclusions
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m35s
Build / Build-and-test-development (pull_request) Successful in 8m11s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 19m7s
2025-12-08 16:04:41 +00:00
M
2fe690e962 chore: package-lock
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m27s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 1m46s
Build / Build-and-test-development (pull_request) Successful in 8m6s
2025-12-08 15:49:46 +01:00
M
b826d37086 chore: package-lock
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 39s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 53s
Build / Build-and-test-development (pull_request) Failing after 42s
2025-12-08 15:42:03 +01:00
Trevor Moody
bfbfd55fe7 fix: (build) rebuilt package-lock files
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 38s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 58s
Build / Build-and-test-development (pull_request) Failing after 44s
2025-12-08 14:16:56 +00:00
Trevor Moody
15f38efd52 chore: (build) use node 24.5.0
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 45s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 56s
Build / Build-and-test-development (pull_request) Failing after 41s
2025-12-08 13:02:50 +00:00
Trevor Moody
5d25681485 chore: lint fixes
Some checks failed
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 57s
Build / Build-and-test-development (pull_request) Failing after 42s
Build / Build-and-ng-test (pull_request) Failing after 41s
2025-12-08 12:30:00 +00:00
Trevor Moody
d26df376f8 chore: CRLF to LF
Some checks failed
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 56s
Build / Build-and-test-development (pull_request) Failing after 41s
Build / Build-and-ng-test (pull_request) Failing after 37s
2025-12-08 11:41:12 +00:00
cff3fb3bad Merge pull request 'fix: configurable audit table on restore check' (#194) from issue193 into main
Some checks failed
Release / Build-production-and-ng-test (push) Successful in 3m36s
Release / Build-and-test-development (push) Successful in 8m15s
Release / release (push) Failing after 3m20s
Reviewed-on: #194
2025-12-08 11:25:44 +00:00
Trevor Moody
fb3c49aa8b fix: improved testing
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 39s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Failing after 57s
Build / Build-and-test-development (pull_request) Failing after 58s
2025-12-08 10:21:37 +00:00
Trevor Moody
af1657e226 fix: (deps) bumped @sasjs/core, @sasjs/cli, @sasjs/utils and @sasjs/adapter 2025-11-28 04:11:46 +00:00
Trevor Moody
d7c7302c12 fix: (deps) bump @sasjs/cli and @sasjs/core 2025-11-25 14:24:36 +00:00
26ce95f7c1 fix: configurable audit table on restore check
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 5m41s
Build / Build-and-test-development (pull_request) Successful in 9m29s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 19m20s
closes #193
2025-10-27 20:13:49 +00:00
semantic-release-bot
4924df2ef3 chore(release): 7.2.4 [skip ci]
## [7.2.4](https://git.datacontroller.io/dc/dc/compare/v7.2.3...v7.2.4) (2025-10-14)

### Bug Fixes

* ensure reload after applying licence key ([cb1978b](cb1978bcaf))
* snyk report security patches ([387f512](387f5122f1))
2025-10-14 11:44:18 +00:00
2e141a5d52 chore(git): merge pull request 'fix: snyk report security patches' (#192) from dc_snyk into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m42s
Release / Build-and-test-development (push) Successful in 8m52s
Release / release (push) Successful in 8m13s
Reviewed-on: #192
Reviewed-by: mihajlo <mihajlo@4gl.io>
Reviewed-by: sead <sead@noreply.git.datacontroller.io>
2025-10-14 11:28:05 +00:00
Trevor Moody
cb1978bcaf fix: ensure reload after applying licence key
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m2s
Build / Build-and-test-development (pull_request) Successful in 8m49s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 18m48s
2025-10-14 12:05:27 +01:00
Trevor Moody
387f5122f1 fix: snyk report security patches
Some checks failed
Build / Build-and-ng-test (pull_request) Successful in 4m0s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 19m23s
Build / Build-and-test-development (pull_request) Failing after 11m1s
2025-10-10 12:54:22 +01:00
semantic-release-bot
db5887de21 chore(release): 7.2.3 [skip ci]
## [7.2.3](https://git.datacontroller.io/dc/dc/compare/v7.2.2...v7.2.3) (2025-10-02)

### Bug Fixes

* opening second table in viewer throws an error ([6c6b1cb](6c6b1cbf46))
2025-10-02 15:10:30 +00:00
fe24d9bcbd Merge pull request 'opening second table in viewer throws an error' (#191) from issue-189 into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m47s
Release / Build-and-test-development (push) Successful in 8m52s
Release / release (push) Successful in 8m25s
Reviewed-on: #191
Reviewed-by: allan <allan@4gl.io>
2025-10-02 14:54:13 +00:00
M
6c6b1cbf46 fix: opening second table in viewer throws an error
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m59s
Build / Build-and-test-development (pull_request) Successful in 9m5s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 19m55s
2025-10-02 16:23:00 +02:00
semantic-release-bot
4d65c9c999 chore(release): 7.2.2 [skip ci]
## [7.2.2](https://git.datacontroller.io/dc/dc/compare/v7.2.1...v7.2.2) (2025-09-23)

### Bug Fixes

* jsrsasign,  @sasjs/cli bump ([365f129](365f12996d))
2025-09-23 21:50:46 +00:00
4417279275 Merge pull request 'Addressing production vulnerabilities' (#188) from vulnerabilities into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m35s
Release / Build-and-test-development (push) Successful in 8m7s
Release / release (push) Successful in 7m48s
Reviewed-on: #188
2025-09-23 21:35:40 +00:00
M
365f12996d fix: jsrsasign, @sasjs/cli bump
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m52s
Build / Build-and-test-development (pull_request) Successful in 8m14s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 18m20s
2025-09-23 16:44:55 +02:00
semantic-release-bot
ef1015f33b chore(release): 7.2.1 [skip ci]
## [7.2.1](https://git.datacontroller.io/dc/dc/compare/v7.2.0...v7.2.1) (2025-08-08)

### Bug Fixes

* removing localhost from index.html ([225e693](225e693d1f))
2025-08-08 17:35:02 +00:00
b43dfb5cf4 Merge pull request 'fix: removing localhost from index.html' (#187) from localhostfix into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 3m54s
Release / Build-and-test-development (push) Successful in 8m25s
Release / release (push) Successful in 7m50s
Reviewed-on: #187
2025-08-08 17:19:17 +00:00
allan
225e693d1f fix: removing localhost from index.html
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 3m47s
Build / Build-and-test-development (pull_request) Successful in 8m27s
Lighthouse Checks / lighthouse (20.15.1) (pull_request) Successful in 18m41s
2025-08-08 18:08:49 +01:00
116 changed files with 10143 additions and 5624 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

View File

@@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.15.1 node-version: 24.5.0
- name: Install Google Chrome - name: Install Google Chrome
run: | run: |
@@ -58,7 +58,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.15.1 node-version: 24.5.0
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [20.15.1] node-version: [24.5.0]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -98,4 +98,4 @@ jobs:
with: with:
name: Lighthouse results name: Lighthouse results
path: client/lighthouse-reports path: client/lighthouse-reports
include-hidden-files: true include-hidden-files: true

View File

@@ -13,12 +13,12 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.14.0 node-version: 24.5.0
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
touch client/.npmrc touch client/.npmrc
echo '${{ secrets.NPMRC}}' > client/.npmrc echo '${{ secrets.NPMRC}}' > client/.npmrc
- name: Install Chrome for Angular tests - name: Install Chrome for Angular tests
run: | run: |
@@ -41,7 +41,7 @@ jobs:
npm ci npm ci
- name: Check audit - name: Check audit
# Audit should fail and stop the CI if critical vulnerability found # Audit should fail and stop the CI if critical vulnerability found
run: | run: |
npm audit --audit-level=critical --omit=dev npm audit --audit-level=critical --omit=dev
cd ./sas cd ./sas
@@ -68,12 +68,12 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.14.0 node-version: 24.5.0
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
touch client/.npmrc touch client/.npmrc
echo '${{ secrets.NPMRC}}' > client/.npmrc echo '${{ secrets.NPMRC}}' > client/.npmrc
- run: apt-get update - run: apt-get update
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
@@ -158,7 +158,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.14.0 node-version: 24.5.0
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
@@ -228,6 +228,8 @@ jobs:
cp sasjs/utils/favicon.ico ../client/dist/favicon.ico cp sasjs/utils/favicon.ico ../client/dist/favicon.ico
sasjs c -t server sasjs c -t server
rm -rf sasjsbuild/tests rm -rf sasjsbuild/tests
server_apploc="/Public/app/dc"
sed -i "s|apploc=\"[^\"]*\"|apploc=\"${server_apploc}\"|g" sasjsbuild/services/web/index.html
sasjs b -t server sasjs b -t server
cp sasjsbuild/server.json.zip ./sasjs_server.json.zip cp sasjsbuild/server.json.zip ./sasjs_server.json.zip

1
.gitignore vendored
View File

@@ -14,6 +14,7 @@ client/documentation
client/**/sheet-crypto.tgz client/**/sheet-crypto.tgz
client/.nx client/.nx
client/libraries/sheet-crypto.tgz client/libraries/sheet-crypto.tgz
client/lighthouse-reports
cypress.env.json cypress.env.json
sasjsbuild sasjsbuild
sasjsresults sasjsresults

View File

@@ -1,3 +1,104 @@
# [7.4.0](https://git.datacontroller.io/dc/dc/compare/v7.3.0...v7.4.0) (2026-02-20)
### Bug Fixes
* cli bump for mf_getscheme support ([a84ba41](https://git.datacontroller.io/dc/dc/commit/a84ba41ea9f0c97ae24f0a572b8cf5ec200f2132))
* missing upcase on SNOW section, plus local sasjs target ([dc20064](https://git.datacontroller.io/dc/dc/commit/dc200646f7df2fd1910841f392c314532aae7581))
### Features
* SAS code changes for snowflake support ([e273e87](https://git.datacontroller.io/dc/dc/commit/e273e870efbf7875db869b760f2c7b1f39d571ae))
# [7.3.0](https://git.datacontroller.io/dc/dc/compare/v7.2.8...v7.3.0) (2026-02-10)
### Bug Fixes
* bump xlsx, add crypto-shim ([8dc18b1](https://git.datacontroller.io/dc/dc/commit/8dc18b155abfc20fd0b043e0d70bbbc17e6b6811))
* correctly applying deletes on viya, also ([46cdeb0](https://git.datacontroller.io/dc/dc/commit/46cdeb0babee6870553a41877cbe85204e7099c4))
* crypto module requirement for sheetjs/crypto package ([505d0af](https://git.datacontroller.io/dc/dc/commit/505d0af2b3b3c1c79c65045dcaffc263e0f8e796))
* disable parsing excel in web worker beacuse it breaks in the stream apps ([280bdee](https://git.datacontroller.io/dc/dc/commit/280bdeeb1b82f00689f46c68a3cde3f2d24bc18f))
* Display all contexts when installing DC on Viya ([d41f88f](https://git.datacontroller.io/dc/dc/commit/d41f88f8bf5bb2c725ee3edba085e8961ab8c727))
* **edit:** use cellValidation keys and hotDataSchema to fill in defaults on add row ([4957548](https://git.datacontroller.io/dc/dc/commit/495754816c0e757b8f8b1c0ad51246dc7b65d957))
* enabling closeouts for UPDATE in CAS tables ([8b8e8ae](https://git.datacontroller.io/dc/dc/commit/8b8e8aec159ff2f50cfa4683bcd7a25aabb75bf8))
* enabling rollback when the table has formatted values ([815d6e9](https://git.datacontroller.io/dc/dc/commit/815d6e97a8e304d79d48cc949ba126e02a318dc1))
* improvements to validations ([6ceb681](https://git.datacontroller.io/dc/dc/commit/6ceb6814633691b6d4ac2cb898cfb75e9d609102))
* remove IE checks and conditions ([ece6bd1](https://git.datacontroller.io/dc/dc/commit/ece6bd1d787d722531334fc4f1396a94cf6d92ec))
* updates to demodata to enable auto CAS promote ([7740d2a](https://git.datacontroller.io/dc/dc/commit/7740d2ac8694295b33b40a30603d8239818896f5))
* upgrade angular core and compiler ([aecd597](https://git.datacontroller.io/dc/dc/commit/aecd5976875a7c01189248c5f5aa3478b28c1ab2))
* using fcopy instead of binary copy for file upload, for Viya 2026 compatibility ([716ee6e](https://git.datacontroller.io/dc/dc/commit/716ee6eba0a28f4f0a7a96b0719caf15da9b6e78))
* **viewer:** search causing blank Handsontable ([338c7a2](https://git.datacontroller.io/dc/dc/commit/338c7a2e418c47e34331bd04718cd816f978837c)), closes [#206](https://git.datacontroller.io/dc/dc/issues/206)
### Features
* adding demo data job ([8c2aeac](https://git.datacontroller.io/dc/dc/commit/8c2aeacc85da5c106c356709cefcb412ed0a71db))
* **dq rules:** notnull validation when invalid cell, will auto populate a default value ([96f2518](https://git.datacontroller.io/dc/dc/commit/96f2518af9e547956be5862a1322d9ab8e07369b))
## [7.2.8](https://git.datacontroller.io/dc/dc/compare/v7.2.7...v7.2.8) (2026-02-06)
### Bug Fixes
* bump adapter version ([f4c8699](https://git.datacontroller.io/dc/dc/commit/f4c8699aaf0b1e01b447296978a4f6dedc8903f9))
## [7.2.7](https://git.datacontroller.io/dc/dc/compare/v7.2.6...v7.2.7) (2026-02-05)
### Bug Fixes
* dclib not found error in getchangeinfo job ([86791db](https://git.datacontroller.io/dc/dc/commit/86791dbaca39034a19bf8f34efbddf898c57f2f7))
## [7.2.6](https://git.datacontroller.io/dc/dc/compare/v7.2.5...v7.2.6) (2026-01-05)
### Bug Fixes
* **deps:** update angular and moment ([8c5b357](https://git.datacontroller.io/dc/dc/commit/8c5b357dd286db331a6dcdeb3fd499fe3b634288))
## [7.2.5](https://git.datacontroller.io/dc/dc/compare/v7.2.4...v7.2.5) (2025-12-09)
### Bug Fixes
* (build) rebuilt package-lock files ([bfbfd55](https://git.datacontroller.io/dc/dc/commit/bfbfd55fe7e2dff3ce707763a2c7939ff365318b))
* (deps) bump @sasjs/cli and @sasjs/core ([d7c7302](https://git.datacontroller.io/dc/dc/commit/d7c7302c12ac60f355ab9b3b1b461fcf7d0719b8))
* (deps) bumped @sasjs/core, @sasjs/cli, @sasjs/utils and @sasjs/adapter ([af1657e](https://git.datacontroller.io/dc/dc/commit/af1657e226a4efd22cc87401a3850c4a665c2680))
* configurable audit table on restore check ([26ce95f](https://git.datacontroller.io/dc/dc/commit/26ce95f7c1d2260f81c240cd6b058db154d997e4)), closes [#193](https://git.datacontroller.io/dc/dc/issues/193)
* improved testing ([fb3c49a](https://git.datacontroller.io/dc/dc/commit/fb3c49aa8bfdc6acf2ae3034b885010dcdce32a6))
* output values to intended macro variables ([43ae73c](https://git.datacontroller.io/dc/dc/commit/43ae73c5f3ad919394201f54984b61bb2a52fcfe))
## [7.2.4](https://git.datacontroller.io/dc/dc/compare/v7.2.3...v7.2.4) (2025-10-14)
### Bug Fixes
* ensure reload after applying licence key ([cb1978b](https://git.datacontroller.io/dc/dc/commit/cb1978bcaf23b0bf45b5d3b78b9707fd4e48a5f4))
* snyk report security patches ([387f512](https://git.datacontroller.io/dc/dc/commit/387f5122f1ea6dff55d23c9223f17737283a94d3))
## [7.2.3](https://git.datacontroller.io/dc/dc/compare/v7.2.2...v7.2.3) (2025-10-02)
### Bug Fixes
* opening second table in viewer throws an error ([6c6b1cb](https://git.datacontroller.io/dc/dc/commit/6c6b1cbf460e5291ec746af017e764b894fff8d5))
## [7.2.2](https://git.datacontroller.io/dc/dc/compare/v7.2.1...v7.2.2) (2025-09-23)
### Bug Fixes
* jsrsasign, @sasjs/cli bump ([365f129](https://git.datacontroller.io/dc/dc/commit/365f12996db3ef50a4f4f099d5af15696c43bb42))
## [7.2.1](https://git.datacontroller.io/dc/dc/compare/v7.2.0...v7.2.1) (2025-08-08)
### Bug Fixes
* removing localhost from index.html ([225e693](https://git.datacontroller.io/dc/dc/commit/225e693d1fd4381f2b8ce42fecb508f0a9e9dad8))
# [7.2.0](https://git.datacontroller.io/dc/dc/compare/v7.1.1...v7.2.0) (2025-08-08) # [7.2.0](https://git.datacontroller.io/dc/dc/compare/v7.1.1...v7.2.0) (2025-08-08)

View File

@@ -41,6 +41,8 @@
"zone.js", "zone.js",
"text-encoding", "text-encoding",
"crypto-js/md5", "crypto-js/md5",
"crypto-js/sha1",
"crypto-js/sha512",
"buffer", "buffer",
"numbro", "numbro",
"@clr/icons", "@clr/icons",
@@ -51,26 +53,22 @@
"base64-arraybuffer", "base64-arraybuffer",
"@handsontable/formulajs" "@handsontable/formulajs"
], ],
"polyfills": [ "polyfills": ["src/polyfills.ts", "zone.js"],
"src/polyfills.ts",
"zone.js"
],
"outputPath": "dist", "outputPath": "dist",
"resourcesOutputPath": "images",
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
"assets": [ "assets": [
"src/images" {
"glob": "**/*",
"input": "src/images",
"output": "images"
}
], ],
"styles": [ "styles": ["src/styles.scss"],
"src/styles.scss" "scripts": ["node_modules/marked/marked.min.js"],
], "webWorkerTsConfig": "tsconfig.worker.json",
"scripts": [ "main": "src/main.ts"
"node_modules/marked/marked.min.js"
],
"webWorkerTsConfig": "tsconfig.worker.json"
}, },
"configurations": { "configurations": {
"production": { "production": {
@@ -103,9 +101,7 @@
} }
}, },
"development": { "development": {
"vendorChunk": true,
"extractLicenses": false, "extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true, "sourceMap": true,
"optimization": false, "optimization": false,
"namedChunks": true "namedChunks": true
@@ -134,20 +130,11 @@
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"polyfills": [ "polyfills": ["src/polyfills.ts", "zone.js", "zone.js/testing"],
"src/polyfills.ts",
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss", "inlineStyleLanguage": "scss",
"assets": [ "assets": ["src/favicon.ico", "src/assets"],
"src/favicon.ico", "styles": ["src/styles.scss"],
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": [], "scripts": [],
"karmaConfig": "karma.conf.js", "karmaConfig": "karma.conf.js",
"webWorkerTsConfig": "tsconfig.worker.json" "webWorkerTsConfig": "tsconfig.worker.json"
@@ -156,10 +143,7 @@
"lint": { "lint": {
"builder": "@angular-eslint/builder:lint", "builder": "@angular-eslint/builder:lint",
"options": { "options": {
"lintFilePatterns": [ "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
"src/**/*.ts",
"src/**/*.html"
]
} }
} }
} }

View File

@@ -309,6 +309,83 @@ context('excel tests: ', function () {
}) })
}) })
it('22 | Uploads password protected Excel and unlocks with correct password', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
cy.get('.buttonBar button:last-child')
.should('exist')
.click()
.then(() => {
cy.get('input[type="file"]#file-upload')
.attachFile(`/${fixturePath}/regular_excel_password.xlsx`)
.then(() => {
// Wait for password modal to appear
cy.get('#filePasswordInput', { timeout: 10000 })
.should('be.visible')
.type('123123')
// Click Unlock button
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
// Click away the overlay
cy.get('.modal-footer .btn.btn-primary', { timeout: 5000 }).click()
// Verify file loads successfully
cy.get('.btn-upload-preview', { timeout: 60000 })
.should('be.visible')
.then(() => {
submitExcel()
rejectExcel(done)
})
})
})
})
it('23 | Uploads password protected Excel and handles wrong password', (done) => {
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
cy.get('.buttonBar button:last-child')
.should('exist')
.click()
.then(() => {
cy.get('input[type="file"]#file-upload')
.attachFile(`/${fixturePath}/regular_excel_password.xlsx`)
.then(() => {
// First attempt: Enter wrong password
cy.get('#filePasswordInput', { timeout: 10000 })
.should('be.visible')
.type('wrongpassword')
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
// Verify error message appears
cy.get('.modal-footer .color-red', { timeout: 10000 })
.should('be.visible')
.should('contain', "Sorry that didn't work, try again.")
// Modal should still be open for retry
cy.get('#filePasswordInput')
.should('be.visible')
.clear()
.type('123123')
// Second attempt: Enter correct password
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
// Click away the overlay
cy.get('.modal-footer .btn.btn-primary', { timeout: 5000 }).click()
// Verify file loads successfully
cy.get('.btn-upload-preview', { timeout: 60000 })
.should('be.visible')
.then(() => {
submitExcel()
rejectExcel(done)
})
})
})
})
// Large files break Cypress // Large files break Cypress
// it ('? | Uploads Excel with size of 5MB', (done) => { // it ('? | Uploads Excel with size of 5MB', (done) => {

Binary file not shown.

View File

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

View File

@@ -2,8 +2,8 @@ module.exports = {
ci: { ci: {
collect: { collect: {
settings: { settings: {
preset: "desktop", preset: 'desktop',
chromeFlags: "--no-sandbox --disable-dev-shm-usage" chromeFlags: '--no-sandbox --disable-dev-shm-usage'
}, },
url: [ url: [
'http://localhost:5000/AppStream/clickme/#/home/tables', 'http://localhost:5000/AppStream/clickme/#/home/tables',
@@ -37,6 +37,10 @@ module.exports = {
{ minScore: 0.4, aggregationMethod: 'median' } { minScore: 0.4, aggregationMethod: 'median' }
] ]
} }
},
upload: {
target: 'filesystem',
outputDir: './lighthouse-reports'
} }
} }
} }

11707
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -32,26 +32,27 @@
"compodoc:build": "compodoc -p tsconfig.doc.json --name 'Data Controller Client'", "compodoc:build": "compodoc -p tsconfig.doc.json --name 'Data Controller Client'",
"compodoc:build-and-serve": "compodoc -p tsconfig.doc.json -s --name 'Data Controller Client'", "compodoc:build-and-serve": "compodoc -p tsconfig.doc.json -s --name 'Data Controller Client'",
"compodoc:serve": "compodoc -s --name 'Data Controller Client'", "compodoc:serve": "compodoc -s --name 'Data Controller Client'",
"lighthouse": "lhci autorun" "lighthouse": "lhci autorun",
"ng": "ng"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^17.3.3", "@angular/animations": "^19.2.18",
"@angular/cdk": "^17.3.3", "@angular/cdk": "^19.2.19",
"@angular/common": "^17.3.3", "@angular/common": "^19.2.18",
"@angular/compiler": "^17.3.3", "@angular/compiler": "^19.2.18",
"@angular/core": "^17.3.3", "@angular/core": "^19.2.18",
"@angular/forms": "^17.3.3", "@angular/forms": "^19.2.18",
"@angular/platform-browser": "^17.3.3", "@angular/platform-browser": "^19.2.18",
"@angular/platform-browser-dynamic": "^17.3.3", "@angular/platform-browser-dynamic": "^19.2.18",
"@angular/router": "^17.3.3", "@angular/router": "^19.2.18",
"@cds/core": "^6.15.1", "@cds/core": "^6.15.1",
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz", "@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
"@clr/icons": "^13.0.2", "@clr/icons": "^13.0.2",
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz", "@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
"@handsontable/angular-wrapper": "16.0.1", "@handsontable/angular-wrapper": "16.0.1",
"@sasjs/adapter": "^4.12.2", "@sasjs/adapter": "^4.16.3",
"@sasjs/utils": "^3.4.0", "@sasjs/utils": "^3.5.3",
"@sheet/crypto": "file:libraries/sheet-crypto.tgz", "@sheet/crypto": "file:libraries/sheet-crypto.tgz",
"@types/d3-graphviz": "^2.6.7", "@types/d3-graphviz": "^2.6.7",
"@types/text-encoding": "0.0.35", "@types/text-encoding": "0.0.35",
@@ -66,9 +67,9 @@
"hyperformula": "^2.5.0", "hyperformula": "^2.5.0",
"iconv-lite": "^0.5.0", "iconv-lite": "^0.5.0",
"jquery-datetimepicker": "^2.5.21", "jquery-datetimepicker": "^2.5.21",
"jsrsasign": "^10.2.0", "jsrsasign": "^11.1.0",
"marked": "^5.0.0", "marked": "^5.0.0",
"moment": "^2.26.0", "moment": "^2.30.1",
"ngx-clipboard": "^16.0.0", "ngx-clipboard": "^16.0.0",
"ngx-json-viewer": "file:libraries/ngx-json-viewer-3.2.1.tgz", "ngx-json-viewer": "file:libraries/ngx-json-viewer-3.2.1.tgz",
"nodejs": "0.0.0", "nodejs": "0.0.0",
@@ -81,18 +82,18 @@
"tslib": "^2.3.0", "tslib": "^2.3.0",
"vm": "^0.1.0", "vm": "^0.1.0",
"webpack": "^5.91.0", "webpack": "^5.91.0",
"xlsx": "^0.18.5", "xlsx": "file:libraries/xlsx-0.20.3.tgz",
"zone.js": "~0.14.4" "zone.js": "~0.15.1"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^17.3.3", "@angular-devkit/build-angular": "^19.2.19",
"@angular-eslint/builder": "17.3.0", "@angular-eslint/builder": "19.8.1",
"@angular-eslint/eslint-plugin": "17.3.0", "@angular-eslint/eslint-plugin": "19.8.1",
"@angular-eslint/eslint-plugin-template": "17.3.0", "@angular-eslint/eslint-plugin-template": "19.8.1",
"@angular-eslint/schematics": "17.3.0", "@angular-eslint/schematics": "19.8.1",
"@angular-eslint/template-parser": "17.3.0", "@angular-eslint/template-parser": "19.8.1",
"@angular/cli": "^17.3.3", "@angular/cli": "^19.2.19",
"@angular/compiler-cli": "^17.3.3", "@angular/compiler-cli": "^19.2.18",
"@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-methods": "^7.18.6",
"@compodoc/compodoc": "^1.1.21", "@compodoc/compodoc": "^1.1.21",
"@cypress/webpack-preprocessor": "^5.17.1", "@cypress/webpack-preprocessor": "^5.17.1",
@@ -128,7 +129,7 @@
"rimraf": "3.0.2", "rimraf": "3.0.2",
"ts-loader": "^9.2.8", "ts-loader": "^9.2.8",
"ts-node": "^3.3.0", "ts-node": "^3.3.0",
"typescript": "~5.4.4", "typescript": "~5.8.3",
"wait-on": "^6.0.1", "wait-on": "^6.0.1",
"watch": "^1.0.2" "watch": "^1.0.2"
} }

View File

@@ -11,7 +11,7 @@ import { Location } from '@angular/common'
import '@clr/icons' import '@clr/icons'
import '@clr/icons/shapes/all-shapes' import '@clr/icons/shapes/all-shapes'
import { globals } from './_globals' import { globals } from './_globals'
import * as moment from 'moment' import moment from 'moment'
import { EventService } from './services/event.service' import { EventService } from './services/event.service'
import { AppService } from './services/app.service' import { AppService } from './services/app.service'
import { InfoModal } from './models/InfoModal' import { InfoModal } from './models/InfoModal'
@@ -42,7 +42,8 @@ ClarityIcons.addIcons(
selector: 'my-app', selector: 'my-app',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'], styleUrls: ['./app.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class AppComponent { export class AppComponent {
private dcAdapterSettings: DcAdapterSettings | undefined private dcAdapterSettings: DcAdapterSettings | undefined

View File

@@ -1,4 +1,4 @@
import { HttpClientModule } from '@angular/common/http' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { BrowserModule } from '@angular/platform-browser' import { BrowserModule } from '@angular/platform-browser'
@@ -36,12 +36,12 @@ import { AppSettingsService } from './services/app-settings.service'
InfoModalComponent, InfoModalComponent,
ViyaApiExplorerComponent ViyaApiExplorerComponent
], ],
bootstrap: [AppComponent],
imports: [ imports: [
BrowserAnimationsModule, BrowserAnimationsModule,
BrowserModule, BrowserModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
HttpClientModule,
ROUTING, ROUTING,
SharedModule, SharedModule,
ClarityModule, ClarityModule,
@@ -50,7 +50,12 @@ import { AppSettingsService } from './services/app-settings.service'
DirectivesModule, DirectivesModule,
NgxJsonViewerModule NgxJsonViewerModule
], ],
providers: [AppService, SasStoreService, LicensingGuard, AppSettingsService], providers: [
bootstrap: [AppComponent] AppService,
SasStoreService,
LicensingGuard,
AppSettingsService,
provideHttpClient(withInterceptorsFromDi())
]
}) })
export class AppModule {} export class AppModule {}

View File

@@ -14,7 +14,8 @@ import { DcAdapterSettings } from '../models/DcAdapterSettings'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class DeployComponent implements OnInit { export class DeployComponent implements OnInit {
public step: number = 0 public step: number = 0

View File

@@ -29,7 +29,8 @@ import {
selector: 'app-automatic-deploy', selector: 'app-automatic-deploy',
templateUrl: './automatic.component.html', templateUrl: './automatic.component.html',
styleUrls: ['./automatic.component.scss'], styleUrls: ['./automatic.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class AutomaticComponent implements OnInit { export class AutomaticComponent implements OnInit {
@Input() sasJs!: SASjs @Input() sasJs!: SASjs

View File

@@ -18,7 +18,8 @@ import { SasService } from 'src/app/services/sas.service'
selector: 'app-manual-deploy', selector: 'app-manual-deploy',
templateUrl: './manual.component.html', templateUrl: './manual.component.html',
styleUrls: ['./manual.component.scss'], styleUrls: ['./manual.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ManualComponent implements OnInit { export class ManualComponent implements OnInit {
@Input() sasJs!: SASjs @Input() sasJs!: SASjs

View File

@@ -20,7 +20,8 @@ import { SasjsService } from 'src/app/services/sasjs.service'
selector: 'app-sasjs-configurator', selector: 'app-sasjs-configurator',
templateUrl: './sasjs-configurator.component.html', templateUrl: './sasjs-configurator.component.html',
styleUrls: ['./sasjs-configurator.component.scss'], styleUrls: ['./sasjs-configurator.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class SasjsConfiguratorComponent implements OnInit { export class SasjsConfiguratorComponent implements OnInit {
@Input() sasJs!: SASjs @Input() sasJs!: SASjs

View File

@@ -7,7 +7,8 @@ import {
} from '@angular/core' } from '@angular/core'
@Directive({ @Directive({
selector: '[appDragNdrop]' selector: '[appDragNdrop]',
standalone: false
}) })
export class DragNdropDirective { export class DragNdropDirective {
@HostBinding('class.fileover') fileOver: boolean = false @HostBinding('class.fileover') fileOver: boolean = false

View File

@@ -9,7 +9,8 @@ import {
import { FileUploader } from '../models/FileUploader.class' import { FileUploader } from '../models/FileUploader.class'
@Directive({ @Directive({
selector: '[appFileDrop]' selector: '[appFileDrop]',
standalone: false
}) })
export class FileDropDirective { export class FileDropDirective {
@Input() uploader?: FileUploader @Input() uploader?: FileUploader

View File

@@ -9,7 +9,8 @@ import {
import { FileUploader } from '../models/FileUploader.class' import { FileUploader } from '../models/FileUploader.class'
@Directive({ @Directive({
selector: '[appFileSelect]' selector: '[appFileSelect]',
standalone: false
}) })
export class FileSelectDirective { export class FileSelectDirective {
@Input() uploader?: FileUploader @Input() uploader?: FileUploader

View File

@@ -6,7 +6,8 @@ import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'
* Calling functions in html is bad for performance * Calling functions in html is bad for performance
*/ */
@Directive({ @Directive({
selector: '[ngVar]' selector: '[ngVar]',
standalone: false
}) })
export class NgVarDirective { export class NgVarDirective {
@Input() @Input()

View File

@@ -1,7 +1,8 @@
import { Directive, HostListener } from '@angular/core' import { Directive, HostListener } from '@angular/core'
@Directive({ @Directive({
selector: '[appStealFocus]' selector: '[appStealFocus]',
standalone: false
}) })
export class StealFocusDirective { export class StealFocusDirective {
constructor() {} constructor() {}

View File

@@ -14,6 +14,7 @@ import { HelperService } from 'src/app/services/helper.service'
import { SasStoreService } from 'src/app/services/sas-store.service' import { SasStoreService } from 'src/app/services/sas-store.service'
import { DcValidator } from 'src/app/shared/dc-validator/dc-validator' import { DcValidator } from 'src/app/shared/dc-validator/dc-validator'
import { DcValidation } from 'src/app/shared/dc-validator/models/dc-validation.model' import { DcValidation } from 'src/app/shared/dc-validator/models/dc-validation.model'
import { isEmpty } from 'src/app/shared/dc-validator/utils/isEmpty'
import { import {
EditRecordDropdownChangeEvent, EditRecordDropdownChangeEvent,
EditRecordInputFocusedEvent EditRecordInputFocusedEvent
@@ -24,7 +25,8 @@ import { EditRecordModal } from '../../models/EditRecordModal'
selector: 'app-edit-record', selector: 'app-edit-record',
templateUrl: './edit-record.component.html', templateUrl: './edit-record.component.html',
styleUrls: ['./edit-record.component.scss'], styleUrls: ['./edit-record.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class EditRecordComponent implements OnInit { export class EditRecordComponent implements OnInit {
@Input() currentRecord!: EditRecordModal @Input() currentRecord!: EditRecordModal
@@ -145,23 +147,63 @@ export class EditRecordComponent implements OnInit {
}, 0) }, 0)
} }
async recordInputChange(event: any, colName: string) { async recordInputChange(event: any, colName: string): Promise<void> {
const colRules = this.currentRecordValidator?.getRule(colName) const colRules = this.currentRecordValidator?.getRule(colName)
const value = event.target.value const value = event.target.value
this.helperService.debounceCall(300, () => { this.helperService.debounceCall(300, () => {
this.validateRecordCol(colRules, value).then((valid: boolean) => { this.validateRecordCol(colRules, value).then((valid: boolean) => {
const index = this.currentRecordInvalidCols.indexOf(colName) this.updateValidationState(colName, valid)
if (valid) { if (!valid) {
if (index > -1) this.currentRecordInvalidCols.splice(index, 1) this.tryAutoPopulateNotNull(event, colName, colRules, value)
} else {
if (index < 0) this.currentRecordInvalidCols.push(colName)
} }
}) })
}) })
} }
/**
* Updates the invalid columns list based on validation result
*/
private updateValidationState(colName: string, valid: boolean): void {
const index = this.currentRecordInvalidCols.indexOf(colName)
if (valid && index > -1) {
this.currentRecordInvalidCols.splice(index, 1)
} else if (!valid && index < 0) {
this.currentRecordInvalidCols.push(colName)
}
}
/**
* Auto-populates NOTNULL default value when the field is empty and has a default
*/
private tryAutoPopulateNotNull(
event: any,
colName: string,
colRules: DcValidation | undefined,
value: any
): void {
if (
!isEmpty(value) ||
!this.currentRecordValidator ||
!this.currentRecord
) {
return
}
const defaultValue =
this.currentRecordValidator.getNotNullDefaultValue(colName)
if (defaultValue === undefined) return
this.currentRecord[colName] = defaultValue
event.target.value = defaultValue
this.validateRecordCol(colRules, defaultValue).then((isValid: boolean) => {
this.updateValidationState(colName, isValid)
})
}
onNextRecordClick() { onNextRecordClick() {
this.onNextRecord.emit() this.onNextRecord.emit()
} }
@@ -171,23 +213,8 @@ export class EditRecordComponent implements OnInit {
} }
public copyToClip(text: string) { public copyToClip(text: string) {
const modalElement = document.querySelector('#recordModalRef .modal-title') navigator.clipboard.writeText(text)
this.generatedRecordUrl = text
if (modalElement) {
const selBox = document.createElement('textarea')
selBox.style.position = 'fixed'
selBox.style.left = '0'
selBox.style.top = '0'
selBox.style.opacity = '0'
selBox.style.zIndex = '5000'
selBox.value = text
modalElement.appendChild(selBox)
selBox.focus()
selBox.select()
document.execCommand('copy')
modalElement.removeChild(selBox)
this.generatedRecordUrl = text
}
} }
async generateEditRecordUrl() { async generateEditRecordUrl() {

View File

@@ -11,7 +11,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
selector: 'app-upload-stater', selector: 'app-upload-stater',
templateUrl: './upload-stater.component.html', templateUrl: './upload-stater.component.html',
styleUrls: ['./upload-stater.component.scss'], styleUrls: ['./upload-stater.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class UploadStaterComponent implements OnInit { export class UploadStaterComponent implements OnInit {
public statesList: string[] = [] //States appended to be displayed public statesList: string[] = [] //States appended to be displayed

View File

@@ -43,6 +43,7 @@ import { Col } from '../shared/dc-validator/models/col.model'
import { DcValidation } from '../shared/dc-validator/models/dc-validation.model' import { DcValidation } from '../shared/dc-validator/models/dc-validation.model'
import { DQRule } from '../shared/dc-validator/models/dq-rules.model' import { DQRule } from '../shared/dc-validator/models/dq-rules.model'
import { getHotDataSchema } from '../shared/dc-validator/utils/getHotDataSchema' import { getHotDataSchema } from '../shared/dc-validator/utils/getHotDataSchema'
import { isEmpty } from '../shared/dc-validator/utils/isEmpty'
import { globals } from '../_globals' import { globals } from '../_globals'
import { UploadStaterComponent } from './components/upload-stater/upload-stater.component' import { UploadStaterComponent } from './components/upload-stater/upload-stater.component'
import { DynamicExtendedCellValidation } from './models/dynamicExtendedCellValidation' import { DynamicExtendedCellValidation } from './models/dynamicExtendedCellValidation'
@@ -70,7 +71,8 @@ import { ParseResult } from '../models/ParseResult.interface'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class EditorComponent implements OnInit, AfterViewInit, OnDestroy { export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChildren('uploadStater') @ViewChildren('uploadStater')
@@ -1044,12 +1046,16 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
} }
/** /**
* Creates a new empty row object with proper structure * Creates a new empty row object with proper structure.
* Columns with NOTNULL DQ rules are pre-populated with their RULE_VALUE.
*/ */
private createEmptyRow(): any { private createEmptyRow(): any {
const newRow: any = {} const newRow: any = {}
this.headerColumns.forEach((col: string) => { this.cellValidation.forEach((rule: any) => {
newRow[col] = '' const dataKey = rule.data
newRow[dataKey] = this.hotDataSchema.hasOwnProperty(dataKey)
? this.hotDataSchema[dataKey]
: ''
}) })
newRow['noLinkOption'] = true newRow['noLinkOption'] = true
return newRow return newRow
@@ -2675,13 +2681,14 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
// Note: this.headerColumns and this.columnHeader contains same data // Note: this.headerColumns and this.columnHeader contains same data
// need to resolve redundancy // need to resolve redundancy
// default schema // default schema - includes NOTNULL defaults from DQ rules
for (let i = 0; i < this.headerColumns.length; i++) { for (let i = 0; i < this.headerColumns.length; i++) {
const colType = this.cellValidation[i].type const colType = this.cellValidation[i].type
this.hotDataSchema[this.cellValidation[i].data] = getHotDataSchema( this.hotDataSchema[this.cellValidation[i].data] = getHotDataSchema(
colType, colType,
this.cellValidation[i] this.cellValidation[i],
this.dcValidator?.getDqDetails()
) )
} }
@@ -2986,6 +2993,29 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
} }
) )
// Auto-populate NOTNULL default when validation fails due to empty value
hot.addHook(
'afterValidate',
(isValid: boolean, value: any, row: number, prop: string | number) => {
if (isValid || !isEmpty(value)) return
const colName =
typeof prop === 'string'
? prop
: (hot.colToProp(prop as number) as string)
const defaultValue = this.dcValidator?.getNotNullDefaultValue(colName)
if (defaultValue === undefined) return
// Auto-populate using setTimeout to avoid modifying during validation
setTimeout(() => {
if (isEmpty(hot.getDataAtRowProp(row, colName))) {
hot.setDataAtRowProp(row, colName, defaultValue, 'autoPopulate')
}
}, 0)
}
)
hot.addHook('beforePaste', (data: any, cords: any) => { hot.addHook('beforePaste', (data: any, cords: any) => {
const startCol = cords[0].startCol const startCol = cords[0].startCol

View File

@@ -15,7 +15,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class GroupComponent implements OnInit { export class GroupComponent implements OnInit {
public groups: Array<any> | undefined public groups: Array<any> | undefined

View File

@@ -19,7 +19,8 @@ import { LicenceService } from '../services/licence.service'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class HomeComponent implements AfterContentInit { export class HomeComponent implements AfterContentInit {
public treeNodeLibraries: Array<any> | null = null public treeNodeLibraries: Array<any> | null = null

View File

@@ -1,178 +1,182 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core' import { Component, OnInit, ViewEncapsulation } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { AppService, LicenceService, SasService } from '../services' import { AppService, LicenceService, SasService } from '../services'
import { LicenseKeyData } from '../models/LicenseKeyData' import { LicenseKeyData } from '../models/LicenseKeyData'
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse' import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
enum LicenseActions { enum LicenseActions {
key = 'key', key = 'key',
register = 'register', register = 'register',
limit = 'limit', limit = 'limit',
update = 'update' update = 'update'
} }
@Component({ @Component({
selector: 'app-licensing', selector: 'app-licensing',
templateUrl: './licensing.component.html', templateUrl: './licensing.component.html',
styleUrls: ['./licensing.component.scss'], styleUrls: ['./licensing.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
}) standalone: false
export class LicensingComponent implements OnInit { })
public action: LicenseActions | null = null export class LicensingComponent implements OnInit {
public action: LicenseActions | null = null
public licenseErrors: { [key: string]: string } = {
missing: `Licence key is missing - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`, public licenseErrors: { [key: string]: string } = {
expired: `Licence key is expired - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`, missing: `Licence key is missing - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
invalid: `Licence key is invalid - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`, expired: `Licence key is expired - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
missmatch: `Your SYSSITE (below) is not found in the licence key - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.` invalid: `Licence key is invalid - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
} missmatch: `Your SYSSITE (below) is not found in the licence key - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`
}
public keyError: string | undefined
public errorDetails: string | undefined public keyError: string | undefined
public missmatchedKey: string | undefined public errorDetails: string | undefined
public licenceKeyValue: string = '' public missmatchedKey: string | undefined
public activationKeyValue: string = '' public licenceKeyValue: string = ''
public activationKeyValue: string = ''
public applyingKeys: boolean = false
public applyingKeys: boolean = false
public syssite = this.appService.syssite
public currentLicenceKey = this.licenceService.licenceKey public syssite = this.appService.syssite
public currentActivationKey = this.licenceService.activationKey public currentLicenceKey = this.licenceService.licenceKey
public isAppFreeTier = this.licenceService.isAppFreeTier public currentActivationKey = this.licenceService.activationKey
public userCountLimitation = this.licenceService.userCountLimitation public isAppFreeTier = this.licenceService.isAppFreeTier
public userCountLimitation = this.licenceService.userCountLimitation
public licenseKeyData: LicenseKeyData | null = null
public licenseKeyData: LicenseKeyData | null = null
public inputType: 'file' | 'paste' = 'file'
public licenceFileError: string | undefined public inputType: 'file' | 'paste' = 'file'
public licenceFileLoading: boolean = false public licenceFileError: string | undefined
public licencefile: { filename: string } = { public licenceFileLoading: boolean = false
filename: '' public licencefile: { filename: string } = {
} filename: ''
}
constructor(
private route: ActivatedRoute, constructor(
private licenceService: LicenceService, private route: ActivatedRoute,
private sasService: SasService, private router: Router,
private appService: AppService private licenceService: LicenceService,
) {} private sasService: SasService,
private appService: AppService
ngOnInit(): void { ) {}
this.licenceKeyValue = this.currentLicenceKey || ''
this.activationKeyValue = this.currentActivationKey || '' ngOnInit(): void {
this.licenceKeyValue = this.currentLicenceKey || ''
this.route.queryParams.subscribe((queryParams: any) => { this.activationKeyValue = this.currentActivationKey || ''
this.keyError = queryParams.error
this.missmatchedKey = queryParams.missmatchId this.route.queryParams.subscribe((queryParams: any) => {
this.keyError = queryParams.error
if (queryParams.details) { this.missmatchedKey = queryParams.missmatchId
this.errorDetails = atob(queryParams.details)
} if (queryParams.details) {
}) this.errorDetails = atob(queryParams.details)
}
this.route.params.subscribe((params: any) => { })
let actionInUrl = params.action
this.route.params.subscribe((params: any) => {
if (actionInUrl) { let actionInUrl = params.action
if (Object.values(LicenseActions).includes(actionInUrl)) {
this.action = actionInUrl if (actionInUrl) {
} if (Object.values(LicenseActions).includes(actionInUrl)) {
} this.action = actionInUrl
}) }
}
this.licenseKeyData = this.licenceService.getLicenseKeyData() })
}
this.licenseKeyData = this.licenceService.getLicenseKeyData()
public trimKeys() { }
this.licenceKeyValue = this.licenceKeyValue.trim()
this.activationKeyValue = this.activationKeyValue.trim() public trimKeys() {
} this.licenceKeyValue = this.licenceKeyValue.trim()
this.activationKeyValue = this.activationKeyValue.trim()
public copySyssite(copyIconRef: any, copyTooltip: any, syssite: string[]) { }
const syssiteString = syssite.join('\n')
public copySyssite(copyIconRef: any, copyTooltip: any, syssite: string[]) {
navigator.clipboard.writeText(syssiteString).then(() => { const syssiteString = syssite.join('\n')
copyIconRef.setAttribute('shape', 'check')
copyIconRef.setAttribute('class', 'is-success') navigator.clipboard.writeText(syssiteString).then(() => {
copyTooltip.innerText = 'Copied!' copyIconRef.setAttribute('shape', 'check')
copyIconRef.setAttribute('class', 'is-success')
setTimeout(() => { copyTooltip.innerText = 'Copied!'
copyIconRef.setAttribute('shape', 'copy')
copyIconRef.removeAttribute('class') setTimeout(() => {
copyTooltip.innerText = 'Copy to clipboard' copyIconRef.setAttribute('shape', 'copy')
}, 1000) copyIconRef.removeAttribute('class')
}) copyTooltip.innerText = 'Copy to clipboard'
} }, 1000)
})
public applyKeys() { }
this.applyingKeys = true
public applyKeys() {
let table = { this.applyingKeys = true
keyupload: [
{ let table = {
ACTIVATION_KEY: this.activationKeyValue, keyupload: [
LICENCE_KEY: this.licenceKeyValue {
} ACTIVATION_KEY: this.activationKeyValue,
] LICENCE_KEY: this.licenceKeyValue
} }
]
this.sasService }
.request('admin/registerkey', table)
.then((res: RequestWrapperResponse) => { this.sasService
if ( .request('admin/registerkey', table)
res.adapterResponse.return && .then((res: RequestWrapperResponse) => {
res.adapterResponse.return[0] && if (
res.adapterResponse.return[0].MSG === 'SUCCESS' res.adapterResponse.return &&
) { res.adapterResponse.return[0] &&
location.replace(location.href.split('#')[0]) res.adapterResponse.return[0].MSG === 'SUCCESS'
} ) {
}) this.router.navigateByUrl('/').then(() => {
.finally(() => { window.location.reload()
this.applyingKeys = false })
}) }
} })
.finally(() => {
public onFileCapture(event: any, dropped = false) { this.applyingKeys = false
let file = dropped ? event[0] : event.target.files[0] })
this.licencefile.filename = file.name }
if (!file) return public onFileCapture(event: any, dropped = false) {
let file = dropped ? event[0] : event.target.files[0]
this.licenceFileLoading = true this.licencefile.filename = file.name
const reader = new FileReader() if (!file) return
reader.onload = (evt) => { this.licenceFileLoading = true
this.licenceFileError = 'Error reading file.'
const reader = new FileReader()
if (!evt || !evt.target) return
if (evt.target.readyState != 2) return reader.onload = (evt) => {
if (evt.target.error) return this.licenceFileError = 'Error reading file.'
if (!evt.target.result) return
if (!evt || !evt.target) return
this.licenceFileLoading = false if (evt.target.readyState != 2) return
this.licenceFileError = undefined if (evt.target.error) return
const fileArr = evt.target.result.toString().split('\n') if (!evt.target.result) return
this.activationKeyValue = fileArr[1]
this.licenceKeyValue = fileArr[0] this.licenceFileLoading = false
} this.licenceFileError = undefined
const fileArr = evt.target.result.toString().split('\n')
reader.readAsText(file) this.activationKeyValue = fileArr[1]
} this.licenceKeyValue = fileArr[0]
}
public switchType(type: 'paste' | 'file') {
this.inputType = type reader.readAsText(file)
} }
get disableApplyButton(): boolean { public switchType(type: 'paste' | 'file') {
if (this.licenceKeyValue.length < 1 || this.activationKeyValue.length < 1) this.inputType = type
return true }
if (
this.licenceKeyValue === this.currentLicenceKey && get disableApplyButton(): boolean {
this.activationKeyValue === this.currentActivationKey if (this.licenceKeyValue.length < 1 || this.activationKeyValue.length < 1)
) return true
return true if (
this.licenceKeyValue === this.currentLicenceKey &&
return false this.activationKeyValue === this.currentActivationKey
} )
} return true
return false
}
}

View File

@@ -239,13 +239,7 @@
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen> <clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div (click)="downloadSVG()" clrDropdownItem>SVG</div> <div (click)="downloadSVG()" clrDropdownItem>SVG</div>
<div <div (click)="downloadPNG()" clrDropdownItem>PNG</div>
*ngIf="!helperService.isMicrosoft"
(click)="downloadPNG()"
clrDropdownItem
>
PNG
</div>
<div (click)="downloadDot()" clrDropdownItem>Dot</div> <div (click)="downloadDot()" clrDropdownItem>Dot</div>
<div *ngIf="flatdata" (click)="downloadCSV()" clrDropdownItem> <div *ngIf="flatdata" (click)="downloadCSV()" clrDropdownItem>
CSV CSV
@@ -366,13 +360,7 @@
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen> <clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div (click)="renderToDownload('SVG')" clrDropdownItem>SVG</div> <div (click)="renderToDownload('SVG')" clrDropdownItem>SVG</div>
<div <div (click)="renderToDownload('PNG')" clrDropdownItem>PNG</div>
*ngIf="!helperService.isMicrosoft"
(click)="renderToDownload('PNG')"
clrDropdownItem
>
PNG
</div>
<div (click)="downloadDot(); cancelRenderingGraph()" clrDropdownItem> <div (click)="downloadDot(); cancelRenderingGraph()" clrDropdownItem>
Dot Dot
</div> </div>

View File

@@ -19,7 +19,8 @@ const moment = require('moment')
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class LineageComponent { export class LineageComponent {
public switchFlag: boolean = false public switchFlag: boolean = false
@@ -746,28 +747,13 @@ export class LineageComponent {
return URL.createObjectURL(svg_blob) return URL.createObjectURL(svg_blob)
} }
private getSVGBlob() {
let svg: any = document.getElementById('graph')
let serializer = new XMLSerializer()
let svg_blob = new Blob([serializer.serializeToString(svg)], {
type: 'image/svg+xml'
})
return svg_blob
}
downloadSVG() { downloadSVG() {
d3Viz.graphviz('#graph').resetZoom() d3Viz.graphviz('#graph').resetZoom()
if (navigator.appVersion.toString().indexOf('.NET') > 0) { let downloadLink = document.createElement('a')
window.navigator.msSaveBlob(this.getSVGBlob(), this.constructName('svg')) downloadLink.href = this.getSVGURL()
} else { downloadLink.download = this.constructName('svg')
let downloadLink = document.createElement('a') downloadLink.click()
downloadLink.href = this.getSVGURL()
downloadLink.download = this.constructName('svg')
document.body.appendChild(downloadLink)
downloadLink.click()
document.body.removeChild(downloadLink)
}
} }
async downloadPNG() { async downloadPNG() {
@@ -795,16 +781,11 @@ export class LineageComponent {
var a = document.createElement('a') var a = document.createElement('a')
var blob = new Blob([csvArray], { type: 'text/csv' }) var blob = new Blob([csvArray], { type: 'text/csv' })
if (navigator.appVersion.toString().indexOf('.NET') > 0) { var url = window.URL.createObjectURL(blob)
window.navigator.msSaveBlob(blob, this.constructName('csv')) a.href = url
} else { a.download = this.constructName('csv')
var url = window.URL.createObjectURL(blob) a.click()
a.href = url window.URL.revokeObjectURL(url)
a.download = this.constructName('csv')
a.click()
window.URL.revokeObjectURL(url)
a.remove()
}
} }
private getDotUrl() { private getDotUrl() {
@@ -813,23 +794,11 @@ export class LineageComponent {
return window.URL.createObjectURL(dot_blob) return window.URL.createObjectURL(dot_blob)
} }
private getDotBlob() {
let data = this.vizInput
let dot_blob = new Blob([data], { type: 'text/plain' })
return dot_blob
}
downloadDot() { downloadDot() {
if (navigator.appVersion.toString().indexOf('.NET') > 0) { let downloadLink = document.createElement('a')
window.navigator.msSaveBlob(this.getDotBlob(), this.constructName('txt')) downloadLink.href = this.getDotUrl()
} else { downloadLink.download = this.constructName('txt')
let downloadLink = document.createElement('a') downloadLink.click()
downloadLink.href = this.getDotUrl()
downloadLink.download = this.constructName('txt')
document.body.appendChild(downloadLink)
downloadLink.click()
document.body.removeChild(downloadLink)
}
} }
public showSvg() { public showSvg() {

View File

@@ -51,7 +51,8 @@ class ValueFilter implements ClrDatagridStringFilterInterface<MetaData> {
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class MetadataComponent implements OnInit { export class MetadataComponent implements OnInit {
metaDataList: Array<any> | undefined metaDataList: Array<any> | undefined

View File

@@ -48,7 +48,8 @@ enum FileLoadingState {
selector: 'app-multi-dataset', selector: 'app-multi-dataset',
templateUrl: './multi-dataset.component.html', templateUrl: './multi-dataset.component.html',
styleUrls: ['./multi-dataset.component.scss'], styleUrls: ['./multi-dataset.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class MultiDatasetComponent implements OnInit, AfterViewInit { export class MultiDatasetComponent implements OnInit, AfterViewInit {
@HostBinding('class.content-container') contentContainerClass = true @HostBinding('class.content-container') contentContainerClass = true

View File

@@ -7,7 +7,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class NotFoundComponent implements OnInit { export class NotFoundComponent implements OnInit {
constructor() {} constructor() {}

View File

@@ -2,7 +2,8 @@ import { Pipe, PipeTransform } from '@angular/core'
import { bytesToSize } from '@sasjs/utils/utils/bytesToSize' import { bytesToSize } from '@sasjs/utils/utils/bytesToSize'
@Pipe({ @Pipe({
name: 'convertSize' name: 'convertSize',
standalone: false
}) })
export class ConvertSizePipe implements PipeTransform { export class ConvertSizePipe implements PipeTransform {
transform(bytes: string | number, ...args: string[]): string { transform(bytes: string | number, ...args: string[]): string {

View File

@@ -1,7 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import * as moment from 'moment' import moment from 'moment'
@Pipe({ @Pipe({
name: 'dateTimeFormatter' name: 'dateTimeFormatter',
standalone: false
}) })
export class DateTimeFormatterPipe implements PipeTransform { export class DateTimeFormatterPipe implements PipeTransform {
transform(value: Date | string, type: string): string { transform(value: Date | string, type: string): string {

View File

@@ -1,7 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
@Pipe({ @Pipe({
name: 'linkinze' name: 'linkinze',
standalone: false
}) })
export class LinkinzePipe implements PipeTransform { export class LinkinzePipe implements PipeTransform {
/** /**

View File

@@ -2,7 +2,8 @@ import { Pipe, PipeTransform } from '@angular/core'
import { HelperService } from '../services/helper.service' import { HelperService } from '../services/helper.service'
@Pipe({ @Pipe({
name: 'sasToJsDate' name: 'sasToJsDate',
standalone: false
}) })
export class sasToJsDatePipe implements PipeTransform { export class sasToJsDatePipe implements PipeTransform {
constructor(private helperService: HelperService) {} constructor(private helperService: HelperService) {}

View File

@@ -1,7 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
@Pipe({ @Pipe({
name: 'pkSpaceSeparate' name: 'pkSpaceSeparate',
standalone: false
}) })
export class PkSpaceSeparatePipe implements PipeTransform { export class PkSpaceSeparatePipe implements PipeTransform {
transform(value: string): string { transform(value: string): string {

View File

@@ -1,7 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
@Pipe({ @Pipe({
name: 'prettyjson' name: 'prettyjson',
standalone: false
}) })
export class PrettyjsonPipe implements PipeTransform { export class PrettyjsonPipe implements PipeTransform {
transform(rawJson: any): string { transform(rawJson: any): string {

View File

@@ -2,7 +2,8 @@ import { Pipe, PipeTransform } from '@angular/core'
import { HelperService } from '../services/helper.service' import { HelperService } from '../services/helper.service'
@Pipe({ @Pipe({
name: 'secondsParser' name: 'secondsParser',
standalone: false
}) })
export class SecondsParserPipe implements PipeTransform { export class SecondsParserPipe implements PipeTransform {
constructor(private helperService: HelperService) {} constructor(private helperService: HelperService) {}

View File

@@ -1,7 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
@Pipe({ @Pipe({
name: 'thousandSeparator' name: 'thousandSeparator',
standalone: false
}) })
export class ThousandSeparatorPipe implements PipeTransform { export class ThousandSeparatorPipe implements PipeTransform {
transform(value: string | number, separator?: string): string { transform(value: string | number, separator?: string): string {

View File

@@ -1,7 +1,8 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
@Pipe({ @Pipe({
name: 'toNumber' name: 'toNumber',
standalone: false
}) })
export class ToNumberPipe implements PipeTransform { export class ToNumberPipe implements PipeTransform {
transform(value: string | number): number { transform(value: string | number): number {

View File

@@ -29,7 +29,8 @@ registerLocaleData(localeEnGB)
templateUrl: './query.component.html', templateUrl: './query.component.html',
styleUrls: ['./query.component.scss'], styleUrls: ['./query.component.scss'],
providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }], providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class QueryComponent export class QueryComponent
implements AfterViewInit, AfterContentInit, OnDestroy implements AfterViewInit, AfterContentInit, OnDestroy

View File

@@ -30,7 +30,8 @@ interface ChangesObj {
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ApproveDetailsComponent implements AfterViewInit, OnDestroy { export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
private _detailsSub: Subscription | undefined private _detailsSub: Subscription | undefined

View File

@@ -38,9 +38,7 @@ class SubmittedFilter implements ClrDatagridStringFilterInterface<ApproveData> {
} }
} }
class SubmitReasonFilter class SubmitReasonFilter implements ClrDatagridStringFilterInterface<ApproveData> {
implements ClrDatagridStringFilterInterface<ApproveData>
{
accepts(data: ApproveData, search: string): boolean { accepts(data: ApproveData, search: string): boolean {
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0 return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
} }
@@ -53,7 +51,8 @@ class SubmitReasonFilter
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ApproveComponent implements OnInit { export class ApproveComponent implements OnInit {
public approveList: Array<ApproveData> | undefined public approveList: Array<ApproveData> | undefined

View File

@@ -38,9 +38,7 @@ class SubmitterFilter implements ClrDatagridStringFilterInterface<HistoryData> {
} }
} }
class SubmitReasonFilter class SubmitReasonFilter implements ClrDatagridStringFilterInterface<HistoryData> {
implements ClrDatagridStringFilterInterface<HistoryData>
{
accepts(data: HistoryData, search: string): boolean { accepts(data: HistoryData, search: string): boolean {
return data.submittedReason.toLowerCase().indexOf(search.toLowerCase()) >= 0 return data.submittedReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
} }
@@ -65,7 +63,8 @@ class ReviewedFilter implements ClrDatagridStringFilterInterface<HistoryData> {
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class HistoryComponent implements OnInit { export class HistoryComponent implements OnInit {
public history: Array<any> = [] public history: Array<any> = []

View File

@@ -17,17 +17,13 @@ interface SubmitterData {
approver: string approver: string
} }
class SubmittedFilter class SubmittedFilter implements ClrDatagridStringFilterInterface<SubmitterData> {
implements ClrDatagridStringFilterInterface<SubmitterData>
{
accepts(data: SubmitterData, search: string): boolean { accepts(data: SubmitterData, search: string): boolean {
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0 return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
} }
} }
class SubmitReasonFilter class SubmitReasonFilter implements ClrDatagridStringFilterInterface<SubmitterData> {
implements ClrDatagridStringFilterInterface<SubmitterData>
{
accepts(data: SubmitterData, search: string): boolean { accepts(data: SubmitterData, search: string): boolean {
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0 return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
} }
@@ -40,7 +36,8 @@ class SubmitReasonFilter
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class SubmitterComponent implements OnInit, AfterViewInit { export class SubmitterComponent implements OnInit, AfterViewInit {
public remained: number = 0 public remained: number = 0

View File

@@ -13,7 +13,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class RoleComponent implements OnInit { export class RoleComponent implements OnInit {
public roles: Array<any> | undefined public roles: Array<any> | undefined

View File

@@ -7,7 +7,8 @@ import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class HomeRouteComponent implements OnInit, OnDestroy { export class HomeRouteComponent implements OnInit, OnDestroy {
constructor() {} constructor() {}

View File

@@ -5,7 +5,8 @@ import { Component, OnInit, OnDestroy } from '@angular/core'
templateUrl: './multi-dataset-route.component.html', templateUrl: './multi-dataset-route.component.html',
host: { host: {
class: 'content-container' class: 'content-container'
} },
standalone: false
}) })
export class MultiDatasetRouteComponent implements OnInit, OnDestroy { export class MultiDatasetRouteComponent implements OnInit, OnDestroy {
constructor() {} constructor() {}

View File

@@ -7,7 +7,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ReviewRouteComponent implements OnInit { export class ReviewRouteComponent implements OnInit {
constructor() {} constructor() {}

View File

@@ -7,7 +7,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class UsernavRouteComponent implements OnInit { export class UsernavRouteComponent implements OnInit {
constructor() {} constructor() {}

View File

@@ -7,7 +7,8 @@ import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ViewRouteComponent implements OnInit, OnDestroy { export class ViewRouteComponent implements OnInit, OnDestroy {
constructor() {} constructor() {}

View File

@@ -7,7 +7,8 @@ import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class XLMapRouteComponent implements OnInit, OnDestroy { export class XLMapRouteComponent implements OnInit, OnDestroy {
constructor() {} constructor() {}

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import cloneDeep from 'lodash-es/cloneDeep' import cloneDeep from 'lodash-es/cloneDeep'
import * as CryptoMD5 from 'crypto-js/md5' import CryptoMD5 from 'crypto-js/md5'
import { SasService } from './sas.service' import { SasService } from './sas.service'
const librariesToShow = 50 const librariesToShow = 50
@@ -11,12 +11,8 @@ const librariesToShow = 50
export class HelperService { export class HelperService {
public shownLibraries: number = librariesToShow public shownLibraries: number = librariesToShow
public loadMoreCount: number = librariesToShow public loadMoreCount: number = librariesToShow
public isMicrosoft: boolean = false
constructor(private sasService: SasService) { constructor(private sasService: SasService) {}
this.isMicrosoft = this.isIEorEDGE()
console.log('Is IE or Edge?', this.isMicrosoft)
}
/** /**
* Converts a JavaScript date object to a SAS Date or Datetime, given the logic below: * Converts a JavaScript date object to a SAS Date or Datetime, given the logic below:
@@ -215,32 +211,6 @@ export class HelperService {
}) })
} }
public isIEorEDGE() {
var ua = window.navigator.userAgent
var msie = ua.indexOf('MSIE ')
if (msie > 0) {
// IE 10 or older => return version number
return true
}
var trident = ua.indexOf('Trident/')
if (trident > 0) {
// IE 11 => return version number
var rv = ua.indexOf('rv:')
return true
}
var edge = ua.indexOf('Edge/')
if (edge > 0) {
// Edge (IE 12+) => return version number
return true
}
// other browser
return false
}
public convertObjectsToArray( public convertObjectsToArray(
objectArray: Array<object>, objectArray: Array<object>,
deepClone: boolean = false deepClone: boolean = false

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'
import { BehaviorSubject } from 'rxjs' import { BehaviorSubject } from 'rxjs'
import { LicenseKeyData } from '../models/LicenseKeyData' import { LicenseKeyData } from '../models/LicenseKeyData'
import { SasService } from './sas.service' import { SasService } from './sas.service'
import * as moment from 'moment' import moment from 'moment'
import * as base64Converter from 'base64-arraybuffer' import * as base64Converter from 'base64-arraybuffer'
import * as encoding from 'text-encoding' import * as encoding from 'text-encoding'
import { Router } from '@angular/router' import { Router } from '@angular/router'

View File

@@ -120,9 +120,12 @@ export class SasViyaService {
} }
getComputeContexts(): Observable<ViyaComputeContexts> { getComputeContexts(): Observable<ViyaComputeContexts> {
return this.get<ViyaComputeContexts>(`${this.serverUrl}/compute/contexts`, { return this.get<ViyaComputeContexts>(
withCredentials: true `${this.serverUrl}/compute/contexts?limit=1000`,
}) {
withCredentials: true
}
)
} }
getComputeContextById(id: string): Observable<ComputeContextDetails> { getComputeContextById(id: string): Observable<ComputeContextDetails> {

View File

@@ -17,7 +17,8 @@ import { AbortDetails, InfoModal } from '../../models/InfoModal'
selector: 'app-info-modal', selector: 'app-info-modal',
templateUrl: './info-modal.component.html', templateUrl: './info-modal.component.html',
styleUrls: ['./info-modal.component.scss'], styleUrls: ['./info-modal.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class InfoModalComponent implements OnInit { export class InfoModalComponent implements OnInit {
@Output() onConfirmModalClick: EventEmitter<any> = new EventEmitter() @Output() onConfirmModalClick: EventEmitter<any> = new EventEmitter()

View File

@@ -7,7 +7,8 @@ import { AlertsService } from './alerts.service'
selector: 'app-alerts', selector: 'app-alerts',
templateUrl: './alerts.component.html', templateUrl: './alerts.component.html',
styleUrls: ['./alerts.component.scss'], styleUrls: ['./alerts.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class AlertsComponent implements OnInit { export class AlertsComponent implements OnInit {
public alerts: Array<Alert> = [] public alerts: Array<Alert> = []

View File

@@ -19,7 +19,8 @@ export type OnLoadingMoreEvent = {
selector: 'app-autocomplete', selector: 'app-autocomplete',
templateUrl: './autocomplete.component.html', templateUrl: './autocomplete.component.html',
styleUrls: ['./autocomplete.component.scss'], styleUrls: ['./autocomplete.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class AutocompleteComponent implements OnInit, AfterViewInit { export class AutocompleteComponent implements OnInit, AfterViewInit {
@ViewChild('input') inputElement: any @ViewChild('input') inputElement: any

View File

@@ -4,7 +4,8 @@ import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'
selector: 'contact-link', selector: 'contact-link',
templateUrl: './contact-link.component.html', templateUrl: './contact-link.component.html',
styleUrls: ['./contact-link.component.scss'], styleUrls: ['./contact-link.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ContactLinkComponent implements OnInit { export class ContactLinkComponent implements OnInit {
@Input() classes: string = '' @Input() classes: string = ''

View File

@@ -15,7 +15,8 @@ import { Tab } from './models/dsmeta-groupped.model'
selector: 'app-dataset-info', selector: 'app-dataset-info',
templateUrl: './dataset-info.component.html', templateUrl: './dataset-info.component.html',
styleUrls: ['./dataset-info.component.scss'], styleUrls: ['./dataset-info.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class DatasetInfoComponent implements OnInit, OnChanges { export class DatasetInfoComponent implements OnInit, OnChanges {
@Input() open: boolean = false @Input() open: boolean = false

View File

@@ -22,7 +22,8 @@ import { TableClickEmitter } from './models/TableClickEmitter'
selector: 'dc-tree', selector: 'dc-tree',
templateUrl: './dc-tree.component.html', templateUrl: './dc-tree.component.html',
styleUrls: ['./dc-tree.component.scss'], styleUrls: ['./dc-tree.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class DcTreeComponent implements OnInit, AfterViewInit, OnChanges { export class DcTreeComponent implements OnInit, AfterViewInit, OnChanges {
// REFACTOR NOTICE // REFACTOR NOTICE

View File

@@ -15,6 +15,7 @@ import {
} from './models/dc-validation.model' } from './models/dc-validation.model'
import { DQRule, DQRuleTypes } from './models/dq-rules.model' import { DQRule, DQRuleTypes } from './models/dq-rules.model'
import { getDqDataCols } from './utils/getDqDataCols' import { getDqDataCols } from './utils/getDqDataCols'
import { getNotNullDefault } from './utils/getNotNullDefault'
import { mergeColsRules } from './utils/mergeColsRules' import { mergeColsRules } from './utils/mergeColsRules'
import { parseColType } from './utils/parseColType' import { parseColType } from './utils/parseColType'
import { dqValidate } from './validations/dq-validation' import { dqValidate } from './validations/dq-validation'
@@ -133,6 +134,19 @@ export class DcValidator {
} }
} }
/**
* Returns the RULE_VALUE for a NOTNULL rule on the given column.
* Used for auto-populating default values when cells are empty.
* Converts to number for numeric columns.
*
* @param col column name
* @returns RULE_VALUE (string or number) if NOTNULL rule exists, otherwise undefined
*/
getNotNullDefaultValue(col: string): string | number | undefined {
const colRule = this.getRule(col)
return getNotNullDefault(col, this.dqrules, colRule?.type)
}
/** /**
* Retrieves dropdown source for given dc validation rule * Retrieves dropdown source for given dc validation rule
* The values comes from MPE_SELECTBOX table * The values comes from MPE_SELECTBOX table

View File

@@ -1,8 +1,9 @@
import Handsontable from 'handsontable' import Handsontable from 'handsontable'
import Core from 'handsontable/core' import Core from 'handsontable/core'
export class CustomAutocompleteEditor extends Handsontable.editors export class CustomAutocompleteEditor
.AutocompleteEditor { extends Handsontable.editors.AutocompleteEditor
{
constructor(instance: Core) { constructor(instance: Core) {
super(instance) super(instance)
} }

View File

@@ -15,7 +15,6 @@ export interface DcColumnSettings {
export interface DcValidation extends HotColumnSettings, DcColumnSettings {} export interface DcValidation extends HotColumnSettings, DcColumnSettings {}
export interface DcValidationRuleUpdate export interface DcValidationRuleUpdate
extends Handsontable.ColumnSettings, extends Handsontable.ColumnSettings, DcColumnSettings {
DcColumnSettings {
data?: string data?: string
} }

View File

@@ -1,3 +1,4 @@
import { DQRule } from '../models/dq-rules.model'
import { getHotDataSchema } from '../utils/getHotDataSchema' import { getHotDataSchema } from '../utils/getHotDataSchema'
describe('DC Validator - hot data schema', () => { describe('DC Validator - hot data schema', () => {
@@ -8,4 +9,58 @@ describe('DC Validator - hot data schema', () => {
).toEqual(1) ).toEqual(1)
expect(getHotDataSchema('missing')).toEqual('') expect(getHotDataSchema('missing')).toEqual('')
}) })
describe('NOTNULL defaults', () => {
const dqRules: DQRule[] = [
{
BASE_COL: 'TEXT_COL',
RULE_TYPE: 'NOTNULL',
RULE_VALUE: 'default_text',
X: 1
},
{ BASE_COL: 'NUM_COL', RULE_TYPE: 'NOTNULL', RULE_VALUE: '42', X: 1 }
]
it('should return NOTNULL default for text column', () => {
expect(getHotDataSchema('text', { data: 'TEXT_COL' }, dqRules)).toEqual(
'default_text'
)
})
it('should return NOTNULL default as number for numeric column', () => {
expect(getHotDataSchema('numeric', { data: 'NUM_COL' }, dqRules)).toEqual(
42
)
})
it('should fall back to type default when no NOTNULL rule exists', () => {
expect(
getHotDataSchema('numeric', { data: 'OTHER_COL' }, dqRules)
).toEqual('')
})
it('should prioritize NOTNULL over autocomplete first option', () => {
const rulesWithAutocomplete: DQRule[] = [
{
BASE_COL: 'SELECT_COL',
RULE_TYPE: 'NOTNULL',
RULE_VALUE: 'priority_value',
X: 1
},
{
BASE_COL: 'SELECT_COL',
RULE_TYPE: 'HARDSELECT',
RULE_VALUE: 'ignored',
X: 1
}
]
expect(
getHotDataSchema(
'autocomplete',
{ data: 'SELECT_COL', source: ['first', 'second'] },
rulesWithAutocomplete
)
).toEqual('priority_value')
})
})
}) })

View File

@@ -0,0 +1,65 @@
import { DQRule } from '../models/dq-rules.model'
import { getNotNullDefault } from '../utils/getNotNullDefault'
describe('DC Validator - getNotNullDefault', () => {
const dqRules: DQRule[] = [
{
BASE_COL: 'TEXT_COL',
RULE_TYPE: 'NOTNULL',
RULE_VALUE: 'default_text',
X: 1
},
{ BASE_COL: 'NUM_COL', RULE_TYPE: 'NOTNULL', RULE_VALUE: '42', X: 1 },
{ BASE_COL: 'EMPTY_RULE', RULE_TYPE: 'NOTNULL', RULE_VALUE: ' ', X: 1 },
{
BASE_COL: 'OTHER_COL',
RULE_TYPE: 'HARDSELECT',
RULE_VALUE: 'some_value',
X: 1
}
]
it('should return string value for text columns', () => {
expect(getNotNullDefault('TEXT_COL', dqRules, 'text')).toEqual(
'default_text'
)
})
it('should return number for numeric columns when RULE_VALUE is numeric', () => {
expect(getNotNullDefault('NUM_COL', dqRules, 'numeric')).toEqual(42)
})
it('should return string for numeric columns when RULE_VALUE is not numeric', () => {
const rulesWithNonNumeric: DQRule[] = [
{
BASE_COL: 'NUM_COL',
RULE_TYPE: 'NOTNULL',
RULE_VALUE: 'not_a_number',
X: 1
}
]
expect(
getNotNullDefault('NUM_COL', rulesWithNonNumeric, 'numeric')
).toEqual('not_a_number')
})
it('should return undefined for empty RULE_VALUE', () => {
expect(getNotNullDefault('EMPTY_RULE', dqRules, 'text')).toBeUndefined()
})
it('should return undefined for columns without NOTNULL rule', () => {
expect(getNotNullDefault('OTHER_COL', dqRules, 'text')).toBeUndefined()
})
it('should return undefined for non-existent columns', () => {
expect(getNotNullDefault('MISSING_COL', dqRules, 'text')).toBeUndefined()
})
it('should return undefined for empty dqRules array', () => {
expect(getNotNullDefault('TEXT_COL', [], 'text')).toBeUndefined()
})
it('should return string when colType is undefined', () => {
expect(getNotNullDefault('NUM_COL', dqRules, undefined)).toEqual('42')
})
})

View File

@@ -0,0 +1,39 @@
import { isEmpty } from '../utils/isEmpty'
describe('DC Validator - isEmpty', () => {
it('should return true for null', () => {
expect(isEmpty(null)).toBe(true)
})
it('should return true for undefined', () => {
expect(isEmpty(undefined)).toBe(true)
})
it('should return true for empty string', () => {
expect(isEmpty('')).toBe(true)
})
it('should return true for whitespace-only string', () => {
expect(isEmpty(' ')).toBe(true)
expect(isEmpty('\t\n')).toBe(true)
})
it('should return false for non-empty string', () => {
expect(isEmpty('hello')).toBe(false)
expect(isEmpty(' hello ')).toBe(false)
})
it('should return false for number zero', () => {
expect(isEmpty(0)).toBe(false)
})
it('should return false for non-zero numbers', () => {
expect(isEmpty(42)).toBe(false)
expect(isEmpty(-1)).toBe(false)
})
it('should return false for boolean values', () => {
expect(isEmpty(true)).toBe(false)
expect(isEmpty(false)).toBe(false)
})
})

View File

@@ -1,4 +1,6 @@
import { DcValidation } from '../models/dc-validation.model' import { DcValidation } from '../models/dc-validation.model'
import { DQRule } from '../models/dq-rules.model'
import { getNotNullDefault } from './getNotNullDefault'
const schemaTypeMap: { [key: string]: any } = { const schemaTypeMap: { [key: string]: any } = {
numeric: '', numeric: '',
@@ -7,14 +9,25 @@ const schemaTypeMap: { [key: string]: any } = {
/** /**
* Schema defines the default values for given types. For example when new row is added. * Schema defines the default values for given types. For example when new row is added.
* Priority: NOTNULL RULE_VALUE > autocomplete first option > type default
*/ */
export const getHotDataSchema = ( export function getHotDataSchema(
type: string | undefined, type: string | undefined,
cellValidation?: DcValidation cellValidation?: DcValidation,
): any => { dqRules?: DQRule[]
): any {
// Check for NOTNULL default first
if (dqRules && cellValidation?.data) {
const defaultValue = getNotNullDefault(cellValidation.data, dqRules, type)
if (defaultValue !== undefined) {
return defaultValue
}
}
if (!type) return schemaTypeMap.default if (!type) return schemaTypeMap.default
switch (type) { switch (type) {
case 'dropdown':
case 'autocomplete': { case 'autocomplete': {
return cellValidation && cellValidation.source return cellValidation && cellValidation.source
? (cellValidation.source as string[] | number[])[0] ? (cellValidation.source as string[] | number[])[0]

View File

@@ -0,0 +1,30 @@
import { DQRule } from '../models/dq-rules.model'
/**
* Returns the NOTNULL default value for a column from DQ rules.
* Converts to number for numeric columns based on colType parameter.
*
* @param colName column name to look up
* @param dqRules array of DQ rules
* @param colType column type (e.g., 'numeric', 'text')
* @returns default value (string or number) if NOTNULL rule exists with non-empty value, otherwise undefined
*/
export function getNotNullDefault(
colName: string,
dqRules: DQRule[],
colType?: string
): string | number | undefined {
const notNullRule = dqRules.find(
(rule: DQRule) => rule.BASE_COL === colName && rule.RULE_TYPE === 'NOTNULL'
)
if (!notNullRule?.RULE_VALUE || notNullRule.RULE_VALUE.trim().length === 0) {
return undefined
}
if (colType === 'numeric' && !isNaN(Number(notNullRule.RULE_VALUE))) {
return Number(notNullRule.RULE_VALUE)
}
return notNullRule.RULE_VALUE
}

View File

@@ -0,0 +1,8 @@
/**
* Checks if a value is considered empty for NOTNULL validation purposes.
* A value is empty if it's null, undefined, or a string that is blank after trimming.
*/
export function isEmpty(value: unknown): boolean {
if (value === null || value === undefined) return true
return value.toString().trim().length === 0
}

View File

@@ -7,7 +7,8 @@ import { Options } from './models/options.interface'
selector: 'app-excel-password-modal', selector: 'app-excel-password-modal',
styleUrls: ['./excel-password-modal.component.scss'], styleUrls: ['./excel-password-modal.component.scss'],
templateUrl: './excel-password-modal.component.html', templateUrl: './excel-password-modal.component.html',
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ExcelPasswordModalComponent { export class ExcelPasswordModalComponent {
options$: Observable<Options> = this.excelPasswordModalService.optionsSubject$ options$: Observable<Options> = this.excelPasswordModalService.optionsSubject$

View File

@@ -7,7 +7,8 @@ import { Service } from '../service.interface'
selector: 'app-loading-indicator', selector: 'app-loading-indicator',
templateUrl: './loading-indicator.component.html', templateUrl: './loading-indicator.component.html',
styleUrls: ['./loading-indicator.component.scss'], styleUrls: ['./loading-indicator.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class LoadingIndicatorComponent implements OnInit, OnDestroy { export class LoadingIndicatorComponent implements OnInit, OnDestroy {
public loading: boolean = false public loading: boolean = false

View File

@@ -13,7 +13,8 @@ interface User {
selector: 'app-login', selector: 'app-login',
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'], styleUrls: ['./login.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class LoginComponent implements OnInit, OnDestroy { export class LoginComponent implements OnInit, OnDestroy {
private _subscription: Subscription = new Subscription() private _subscription: Subscription = new Subscription()

View File

@@ -7,7 +7,7 @@ import {
ViewEncapsulation ViewEncapsulation
} from '@angular/core' } from '@angular/core'
import { SASjsRequest } from '@sasjs/adapter' import { SASjsRequest } from '@sasjs/adapter'
import * as moment from 'moment' import moment from 'moment'
import { HelperService } from 'src/app/services/helper.service' import { HelperService } from 'src/app/services/helper.service'
import { LoggerService } from '../../services/logger.service' import { LoggerService } from '../../services/logger.service'
import { SasService } from '../../services/sas.service' import { SasService } from '../../services/sas.service'
@@ -23,7 +23,8 @@ interface SASjsRequestExtended extends SASjsRequest {
selector: 'app-requests-modal', selector: 'app-requests-modal',
templateUrl: './requests-modal.component.html', templateUrl: './requests-modal.component.html',
styleUrls: ['./requests-modal.component.scss'], styleUrls: ['./requests-modal.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class RequestsModalComponent implements OnInit { export class RequestsModalComponent implements OnInit {
private _opened: boolean = false private _opened: boolean = false

View File

@@ -18,7 +18,8 @@ import { globals } from '../../_globals'
selector: 'app-sidebar', selector: 'app-sidebar',
templateUrl: './sidebar.component.html', templateUrl: './sidebar.component.html',
styleUrls: ['./sidebar.component.scss'], styleUrls: ['./sidebar.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class SidebarComponent implements OnInit { export class SidebarComponent implements OnInit {
@ViewChild('sidebarNav') sidebarNav!: ElementRef @ViewChild('sidebarNav') sidebarNav!: ElementRef

View File

@@ -15,7 +15,8 @@ import { OnLoadingMoreEvent } from '../autocomplete/autocomplete.component'
selector: 'app-soft-select', selector: 'app-soft-select',
templateUrl: './soft-select.component.html', templateUrl: './soft-select.component.html',
styleUrls: ['./soft-select.component.scss'], styleUrls: ['./soft-select.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class SoftSelectComponent implements OnInit, OnChanges { export class SoftSelectComponent implements OnInit, OnChanges {
@Input() inputId: string = '' @Input() inputId: string = ''

View File

@@ -511,6 +511,21 @@ export class SpreadsheetUtil {
return resolve(XLSX.read(data, opts)) return resolve(XLSX.read(data, opts))
} }
// TEMPORARILY DISABLED: Web Worker for XLSX parsing
// Worker is disabled because Angular/webpack bundles it as a separate chunk
// with a numeric filename (e.g., 411.hash.js). In SAS9/Viya streaming
// environments, all JS files need to be served through SASJobExecution
// with _program= parameter, but our post-build processor can't reliably
// find and replace the worker chunk reference in the minified output.
// FIX: Add "namedChunks": true to production config in angular.json
// (under projects.datacontroller.architect.build.configurations.production)
// This will output worker as "spreadsheet-worker.hash.js" instead of
// numeric ID, making it findable by post-processor.
// Trade-off: UI may briefly freeze when parsing large Excel files.
return resolve(XLSX.read(data, opts))
/*
if (typeof Worker === 'undefined') { if (typeof Worker === 'undefined') {
console.info( console.info(
'Not using worker to parse the XLSX - no Worker available in this environment' 'Not using worker to parse the XLSX - no Worker available in this environment'
@@ -551,6 +566,7 @@ export class SpreadsheetUtil {
setTimeout(() => { setTimeout(() => {
return resolve(XLSX.read(data, opts)) return resolve(XLSX.read(data, opts))
}, 600 * 1000) // 10 minutes }, 600 * 1000) // 10 minutes
*/
}) })
} }

View File

@@ -15,7 +15,8 @@ import { RequestWrapperResponse } from 'src/app/models/request-wrapper/RequestWr
selector: 'app-terms', selector: 'app-terms',
templateUrl: './terms.component.html', templateUrl: './terms.component.html',
styleUrls: ['./terms.component.scss'], styleUrls: ['./terms.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class TermsComponent implements OnInit, AfterViewInit { export class TermsComponent implements OnInit, AfterViewInit {
@ViewChild('markdownCard') markdownCard!: ElementRef @ViewChild('markdownCard') markdownCard!: ElementRef

View File

@@ -12,7 +12,8 @@ import { globals } from 'src/app/_globals'
selector: 'app-header-actions', selector: 'app-header-actions',
templateUrl: './header-actions.component.html', templateUrl: './header-actions.component.html',
styleUrls: ['./header-actions.component.scss'], styleUrls: ['./header-actions.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class HeaderActions implements OnInit, OnDestroy { export class HeaderActions implements OnInit, OnDestroy {
public userName: string = 'Not logged in' public userName: string = 'Not logged in'

View File

@@ -48,7 +48,8 @@ import { Viewbox } from './models/viewbox.model'
selector: 'app-viewboxes', selector: 'app-viewboxes',
templateUrl: './viewboxes.component.html', templateUrl: './viewboxes.component.html',
styleUrls: ['./viewboxes.component.scss'], styleUrls: ['./viewboxes.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy { export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChildren('resizeBox') resizeBoxQuery!: QueryList<ElementRef> //make query list, handle multiple @ViewChildren('resizeBox') resizeBoxQuery!: QueryList<ElementRef> //make query list, handle multiple

View File

@@ -4,6 +4,9 @@
* We use normal version of the XLSX (SheetJS) * We use normal version of the XLSX (SheetJS)
* Because at the moment "@sheet/crypto" can't work in the Web Worker environment * Because at the moment "@sheet/crypto" can't work in the Web Worker environment
* Because of the missing "global" variable. * Because of the missing "global" variable.
*
* Version bumped to v0.20.3 (`libraries/xlsx-0.20.3.tgz`)
* @see https://cdn.sheetjs.com/
*/ */
import * as XLSX from 'xlsx' import * as XLSX from 'xlsx'

View File

@@ -24,7 +24,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class StageComponent implements OnInit, AfterViewInit { export class StageComponent implements OnInit, AfterViewInit {
public table_id: any public table_id: any

View File

@@ -18,7 +18,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class SystemComponent implements OnInit { export class SystemComponent implements OnInit {
appInfo: AppInfo = { appInfo: AppInfo = {

View File

@@ -15,7 +15,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class UserComponent implements OnInit { export class UserComponent implements OnInit {
public users: Array<any> | undefined public users: Array<any> | undefined

View File

@@ -49,7 +49,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ViewerComponent export class ViewerComponent
implements AfterContentInit, AfterViewInit, OnDestroy implements AfterContentInit, AfterViewInit, OnDestroy
@@ -157,15 +158,26 @@ export class ViewerComponent
return ' ' return ' '
}, },
afterGetColHeader: (col: number, th: any, headerLevel: number) => { afterGetColHeader: (col: number, th: any, headerLevel: number) => {
const column = this.hotInstance?.colToProp(col) as string // CRITICAL: Prevent "colToProp method cannot be called because this Handsontable instance has been destroyed" error
// This callback can be triggered even after the instance is destroyed during rapid table switching
if (!this.hotInstance || this.hotInstance.isDestroyed) {
// Graceful fallback: apply only dark mode styling when instance is unavailable
th.classList.add(globals.handsontable.darkTableHeaderClass)
return
}
// header columns styling - primary keys try {
const isPKCol = column && this.headerPks.indexOf(column) > -1 const column = this.hotInstance.colToProp(col) as string
if (isPKCol) th.classList.add('primaryKeyHeaderStyle') const isPKCol = column && this.headerPks.indexOf(column) > -1
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
// Dark mode // Apply dark mode styling to all headers
th.classList.add(globals.handsontable.darkTableHeaderClass) th.classList.add(globals.handsontable.darkTableHeaderClass)
} catch (error) {
// Safety net: if colToProp() fails, still apply basic styling
th.classList.add(globals.handsontable.darkTableHeaderClass)
}
}, },
rowHeaderWidth: 15, rowHeaderWidth: 15,
rowHeights: 20, rowHeights: 20,
@@ -199,12 +211,21 @@ export class ViewerComponent
let colInfo: DataFormat | undefined let colInfo: DataFormat | undefined
let textInfo = 'No info found' let textInfo = 'No info found'
if (this.hotInstance) { if (
const hotSelected: [number, number, number, number][] = this.hotInstance &&
this.hotInstance.getSelected() || [] !this.hotInstance.isDestroyed &&
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1 !this.isTableSwitching
const colName = this.hotInstance?.colToProp(selectedCol) ) {
colInfo = this.$dataFormats?.vars[colName] try {
const hotSelected: [number, number, number, number][] =
this.hotInstance.getSelected() || []
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
const colName = this.hotInstance.colToProp(selectedCol)
colInfo = this.$dataFormats?.vars[colName]
} catch (error) {
// Ignore errors during table switching
colInfo = undefined
}
if (colInfo) if (colInfo)
textInfo = `LABEL: ${colInfo?.label}<br>TYPE: ${colInfo?.type}<br>LENGTH: ${colInfo?.length}<br>FORMAT: ${colInfo?.format}` textInfo = `LABEL: ${colInfo?.label}<br>TYPE: ${colInfo?.type}<br>LENGTH: ${colInfo?.length}<br>FORMAT: ${colInfo?.format}`
@@ -224,6 +245,13 @@ export class ViewerComponent
private hotInstance: Handsontable | null = null private hotInstance: Handsontable | null = null
public hotInstanceClickListener: boolean = false public hotInstanceClickListener: boolean = false
// Race condition prevention for rapid table switching
private isTableSwitching: boolean = false
private switchingTimeout: any = null
// Prevents duplicate setupHot() calls within short time windows
private lastSetupTime: number = 0
public viewboxOpen: boolean = false public viewboxOpen: boolean = false
constructor( constructor(
@@ -505,17 +533,7 @@ export class ViewerComponent
} }
public copyToClip() { public copyToClip() {
let selBox = document.createElement('textarea') navigator.clipboard.writeText(this.webQueryText)
selBox.style.position = 'fixed'
selBox.style.left = '0'
selBox.style.top = '0'
selBox.style.opacity = '0'
selBox.value = this.webQueryText
document.body.appendChild(selBox)
selBox.focus()
selBox.select()
document.execCommand('copy')
document.body.removeChild(selBox)
} }
public goToViewer() { public goToViewer() {
@@ -599,10 +617,24 @@ export class ViewerComponent
} }
public onTableClick(libTable: any, library: any) { public onTableClick(libTable: any, library: any) {
this.lib = library.LIBRARYREF // OPTIMIZATION: Prevent race conditions and destroyed instance errors during rapid table switching
this.table = libTable if (this.isTableSwitching) {
this.selectLibTable(libTable) return
this.viewData(0) }
// Clear any existing timeout to prevent stale operations
if (this.switchingTimeout) {
clearTimeout(this.switchingTimeout)
}
// PERFORMANCE: Debounce table switches to prevent rapid successive calls
// This ensures only the final table selection is processed
this.switchingTimeout = setTimeout(() => {
this.lib = library.LIBRARYREF
this.table = libTable
this.selectLibTable(libTable)
this.viewData(0)
}, 50) // 50ms debounce - fast enough for good UX, slow enough to prevent issues
} }
public async selectTable(lib: string, initial?: boolean, library?: any) { public async selectTable(lib: string, initial?: boolean, library?: any) {
@@ -721,6 +753,10 @@ export class ViewerComponent
} }
public async viewData(filter_pk: number) { public async viewData(filter_pk: number) {
// CRITICAL: Set switching flag to prevent concurrent operations and race conditions
// This prevents callbacks from accessing destroyed instances during table switching
this.isTableSwitching = true
this.loadingTableView = true this.loadingTableView = true
let libDataset: any let libDataset: any
@@ -961,14 +997,25 @@ export class ViewerComponent
this.loadingTableView = false this.loadingTableView = false
//If we try to setup hot when no data is returned it errors `isDestoryed`. // Setup Handsontable after async operations complete
//That is intorduced by HOT update // Original issue: setupHot() called before API responses populated headerPks array
if (!this.noData && !this.noDataReqErr && libDataset) this.setupHot() // Solution: Delay ensures both API paths (lines 328 & 886) have chance to set headerPks
setTimeout(() => {
if (!this.noData && !this.noDataReqErr && libDataset) {
this.setupHot()
}
}, 50) // Optimized from 100ms - fast enough for API completion, slow enough to prevent race conditions
// RACE CONDITION PREVENTION: Reset switching flag after setup completion
// This allows new table switches after current operation finishes
setTimeout(() => {
this.isTableSwitching = false
}, 300) // Optimized from 700ms to match reduced setup times
// Fix ARIA accessibility issues after data loading // Fix ARIA accessibility issues after data loading
setTimeout(() => { setTimeout(() => {
this.fixAriaAccessibility() this.fixAriaAccessibility()
}, 1500) }, 500)
/** /**
* This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area. * This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area.
@@ -988,7 +1035,7 @@ export class ViewerComponent
}) })
this.hotInstanceClickListener = true this.hotInstanceClickListener = true
} }
}, 2000) }, 1000)
} }
} }
@@ -1116,51 +1163,135 @@ export class ViewerComponent
} }
} }
/**
* CRITICAL CLEANUP (workaround needed for HOT version 16 and above): Safely destroys Handsontable instances
*
* Purpose: Prevents "instance destroyed" errors and memory leaks during table switching
*
* Called from:
* - ngOnDestroy() - component cleanup
*
* Safety features:
* - Checks if instance exists and is not already destroyed
* - Try-catch prevents errors during destruction
* - Sets instance to null to prevent stale references
*/
private cleanupHotInstance() {
if (this.hotInstance && !this.hotInstance.isDestroyed) {
try {
this.hotInstance.destroy()
} catch (error) {
console.warn('Error destroying Handsontable instance:', error)
}
}
this.hotInstance = null
this.hooksAttached = false
}
/**
* PERFORMANCE: Configures Handsontable with enhanced error handling (workaround needed for HOT version 16 and above)
*
* 1. Duplicate call prevention (500ms window)
* 2. Multiple validation checks to prevent race conditions
* 3. Forced render for immediate primary key styling
*
* Instance lifecycle is managed by Angular's hot-table component via [data] and [settings] bindings.
* This method only applies additional config that can't go through bindings (hooks, PK styling).
*/
private setupHot() { private setupHot() {
const now = Date.now()
if (now - this.lastSetupTime < 500) {
return
}
this.lastSetupTime = now
// VALIDATION: Don't setup if we're currently switching tables or still loading
if (this.loadingTableView || !this.libDataset) {
return
}
if (!this.hotInstance || this.hotInstance.isDestroyed) {
this.hotInstance = this.hotTableComponent?.hotInstance
}
if (this.hotInstance && !this.hotInstance.isDestroyed) {
this.configureHotInstance()
return
}
// Instance not ready yet — Angular may still be creating the component
setTimeout(() => { setTimeout(() => {
if (!this.loadingTableView && this.libDataset) { if (this.isTableSwitching || this.loadingTableView || !this.libDataset) {
this.hotInstance = this.hotTableComponent?.hotInstance return
if (this.hotInstance) {
this.hotInstance.updateSettings({
height: this.hotTable.height,
modifyColWidth: (width: any, col: any) => {
if (width > 500) return 500
else return width
},
afterGetColHeader: (col: number, th: any) => {
const column = this.hotInstance?.colToProp(col) as string
// header columns styling - primary keys
const isPKCol = column && this.headerPks.indexOf(column) > -1
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
// Dark mode
th.classList.add(globals.handsontable.darkTableHeaderClass)
}
})
// Add hooks for accessibility fixes
this.hotInstance.addHook('afterRender', () => {
// Fix ARIA accessibility issues after each render
this.fixAriaAccessibility()
})
this.hotInstance.addHook('afterChange', () => {
// Fix ARIA accessibility issues after any data change
setTimeout(() => {
this.fixAriaAccessibility()
}, 50)
})
}
} }
// Fix ARIA accessibility issues after table setup this.hotInstance = this.hotTableComponent?.hotInstance
setTimeout(() => { this.configureHotInstance()
}, 250)
}
private hooksAttached = false
/**
* Applies settings that can't go through Angular [settings] binding:
* - Primary key column header styling
* - Column width cap
* - ARIA accessibility hooks (attached once per instance)
*/
private configureHotInstance() {
if (!this.hotInstance || this.hotInstance.isDestroyed) return
this.hotInstance.updateSettings({
height: this.hotTable.height,
modifyColWidth: (width: any, col: any) => {
if (width > 500) return 500
else return width
},
afterGetColHeader: (col: number, th: any) => {
// CRITICAL: Same safety checks as initial callback to prevent destroyed instance errors
if (!this.hotInstance || this.hotInstance.isDestroyed) {
th.classList.add(globals.handsontable.darkTableHeaderClass)
return
}
try {
const column = this.hotInstance.colToProp(col) as string
// PRIMARY KEY STYLING: Apply special styling to PK columns (populated from API response)
const isPKCol = column && this.headerPks.indexOf(column) > -1
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
// DARK MODE: Apply to all headers
th.classList.add(globals.handsontable.darkTableHeaderClass)
} catch (error) {
// SAFETY NET: Ensure basic styling is always applied
th.classList.add(globals.handsontable.darkTableHeaderClass)
}
}
})
// Add hooks for accessibility fixes
// Hooks are attached once per instance to avoid accumulating duplicate listeners
if (!this.hooksAttached) {
this.hotInstance.addHook('afterRender', () => {
// Fix ARIA accessibility issues after each render
this.fixAriaAccessibility() this.fixAriaAccessibility()
}, 500) })
}, 1000)
this.hotInstance.addHook('afterChange', () => {
// Fix ARIA accessibility issues after any data change
setTimeout(() => {
this.fixAriaAccessibility()
}, 50)
})
this.hooksAttached = true
}
// Force immediate render to apply primary key styling
// Without this, styling would wait for ~2 seconds to be applied
// (workaround needed for HOT version 16 and above)
this.hotInstance.render()
} }
async loadWithParameters() { async loadWithParameters() {
@@ -1233,13 +1364,27 @@ export class ViewerComponent
} }
ngOnDestroy() { ngOnDestroy() {
// Clean up the MutationObserver // Proper component destruction to prevent memory leaks and errors
// Prevent any new operations during cleanup
this.isTableSwitching = true
// Clear any pending debounced table switches
if (this.switchingTimeout) {
clearTimeout(this.switchingTimeout)
this.switchingTimeout = null
}
// Safely destroy Handsontable instance
this.cleanupHotInstance()
// Clean up ARIA accessibility observers
if (this.ariaObserver) { if (this.ariaObserver) {
this.ariaObserver.disconnect() this.ariaObserver.disconnect()
this.ariaObserver = undefined this.ariaObserver = undefined
} }
// Clean up the interval // Clear ARIA check intervals
if (this.ariaCheckInterval) { if (this.ariaCheckInterval) {
clearInterval(this.ariaCheckInterval) clearInterval(this.ariaCheckInterval)
this.ariaCheckInterval = undefined this.ariaCheckInterval = undefined

View File

@@ -14,7 +14,8 @@ import { ViyaApis } from './models/viya-apis.models'
host: { host: {
class: 'content-container' class: 'content-container'
}, },
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class ViyaApiExplorerComponent implements OnInit { export class ViyaApiExplorerComponent implements OnInit {
collections: ViyaApis = {} collections: ViyaApis = {}

View File

@@ -62,7 +62,8 @@ enum Tabs {
selector: 'app-xlmap', selector: 'app-xlmap',
templateUrl: './xlmap.component.html', templateUrl: './xlmap.component.html',
styleUrls: ['./xlmap.component.scss'], styleUrls: ['./xlmap.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None,
standalone: false
}) })
export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit { export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
@HostBinding('class.content-container') contentContainerClass = true @HostBinding('class.content-container') contentContainerClass = true

View File

@@ -44,7 +44,7 @@
--> -->
<sasjs <sasjs
serverUrl="http://localhost:5000" serverUrl=""
appLoc="/Public/app/devtest" appLoc="/Public/app/devtest"
serverType="SASJS" serverType="SASJS"
loginMechanism="Redirected" loginMechanism="Redirected"

View File

@@ -10,12 +10,6 @@
"outDir": "./app", "outDir": "./app",
"types": [] "types": []
}, },
"files": [ "files": ["src/polyfills.ts", "src/main.ts", "src/app/app.d.ts"],
"src/polyfills.ts", "include": ["src/**/*.d.ts"]
"src/main.ts", }
"src/app/app.d.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View File

@@ -1,55 +1,37 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */ /* To learn more about this file see: https://angular.io/config/tsconfig. */
{ {
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"baseUrl": "", "baseUrl": "",
"outDir": "dist", "outDir": "dist",
"downlevelIteration": true, "downlevelIteration": true,
"declaration": false, "declaration": false,
"experimentalDecorators": true, "experimentalDecorators": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
"lib": [ "lib": ["ES2022", "dom"],
"ES2022", "skipLibCheck": true,
"dom" "module": "ES2022",
], "importHelpers": true,
"skipLibCheck": true, "moduleResolution": "node",
"module": "ES2022", "sourceMap": true,
"importHelpers": true, "resolveJsonModule": true,
"moduleResolution": "node", "target": "ES2022",
"sourceMap": true, "paths": {
"resolveJsonModule": true, "crypto": ["./node_modules/crypto-browserify"],
"target": "ES2022", "stream": ["./node_modules/stream-browserify"],
"paths": { "assert": ["./node_modules/assert"],
"crypto": [ "http": ["./node_modules/stream-http"],
"./node_modules/crypto-browserify" "https": ["./node_modules/https-browserify"],
], "os": ["./node_modules/os-browserify"]
"stream": [
"./node_modules/stream-browserify"
],
"assert": [
"./node_modules/assert"
],
"http": [
"./node_modules/stream-http"
],
"https": [
"./node_modules/https-browserify"
],
"os": [
"./node_modules/os-browserify"
]
},
"useDefineForClassFields": false
}, },
"angularCompilerOptions": { "useDefineForClassFields": false
"enableI18nLegacyMessageIdFormat": false, },
"strictInjectionParameters": true, "angularCompilerOptions": {
"strictInputAccessModifiers": true, "enableI18nLegacyMessageIdFormat": false,
"strictTemplates": true, "strictInjectionParameters": true,
}, "strictInputAccessModifiers": true,
"exclude": [ "strictTemplates": true
"cypress/**/*.ts", },
"cypress.config.ts" "exclude": ["cypress/**/*.ts", "cypress.config.ts"]
]
} }

View File

@@ -3,15 +3,8 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/spec", "outDir": "./out-tsc/spec",
"types": [ "types": ["jasmine"]
"jasmine"
]
}, },
"files": [ "files": ["src/polyfills.ts"],
"src/polyfills.ts" "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
} }

559
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "dcfrontend", "name": "dcfrontend",
"version": "7.2.0", "version": "7.4.0",
"description": "Data Controller", "description": "Data Controller",
"devDependencies": { "devDependencies": {
"@saithodev/semantic-release-gitea": "^2.1.0", "@saithodev/semantic-release-gitea": "^2.1.0",
@@ -10,7 +10,7 @@
"@semantic-release/npm": "11.0.0", "@semantic-release/npm": "11.0.0",
"@semantic-release/release-notes-generator": "^11.0.4", "@semantic-release/release-notes-generator": "^11.0.4",
"commit-and-tag-version": "^11.2.2", "commit-and-tag-version": "^11.2.2",
"prettier": "3.6.2" "prettier": "^3.7.4"
}, },
"scripts": { "scripts": {
"install": "cd client && npm i && cd ../sas && npm i", "install": "cd client && npm i && cd ../sas && npm i",
@@ -32,6 +32,5 @@
"//": [ "//": [
"Readme", "Readme",
"We must set private: true so that semantic-release/npm plugin will update the package.json version but not try to release it as NPM package" "We must set private: true so that semantic-release/npm plugin will update the package.json version but not try to release it as NPM package"
], ]
"dependencies": {}
} }

View File

@@ -2,7 +2,7 @@
"fromjs": [ "fromjs": [
{ {
"ADMIN": "DCDEFAULT", "ADMIN": "DCDEFAULT",
"DCPATH": "/tmp/mihajlo/dcserverfrs" "DCPATH": "/tmp/dcdata"
} }
] ]
} }

938
sas/package-lock.json generated

File diff suppressed because it is too large Load Diff

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