90 Commits

Author SHA1 Message Date
485783a782 chore(release): 6.8.1 [skip ci]
## [6.8.1](https://git.datacontroller.io/dc/dc/compare/v6.8.0...v6.8.1) (2024-05-02)

### Bug Fixes

* hide approve button when table revertable ([ec0f539](ec0f539a33))
2024-05-02 16:10:44 +00:00
ee07bef2b8 Merge pull request 'fix: hide approve button when table revertable' (#95) from ci-fix into main
Some checks failed
Release / Build-production-and-ng-test (push) Successful in 3m59s
Release / Build-and-test-development (push) Successful in 7m45s
Release / release (push) Failing after 1m55s
Reviewed-on: #95
2024-05-02 15:57:22 +00:00
0de8481314 ci: ng build
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 4m4s
2024-05-02 17:48:30 +02:00
ec0f539a33 fix: hide approve button when table revertable
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 2m1s
2024-05-02 17:44:53 +02:00
173ee2daff chore(release): 6.8.0 [skip ci]
# [6.8.0](https://git.datacontroller.io/dc/dc/compare/v6.7.0...v6.8.0) (2024-05-02)

### Bug Fixes

* ci sheet lib, submit message auto focus ([c5e4650](c5e4650327))
* **clarity:** new version style issues ([8c7de5a](8c7de5aad7))
* cypress tests ([3dd85cc](3dd85cc60b))
* ensuring that only restorable versions are restorable ([a402856](a4028562ce))
* ensuring version history only includes loaded versions ([51ebd25](51ebd25aa3))
* final testing on restore feature ([297a84d](297a84d3a4))
* issue with multiple adds/deletes, [#84](#84) ([904ca30](904ca30f91))
* load_ref var ([aaad9f7](aaad9f7207))
* removing alerts dummy data, closes [#93](#93) ([eba21e9](eba21e96b4))
* restore table version improvement ([549f357](549f35766b))
* **sas:** viewer versions fix ([c6595c1](c6595c1f61))
* stage and approve buttons renaming ([ef81e33](ef81e33f70))
* supporting SCD2 data reversions ([fa8396f](fa8396f039))
* table info modal, versions - column names ([801c8c6](801c8c6a9f))
* **updates:** angular, clarity, resolved legacy-peer-deps ([c60dd65](c60dd65a16))

### Features

* backend to show in getchangeinfo whether a user is allowed to restore ([8769841](8769841f08))
* list versions of target tables (backend) ([f8a14d4](f8a14d4bde))
* restore ([604c2e7](604c2e70bd))
* SAS services & tests for RESTORE, [#84](#84) ([9ad7ae4](9ad7ae47b5))
* staging page, restore buttons ([02a8a1c](02a8a1c565))
* table metadata modal, versions tab (and link) ([b27fea5](b27fea5b91))
* **versions:** getting list of versions (plus test) ([8003da9](8003da94e6))
2024-05-02 12:08:05 +00:00
5c0091b5e8 chore: test workaround
Some checks failed
Release / Build-production-and-ng-test (push) Successful in 3m58s
Release / Build-and-test-development (push) Successful in 7m44s
Release / release (push) Failing after 1m55s
2024-05-02 13:54:44 +02:00
84dce7f6e8 Merge pull request 'Restore Previous State / Data Rollback' (#94) from restore into main
Some checks failed
Release / Build-production-and-ng-test (push) Successful in 4m1s
Release / Build-and-test-development (push) Failing after 8m26s
Release / release (push) Has been skipped
Reviewed-on: #94
2024-05-02 11:18:28 +00:00
e8a943a35a chore: ng test
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 2m1s
2024-05-02 13:08:06 +02:00
b3171a8125 ci: fix
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m57s
2024-05-02 12:56:52 +02:00
d77f2eb674 chore(git): Merge branch 'deps-update' into restore
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m30s
2024-05-02 12:53:32 +02:00
5474fad9cc chore: package-lock
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m2s
2024-05-02 12:46:50 +02:00
3dd85cc60b fix: cypress tests 2024-05-02 12:39:07 +02:00
510e412ff2 chore: cypress tests fix
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 15s
2024-05-02 11:25:46 +02:00
3dfdccbc6b ci: sheet lib
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m2s
2024-05-02 10:04:31 +02:00
a55661548a chore: licence checker
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m2s
2024-05-02 09:59:21 +02:00
69363b37e9 ci: sheet lib
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m2s
2024-05-02 09:54:10 +02:00
2d6a753921 ci: sheet lib
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 15s
2024-05-02 09:52:22 +02:00
dc989e5668 ci: fix
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 58s
2024-05-02 09:38:43 +02:00
227ac480d5 style: lint
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 16s
2024-05-02 09:30:23 +02:00
c5e4650327 fix: ci sheet lib, submit message auto focus
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 16s
2024-05-02 09:28:39 +02:00
56cf271e77 Merge branch 'main' into deps-update
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 57s
2024-04-30 17:41:54 +00:00
^
fa8396f039 fix: supporting SCD2 data reversions
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 15s
2024-04-30 18:39:00 +01:00
^
904ca30f91 fix: issue with multiple adds/deletes, #84
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 16s
2024-04-30 17:46:00 +01:00
549f35766b fix: restore table version improvement
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 15s
2024-04-30 15:43:17 +02:00
1589c799ec chore(git): Merge branch 'restore' of ssh://git.datacontroller.io:29419/dc/dc into restore
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 15s
2024-04-30 15:26:07 +02:00
604c2e70bd feat: restore 2024-04-30 15:25:58 +02:00
^
297a84d3a4 fix: final testing on restore feature
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 59s
2024-04-30 14:21:02 +01:00
^
aaad9f7207 fix: load_ref var
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 58s
2024-04-30 12:34:22 +01:00
^
a4028562ce fix: ensuring that only restorable versions are restorable
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 58s
2024-04-30 11:14:47 +01:00
5ab3f98855 chore(git): Merge branch 'main' into restore
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 59s
2024-04-30 09:04:46 +02:00
^
eba21e96b4 fix: removing alerts dummy data, closes #93
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 18s
2024-04-30 00:22:44 +01:00
^
9ad7ae47b5 feat: SAS services & tests for RESTORE, #84 2024-04-30 00:20:40 +01:00
cf54e4c8f3 Update README.md
Some checks failed
Release / Build-production-and-ng-test (push) Failing after 1m31s
Release / Build-and-test-development (push) Has been skipped
Release / release (push) Has been skipped
2024-04-23 13:06:51 +00:00
57aa6fa0fc ci: fix
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 58s
2024-04-15 09:37:20 +02:00
4a01f3d490 ci: fix
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 57s
2024-04-12 21:37:49 +02:00
80a0db951d chore: angular testing
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m5s
2024-04-12 15:48:00 +02:00
3202cb8e08 ci: ng test fix
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m4s
2024-04-12 14:23:04 +02:00
ddf36230bf ci: ng test fix
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m4s
2024-04-12 14:06:29 +02:00
b67c2be968 chore: package json test script fix
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m3s
2024-04-12 13:53:35 +02:00
dc2c8da92b ci: added angular tests on PR
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m10s
2024-04-12 13:30:44 +02:00
5d6c3701d0 chore: licence checker update
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m3s
2024-04-12 13:28:18 +02:00
b024e263b4 chore: updated package-lock
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m2s
2024-04-12 13:20:08 +02:00
b706864e40 Merge pull request 'Angular and Clarity Update' (#91) from deps-update into main
Some checks failed
Release / Build-production-and-ng-test (push) Failing after 1m37s
Release / Build-and-test-development (push) Has been skipped
Release / release (push) Has been skipped
Reviewed-on: #91
2024-04-12 10:45:57 +00:00
60cc666b67 chore: licence checker
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m15s
2024-04-12 12:34:01 +02:00
efff4dd553 chore(cypress): bigger viewport size
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m1s
2024-04-12 11:48:23 +02:00
2b1dad8e48 style: lint
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m2s
2024-04-12 10:51:32 +02:00
8c7de5aad7 fix(clarity): new version style issues 2024-04-12 10:51:11 +02:00
1db8bc2573 style: lint
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m12s
2024-04-11 13:23:40 +02:00
c60dd65a16 fix(updates): angular, clarity, resolved legacy-peer-deps 2024-04-11 13:23:24 +02:00
f7f59a4b0a Merge pull request 'feat: Display Previous Versions' (#88) from restore into main
Some checks failed
Release / Build-production-and-ng-test (push) Successful in 4m18s
Release / Build-and-test-development (push) Failing after 15m54s
Release / release (push) Has been skipped
Reviewed-on: #88
2024-04-04 13:24:25 +00:00
^
ec7615e7e3 chore: adding sheet-crypto to gitignoreg
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m5s
2024-04-03 10:56:49 +01:00
f411c33754 chore(git): Merge branch 'main' into restore
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m4s
2024-04-01 13:51:33 +02:00
79121168e4 style: lint
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m4s
2024-04-01 13:50:50 +02:00
ef81e33f70 fix: stage and approve buttons renaming
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 15s
2024-04-01 13:37:41 +02:00
96066c66cb chore(release): 6.7.0 [skip ci]
# [6.7.0](https://git.datacontroller.io/dc/dc/compare/v6.6.4...v6.7.0) (2024-04-01)

### Features

* numeric values in hot dropdown aligned right ([9635626](963562621d))
2024-04-01 11:23:31 +00:00
b1819b776d chore(git): Merge branch 'restore' of ssh://git.datacontroller.io:29419/dc/dc into restore
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 15s
2024-04-01 13:09:34 +02:00
bd7a392ffc chore(git): Merge branch 'main' into restore 2024-04-01 13:09:27 +02:00
7997b77158 Merge pull request 'Numeric values in hot dropdown aligned right' (#86) from numeric-values-diff into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 4m26s
Release / Build-and-test-development (push) Successful in 8m10s
Release / release (push) Successful in 6m32s
Reviewed-on: #86
2024-04-01 11:08:53 +00:00
d1966bcdc5 chore(git): Merge branch 'main' into numeric-values-diff
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m3s
2024-04-01 12:33:43 +02:00
7b5bbe024d chore(release): 6.6.4 [skip ci]
## [6.6.4](https://git.datacontroller.io/dc/dc/compare/v6.6.3...v6.6.4) (2024-04-01)

### Bug Fixes

* ordering SOFTSELECT numerically in dropdown ([f522038](f522038b8d)), closes [#85](#85)
* reverting col ([fbbcf90](fbbcf90956))
* typo ([31d4e5c](31d4e5c727))
2024-04-01 09:51:54 +00:00
4d84f15aca Merge pull request 'ci: install sheet temporarily' (#89) from ci into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 4m16s
Release / Build-and-test-development (push) Successful in 8m1s
Release / release (push) Successful in 6m22s
Reviewed-on: #89
2024-04-01 09:37:56 +00:00
928937daab ci: sheet
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m4s
2024-04-01 11:14:00 +02:00
3bd8d247e5 ci: sheet
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m3s
2024-04-01 10:59:03 +02:00
cf6c9dd5f2 ci: sheet
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 1m0s
2024-04-01 10:55:59 +02:00
ff55cbbaad ci: sheet
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 17s
2024-04-01 10:45:16 +02:00
3eda4e2c58 ci: install sheet temporarily
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 17s
2024-04-01 10:39:30 +02:00
02a8a1c565 feat: staging page, restore buttons 2024-03-29 13:08:48 +01:00
^
8769841f08 feat: backend to show in getchangeinfo whether a user is allowed to restore
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 15s
2024-03-28 16:03:27 +00:00
7208fe1c3b chore(git): Merge branch 'restore' of ssh://git.datacontroller.io:29419/dc/dc into restore
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 15s
2024-03-26 19:14:53 +01:00
801c8c6a9f fix: table info modal, versions - column names 2024-03-26 19:14:41 +01:00
^
51ebd25aa3 fix: ensuring version history only includes loaded versions
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 48s
2024-03-26 14:25:58 +00:00
c6595c1f61 fix(sas): viewer versions fix
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 46s
2024-03-26 14:20:01 +01:00
a267666e99 style: lint
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 46s
2024-03-25 22:41:25 +01:00
b27fea5b91 feat: table metadata modal, versions tab (and link) 2024-03-25 22:41:00 +01:00
^
f8a14d4bde feat: list versions of target tables (backend)
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 47s
2024-03-25 12:37:11 +00:00
^
633e35338d chore: remove unnecessary concatenation 2024-03-20 21:38:44 +00:00
^
8003da94e6 feat(versions): getting list of versions (plus test) 2024-03-20 21:37:13 +00:00
c3af97ef57 Merge pull request 'issue85' (#87) from issue85 into main
Some checks failed
Release / Build-production-and-ng-test (push) Failing after 1m13s
Release / Build-and-test-development (push) Has been skipped
Release / release (push) Has been skipped
Reviewed-on: #87
2024-03-19 22:41:04 +00:00
31d4e5c727 fix: typo
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 47s
2024-03-19 22:36:00 +00:00
fbbcf90956 fix: reverting col 2024-03-19 22:35:16 +00:00
f522038b8d fix: ordering SOFTSELECT numerically in dropdown
Closes #85
2024-03-19 22:33:39 +00:00
ace599b39f style: lint
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 46s
2024-03-19 10:01:30 +01:00
963562621d feat: numeric values in hot dropdown aligned right
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 29s
2024-03-18 17:32:29 +01:00
5171d07441 chore(release): 6.6.3 [skip ci]
## [6.6.3](https://git.datacontroller.io/dc/dc/compare/v6.6.2...v6.6.3) (2024-02-26)

### Bug Fixes

* allow empty clause value when NE or CONTAINS ([432450a](432450a15b))
2024-02-26 14:17:24 +00:00
9a0b9573d5 Merge pull request 'Allow empty clause value when operator is NE or CONTAINS' (#83) from issue-82 into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 4m18s
Release / Build-and-test-development (push) Successful in 7m46s
Release / release (push) Successful in 6m18s
Reviewed-on: #83
2024-02-26 14:03:36 +00:00
4733311ef3 style: lint
All checks were successful
Build / Build-and-ng-test (pull_request) Successful in 1m0s
2024-02-26 14:15:34 +01:00
432450a15b fix: allow empty clause value when NE or CONTAINS 2024-02-26 14:14:51 +01:00
47638becc0 chore(release): 6.6.2 [skip ci]
## [6.6.2](https://git.datacontroller.io/dc/dc/compare/v6.6.1...v6.6.2) (2024-02-22)

### Bug Fixes

* excel with commas getting wrapped in quotes ([3860134](38601346a5))
2024-02-22 12:33:10 +00:00
bdd3a95685 Merge pull request 'excel with commas getting wrapped in quotes' (#80) from issue-77 into main
All checks were successful
Release / Build-production-and-ng-test (push) Successful in 4m13s
Release / Build-and-test-development (push) Successful in 7m44s
Release / release (push) Successful in 6m15s
Reviewed-on: #80
Reviewed-by: sabir <sabir@4gl.io>
2024-02-22 12:19:34 +00:00
38601346a5 fix: excel with commas getting wrapped in quotes
Some checks failed
Build / Build-and-ng-test (pull_request) Failing after 15s
2024-02-21 14:48:42 +01:00
87 changed files with 9017 additions and 28436 deletions

View File

@ -10,7 +10,14 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 20
- name: Install Google Chrome
run: |
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
apt-get update
apt-get install -y google-chrome-stable xvfb
- name: Write .npmrc file - name: Write .npmrc file
run: echo "$NPMRC" > client/.npmrc run: echo "$NPMRC" > client/.npmrc
@ -21,8 +28,26 @@ jobs:
- name: Lint check - name: Lint check
run: npm run lint:check run: npm run lint:check
- name: Licence checker - name: Install dependencies
run: | run: |
cd client cd client
npm ci npm ci
# Decrypt and Install sheet
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
npm i ./libraries/sheet-crypto.tgz
# End
- name: Licence checker
run: |
cd client
npm run license-checker npm run license-checker
- name: Angular Tests
run: |
cd client
npm run test:headless
- name: Production Build
run: |
cd client
npm run build

View File

@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 20
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
@ -34,7 +34,13 @@ jobs:
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }} CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
- name: Install dependencies - name: Install dependencies
run: npm ci run: |
cd client
npm ci
# Decrypt and Install sheet
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
npm i ./libraries/sheet-crypto.tgz
# End
- 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
@ -48,7 +54,7 @@ jobs:
- name: Angular Tests - name: Angular Tests
run: | run: |
cd client cd client
npm test -- --no-watch --no-progress --browsers=ChromeHeadlessCI npm run test:headless
- name: Angular Production Build - name: Angular Production Build
run: | run: |
@ -64,7 +70,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 20
- name: Write .npmrc file - name: Write .npmrc file
run: | run: |
@ -86,7 +92,13 @@ jobs:
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }} CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
- name: Install dependencies - name: Install dependencies
run: npm ci run: |
cd client
npm ci
# Decrypt and Install sheet
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
npm i ./libraries/sheet-crypto.tgz
# End
# Install pm2 and prepare SASJS server # Install pm2 and prepare SASJS server
- run: npm i -g pm2 - run: npm i -g pm2
@ -185,6 +197,10 @@ jobs:
run: | run: |
cd client cd client
npm ci npm ci
# Decrypt and Install sheet
echo ${{ secrets.SHEET_PWD }} | gpg --batch --yes --passphrase-fd 0 ./libraries/sheet-crypto.tgz.gpg
npm i ./libraries/sheet-crypto.tgz
# End
npm run build npm run build
- name: Build SAS9 EBI Release - name: Build SAS9 EBI Release

2
.gitignore vendored
View File

@ -11,6 +11,8 @@ client/cypress/screenshots
client/cypress/results client/cypress/results
client/cypress/videos client/cypress/videos
client/documentation client/documentation
client/sheet-crypto*
client/.nx
cypress.env.json cypress.env.json
sasjsbuild sasjsbuild
sasjsresults sasjsresults

View File

@ -1,3 +1,72 @@
## [6.8.1](https://git.datacontroller.io/dc/dc/compare/v6.8.0...v6.8.1) (2024-05-02)
### Bug Fixes
* hide approve button when table revertable ([ec0f539](https://git.datacontroller.io/dc/dc/commit/ec0f539a337b176c83a661ff520a6892d47efa02))
# [6.8.0](https://git.datacontroller.io/dc/dc/compare/v6.7.0...v6.8.0) (2024-05-02)
### Bug Fixes
* ci sheet lib, submit message auto focus ([c5e4650](https://git.datacontroller.io/dc/dc/commit/c5e46503272f3f3d9cd83ac04225babf79d4de44))
* **clarity:** new version style issues ([8c7de5a](https://git.datacontroller.io/dc/dc/commit/8c7de5aad7e7e32a64769696af9b93eb9a6225d3))
* cypress tests ([3dd85cc](https://git.datacontroller.io/dc/dc/commit/3dd85cc60bd5ac99bc930b6b9c89a8e707e4d51d))
* ensuring that only restorable versions are restorable ([a402856](https://git.datacontroller.io/dc/dc/commit/a4028562ce91b32ff971ab9821328b97cd23f381))
* ensuring version history only includes loaded versions ([51ebd25](https://git.datacontroller.io/dc/dc/commit/51ebd25aa362aa8e66c83b29b2c64aa0f206f5bd))
* final testing on restore feature ([297a84d](https://git.datacontroller.io/dc/dc/commit/297a84d3a4ebb47bef7f3ca9758978d727afed8d))
* issue with multiple adds/deletes, [#84](https://git.datacontroller.io/dc/dc/issues/84) ([904ca30](https://git.datacontroller.io/dc/dc/commit/904ca30f918da085fa05dae066367b512933d1a9))
* load_ref var ([aaad9f7](https://git.datacontroller.io/dc/dc/commit/aaad9f7207115599a006980fff099d59738dd2cd))
* removing alerts dummy data, closes [#93](https://git.datacontroller.io/dc/dc/issues/93) ([eba21e9](https://git.datacontroller.io/dc/dc/commit/eba21e96b4fa34e63b4477281f47d9a01d621f2e))
* restore table version improvement ([549f357](https://git.datacontroller.io/dc/dc/commit/549f35766ba7b5bbe55694845e85bfefc4193375))
* **sas:** viewer versions fix ([c6595c1](https://git.datacontroller.io/dc/dc/commit/c6595c1f618803d9202cba1a1fe76986449cf2e2))
* stage and approve buttons renaming ([ef81e33](https://git.datacontroller.io/dc/dc/commit/ef81e33f704d0b4f99405d96498e1d29ac817982))
* supporting SCD2 data reversions ([fa8396f](https://git.datacontroller.io/dc/dc/commit/fa8396f0394cbddb6dbacb4b355de078fad49980))
* table info modal, versions - column names ([801c8c6](https://git.datacontroller.io/dc/dc/commit/801c8c6a9fb95388a06a6c6284fec4dc25bb77c5))
* **updates:** angular, clarity, resolved legacy-peer-deps ([c60dd65](https://git.datacontroller.io/dc/dc/commit/c60dd65a1637333f11a0c39ef697c7292a6ede07))
### Features
* backend to show in getchangeinfo whether a user is allowed to restore ([8769841](https://git.datacontroller.io/dc/dc/commit/8769841f08694f672ef7ae1a17beacd0dbedda52))
* list versions of target tables (backend) ([f8a14d4](https://git.datacontroller.io/dc/dc/commit/f8a14d4bdef055b99930491d1f6fabe55a50a497))
* restore ([604c2e7](https://git.datacontroller.io/dc/dc/commit/604c2e70bdeeeb1aa5bb18b94f525ebd049397fa))
* SAS services & tests for RESTORE, [#84](https://git.datacontroller.io/dc/dc/issues/84) ([9ad7ae4](https://git.datacontroller.io/dc/dc/commit/9ad7ae47b5e793ce68ab21c9eeb8dee6cb85e496))
* staging page, restore buttons ([02a8a1c](https://git.datacontroller.io/dc/dc/commit/02a8a1c5654350cafc53b749cceb686fc6848c33))
* table metadata modal, versions tab (and link) ([b27fea5](https://git.datacontroller.io/dc/dc/commit/b27fea5b91e33b4673a3a991aedae558e366ca29))
* **versions:** getting list of versions (plus test) ([8003da9](https://git.datacontroller.io/dc/dc/commit/8003da94e615463ed3ddfd60b0cbf2e58615eab1))
# [6.7.0](https://git.datacontroller.io/dc/dc/compare/v6.6.4...v6.7.0) (2024-04-01)
### Features
* numeric values in hot dropdown aligned right ([9635626](https://git.datacontroller.io/dc/dc/commit/963562621ddf0e8d24a29a8481c5e6da1b040708))
## [6.6.4](https://git.datacontroller.io/dc/dc/compare/v6.6.3...v6.6.4) (2024-04-01)
### Bug Fixes
* ordering SOFTSELECT numerically in dropdown ([f522038](https://git.datacontroller.io/dc/dc/commit/f522038b8ddb1da14b8adbf8346d0a4539a94cc8)), closes [#85](https://git.datacontroller.io/dc/dc/issues/85)
* reverting col ([fbbcf90](https://git.datacontroller.io/dc/dc/commit/fbbcf90956bf538b032b0107c07b8576d20353b9))
* typo ([31d4e5c](https://git.datacontroller.io/dc/dc/commit/31d4e5c727f790d428fb2ea8da60dca929561805))
## [6.6.3](https://git.datacontroller.io/dc/dc/compare/v6.6.2...v6.6.3) (2024-02-26)
### Bug Fixes
* allow empty clause value when NE or CONTAINS ([432450a](https://git.datacontroller.io/dc/dc/commit/432450a15b51a269821ba1d430854f5d1dd04703))
## [6.6.2](https://git.datacontroller.io/dc/dc/compare/v6.6.1...v6.6.2) (2024-02-22)
### Bug Fixes
* excel with commas getting wrapped in quotes ([3860134](https://git.datacontroller.io/dc/dc/commit/38601346a529cfe3787bb286a639e0293c365020))
## [6.6.1](https://git.datacontroller.io/dc/dc/compare/v6.6.0...v6.6.1) (2024-02-19) ## [6.6.1](https://git.datacontroller.io/dc/dc/compare/v6.6.0...v6.6.1) (2024-02-19)

View File

@ -28,3 +28,5 @@ For more information:
* Main site: https://datacontroller.io * Main site: https://datacontroller.io
* Docs: https://docs.datacontroller.io * Docs: https://docs.datacontroller.io
* Code: https://code.datacontroller.io * Code: https://code.datacontroller.io
For support, contact support@4gl.io or reach out on [Matrix](https://matrix.to/#/#dc:4gl.io)!

View File

@ -45,6 +45,7 @@
"numbro", "numbro",
"@clr/icons", "@clr/icons",
"@sasjs/adapter", "@sasjs/adapter",
"@sasjs/utils/types/serverType",
"@sasjs/utils/input/validators", "@sasjs/utils/input/validators",
"@sasjs/utils/utils/bytesToSize", "@sasjs/utils/utils/bytesToSize",
"base64-arraybuffer", "base64-arraybuffer",
@ -67,7 +68,6 @@
"src/styles.scss" "src/styles.scss"
], ],
"scripts": [ "scripts": [
"node_modules/@clr/icons/clr-icons.min.js",
"node_modules/marked/marked.min.js" "node_modules/marked/marked.min.js"
] ]
}, },
@ -116,10 +116,10 @@
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "datacontroller:build:production" "buildTarget": "datacontroller:build:production"
}, },
"development": { "development": {
"browserTarget": "datacontroller:build:development" "buildTarget": "datacontroller:build:development"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"
@ -127,30 +127,27 @@
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"browserTarget": "datacontroller:build" "buildTarget": "datacontroller:build"
} }
}, },
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"codeCoverage": true,
"polyfills": [ "polyfills": [
"src/polyfills.ts", "src/polyfills.ts",
"zone.js", "zone.js",
"zone.js/testing" "zone.js/testing"
], ],
"styles": [ "tsConfig": "tsconfig.spec.json",
"src/styles.scss" "inlineStyleLanguage": "scss",
],
"scripts": [
],
"assets": [ "assets": [
"src/favicon.ico", "src/favicon.ico",
"src/assets" "src/assets"
], ],
"styles": [
"src/styles.scss"
],
"scripts": [],
"karmaConfig": "karma.conf.js" "karmaConfig": "karma.conf.js"
} }
}, },

View File

@ -9,6 +9,8 @@ export default defineConfig({
html: true, html: true,
json: false, json: false,
}, },
viewportHeight: 900,
viewportWidth: 1600,
chromeWebSecurity: false, chromeWebSecurity: false,
defaultCommandTimeout: 30000, defaultCommandTimeout: 30000,

View File

@ -221,13 +221,13 @@ const submitExcel = (callback?: any) => {
const rejectExcel = (callback?: any) => { const rejectExcel = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout }) cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Go to approvals screen') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (
approvalButton.innerText approvalButton.innerText
.toLowerCase() .toLowerCase()
.includes('go to approvals screen') .includes('approve')
) { ) {
approvalButton.click() approvalButton.click()
break break

View File

@ -405,13 +405,13 @@ const submitExcel = (callback?: any) => {
const rejectExcel = (callback?: any) => { const rejectExcel = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout }) cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Go to approvals screen') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (
approvalButton.innerText approvalButton.innerText
.toLowerCase() .toLowerCase()
.includes('go to approvals screen') .includes('approve')
) { ) {
approvalButton.click() approvalButton.click()
break break
@ -438,13 +438,13 @@ const rejectExcel = (callback?: any) => {
const acceptExcel = (callback?: any) => { const acceptExcel = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout }) cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Go to approvals screen') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (
approvalButton.innerText approvalButton.innerText
.toLowerCase() .toLowerCase()
.includes('go to approvals screen') .includes('approve')
) { ) {
approvalButton.click() approvalButton.click()
break break

View File

@ -159,20 +159,21 @@ context('filtering tests: ', function () {
}) })
}) })
it('7 | filter bestnum field BETWEEN', (done) => { // TODO: fix
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test') // it('7 | filter bestnum field BETWEEN', (done) => {
// openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
openFilterPopup(() => { // openFilterPopup(() => {
setFilterWithValue('SOME_BESTNUM', '0-10', 'between', () => { // setFilterWithValue('SOME_BESTNUM', '0-10', 'between', () => {
checkInfoBarIncludes( // checkInfoBarIncludes(
`AND,AND,0,SOME_BESTNUM,BETWEEN,0 AND 10`, // `AND,AND,0,SOME_BESTNUM,BETWEEN,0 AND 10`,
(includes: boolean) => { // (includes: boolean) => {
if (includes) done() // if (includes) done()
} // }
) // )
}) // })
}) // })
}) // })
this.afterEach(() => { this.afterEach(() => {
// cy.visit(`${hostUrl}/SASLogon/logout`) // cy.visit(`${hostUrl}/SASLogon/logout`)

View File

@ -699,13 +699,13 @@ const submitTable = (callback?: any) => {
const approveTable = (callback?: any) => { const approveTable = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout }) cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Go to approvals screen') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (
approvalButton.innerText approvalButton.innerText
.toLowerCase() .toLowerCase()
.includes('go to approvals screen') .includes('approve')
) { ) {
approvalButton.click() approvalButton.click()
break break

View File

@ -125,13 +125,13 @@ const submitExcel = (callback?: any) => {
const rejectExcel = (callback?: any) => { const rejectExcel = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout }) cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Go to approvals screen') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (
approvalButton.innerText approvalButton.innerText
.toLowerCase() .toLowerCase()
.includes('go to approvals screen') .includes('approve')
) { ) {
approvalButton.click() approvalButton.click()
break break

View File

@ -221,14 +221,10 @@ const submitExcel = (callback?: any) => {
const rejectExcel = (callback?: any) => { const rejectExcel = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout }) cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Go to approvals screen') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.innerText
.toLowerCase()
.includes('go to approvals screen')
) {
approvalButton.click() approvalButton.click()
break break
} }

View File

@ -407,14 +407,10 @@ const submitExcel = (callback?: any) => {
const rejectExcel = (callback?: any) => { const rejectExcel = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout }) cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Go to approvals screen') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.innerText
.toLowerCase()
.includes('go to approvals screen')
) {
approvalButton.click() approvalButton.click()
break break
} }
@ -440,14 +436,10 @@ const rejectExcel = (callback?: any) => {
const acceptExcel = (callback?: any) => { const acceptExcel = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout }) cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Go to approvals screen') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.innerText
.toLowerCase()
.includes('go to approvals screen')
) {
approvalButton.click() approvalButton.click()
break break
} }

View File

@ -699,14 +699,10 @@ const submitTable = (callback?: any) => {
const approveTable = (callback?: any) => { const approveTable = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout }) cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Go to approvals screen') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.innerText
.toLowerCase()
.includes('go to approvals screen')
) {
approvalButton.click() approvalButton.click()
break break
} }

View File

@ -125,14 +125,10 @@ const submitExcel = (callback?: any) => {
const rejectExcel = (callback?: any) => { const rejectExcel = (callback?: any) => {
cy.get('button', { timeout: longerCommandTimeout }) cy.get('button', { timeout: longerCommandTimeout })
.should('contain', 'Go to approvals screen') .should('contain', 'Approve')
.then((allButtons: any) => { .then((allButtons: any) => {
for (let approvalButton of allButtons) { for (let approvalButton of allButtons) {
if ( if (approvalButton.innerText.toLowerCase().includes('approve')) {
approvalButton.innerText
.toLowerCase()
.includes('go to approvals screen')
) {
approvalButton.click() approvalButton.click()
break break
} }

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@13.1.0;handsontable@13.1.0;hyperformula@2.5.0;jackspeak@2.2.0;path-scurry@1.7.0' '@cds/city@1.1.0;@handsontable/angular@13.1.0;handsontable@13.1.0;hyperformula@2.7.0;jackspeak@2.2.0;path-scurry@1.7.0'
}, },
(error, json) => { (error, json) => {
if (error) { if (error) {

25692
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,8 +18,8 @@
"deploy_sasjs": "rsync -avhe ssh ./dist/* --delete root@${npm_config_account}.4gl.io:/var/www/html/dc/dev", "deploy_sasjs": "rsync -avhe ssh ./dist/* --delete root@${npm_config_account}.4gl.io:/var/www/html/dc/dev",
"viyabuild": "cd build; ./viyabuild.sh", "viyabuild": "cd build; ./viyabuild.sh",
"lint": "cd .. && npm run lint", "lint": "cd .. && npm run lint",
"test": "ng test", "test": "npx ng test",
"test:headless": "ng test --browsers ChromeHeadless", "test:headless": "npx ng test --no-watch --no-progress --browsers ChromeHeadlessCI",
"watch": "ng test watch=true", "watch": "ng test watch=true",
"pree2e": "webdriver-manager update", "pree2e": "webdriver-manager update",
"e2e": "protractor protractor.config.js", "e2e": "protractor protractor.config.js",
@ -35,23 +35,22 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^16.1.2", "@angular/animations": "^17.3.3",
"@angular/cdk": "^15.2.0", "@angular/cdk": "^17.3.3",
"@angular/common": "^16.1.2", "@angular/common": "^17.3.3",
"@angular/compiler": "^16.1.2", "@angular/compiler": "^17.3.3",
"@angular/core": "^16.1.2", "@angular/core": "^17.3.3",
"@angular/forms": "^16.1.2", "@angular/forms": "^17.3.3",
"@angular/platform-browser": "^16.1.2", "@angular/platform-browser": "^17.3.3",
"@angular/platform-browser-dynamic": "^16.1.2", "@angular/platform-browser-dynamic": "^17.3.3",
"@angular/router": "^16.1.2", "@angular/router": "^17.3.3",
"@cds/core": "^6.4.2", "@cds/core": "^6.10.0",
"@clr/angular": "^13.17.0", "@clr/angular": "^17.0.1",
"@clr/icons": "^13.0.2", "@clr/icons": "^13.0.2",
"@clr/ui": "^13.17.0", "@clr/ui": "^17.0.1",
"@handsontable/angular": "^13.1.0", "@handsontable/angular": "^13.1.0",
"@sasjs/adapter": "4.10.2", "@sasjs/adapter": "4.10.2",
"@sasjs/utils": "^3.4.0", "@sasjs/utils": "^3.4.0",
"@sheet/crypto": "1.20211122.1",
"@types/d3-graphviz": "^2.6.7", "@types/d3-graphviz": "^2.6.7",
"@types/text-encoding": "0.0.35", "@types/text-encoding": "0.0.35",
"base64-arraybuffer": "^0.2.0", "base64-arraybuffer": "^0.2.0",
@ -78,24 +77,25 @@
"stream-http": "3.2.0", "stream-http": "3.2.0",
"text-encoding": "^0.7.0", "text-encoding": "^0.7.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.13.0" "vm": "^0.1.0",
"zone.js": "~0.14.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^16.1.0", "@angular-devkit/build-angular": "^17.3.3",
"@angular-eslint/builder": "16.0.3", "@angular-eslint/builder": "17.3.0",
"@angular-eslint/eslint-plugin": "16.0.3", "@angular-eslint/eslint-plugin": "17.3.0",
"@angular-eslint/eslint-plugin-template": "16.0.3", "@angular-eslint/eslint-plugin-template": "17.3.0",
"@angular-eslint/schematics": "16.0.3", "@angular-eslint/schematics": "17.3.0",
"@angular-eslint/template-parser": "16.0.3", "@angular-eslint/template-parser": "17.3.0",
"@angular/cli": "^16.1.0", "@angular/cli": "^17.3.3",
"@angular/compiler-cli": "^16.1.2", "@angular/compiler-cli": "^17.3.3",
"@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",
"@types/core-js": "^2.5.5", "@types/core-js": "^2.5.5",
"@types/crypto-js": "^4.2.1", "@types/crypto-js": "^4.2.1",
"@types/es6-shim": "^0.31.39", "@types/es6-shim": "^0.31.39",
"@types/jasmine": "~3.6.0", "@types/jasmine": "~5.1.4",
"@types/lodash-es": "^4.17.3", "@types/lodash-es": "^4.17.3",
"@types/marked": "^4.3.0", "@types/marked": "^4.3.0",
"@types/node": "12.20.50", "@types/node": "12.20.50",
@ -109,12 +109,12 @@
"es6-shim": "^0.35.5", "es6-shim": "^0.35.5",
"eslint": "^8.33.0", "eslint": "^8.33.0",
"git-describe": "^4.0.4", "git-describe": "^4.0.4",
"jasmine-core": "~3.6.0", "jasmine-core": "~5.1.2",
"karma": "~6.3.0", "karma": "~6.4.3",
"karma-chrome-launcher": "~3.1.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.1.0", "karma-coverage": "~2.2.1",
"karma-jasmine": "~4.0.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~1.7.0", "karma-jasmine-html-reporter": "~2.1.0",
"license-checker": "25.0.1", "license-checker": "25.0.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mochawesome": "^7.1.3", "mochawesome": "^7.1.3",
@ -123,9 +123,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",
"typedoc": "^0.24.8", "typescript": "~5.4.4",
"typedoc-plugin-external-module-name": "^4.0.6",
"typescript": "~4.9.4",
"wait-on": "^6.0.1", "wait-on": "^6.0.1",
"watch": "^1.0.2" "watch": "^1.0.2"
} }

View File

@ -12,7 +12,7 @@
<div class="alert-items"> <div class="alert-items">
<div class="alert-item static"> <div class="alert-item static">
<div class="alert-icon-wrapper"> <div class="alert-icon-wrapper">
<clr-icon class="mt-2" shape="warning-standard"></clr-icon> <cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
</div> </div>
<div class="alert-text"> <div class="alert-text">
Data Controller (FREE Tier) - to upgrade contact Data Controller (FREE Tier) - to upgrade contact
@ -30,7 +30,7 @@
<div class="alert-items"> <div class="alert-items">
<div class="alert-item static"> <div class="alert-item static">
<div class="alert-icon-wrapper"> <div class="alert-icon-wrapper">
<clr-icon class="mt-2" shape="warning-standard"></clr-icon> <cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
</div> </div>
<div class="alert-text"> <div class="alert-text">
Data Controller (FREE Tier) - Problem with licence Data Controller (FREE Tier) - Problem with licence
@ -55,7 +55,7 @@
<div class="alert-items"> <div class="alert-items">
<div class="alert-item static"> <div class="alert-item static">
<div class="alert-icon-wrapper"> <div class="alert-icon-wrapper">
<clr-icon class="mt-2" shape="warning-standard"></clr-icon> <cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
</div> </div>
<div class="alert-text"> <div class="alert-text">
@ -85,7 +85,7 @@
<div class="alert-items"> <div class="alert-items">
<div class="alert-item static"> <div class="alert-item static">
<div class="alert-icon-wrapper"> <div class="alert-icon-wrapper">
<clr-icon class="mt-2" shape="warning-standard"></clr-icon> <cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
</div> </div>
<div class="alert-text"> <div class="alert-text">
@ -204,14 +204,7 @@
</div> </div>
</ng-container> </ng-container>
<div class="header-actions"> <app-header-actions></app-header-actions>
<div class="nav-text">
<app-loading-indicator></app-loading-indicator>
</div>
<div class="dropdown">
<app-user-nav-dropdown></app-user-nav-dropdown>
</div>
</div>
</header> </header>
<nav <nav
*ngIf=" *ngIf="

View File

@ -91,33 +91,12 @@ header {
} }
} }
.nav
.nav-link {
color: #fafafa;
opacity: .9;
line-height: 1.45rem;
}
.nav .nav-link:hover {
box-shadow: inset 0 -3px 0 transparent;
transition: box-shadow .2s ease-in;
}
.nav
.nav-link:hover { .nav-link:hover {
color: #fafafa; color: #fafafa;
opacity: 1;
} }
.nav .nav-link.active { .nav-link.active {
background: #61717D; background: #61717D;
opacity: 1;
box-shadow: inset 0 -3px transparent;
// padding: 0 1rem 0 1rem;
}
.nav .nav-item {
margin-right: 1rem;
} }
} }

View File

@ -46,7 +46,6 @@ import { NgxJsonViewerModule } from 'ngx-json-viewer'
SharedModule, SharedModule,
ClarityModule, ClarityModule,
AppSharedModule, AppSharedModule,
HomeModule,
PipesModule, PipesModule,
DirectivesModule, DirectivesModule,
NgxJsonViewerModule NgxJsonViewerModule

View File

@ -45,7 +45,10 @@ export const ROUTES: Routes = [
path: 'licensing', path: 'licensing',
loadChildren: () => LicensingModule loadChildren: () => LicensingModule
}, },
{ path: 'home', loadChildren: () => HomeModule }, {
path: 'home',
loadChildren: () => HomeModule
},
{ {
/** /**
* Load editor module with subroutes * Load editor module with subroutes

View File

@ -112,7 +112,7 @@
<div <div
*ngIf=" *ngIf="
['autocomplete'].includes( ['autocomplete', 'autocomplete.custom'].includes(
$any(currentRecordValidator?.getRule(col.key)?.editor) $any(currentRecordValidator?.getRule(col.key)?.editor)
) )
" "
@ -163,7 +163,7 @@
<div <div
*ngIf=" *ngIf="
['autocomplete'].includes( ['autocomplete', 'autocomplete.custom'].includes(
$any(currentRecordValidator?.getRule(col.key)?.editor) $any(currentRecordValidator?.getRule(col.key)?.editor)
) )
" "

View File

@ -203,11 +203,13 @@
<span clrTooltipTrigger> <span clrTooltipTrigger>
{{ libdsParsed.libName }}.<a {{ libdsParsed.libName }}.<a
class="mr-10" class="mr-10 view-table"
[routerLink]="'/view/data/' + libds!" [routerLink]="'/view/data/' + libds!"
>{{ libdsParsed.tableName.replace('-FC', '') }}</a >{{ libdsParsed.tableName.replace('-FC', '') }}</a
> >
</span> </span>
<ng-container *ngIf="this.dsNote && this.dsNote.length > 0">
<clr-tooltip-content <clr-tooltip-content
clrPosition="bottom-left" clrPosition="bottom-left"
clrSize="lg" clrSize="lg"
@ -215,6 +217,7 @@
> >
{{ this.dsNote }} {{ this.dsNote }}
</clr-tooltip-content> </clr-tooltip-content>
</ng-container>
</clr-tooltip> </clr-tooltip>
<ng-container *ngIf="dataSource"> <ng-container *ngIf="dataSource">
@ -843,6 +846,12 @@
</div> </div>
</clr-modal> </clr-modal>
<app-dataset-info [(open)]="datasetInfo" [dsmeta]="dsmeta"></app-dataset-info> <app-dataset-info
[(open)]="datasetInfo"
[dsmeta]="dsmeta"
[versions]="versions"
(rowClicked)="datasetInfoModalRowClicked($event)"
>
</app-dataset-info>
<app-viewboxes [(viewboxModal)]="viewboxes"></app-viewboxes> <app-viewboxes [(viewboxModal)]="viewboxes"></app-viewboxes>

View File

@ -214,6 +214,10 @@ hot-table {
width: 150px; width: 150px;
} }
.view-table {
font-size: inherit !important;
}
// FIXME // FIXME
// Let's leave it here for a reference if there // Let's leave it here for a reference if there
// is an issue with viewboxes/filter modal overlaying // is an issue with viewboxes/filter modal overlaying

View File

@ -38,7 +38,8 @@ import { HotTableInterface } from '../models/HotTable.interface'
import { import {
$DataFormats, $DataFormats,
DSMeta, DSMeta,
EditorsGetDataServiceResponse EditorsGetDataServiceResponse,
Version
} from '../models/sas/editors-getdata.model' } from '../models/sas/editors-getdata.model'
import { DataFormat } from '../models/sas/common/DateFormat' import { DataFormat } from '../models/sas/common/DateFormat'
import SheetInfo from '../models/SheetInfo' import SheetInfo from '../models/SheetInfo'
@ -121,6 +122,7 @@ export class EditorComponent implements OnInit, AfterViewInit {
datasetInfo: boolean = false datasetInfo: boolean = false
dsmeta: DSMeta[] = [] dsmeta: DSMeta[] = []
versions: Version[] = []
dsNote = '' dsNote = ''
viewboxes: boolean = false viewboxes: boolean = false
@ -940,13 +942,30 @@ export class EditorComponent implements OnInit, AfterViewInit {
return row.map((col: any, index: number) => { return row.map((col: any, index: number) => {
if (!col && col !== 0) col = '' if (!col && col !== 0) col = ''
if (isNaN(col)) { /**
col = col.replace(/"/g, '""') * Keeping this for the reference
* Code below used to convert JSON to CSV
* now the XLSX is converting to CSV
*/
// if (isNaN(col)) {
// // Match and replace the double quotes, ignore the first and last char
// // in case they are double quotes already
// col = col.replace(/(?<!^)"(?!$)/g, '""')
if (col.search(/,/g) > -1) { // if (col.search(/,/g) > -1 ||
col = '"' + col + '"' // col.search(/\r|\n/g) > -1
} // ) {
} // // Missing quotes at the end
// if (col.search(/"$/g) < 0) {
// col = col + '"' // So we add them
// }
// // Missing quotes at the start
// if (col.search(/^"/g) < 0) {
// col = '"' + col // So we add them
// }
// }
// }
const colName = this.headerShow[index] const colName = this.headerShow[index]
const colRule = this.dcValidator?.getRule(colName) const colRule = this.dcValidator?.getRule(colName)
@ -961,20 +980,30 @@ export class EditorComponent implements OnInit, AfterViewInit {
this.data = csvArrayData this.data = csvArrayData
let csvContent = csvArrayHeaders.join(',') + '\n' // Apply licence rows limitation if exists, it is only affecting data
// Apply licence rows limitation if exists // which will be send to SAS
csvContent += csvArrayData const strippedCsvArrayData = csvArrayData.slice(
.slice(0, this.licenceState.value.submit_rows_limit) 0,
.map((e) => e.join(',')) this.licenceState.value.submit_rows_limit
.join('\n') )
// To submit to sas service, we need clean version of CSV of file
// attached. XLSX will do the parsing and heavy lifting
// First we create worksheet of json (data we extracted)
let ws = XLSX.utils.json_to_sheet(strippedCsvArrayData, {
skipHeader: true
})
// create CSV to be uploaded from worksheet
let csvContentClean = XLSX.utils.sheet_to_csv(ws)
// Prepend headers
csvContentClean = csvArrayHeaders.join(',') + '\n' + csvContentClean
if (this.encoding === 'WLATIN1') { if (this.encoding === 'WLATIN1') {
let encoded = iconv.decode(Buffer.from(csvContent), 'CP-1252') let encoded = iconv.decode(Buffer.from(csvContentClean), 'CP-1252')
let blob = new Blob([encoded], { type: 'application/csv' }) let blob = new Blob([encoded], { type: 'application/csv' })
let newCSVFile: File = this.blobToFile(blob, this.filename + '.csv') let newCSVFile: File = this.blobToFile(blob, this.filename + '.csv')
this.uploader.addToQueue([newCSVFile]) this.uploader.addToQueue([newCSVFile])
} else { } else {
let blob = new Blob([csvContent], { type: 'application/csv' }) let blob = new Blob([csvContentClean], { type: 'application/csv' })
let newCSVFile: File = this.blobToFile(blob, this.filename + '.csv') let newCSVFile: File = this.blobToFile(blob, this.filename + '.csv')
this.uploader.addToQueue([newCSVFile]) this.uploader.addToQueue([newCSVFile])
} }
@ -1929,13 +1958,13 @@ export class EditorComponent implements OnInit, AfterViewInit {
if (entry.values.length > 0) { if (entry.values.length > 0) {
hot.setCellMeta(entry.row, entry.col, 'renderer', 'autocomplete') hot.setCellMeta(entry.row, entry.col, 'renderer', 'autocomplete')
hot.setCellMeta(entry.row, entry.col, 'editor', 'autocomplete') hot.setCellMeta(entry.row, entry.col, 'editor', 'autocomplete.custom')
hot.setCellMeta(entry.row, entry.col, 'strict', entry.strict) hot.setCellMeta(entry.row, entry.col, 'strict', entry.strict)
hot.setCellMeta(entry.row, entry.col, 'filter', false) hot.setCellMeta(entry.row, entry.col, 'filter', false)
this.currentEditRecordValidator?.updateRule(entry.col, { this.currentEditRecordValidator?.updateRule(entry.col, {
renderer: 'autocomplete', renderer: 'autocomplete',
editor: 'autocomplete', editor: 'autocomplete.custom',
strict: entry.strict, strict: entry.strict,
filter: false filter: false
}) })
@ -2030,13 +2059,13 @@ export class EditorComponent implements OnInit, AfterViewInit {
} }
hot.setCellMeta(row, cellCol, 'renderer', 'autocomplete') hot.setCellMeta(row, cellCol, 'renderer', 'autocomplete')
hot.setCellMeta(row, cellCol, 'editor', 'autocomplete') hot.setCellMeta(row, cellCol, 'editor', 'autocomplete.custom')
hot.setCellMeta(row, cellCol, 'strict', cellValidationEntry.strict) hot.setCellMeta(row, cellCol, 'strict', cellValidationEntry.strict)
hot.setCellMeta(row, cellCol, 'filter', false) hot.setCellMeta(row, cellCol, 'filter', false)
this.currentEditRecordValidator?.updateRule(cellCol, { this.currentEditRecordValidator?.updateRule(cellCol, {
renderer: 'autocomplete', renderer: 'autocomplete',
editor: 'autocomplete', editor: 'autocomplete.custom',
strict: cellValidationEntry.strict, strict: cellValidationEntry.strict,
filter: false filter: false
}) })
@ -2234,8 +2263,8 @@ export class EditorComponent implements OnInit, AfterViewInit {
setTimeout(() => { setTimeout(() => {
let txt: any = document.getElementById('formFields_8') let txt: any = document.getElementById('formFields_8')
txt.focus() if (txt) txt.focus()
}) }, 200)
}) })
// let cnt = 0; // let cnt = 0;
@ -2679,13 +2708,13 @@ export class EditorComponent implements OnInit, AfterViewInit {
const strict = this.cellValidationSource[validationSourceIndex].strict const strict = this.cellValidationSource[validationSourceIndex].strict
hot.setCellMeta(row, column, 'renderer', 'autocomplete') hot.setCellMeta(row, column, 'renderer', 'autocomplete')
hot.setCellMeta(row, column, 'editor', 'autocomplete') hot.setCellMeta(row, column, 'editor', 'autocomplete.custom')
hot.setCellMeta(row, column, 'strict', strict) hot.setCellMeta(row, column, 'strict', strict)
hot.setCellMeta(row, column, 'filter', false) hot.setCellMeta(row, column, 'filter', false)
this.currentEditRecordValidator?.updateRule(column, { this.currentEditRecordValidator?.updateRule(column, {
renderer: 'autocomplete', renderer: 'autocomplete',
editor: 'autocomplete', editor: 'autocomplete.custom',
strict: strict, strict: strict,
filter: false filter: false
}) })
@ -2907,6 +2936,16 @@ export class EditorComponent implements OnInit, AfterViewInit {
} }
} }
datasetInfoModalRowClicked(value: Version | DSMeta) {
if ((<Version>value).LOAD_REF !== undefined) {
// Type is Version
const row = value as Version
const url = `/stage/${row.LOAD_REF}`
this.router.navigate([url])
}
}
viewboxManager() { viewboxManager() {
this.viewboxes = true this.viewboxes = true
} }
@ -2919,6 +2958,37 @@ export class EditorComponent implements OnInit, AfterViewInit {
) )
} }
/**
* Function checks if selected hot cell is solo cell selected
* and if it is, set the `filter` property based on filter param.
*
* @param filter
*/
private setCellFilter(filter: boolean) {
const hotSelected = this.hotInstance.getSelected()
const selection = hotSelected ? hotSelected[0] : hotSelected
// When we open a dropdown we want filter disabled so value in cell
// don't filter out items, since we want to see them all.
// But when we start typing we want to be able to start filtering values
// again
if (selection) {
const startRow = selection[0]
const endRow = selection[2]
const startCell = selection[1]
const endCell = selection[3]
if (startRow === endRow && startCell === endCell) {
const cellMeta = this.hotInstance.getCellMeta(startRow, startCell)
// If filter is not already set at the value in the param, set it
if (cellMeta && cellMeta.filter === !filter) {
this.hotInstance.setCellMeta(startRow, startCell, 'filter', filter)
}
}
}
}
async ngOnInit() { async ngOnInit() {
this.licenceService.hot_license_key.subscribe( this.licenceService.hot_license_key.subscribe(
(hot_license_key: string | undefined) => { (hot_license_key: string | undefined) => {
@ -2986,6 +3056,7 @@ export class EditorComponent implements OnInit, AfterViewInit {
this.cols = response.data.cols this.cols = response.data.cols
this.dsmeta = response.data.dsmeta this.dsmeta = response.data.dsmeta
this.versions = response.data.versions || []
const notes = this.dsmeta.find((item) => item.NAME === 'NOTES') const notes = this.dsmeta.find((item) => item.NAME === 'NOTES')
const longDesc = this.dsmeta.find((item) => item.NAME === 'DD_LONGDESC') const longDesc = this.dsmeta.find((item) => item.NAME === 'DD_LONGDESC')
@ -3274,28 +3345,16 @@ export class EditorComponent implements OnInit, AfterViewInit {
} }
) )
hot.addHook('beforeKeyDown', (e: any) => { hot.addHook('afterBeginEditing', () => {
const hotSelected = this.hotInstance.getSelected()
const selection = hotSelected ? hotSelected[0] : hotSelected
// When we open a dropdown we want filter disabled so value in cell // When we open a dropdown we want filter disabled so value in cell
// don't filter out items, since we want to see them all. // don't filter out items, since we want to see them all.
this.setCellFilter(false)
})
hot.addHook('beforeKeyDown', () => {
// When we start typing, we are enabling the filter since we want to find // When we start typing, we are enabling the filter since we want to find
// values faster. // values faster.
if (selection) { this.setCellFilter(true)
const startRow = selection[0]
const endRow = selection[2]
const startCell = selection[1]
const endCell = selection[3]
if (startRow === endRow && startCell === endCell) {
const cellMeta = this.hotInstance.getCellMeta(startRow, startCell)
if (cellMeta && cellMeta.filter === false) {
this.hotInstance.setCellMeta(startRow, startCell, 'filter', true)
}
}
}
}) })
hot.addHook('afterChange', (source: any, change: any) => { hot.addHook('afterChange', (source: any, change: any) => {

View File

@ -12,7 +12,6 @@ import { EditRecordComponent } from './components/edit-record/edit-record.compon
import { UploadStaterComponent } from './components/upload-stater/upload-stater.component' import { UploadStaterComponent } from './components/upload-stater/upload-stater.component'
import { EditorRoutingModule } from './editor-routing.module' import { EditorRoutingModule } from './editor-routing.module'
import { EditorComponent } from './editor.component' import { EditorComponent } from './editor.component'
import { HomeModule } from '../home/home.module'
import { DcTreeModule } from '../shared/dc-tree/dc-tree.module' import { DcTreeModule } from '../shared/dc-tree/dc-tree.module'
import { DragDropModule } from '@angular/cdk/drag-drop' import { DragDropModule } from '@angular/cdk/drag-drop'
import { ViewboxesModule } from '../shared/viewboxes/viewboxes.module' import { ViewboxesModule } from '../shared/viewboxes/viewboxes.module'
@ -33,7 +32,6 @@ registerAllModules()
AppSharedModule, AppSharedModule,
DirectivesModule, DirectivesModule,
SharedModule, SharedModule,
HomeModule,
PipesModule, PipesModule,
DcTreeModule, DcTreeModule,
DragDropModule, DragDropModule,

View File

@ -94,15 +94,17 @@
{{ libTable.replace('-FC', '') }} {{ libTable.replace('-FC', '') }}
</button> </button>
<ng-container *ngIf="tableLocked">
<clr-tooltip-content <clr-tooltip-content
clrPosition="bottom-right" clrPosition="bottom-right"
clrSize="lg" clrSize="lg"
*clrIfOpen *clrIfOpen
> >
<span *ngIf="tableLocked"> <span>
To unlock all tables, contact support&#64;datacontroller.io To unlock all tables, contact support&#64;datacontroller.io
</span> </span>
</clr-tooltip-content> </clr-tooltip-content>
</ng-container>
</clr-tooltip> </clr-tooltip>
</clr-tree-node> </clr-tree-node>
</clr-tree-node> </clr-tree-node>

View File

@ -2,9 +2,11 @@
<div class="card-header">Licencing</div> <div class="card-header">Licencing</div>
<div [ngSwitch]="action" class="card-block"> <div [ngSwitch]="action" class="card-block">
<div class="card-text">
<ng-container *ngSwitchCase="'key'"> <ng-container *ngSwitchCase="'key'">
<p class="key-error" *ngIf="!keyError"> <p class="key-error" *ngIf="!keyError">
Licence key is invalid. We can't provide you more details at the moment Licence key is invalid. We can't provide you more details at the
moment
</p> </p>
<p <p
@ -126,7 +128,9 @@
</clr-tab-content> </clr-tab-content>
</clr-tab> </clr-tab>
</clr-tabs> </clr-tabs>
</div>
<div class="card-footer d-flex clr-align-items-center">
<button <button
(click)="applyKeys()" (click)="applyKeys()"
class="btn btn-primary apply-keys" class="btn btn-primary apply-keys"
@ -144,6 +148,7 @@
Continue with free tier Continue with free tier
</button> </button>
</div> </div>
</div>
</div> </div>
<app-terms *ngIf="action === 'register'"></app-terms> <app-terms *ngIf="action === 'register'"></app-terms>

View File

@ -33,7 +33,6 @@
.apply-keys { .apply-keys {
height: 40px; height: 40px;
width: 200px;
} }
.drop-area { .drop-area {

View File

@ -17,6 +17,7 @@ export interface EditorsGetDataSASResponse extends BaseSASResponse {
dqrules: DQRule[] dqrules: DQRule[]
dsmeta: DSMeta[] dsmeta: DSMeta[]
dqdata: DQData[] dqdata: DQData[]
versions: Version[]
cols: Col[] cols: Col[]
maxvarlengths: Maxvarlength[] maxvarlengths: Maxvarlength[]
xl_rules: any[] xl_rules: any[]
@ -27,6 +28,18 @@ export interface DSMeta {
ODS_TABLE: string ODS_TABLE: string
NAME: string NAME: string
VALUE: string VALUE: string
[key: string]: string
}
export interface Version {
LOAD_REF: string
USER_NM: string
VERSION_DTTM: string
VERSION_DESC: string
CHANGED_RECORDS: number
NEW_RECORDS: number
DELETED_RECORDS: number
[key: string]: string | number
} }
export interface Sasdata { export interface Sasdata {

View File

@ -0,0 +1,9 @@
import { BaseSASResponse } from './common/BaseSASResponse'
export interface EditorsRestoreServiceResponse extends BaseSASResponse {
restore_out: RestoreOut[]
}
export interface RestoreOut {
LOADREF: string
}

View File

@ -143,7 +143,6 @@
(ngModelChange)=" (ngModelChange)="
setVariableOperator(queryIndex, query.operator, clauseIndex) setVariableOperator(queryIndex, query.operator, clauseIndex)
" "
class="mt-2"
clrSelect clrSelect
> >
<option *ngFor="let opr of query.operators">{{ opr }}</option> <option *ngFor="let opr of query.operators">{{ opr }}</option>

View File

@ -878,17 +878,25 @@ export class QueryComponent
*/ */
public hasInvalidCluase(clauses: any): boolean { public hasInvalidCluase(clauses: any): boolean {
for (let clause of clauses) { for (let clause of clauses) {
clause['invalidClause'] = false
if ( if (
clause.variable === null || clause.value === '' &&
clause.operator === null || !(clause.operator === 'NE' || clause.operator === 'CONTAINS')
clause.value === null || ) {
clause.value === '' clause['invalidClause'] = true
return true
}
if (
clause.variable === null ||
clause.operator === null ||
clause.value === null
) { ) {
clause['invalidClause'] = true clause['invalidClause'] = true
return true return true
} else {
clause['invalidClause'] = false
} }
} }

View File

@ -139,10 +139,7 @@
<div class="card-header p-0"> <div class="card-header p-0">
<div class="clr-row"> <div class="clr-row">
<div class="clr-col-md-4 approvalBack"> <div class="clr-col-md-4 approvalBack">
<span <span class="btn btn-outline m-0" (click)="goToApprovalsList()">
class="btn btn-sm btn-outline m-0"
(click)="goToApprovalsList()"
>
<clr-icon shape="caret" dir="left" size="20"></clr-icon>Back to <clr-icon shape="caret" dir="left" size="20"></clr-icon>Back to
approvals list approvals list
</span> </span>
@ -209,22 +206,22 @@
<div class="d-flex justify-content-center mt-0"> <div class="d-flex justify-content-center mt-0">
<div class="clr-row clr-gap-5 clr-gap-sm-0"> <div class="clr-row clr-gap-5 clr-gap-sm-0">
<button <button
class="btn btn-sm btn-outline text-center mt-5" class="btn btn-sm btn-outline text-center mt-5 mr-5i"
(click)="goToBase(jsParams?.TABLE_NM)" (click)="goToBase(jsParams?.TABLE_NM)"
> >
Go to base table screen View base table
</button> </button>
<button <button
class="btn btn-sm btn-success-outline text-center mt-5" class="btn btn-sm btn-success-outline text-center mt-5 mr-5i"
(click)="getTable(tableId)" (click)="getTable(tableId)"
> >
Go to edited screen View staged data
</button> </button>
<button <button
class="btn btn-sm btn-info-outline text-center mt-5" class="btn btn-sm btn-info-outline text-center mt-5"
(click)="goBack(jsParams?.TABLE_NM)" (click)="goBack(jsParams?.TABLE_NM)"
> >
Go back to editor Edit base table
</button> </button>
</div> </div>
</div> </div>
@ -236,7 +233,7 @@
id="acceptBtn" id="acceptBtn"
[clrLoading]="acceptLoading" [clrLoading]="acceptLoading"
type="submit" type="submit"
class="btn btn-sm btn-success" class="btn btn-sm btn-success mr-5i"
(click)="approveTable()" (click)="approveTable()"
[disabled]=" [disabled]="
!loadingTable || params?.ISAPPROVER === 'NO' || noChanges !loadingTable || params?.ISAPPROVER === 'NO' || noChanges
@ -246,7 +243,7 @@
</button> </button>
<button <button
id="rejectBtn" id="rejectBtn"
class="btn btn-sm btn btn-danger mr-0" class="btn btn-sm btn btn-danger mr-5i"
(click)="rejectOpen = true" (click)="rejectOpen = true"
[disabled]=" [disabled]="
!loadingTable || params?.ISAPPROVER === 'NO' || noChanges !loadingTable || params?.ISAPPROVER === 'NO' || noChanges
@ -394,9 +391,9 @@
<div class="card-header"> <div class="card-header">
<div class="clr-row"> <div class="clr-row">
<div class="clr-col-md-4 approvalBack"> <div class="clr-col-md-4 approvalBack">
<span class="btn btn-sm btn-outline" (click)="goToSubmitList()"> <span class="btn btn-outline" (click)="goToSubmitList()">
<clr-icon shape="caret" dir="left" size="20"></clr-icon>Back to <cds-icon shape="angle" direction="left" size="20"></cds-icon
submitted list >Back to submitted list
</span> </span>
</div> </div>
<div class="clr-col-md-4"> <div class="clr-col-md-4">
@ -443,22 +440,22 @@
<div class="d-flex justify-content-center mt-0"> <div class="d-flex justify-content-center mt-0">
<div class="clr-row clr-gap-5 clr-gap-sm-0"> <div class="clr-row clr-gap-5 clr-gap-sm-0">
<button <button
class="btn btn-sm btn-outline text-center mt-5" class="btn btn-sm btn-outline text-center mt-5 mr-5i"
(click)="goToBase(subObj.base)" (click)="goToBase(subObj.base)"
> >
Go to base table screen View base table
</button> </button>
<button <button
class="btn btn-sm btn-success-outline text-center mt-5" class="btn btn-sm btn-success-outline text-center mt-5 mr-5i"
(click)="getTable(subObj.tableId)" (click)="getTable(subObj.tableId)"
> >
Go to edited screen View staged data
</button> </button>
<button <button
class="btn btn-sm btn-info-outline text-center mt-5" class="btn btn-sm btn-info-outline text-center mt-5"
(click)="goBack(subObj.base)" (click)="goBack(subObj.base)"
> >
Go back to editor Edit base table
</button> </button>
</div> </div>
</div> </div>

View File

@ -6,25 +6,31 @@
> >
<h3 class="modal-title center text-center color-darker-gray">Dataset Meta</h3> <h3 class="modal-title center text-center color-darker-gray">Dataset Meta</h3>
<div class="modal-body"> <div class="modal-body">
<p *ngIf="dsmetaGroupped.length < 1" class="text-center"> <p *ngIf="dsmetaTabs.length < 1" class="text-center">
No dataset meta to show. No dataset meta to show.
</p> </p>
<clr-tabs clrLayout="vertical"> <clr-tabs clrLayout="vertical">
<clr-tab *ngFor="let dsmeta of dsmetaGroupped; let index = index"> <clr-tab *ngFor="let tab of tabs; let index = index">
<button clrTabLink id="link1">{{ dsmeta.group }}</button> <button clrTabLink id="link1">{{ tab.name }}</button>
<clr-tab-content <clr-tab-content
id="content1" id="content1"
*clrIfActive="index === 0" *clrIfActive="index === 0"
class="d-flex clr-justify-content-center w-100" class="d-flex clr-justify-content-center w-100"
> >
<clr-datagrid> <clr-datagrid>
<clr-dg-column>Name</clr-dg-column> <ng-container *ngFor="let col of tab.colsToDisplay">
<clr-dg-column>Value</clr-dg-column> <clr-dg-column>{{ col.colName || col.colKey }}</clr-dg-column>
</ng-container>
<clr-dg-row *ngFor="let info of dsmeta.dsmeta"> <clr-dg-row
<clr-dg-cell>{{ info.NAME }}</clr-dg-cell> (click)="tab.onRowClick ? tab.onRowClick(info) : ''"
<clr-dg-cell>{{ info.VALUE }}</clr-dg-cell> class="clickable-row"
*ngFor="let info of tab.meta"
>
<ng-container *ngFor="let col of tab.colsToDisplay">
<clr-dg-cell>{{ info[col.colKey] }}</clr-dg-cell>
</ng-container>
</clr-dg-row> </clr-dg-row>
</clr-datagrid> </clr-datagrid>
</clr-tab-content> </clr-tab-content>

View File

@ -14,3 +14,22 @@
} }
} }
} }
clr-modal {
::ng-deep {
.modal-dialog {
height: 100%;
}
}
}
.clickable-row {
cursor: pointer;
}
::ng-deep {
.datagrid-table .datagrid-cell:focus {
outline: none;
outline-offset: 0;
}
}

View File

@ -7,8 +7,8 @@ import {
Output, Output,
SimpleChanges SimpleChanges
} from '@angular/core' } from '@angular/core'
import { DSMeta } from 'src/app/models/sas/editors-getdata.model' import { DSMeta, Version } from 'src/app/models/sas/editors-getdata.model'
import { DSMetaGroupped } from './models/dsmeta-groupped.model' import { Tab } from './models/dsmeta-groupped.model'
@Component({ @Component({
selector: 'app-dataset-info', selector: 'app-dataset-info',
@ -18,10 +18,15 @@ import { DSMetaGroupped } from './models/dsmeta-groupped.model'
export class DatasetInfoComponent implements OnInit, OnChanges { export class DatasetInfoComponent implements OnInit, OnChanges {
@Input() open: boolean = false @Input() open: boolean = false
@Input() dsmeta: DSMeta[] = [] @Input() dsmeta: DSMeta[] = []
@Input() versions: Version[] = []
@Output() openChange = new EventEmitter<boolean>() @Output() openChange = new EventEmitter<boolean>()
@Output() rowClicked = new EventEmitter<Version | DSMeta>()
dsmetaGroupped: DSMetaGroupped[] = [] dsmetaTabs: Tab<DSMeta>[] = []
versionsTabs: Tab<Version>[] = []
tabs: Tab<DSMeta | Version>[] = []
constructor() {} constructor() {}
@ -30,28 +35,58 @@ export class DatasetInfoComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes.dsmeta?.currentValue?.length > 0) { if (changes.dsmeta?.currentValue?.length > 0) {
this.parseDSMeta() this.parseDSMeta()
this.parseVersions()
this.tabs = [...[...this.dsmetaTabs], ...[...this.versionsTabs]]
} }
} }
parseDSMeta() { parseDSMeta() {
this.dsmetaGroupped = [] this.dsmetaTabs = []
for (let info of this.dsmeta) { for (let info of this.dsmeta) {
let groupIndex = this.dsmetaGroupped.findIndex( let groupIndex = this.dsmetaTabs.findIndex(
(x) => x.group === info.ODS_TABLE (x) => x.name === info.ODS_TABLE
) )
if (groupIndex < 0) if (groupIndex < 0)
groupIndex = groupIndex =
this.dsmetaGroupped.push({ this.dsmetaTabs.push({
group: info.ODS_TABLE, name: info.ODS_TABLE,
dsmeta: [] title: 'Dataset Meta',
colsToDisplay: [{ colKey: 'NAME' }, { colKey: 'VALUE' }],
meta: [],
onRowClick: (value: DSMeta) => {
this.rowClicked.emit(value)
}
}) - 1 }) - 1
this.dsmetaGroupped[groupIndex].dsmeta.push(info) this.dsmetaTabs[groupIndex].meta.push(info)
} }
} }
parseVersions() {
this.versionsTabs = [
{
name: 'VERSIONS',
title: 'Dataset Meta',
colsToDisplay: [
{ colKey: 'LOAD_REF' },
{ colKey: 'USER_NM' },
{ colKey: 'VERSION_DTTM' },
{ colKey: 'NEW_RECORDS', colName: 'ADD' },
{ colKey: 'CHANGED_RECORDS', colName: 'MOD' },
{ colKey: 'DELETED_RECORDS', colName: 'DEL' },
{ colKey: 'VERSION_DESC' }
],
meta: this.versions,
onRowClick: (value: Version) => {
this.rowClicked.emit(value)
}
}
]
}
onOpenChange(open: boolean) { onOpenChange(open: boolean) {
this.open = open this.open = open
this.openChange.emit(open) this.openChange.emit(open)

View File

@ -1,6 +1,14 @@
import { DSMeta } from 'src/app/models/sas/editors-getdata.model' export interface Tab<T> {
name: string
export interface DSMetaGroupped { title: string
group: string /**
dsmeta: DSMeta[] * Columns to be displayed in the the grid
* If empty, all columns will be displayed
*/
colsToDisplay: {
colKey: string
colName?: string
}[]
meta: T[]
onRowClick?: (value: any) => void
} }

View File

@ -20,6 +20,7 @@ import { parseColType } from './utils/parseColType'
import { dqValidate } from './validations/dq-validation' import { dqValidate } from './validations/dq-validation'
import { specialMissingNumericValidator } from './validations/hot-custom-validators' import { specialMissingNumericValidator } from './validations/hot-custom-validators'
import { applyNumericFormats } from './utils/applyNumericFormats' import { applyNumericFormats } from './utils/applyNumericFormats'
import { CustomAutocompleteEditor } from './editors/numericAutocomplete'
export class DcValidator { export class DcValidator {
private rules: DcValidation[] = [] private rules: DcValidation[] = []
@ -38,6 +39,8 @@ export class DcValidator {
dqData: DQData[], dqData: DQData[],
hotInstance?: Handsontable hotInstance?: Handsontable
) { ) {
this.registerCustomEditors()
this.sasparams = sasparams this.sasparams = sasparams
this.hotInstance = hotInstance this.hotInstance = hotInstance
this.rules = parseColType(sasparams.COLTYPE) this.rules = parseColType(sasparams.COLTYPE)
@ -51,6 +54,13 @@ export class DcValidator {
this.setupValidations() this.setupValidations()
} }
registerCustomEditors() {
Handsontable.editors.registerEditor(
'autocomplete.custom',
CustomAutocompleteEditor
)
}
getRules(): DcValidation[] { getRules(): DcValidation[] {
return this.rules return this.rules
} }
@ -262,6 +272,7 @@ export class DcValidator {
if (source.length > 0) { if (source.length > 0) {
this.rules[i].source = source this.rules[i].source = source
this.rules[i].type = 'autocomplete' this.rules[i].type = 'autocomplete'
this.rules[i].editor = 'autocomplete.custom'
this.rules[i].filter = false this.rules[i].filter = false
} }
@ -315,7 +326,10 @@ export class DcValidator {
// Because of dynamic cell validation, that will change the type of cell to dropdown // Because of dynamic cell validation, that will change the type of cell to dropdown
// `rules[i].colType` could be different type (eg. numeric). So we check if current cell is dropdown, to call HOT native dropdown validator // `rules[i].colType` could be different type (eg. numeric). So we check if current cell is dropdown, to call HOT native dropdown validator
if (this.editor === 'autocomplete') { if (
this.editor === 'autocomplete' ||
this.editor === 'autocomplete.custom'
) {
self self
.getHandsontableValidator('autocomplete') .getHandsontableValidator('autocomplete')
.call(this, value, (valid: boolean) => { .call(this, value, (valid: boolean) => {

View File

@ -0,0 +1,28 @@
import Handsontable from 'handsontable'
import Core from 'handsontable/core'
export class CustomAutocompleteEditor extends Handsontable.editors
.AutocompleteEditor {
constructor(instance: Core) {
super(instance)
}
createElements() {
super.createElements()
}
// Listbox open
open(event?: Event | undefined): void {
super.open(event)
if (this.isCellNumeric()) {
this.htContainer.classList.add('numericListbox')
} else {
this.htContainer.classList.remove('numericListbox')
}
}
isCellNumeric() {
return this.cellProperties?.className?.includes('htNumeric')
}
}

View File

@ -11,6 +11,8 @@ $clr-green: #60b515;
height: $clr-header-height; height: $clr-header-height;
display: flex; display: flex;
align-items: center; align-items: center;
height: 100%;
margin-right: 10px;
.spinner { .spinner {
vertical-align: middle; vertical-align: middle;

View File

@ -16,8 +16,6 @@
*ngFor="let programLog of sasjsRequests; let i = index" *ngFor="let programLog of sasjsRequests; let i = index"
[id]="'request_' + i" [id]="'request_' + i"
[clrStackViewLevel]="1" [clrStackViewLevel]="1"
[clrStackViewSetsize]="3"
[clrStackViewPosinset]="3"
> >
<clr-stack-label> <clr-stack-label>
{{ programLog.serviceLink }} {{ programLog.serviceLink }}

View File

@ -8,7 +8,7 @@ import { LoadingIndicatorComponent } from './loading-indicator/loading-indicator
import { LoginComponent } from './login/login.component' import { LoginComponent } from './login/login.component'
import { UserService } from './user.service' import { UserService } from './user.service'
import { AlertsService } from './alerts/alerts.service' import { AlertsService } from './alerts/alerts.service'
import { UserNavDropdownComponent } from './user-nav-dropdown/user-nav-dropdown.component' import { HeaderActions } from './user-nav-dropdown/header-actions.component'
import { AlertsComponent } from './alerts/alerts.component' import { AlertsComponent } from './alerts/alerts.component'
import { TermsComponent } from './terms/terms.component' import { TermsComponent } from './terms/terms.component'
import { DirectivesModule } from '../directives/directives.module' import { DirectivesModule } from '../directives/directives.module'
@ -26,7 +26,7 @@ import { ContactLinkComponent } from './contact-link/contact-link.component'
declarations: [ declarations: [
LoadingIndicatorComponent, LoadingIndicatorComponent,
LoginComponent, LoginComponent,
UserNavDropdownComponent, HeaderActions,
AlertsComponent, AlertsComponent,
TermsComponent, TermsComponent,
DatasetInfoComponent, DatasetInfoComponent,
@ -35,7 +35,7 @@ import { ContactLinkComponent } from './contact-link/contact-link.component'
exports: [ exports: [
LoadingIndicatorComponent, LoadingIndicatorComponent,
LoginComponent, LoginComponent,
UserNavDropdownComponent, HeaderActions,
AlertsComponent, AlertsComponent,
TermsComponent, TermsComponent,
DatasetInfoComponent, DatasetInfoComponent,

View File

@ -0,0 +1,69 @@
<div class="header-actions">
<app-loading-indicator></app-loading-indicator>
<clr-dropdown class="app-nav-dropdown">
<button class="nav-text color-white" clrDropdownToggle>
<span>{{ userName }}</span>
<span *ngIf="userName !== 'Not logged in' && isViya"
><img class="avatar-img" src="{{ getPictureUrl() }}" alt=""
/></span>
<span
class="badge badge-danger"
*ngIf="!sasjsConfig.debug"
[class.hidden]="failedReqs.length === 0"
>{{ failedReqs.length }}</span
>
<span
class="badge badge-info"
*ngIf="sasjsConfig.debug"
[class.hidden]="debugLogs.length === 0"
>{{ debugLogs.length }}</span
>
<clr-icon *ngIf="!isViya" shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
<div #dropdownItemDebug class="debug-switch-item" clrDropdownItem>
<clr-toggle-container
class="toggle-switch"
(click)="onDebugRowClick($event, dropdownItemDebug)"
>
<clr-toggle-wrapper>
<input
id="debug-toggle1"
type="checkbox"
[(ngModel)]="sasjsConfig.debug"
(ngModelChange)="onDebugModeChange()"
clrToggle
/>
<label>Debug Mode</label>
</clr-toggle-wrapper>
</clr-toggle-container>
</div>
<a (click)="openRequestsModal()" clrDropdownItem>
<span>SAS Requests</span>
</a>
<ng-container *ngIf="!isDeployPage">
<a
target="_blank"
href="https://docs.datacontroller.io"
clrDropdownItem
>
<span class="dropdown-text">Documentation</span>
</a>
</ng-container>
<div class="separator"></div>
<a href="..." routerLink="/system" clrDropdownItem>
<span>System</span>
</a>
<a href="..." (click)="logout($event)" clrDropdownItem>
<span>Log Out</span>
<clr-icon class="clr-logout" shape="logout"></clr-icon>
</a>
<div class="copyRight">
<span>v{{ commitVer }}</span>
</div>
</clr-dropdown-menu>
</clr-dropdown>
</div>

View File

@ -1,17 +1,12 @@
// it must be a better way to read clarity variables...
//@import '../../../../node_modules/@clr/ui/src/utils/helpers.clarity';
//@import '../../../../node_modules/@clr/ui/src/color/utils/colors.clarity';
//@import '../../../../node_modules/@clr/ui/src/color/utils/contrast-cache.clarity';
//@import '../../../../node_modules/@clr/ui/src/color/utils/helpers.clarity';
//@import '../../../../node_modules/@clr/ui/src/utils/variables.clarity';
$clr-header-height: 3rem; $clr-header-height: 3rem;
$clr-near-white: #fafafa; $clr-near-white: #fafafa;
$clr-dark-gray: #565656; $clr-dark-gray: #565656;
$clr-light-gray: #eee; $clr-light-gray: #eee;
:host {
display: contents;
}
.copyRight { .copyRight {
margin-top: 10px; margin-top: 10px;

View File

@ -8,11 +8,11 @@ import { EventService } from '../../services/event.service'
import { Router } from '@angular/router' import { Router } from '@angular/router'
@Component({ @Component({
selector: 'app-user-nav-dropdown', selector: 'app-header-actions',
templateUrl: './user-nav-dropdown.component.html', templateUrl: './header-actions.component.html',
styleUrls: ['./user-nav-dropdown.component.scss'] styleUrls: ['./header-actions.component.scss']
}) })
export class UserNavDropdownComponent implements OnInit, OnDestroy { export class HeaderActions implements OnInit, OnDestroy {
public userName: string = 'Not logged in' public userName: string = 'Not logged in'
private reqSub: Subscription = new Subscription() private reqSub: Subscription = new Subscription()
private userSub: Subscription = new Subscription() private userSub: Subscription = new Subscription()

View File

@ -1,121 +0,0 @@
<clr-dropdown class="app-nav-dropdown d-md-block">
<button class="nav-text color-white" clrDropdownToggle>
<span>{{ userName }}</span>
<span *ngIf="userName !== 'Not logged in' && isViya"
><img class="avatar-img" src="{{ getPictureUrl() }}" alt=""
/></span>
<span
class="badge badge-danger"
*ngIf="!sasjsConfig.debug"
[class.hidden]="failedReqs.length === 0"
>{{ failedReqs.length }}</span
>
<span
class="badge badge-info"
*ngIf="sasjsConfig.debug"
[class.hidden]="debugLogs.length === 0"
>{{ debugLogs.length }}</span
>
<clr-icon *ngIf="!isViya" shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
<div #dropdownItemDebug class="debug-switch-item" clrDropdownItem>
<clr-toggle-container
class="toggle-switch"
(click)="onDebugRowClick($event, dropdownItemDebug)"
>
<clr-toggle-wrapper>
<input
id="debug-toggle1"
type="checkbox"
[(ngModel)]="sasjsConfig.debug"
(ngModelChange)="onDebugModeChange()"
clrToggle
/>
<label>Debug Mode</label>
</clr-toggle-wrapper>
</clr-toggle-container>
</div>
<a (click)="openRequestsModal()" clrDropdownItem>
<span>SAS Requests</span>
</a>
<ng-container *ngIf="!isDeployPage">
<a target="_blank" href="https://docs.datacontroller.io" clrDropdownItem>
<span class="dropdown-text">Documentation</span>
</a>
</ng-container>
<div class="separator"></div>
<a href="..." routerLink="/system" clrDropdownItem>
<span>System</span>
</a>
<a href="..." (click)="logout($event)" clrDropdownItem>
<span>Log Out</span>
<clr-icon class="clr-logout" shape="logout"></clr-icon>
</a>
<div class="copyRight">
<span>v{{ commitVer }}</span>
</div>
</clr-dropdown-menu>
</clr-dropdown>
<div class="content-container h-auto">
<nav class="sidenav d-block d-md-none" [clr-nav-level]="2">
<section class="sidenav-content">
<a href="..." class="nav-link active">
{{ userName }}
</a>
<div>
<form>
<div class="toggle-switch">
<input
id="debug-toggle2"
type="checkbox"
[(ngModel)]="sasjsConfig.debug"
(ngModelChange)="onDebugModeChange()"
[ngModelOptions]="{ standalone: true }"
/>
<label
for="debug-toggle2"
class="debug-toggle-label color-dark-gray"
>Debug Mode</label
>
</div>
</form>
</div>
<!-- <a href="..." class="nav-link d-block" [routerLink]="['/application-logs']">
<span>Application Logs</span>
<span class="badge" *ngIf="appLogs.length > 0">{{appLogs.length}}</span>
</a>
<a *ngIf="debugMode" class="nav-link d-block" href="..." [routerLink]="['/debug-logs']">
<span>Debug Logs</span>
<span class="badge badge-info" *ngIf="debugLogs.length > 0">{{debugLogs.length}}</span>
</a>
<a *ngIf="!debugMode" class="nav-link d-block" href="..." [routerLink]="['/failed-requests']">
<span>Failed Requests</span>
<span class="badge badge-danger" *ngIf="failedReqs.length > 0">{{failedReqs.length}}</span>
</a>
<a href="..." class="nav-link d-block" [routerLink]="['/errors']">
<span>Errors</span>
<span class="badge badge-warning" *ngIf="sasErrors.length > 0">{{sasErrors.length}}</span>
</a> -->
<a
class="nav-link d-block"
target="_blank"
href="https://docs.datacontroller.io"
>
<span>Documentation</span>
</a>
<div class="separator"></div>
<a routerLink="/system" class="nav-link d-block">
<span>System</span>
<clr-icon shape="logout"></clr-icon>
</a>
<a href="..." class="nav-link d-block" (click)="logout($event)">
<span>Log Out</span>
<clr-icon shape="logout"></clr-icon>
</a>
</section>
</nav>
</div>

View File

@ -58,34 +58,58 @@
<div class="mt-20"> <div class="mt-20">
<div class="row"> <div class="row">
<button <button
class="btn btn-sm btn-outline text-center mt-20" class="btn btn-sm btn-outline text-center mr-5i"
(click)="viewerTableScreen()" (click)="viewerTableScreen()"
[disabled]="revertingChanges"
> >
Go to base table screen View base table
</button> </button>
<button <button
*ngIf="!(tableDetails?.['ALLOW_RESTORE'] === 'YES')"
id="approval-btn" id="approval-btn"
class="btn btn-sm btn-success-outline text-center mt-20" class="btn btn-sm btn-success-outline text-center mr-5i"
[disabled]=" [disabled]="
tableDetails?.REVIEW_STATUS_ID === 'APPROVED' || tableDetails?.REVIEW_STATUS_ID === 'APPROVED' ||
tableDetails?.REVIEW_STATUS_ID === 'REJECTED' tableDetails?.REVIEW_STATUS_ID === 'REJECTED'
" "
(click)="approveTableScreen()" (click)="approveTableScreen()"
[disabled]="revertingChanges"
> >
Go to approvals screen Approve
</button> </button>
<button <button
class="btn btn-sm btn-info-outline text-center mt-20" class="btn btn-sm btn-info-outline text-center mr-5i"
(click)="goBack()" (click)="goBack()"
[disabled]="revertingChanges"
> >
Go back to editor Edit base table
</button> </button>
<button <button
class="btn btn-sm btn-success text-center mt-20 min-w-0" class="btn btn-sm btn-success text-center mr-5i min-w-0"
(click)="download(tableDetails?.TABLE_ID)" (click)="download(tableDetails?.TABLE_ID)"
> >
<clr-icon shape="download"></clr-icon> <clr-icon shape="download"></clr-icon>
</button> </button>
<clr-tooltip>
<button
*ngIf="tableDetails?.['ALLOW_RESTORE'] === 'YES'"
(click)="revertChanges()"
clrTooltipTrigger
[clrLoading]="revertingChanges"
class="btn btn-sm btn-danger text-center mt-20"
>
REVERT
<clr-tooltip-content
clrPosition="bottom-left"
clrSize="lg"
*clrIfOpen
>
<span> Revert this and all subsequent changes </span>
</clr-tooltip-content>
</button>
</clr-tooltip>
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,10 +4,10 @@ import { Router } from '@angular/router'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { SasService } from '../services/sas.service' import { SasService } from '../services/sas.service'
import { EventService } from '../services/event.service' import { EventService } from '../services/event.service'
import { AppService } from '../services/app.service'
import { HotTableInterface } from '../models/HotTable.interface' import { HotTableInterface } from '../models/HotTable.interface'
import { LicenceService } from '../services/licence.service' import { LicenceService } from '../services/licence.service'
import { globals } from '../_globals' import { globals } from '../_globals'
import { EditorsRestoreServiceResponse } from '../models/sas/editors-restore.model'
@Component({ @Component({
selector: 'app-stage', selector: 'app-stage',
@ -23,6 +23,7 @@ export class StageComponent implements OnInit {
public keysArray: any public keysArray: any
public tableDetails: any public tableDetails: any
public loaded: boolean = false public loaded: boolean = false
public revertingChanges: boolean = false
public licenceState = this.licenceService.licenceState public licenceState = this.licenceService.licenceState
public hotTable: HotTableInterface = { public hotTable: HotTableInterface = {
data: [], data: [],
@ -162,6 +163,31 @@ export class StageComponent implements OnInit {
} }
} }
revertChanges() {
this.revertingChanges = true
const data = {
restore_in: [
{
load_ref: this.table_id
}
]
}
this.sasService
.request('editors/restore', data)
.then((res: EditorsRestoreServiceResponse) => {
if (res.restore_out) {
this.route.navigate([`/stage`]).then(() => {
this.route.navigate([`/stage/${res.restore_out[0].LOADREF}`])
})
}
})
.finally(() => {
this.revertingChanges = false
})
}
private setFocus() { private setFocus() {
setTimeout(() => { setTimeout(() => {
let approvalBtn: any = window.document.getElementById('approval-btn') let approvalBtn: any = window.document.getElementById('approval-btn')

View File

@ -9,43 +9,45 @@
class="sys-info d-flex clr-justify-content-center clr-flex-column clr-flex-lg-row" class="sys-info d-flex clr-justify-content-center clr-flex-column clr-flex-lg-row"
> >
<div> <div>
<h6 class="m-0">Environment Details <span class="dark"></span></h6> <h6 cds-text="subsection" class="mb-10">
<p class="m-0"> Environment Details <span class="dark"></span>
</h6>
<p cds-text="label" class="m-0">
SYSSITE: <span class="dark">{{ environmentInfo?.SYSSITE }}</span> SYSSITE: <span class="dark">{{ environmentInfo?.SYSSITE }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
SYSSCPL: <span class="dark">{{ environmentInfo?.SYSSCPL }}</span> SYSSCPL: <span class="dark">{{ environmentInfo?.SYSSCPL }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
SYSTCPIPHOSTNAME: SYSTCPIPHOSTNAME:
<span class="dark">{{ environmentInfo?.SYSTCPIPHOSTNAME }}</span> <span class="dark">{{ environmentInfo?.SYSTCPIPHOSTNAME }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
SYSVLONG: <span class="dark">{{ environmentInfo?.SYSVLONG }}</span> SYSVLONG: <span class="dark">{{ environmentInfo?.SYSVLONG }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
MEMSIZE: <span class="dark">{{ environmentInfo?.MEMSIZE }}</span> MEMSIZE: <span class="dark">{{ environmentInfo?.MEMSIZE }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
SYSPROCESSMODE: SYSPROCESSMODE:
<span class="dark">{{ environmentInfo?.SYSPROCESSMODE }}</span> <span class="dark">{{ environmentInfo?.SYSPROCESSMODE }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
SYSHOSTNAME: SYSHOSTNAME:
<span class="dark">{{ environmentInfo?.SYSHOSTNAME }}</span> <span class="dark">{{ environmentInfo?.SYSHOSTNAME }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
SYSHOSTINFOLONG: SYSHOSTINFOLONG:
<span class="dark">{{ environmentInfo?.SYSHOSTINFOLONG }}</span> <span class="dark">{{ environmentInfo?.SYSHOSTINFOLONG }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
SYSENCODING: SYSENCODING:
<span class="dark">{{ environmentInfo?.SYSENCODING }}</span> <span class="dark">{{ environmentInfo?.SYSENCODING }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
AUTOEXEC: <span class="dark">{{ environmentInfo?.AUTOEXEC }}</span> AUTOEXEC: <span class="dark">{{ environmentInfo?.AUTOEXEC }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
DC ADMIN GROUP: DC ADMIN GROUP:
<span class="dark">{{ environmentInfo?.DC_ADMIN_GROUP }}</span> <span class="dark">{{ environmentInfo?.DC_ADMIN_GROUP }}</span>
</p> </p>
@ -53,42 +55,44 @@
<div class="d-flex clr-justify-content-lg-center"> <div class="d-flex clr-justify-content-lg-center">
<div> <div>
<h6 class="m-0"> <h6 cds-text="subsection" class="mb-10">
Data Controller Details <span class="dark"></span> Data Controller Details <span class="dark"></span>
</h6> </h6>
<p class="m-0"> <p cds-text="label" class="m-0">
Application version: Application version:
<span class="dark">{{ appInfo.appVersion }}</span> <span class="dark">{{ appInfo.appVersion }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
Build timestamp: Build timestamp:
<span class="dark">{{ appInfo.buildTimestamp }}</span> <span class="dark">{{ appInfo.buildTimestamp }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
Adapter version: Adapter version:
<span class="dark">{{ appInfo.adapterVersion }}</span> <span class="dark">{{ appInfo.adapterVersion }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
HTTP: <span class="dark">{{ http ? 'YES' : 'NO' }}</span> HTTP: <span class="dark">{{ http ? 'YES' : 'NO' }}</span>
</p> </p>
</div> </div>
</div> </div>
<div> <div>
<h6 class="m-0">Licence details <span class="dark"></span></h6> <h6 cds-text="subsection" class="mb-10">
<p class="m-0"> Licence details <span class="dark"></span>
</h6>
<p cds-text="label" class="m-0">
Valid until: Valid until:
<span class="dark">{{ licenceInfo?.valid_until }}</span> <span class="dark">{{ licenceInfo?.valid_until }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
Users allowed: Users allowed:
<span class="dark">{{ licenceInfo?.users_allowed }}</span> <span class="dark">{{ licenceInfo?.users_allowed }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
Site IDs: Site IDs:
<span class="dark">{{ licenceInfo?.site_id_multiple }}</span> <span class="dark">{{ licenceInfo?.site_id_multiple }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
Free Tier: Free Tier:
<span class="dark">{{ licenceInfo?.demo ? 'YES' : 'NO' }}</span> <span class="dark">{{ licenceInfo?.demo ? 'YES' : 'NO' }}</span>
</p> </p>
@ -157,25 +161,25 @@
licenceState.value.lineage_daily_limit licenceState.value.lineage_daily_limit
}}</span> }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
Viewboxes: Viewboxes:
<span class="dark">{{ <span class="dark">{{
licenceState.value.viewbox ? 'YES' : 'NO' licenceState.value.viewbox ? 'YES' : 'NO'
}}</span> }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
File Upload: File Upload:
<span class="dark">{{ <span class="dark">{{
licenceState.value.fileUpload ? 'YES' : 'NO' licenceState.value.fileUpload ? 'YES' : 'NO'
}}</span> }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
Edit record: Edit record:
<span class="dark">{{ <span class="dark">{{
licenceState.value.editRecord ? 'YES' : 'NO' licenceState.value.editRecord ? 'YES' : 'NO'
}}</span> }}</span>
</p> </p>
<p class="m-0"> <p cds-text="label" class="m-0">
Add record: Add record:
<span class="dark">{{ <span class="dark">{{
licenceState.value.addRecord ? 'YES' : 'NO' licenceState.value.addRecord ? 'YES' : 'NO'

View File

@ -99,15 +99,18 @@
</ng-container> </ng-container>
{{ libTable.replace('-FC', '') }} {{ libTable.replace('-FC', '') }}
</button> </button>
<ng-container *ngIf="tableLocked">
<clr-tooltip-content <clr-tooltip-content
clrPosition="bottom-right" clrPosition="bottom-right"
clrSize="lg" clrSize="lg"
*clrIfOpen *clrIfOpen
> >
<span *ngIf="tableLocked"> <span>
To unlock all tables, contact support&#64;datacontroller.io To unlock all tables, contact support&#64;datacontroller.io
</span> </span>
</clr-tooltip-content> </clr-tooltip-content>
</ng-container>
</clr-tooltip> </clr-tooltip>
</clr-tree-node> </clr-tree-node>
</clr-tree-node> </clr-tree-node>
@ -361,7 +364,7 @@
<h3 <h3
class="viewerTitle clr-flex-column d-flex clr-flex-sm-row clr-align-items-center clr-justify-content-center" class="viewerTitle clr-flex-column d-flex clr-flex-sm-row clr-align-items-center clr-justify-content-center"
> >
<clr-tooltip class="d-flex"> <clr-tooltip class="d-flex clr-align-items-center">
<clr-icon <clr-icon
clrTooltipTrigger clrTooltipTrigger
(click)="datasetInfo = true" (click)="datasetInfo = true"
@ -373,12 +376,14 @@
<clr-icon <clr-icon
*ngIf="tableTitle?.includes('-FC')" *ngIf="tableTitle?.includes('-FC')"
shape="bolt" shape="bolt"
class="color-yellow mt-5 mr-5" class="color-yellow mr-5"
></clr-icon> ></clr-icon>
<span clrTooltipTrigger *ngIf="tableTitle && tableTitle.length > 0"> <span clrTooltipTrigger *ngIf="tableTitle && tableTitle.length > 0">
{{ tableTitle?.replace('-FC', '') }} {{ tableTitle?.replace('-FC', '') }}
</span> </span>
<ng-container *ngIf="this.dsNote && this.dsNote.length > 0">
<clr-tooltip-content <clr-tooltip-content
clrPosition="bottom-left" clrPosition="bottom-left"
clrSize="lg" clrSize="lg"
@ -386,6 +391,7 @@
> >
{{ this.dsNote }} {{ this.dsNote }}
</clr-tooltip-content> </clr-tooltip-content>
</ng-container>
</clr-tooltip> </clr-tooltip>
<ng-container *ngIf="tableTitle && tableTitle.length > 0"> <ng-container *ngIf="tableTitle && tableTitle.length > 0">
@ -419,62 +425,34 @@
options options
</button> </button>
<clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen> <clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
<button <div (click)="newViewbox()" clrDropdownItem>
type="button"
class="btn btn-sm btn-success-outline"
(click)="newViewbox()"
clrDropdownItem
>
<clr-icon shape="view-cards"></clr-icon> <clr-icon shape="view-cards"></clr-icon>
<span>Viewboxes</span> <span>Viewboxes</span>
</button> </div>
<button <div
type="button"
class="btn btn-sm btn-success-outline"
*ngIf="tableEditExists()" *ngIf="tableEditExists()"
(click)="editTable()" (click)="editTable()"
clrDropdownItem clrDropdownItem
> >
<clr-icon shape="pencil"></clr-icon> <clr-icon shape="pencil"></clr-icon>
<span>Edit</span> <span>Edit</span>
</button> </div>
<button <div *ngIf="tableuri" (click)="goToLineage()" clrDropdownItem>
type="button"
class="btn btn-sm btn-success-outline"
*ngIf="tableuri"
(click)="goToLineage()"
clrDropdownItem
>
<clr-icon shape="switch"></clr-icon> <clr-icon shape="switch"></clr-icon>
<span>Lineage</span> <span>Lineage</span>
</button> </div>
<button <div (click)="openQb()" clrDropdownItem>
type="button"
class="btn btn-sm btn-outline btn-block"
(click)="openQb()"
clrDropdownItem
>
<clr-icon shape="filter"></clr-icon> <clr-icon shape="filter"></clr-icon>
<span>Filter</span> <span>Filter</span>
</button> </div>
<button <div (click)="openDownload = true" clrDropdownItem>
type="button"
class="btn btn-sm btn-success-outline"
(click)="openDownload = true"
clrDropdownItem
>
<clr-icon shape="download"></clr-icon> <clr-icon shape="download"></clr-icon>
<span>Download</span> <span>Download</span>
</button> </div>
<button <div (click)="showWebQuery()" clrDropdownItem>
type="button"
class="btn btn-sm btn-success-outline"
(click)="showWebQuery()"
clrDropdownItem
>
<clr-icon shape="download-cloud"></clr-icon> <clr-icon shape="download-cloud"></clr-icon>
<span>Web Query URL</span> <span>Web Query URL</span>
</button> </div>
</clr-dropdown-menu> </clr-dropdown-menu>
</clr-dropdown> </clr-dropdown>
</div> </div>
@ -667,6 +645,12 @@
</div> </div>
</div> </div>
<app-dataset-info [(open)]="datasetInfo" [dsmeta]="dsmeta"></app-dataset-info> <app-dataset-info
[(open)]="datasetInfo"
[dsmeta]="dsmeta"
[versions]="versions"
(rowClicked)="datasetInfoModalRowClicked($event)"
>
</app-dataset-info>
<app-viewboxes [(viewboxModal)]="viewboxOpen"></app-viewboxes> <app-viewboxes [(viewboxModal)]="viewboxOpen"></app-viewboxes>

View File

@ -57,6 +57,7 @@ clr-tree-node button {
.viewerTitle { .viewerTitle {
text-align:center; text-align:center;
margin-bottom: 15px; margin-bottom: 15px;
margin-top: 16px;
} }
.dropdown-menu { .dropdown-menu {

View File

@ -25,7 +25,11 @@ import { FilterGroup, FilterQuery } from '../models/FilterQuery'
import { HotTableInterface } from '../models/HotTable.interface' import { HotTableInterface } from '../models/HotTable.interface'
import { LoggerService } from '../services/logger.service' import { LoggerService } from '../services/logger.service'
import Handsontable from 'handsontable' import Handsontable from 'handsontable'
import { $DataFormats, DSMeta } from '../models/sas/editors-getdata.model' import {
$DataFormats,
DSMeta,
Version
} from '../models/sas/editors-getdata.model'
import { mergeColsRules } from '../shared/dc-validator/utils/mergeColsRules' import { mergeColsRules } from '../shared/dc-validator/utils/mergeColsRules'
import { PublicViewtablesServiceResponse } from '../models/sas/public-viewtables.model' import { PublicViewtablesServiceResponse } from '../models/sas/public-viewtables.model'
import { PublicViewlibsServiceResponse } from '../models/sas/public-viewlibs.model' import { PublicViewlibsServiceResponse } from '../models/sas/public-viewlibs.model'
@ -95,6 +99,7 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
public $dataFormats: $DataFormats | null = null public $dataFormats: $DataFormats | null = null
public datasetInfo: boolean = false public datasetInfo: boolean = false
public dsmeta: DSMeta[] = [] public dsmeta: DSMeta[] = []
public versions: Version[] = []
public dsNote = '' public dsNote = ''
public licenceState = this.licenceService.licenceState public licenceState = this.licenceService.licenceState
@ -247,6 +252,7 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
this.hotTable.data = res.viewdata this.hotTable.data = res.viewdata
this.$dataFormats = res.$viewdata this.$dataFormats = res.$viewdata
this.dsmeta = res.dsmeta this.dsmeta = res.dsmeta
this.versions = res.versions || []
this.setDSNote() this.setDSNote()
this.numberOfRows = res.sasparams[0].NOBS this.numberOfRows = res.sasparams[0].NOBS
this.queryText = res.sasparams[0].FILTER_TEXT this.queryText = res.sasparams[0].FILTER_TEXT
@ -805,6 +811,7 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
this.hotTable.data = res.viewdata this.hotTable.data = res.viewdata
this.$dataFormats = res.$viewdata this.$dataFormats = res.$viewdata
this.dsmeta = res.dsmeta this.dsmeta = res.dsmeta
this.versions = res.versions || []
this.setDSNote() this.setDSNote()
this.queryText = res.sasparams[0].FILTER_TEXT this.queryText = res.sasparams[0].FILTER_TEXT
let columns: any[] = [] let columns: any[] = []
@ -1019,6 +1026,16 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
this.sasStoreService.removeClause() this.sasStoreService.removeClause()
} }
public datasetInfoModalRowClicked(value: Version | DSMeta) {
if ((<Version>value).LOAD_REF !== undefined) {
// Type is Version
const row = value as Version
const url = `/stage/${row.LOAD_REF}`
this.router.navigate([url])
}
}
private setDSNote() { private setDSNote() {
const notes = this.dsmeta.find((item) => item.NAME === 'NOTES') const notes = this.dsmeta.find((item) => item.NAME === 'NOTES')
const longDesc = this.dsmeta.find((item) => item.NAME === 'DD_LONGDESC') const longDesc = this.dsmeta.find((item) => item.NAME === 'DD_LONGDESC')

View File

@ -94,13 +94,21 @@
}}</i> }}</i>
<h5 class="d-flex clr-col-12 clr-justify-content-center mt-5-i"> <h5 class="d-flex clr-col-12 clr-justify-content-center mt-5-i">
Rules Source: Rules Source:
<a class="ml-10" [routerLink]="'/view/data/' + rulesSource"> <a
cds-text="labelLink"
class="ml-10"
[routerLink]="'/view/data/' + rulesSource"
>
{{ rulesSource }} {{ rulesSource }}
</a> </a>
</h5> </h5>
<h5 class="d-flex clr-col-12 clr-justify-content-center mt-5-i"> <h5 class="d-flex clr-col-12 clr-justify-content-center mt-5-i">
Target dataset: Target dataset:
<a class="ml-10" [routerLink]="'/view/data/' + selectedXLMap.targetDS"> <a
cds-text="labelLink"
class="ml-10"
[routerLink]="'/view/data/' + selectedXLMap.targetDS"
>
{{ selectedXLMap.targetDS }} {{ selectedXLMap.targetDS }}
</a> </a>
</h5> </h5>

View File

@ -15,7 +15,7 @@ try {
writeFileSync( writeFileSync(
file, file,
`//IMPORTANT: THIS FILE IS AUTO GENERATED BASED ON LICENCE.MD FILE!\nexport const EULA = \`\n${licence}\n\``, `//IMPORTANT: THIS FILE IS AUTO GENERATED BASED ON LICENCE.MD FILE!\nexport const EULA = \`\n${licence}\n\`\n`,
{ encoding: 'utf-8' } { encoding: 'utf-8' }
) )

View File

@ -57,7 +57,7 @@
> >
</sasjs> </sasjs>
<body class="m-0"> <body cds-theme="light" class="m-0">
<my-app></my-app> <my-app></my-app>
</body> </body>
</html> </html>

View File

@ -1,9 +1,12 @@
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
@import '~handsontable/dist/handsontable.full.css'; @import '~handsontable/dist/handsontable.full.css';
@import '~@clr/ui/clr-ui.min.css';
@import '~@clr/icons/clr-icons.min.css'; @import '~@clr/icons/clr-icons.min.css';
@import '@cds/core/global.min.css';
@import '@cds/core/styles/theme.dark.min.css';
@import '@clr/ui/clr-ui.min.css';
@font-face { @font-face {
font-family: text-security-disc; font-family: text-security-disc;
src: url('https://raw.githubusercontent.com/noppa/text-security/master/dist/text-security-disc.woff'); src: url('https://raw.githubusercontent.com/noppa/text-security/master/dist/text-security-disc.woff');
@ -29,6 +32,14 @@ button {
} }
} }
[cds-text=label] {
color: var(--cds-global-typography-color-200);
}
[cds-text=labelLink] {
line-height: 1.8 !important;
}
// Custom loading spinner // Custom loading spinner
.slider { .slider {
position: absolute; position: absolute;
@ -261,6 +272,10 @@ button {
margin-right: 5px; margin-right: 5px;
} }
.mr-5i {
margin-right: 5px !important;
}
.mr-10 { .mr-10 {
margin-right: 10px; margin-right: 10px;
} }
@ -660,6 +675,10 @@ clr-icon.is-info {
-webkit-box-direction: normal; -webkit-box-direction: normal;
} }
.btn .clr-loading-btn-content {
justify-content: center;
}
.btn.btn-danger, .btn.btn-danger,
.btn.btn-warning { .btn.btn-warning {
border-color: #ef4f2e; border-color: #ef4f2e;
@ -667,6 +686,11 @@ clr-icon.is-info {
color: #fff; color: #fff;
} }
// Vertical align fix for small buttons with icons
.btn.btn-sm:has(clr-icon) {
line-height: 2;
}
.d-none { .d-none {
display: none !important; display: none !important;
} }
@ -713,6 +737,11 @@ clr-icon.is-info {
border: 1px solid red !important; border: 1px solid red !important;
color: #ffffff !important; color: #ffffff !important;
} }
.handsontable .numericListbox {
text-align: right;
}
.margin-top-20 { .margin-top-20 {
margin-top: 20px; margin-top: 20px;
} }

9226
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": "6.6.1", "version": "6.8.1",
"description": "Data Controller", "description": "Data Controller",
"devDependencies": { "devDependencies": {
"@saithodev/semantic-release-gitea": "^2.1.0", "@saithodev/semantic-release-gitea": "^2.1.0",

View File

@ -14,6 +14,10 @@ _webout = `{"SYSDATE" : "26SEP22"
"APPROVER": "sasdemo" "APPROVER": "sasdemo"
} }
] ]
, "histparams":
[
{"HIST":100 ,"STARTROW":1 ,"NOBS":3 }
]
,"_DEBUG" : "" ,"_DEBUG" : ""
,"_METAUSER": "sasdemo@SAS" ,"_METAUSER": "sasdemo@SAS"
,"_METAPERSON": "sasdemo" ,"_METAPERSON": "sasdemo"

29
sas/package-lock.json generated
View File

@ -7,7 +7,7 @@
"name": "dc-sas", "name": "dc-sas",
"dependencies": { "dependencies": {
"@sasjs/cli": "^4.11.1", "@sasjs/cli": "^4.11.1",
"@sasjs/core": "^4.49.0" "@sasjs/core": "^4.52.0"
} }
}, },
"node_modules/@coolaj86/urequest": { "node_modules/@coolaj86/urequest": {
@ -116,9 +116,9 @@
"integrity": "sha512-Grwydm5GxBsYk238PZw41XPjXVVQ9vWcvfZ06L2P0bQbvK0sGn7l69JA7H5MGr3QcaLpiD4Kg70cAh7PgE+JOw==" "integrity": "sha512-Grwydm5GxBsYk238PZw41XPjXVVQ9vWcvfZ06L2P0bQbvK0sGn7l69JA7H5MGr3QcaLpiD4Kg70cAh7PgE+JOw=="
}, },
"node_modules/@sasjs/core": { "node_modules/@sasjs/core": {
"version": "4.49.0", "version": "4.52.0",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.49.0.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.52.0.tgz",
"integrity": "sha512-hp3Hb4DkT6FmowyNHTOvSlgmSObW9WeuTJj+TQlwPgnBo59mAB4XFUnUaYSA+7ghvsHqUZf1OP2eSYqmnN5swQ==" "integrity": "sha512-r+yQYSTYdvNPJ82n2xW/rbJd/cJpk3HuFhCa49RTDlDWjXMXJlymvGB0ltv3/L8rNk4+TwoyMzb7cJYnILi6ag=="
}, },
"node_modules/@sasjs/lint": { "node_modules/@sasjs/lint": {
"version": "2.3.1", "version": "2.3.1",
@ -229,12 +229,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/tough-cookie": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
"peer": true
},
"node_modules/abab": { "node_modules/abab": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@ -1834,9 +1828,9 @@
} }
}, },
"@sasjs/core": { "@sasjs/core": {
"version": "4.49.0", "version": "4.52.0",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.49.0.tgz", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.52.0.tgz",
"integrity": "sha512-hp3Hb4DkT6FmowyNHTOvSlgmSObW9WeuTJj+TQlwPgnBo59mAB4XFUnUaYSA+7ghvsHqUZf1OP2eSYqmnN5swQ==" "integrity": "sha512-r+yQYSTYdvNPJ82n2xW/rbJd/cJpk3HuFhCa49RTDlDWjXMXJlymvGB0ltv3/L8rNk4+TwoyMzb7cJYnILi6ag=="
}, },
"@sasjs/lint": { "@sasjs/lint": {
"version": "2.3.1", "version": "2.3.1",
@ -1933,12 +1927,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/tough-cookie": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
"peer": true
},
"abab": { "abab": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@ -2965,8 +2953,7 @@
"ws": { "ws": {
"version": "8.13.0", "version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA=="
"requires": {}
}, },
"xml": { "xml": {
"version": "1.0.1", "version": "1.0.1",

View File

@ -29,6 +29,6 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@sasjs/cli": "^4.11.1", "@sasjs/cli": "^4.11.1",
"@sasjs/core": "^4.49.0" "@sasjs/core": "^4.52.0"
} }
} }

View File

@ -75,7 +75,9 @@ data basetable;
run; run;
/* create results table */ /* create results table */
data results; data results;
format test $7. result $4. reason $50.; stop; format test $7. result $4. reason $50.;
call missing(of _all_);
stop;
run; run;
%put assigning lib..; %put assigning lib..;

View File

@ -0,0 +1,135 @@
/**
@file
@brief Checks if a user is able to restore a LOAD_REF
@details Not all LOAD_REFs can be restored - maybe the user does not have
permission, maybe the load was never loaded, or maybe the load was not
tracked.
The macro creates two output (global) macro variables.
@param [in] LOAD_REF The Load Reference to check
@param [out] outresult= (ALLOW_RESTORE) Output macro variable NAME. Will be
given the value of YES or NO depending on whether the user is allowed to
restore the load ref.
@param [out] outreason= (REASON) Output macro variable NAME.
Will be populated with the reason for which the restore decision was made.
<h4> SAS Macros </h4>
@li mf_nobs.sas
@li mf_getuser.sas
@li mpe_accesscheck.sas
@li mpe_getgroups.sas
<h4> Related Macros </h4>
@li mpe_checkrestore.test.sas
@version 9.2
@author 4GL Apps Ltd
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
and may not be re-distributed or re-sold without the express permission of
4GL Apps Ltd.
**/
%macro mpe_checkrestore(load_ref,
outresult=ALLOW_RESTORE,
outreason=REASON
);
%global &outresult &outreason;
%let &outresult=NO;
%let &outreason=NOTFOUND;
/* check if there is actually a version to restore */
%local chk;
%let chk=0;
proc sql noprint;
select count(*) into: chk from &dc_libref..mpe_audit
where load_ref="&load_ref";
%if &chk=0 %then %do;
%let allow_restore=NO;
%let reason=No entry for &load_ref in MPE_AUDIT;
%return;
%end;
/* grab user groups */
%local user;
%let user=%mf_getuser();
%mpe_getgroups(user=&user,outds=work.groups)
/* check if user is admin */
%local is_admin;
%let is_admin=0;
proc sql;
select count(*) into: is_admin from work.groups
where groupname="&dc_admin_group";
%if &is_admin>0 %then %do;
%let allow_restore=YES;
%let reason=IS ADMIN;
%return;
%end;
/* check if user has basic access */
%local libds;
proc sql noprint;
select cats(base_lib,'.',base_ds) into: libds
from &mpelib..mpe_submit
where TABLE_ID="&load_ref";
%mpe_accesscheck(&libds,outds=work.access_check
,user=&user
,access_level=EDIT
)
%if %mf_nobs(access_check)=0 %then %do;
%let allow_restore=NO;
%let reason=No access in MPE_TABLES;
%return;
%end;
/* check if user has column level security rules */
proc sql;
create table work.cls_rules as
select *
from &mpelib..mpe_column_level_security
where &dc_dttmtfmt. lt tx_to
and CLS_SCOPE in ("EDIT",'ALL')
and CLS_ACTIVE=1
and upcase(CLS_GROUP) in (select upcase(groupname) from work.groups)
and CLS_LIBREF="%upcase(&base_lib)"
and CLS_TABLE="%upcase(&base_ds)";
%if %mf_nobs(work.cls_rules)>0 %then %do;
%let allow_restore=NO;
%let reason=User has restrictions in MPE_COLUMN_LEVEL_SECURITY;
data _null_;
set work.cls_rules;
putlog (_all_)(=);
if _n_>5 then stop;
run;
%return;
%end;
/* check if user has row level security rules */
proc sql;
create table work.rls_rules as
select *
from &mpelib..mpe_row_level_security
where &dc_dttmtfmt. lt tx_to
and rls_scope in ("EDIT",'ALL')
and upcase(rls_group) in (select upcase(groupname) from work.groups)
and rls_libref="&base_lib"
and rls_table="&base_ds"
and rls_active=1;
%if %mf_nobs(work.rls_rules)>0 %then %do;
%let allow_restore=NO;
%let reason=User has restrictions in MPE_ROW_LEVEL_SECURITY;
data _null_;
set work.rls_rules;
putlog (_all_)(=);
if _n_>5 then stop;
run;
%return;
%end;
%else %do;
%let allow_restore=YES;
%let reason=CHECKS PASSED;
%end;
%mend mpe_checkrestore;

View File

@ -0,0 +1,59 @@
/**
@file
@brief Testing mpe_checkrestore macro
@details Checking functionality of mpe_checkrestore.sas macro
<h4> SAS Macros </h4>
@li dc_getsettings.sas
@li mpe_checkrestore.sas
@li mp_assert.sas
@li mp_assertscope.sas
@li mp_testservice.sas
@author 4GL Apps Ltd
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
and may not be re-distributed or re-sold without the express permission of
4GL Apps Ltd.
**/
/* first, run a data update */
%mp_testservice(&appLoc/tests/services/auditors/postdata.test.1,
viyacontext=&defaultcontext
)
/* now grab the latest update */
%let loadref=0;
data APPROVE1;
set &mpelib..mpe_submit end=last;
if last then call symputx('loadref',table_id);
run;
%global dc_repo_users dc_macros dc_libref dc_staging_area dc_admin_group
mpelib dc_dttmtfmt;
%dc_getsettings()
%put checking it is restorable;
%global allow_restore reason;
%mp_assertscope(SNAPSHOT)
%mpe_checkrestore(&loadref,outresult=ALLOW_RESTORE,outreason=REASON)
%mp_assertscope(COMPARE,
desc=Checking macro variables against previous snapshot,
ignorelist=ALLOW_RESTORE REASON
MCLIB0_JADP1LEN MCLIB0_JADP2LEN MCLIB0_JADPNUM MCLIB0_JADVLEN
MCLIB2_JADP1LEN MCLIB2_JADP2LEN MCLIB2_JADPNUM MCLIB2_JADVLEN
)
%mp_assert(
iftrue=(&syscc=0),
desc=Checking successful submission
)
%mp_assert(
iftrue=(&ALLOW_RESTORE=YES),
desc=Checking restoring is possible
)
%mp_assert(
iftrue=(&REASON=IS ADMIN),
desc=Checking reason code returned
)

View File

@ -0,0 +1,34 @@
/**
@file
@brief Get previous versions of a table
@details Used to fetch version data for a particular table
Delivered as part of this issue: https://git.datacontroller.io/dc/dc/issues/84
@param [in] dclib The DC libref
@param [in] lib The library of the dataset for which to fetch versions
@param [in] ds The dataset to fetch versions for
@param [out] outds= (work.mpe_getversions) the DS to create
**/
%macro mpe_getversions(dclib,libref,ds,outds=work.mpe_getversions);
proc sql;
create table &outds as
select a.table_id as LOAD_REF
,a.reviewed_by_nm as user_nm
,a.reviewed_on_dttm as version_dttm_num
,put(a.reviewed_on_dttm,datetime19.) as VERSION_DTTM
,a.submitted_reason_txt as VERSION_DESC
,b.changed_records
,b.new_records
,b.deleted_records
from &dclib..mpe_submit a
left join &dclib..MPE_DATALOADS(where=(libref="&libref" & dsn="&ds")) b
on a.table_id=b.etlsource
where a.base_lib="&libref" and a.base_ds="&ds"
and a.submit_status_cd='APPROVED' and a.num_of_approvals_remaining=0
order by a.reviewed_on_dttm desc;
%mend mpe_getversions;

View File

@ -0,0 +1,84 @@
/**
@file
@brief Testing mpe_getversions macro
@details Checking functionality of mpe_getversions.sas macro
<h4> SAS Macros </h4>
@li mf_nobs.sas
@li mp_assert.sas
@li mp_assertscope.sas
@li mpe_getversions.sas
@li mpe_targetloader.sas
**/
/* run the macro*/
%mp_assertscope(SNAPSHOT)
%mpe_getversions(&mpelib,&mpelib,MPE_DATADICTIONARY, outds=ds0)
%mp_assertscope(COMPARE,
desc=Checking macro variables against previous snapshot
)
/* now stage some data */
%let f1=%mf_getuniquefileref();
data _null_;
file &f1 termstr=crlf;
put 'ACTION:$char4. MESSAGE:$char40. LIBDS:$char38.';
put "LOAD,staging some data,&dclib..MPE_DATADICTIONARY";
run;
data work.jsdata;
set &mpelib..MPE_DATADICTIONARY;
_____DELETE__THIS__RECORD_____='No';
dd_source=cats(ranuni(0));
output;
stop;
run;
%mx_testservice(&appLoc/services/editors/stagedata,
viyacontext=&defaultcontext,
inputfiles=&f1:sascontroltable,
inputdatasets=jsdata,
outlib=web1,
mdebug=&sasjs_mdebug
)
%let status=0;
data work.sasparams;
set web1.sasparams;
putlog (_all_)(=);
if status='SUCCESS' then call symputx('status',1);
call symputx('dsid',dsid);
run;
%mp_assert(
iftrue=(&status=1),
desc=Checking staged data component,
outds=work.test_results
)
/* now approve the data so the change is applied */
data work.sascontroltable;
ACTION='APPROVE_TABLE';
TABLE="&dsid";
DIFFTIME="%sysfunc(datetime(),datetime19.)";
output;
stop;
run;
%mx_testservice(&appLoc/services/auditors/postdata,
viyacontext=&defaultcontext,
inputdatasets=work.sascontroltable,
outlib=web2,
outref=wbout,
mdebug=&sasjs_mdebug
)
/* finally - check that we have an extra version! */
%mpe_getversions(&mpelib,&mpelib,MPE_DATADICTIONARY, outds=ds1)
%mp_assert(
iftrue=(%mf_nobs(ds0) = %mf_nobs(ds1)-1),
desc=Checking one extra version was created
)

View File

@ -121,7 +121,8 @@ insert into &mpelib..mpe_loads
set USER_NM="&user" set USER_NM="&user"
,STATUS='IN PROGRESS' ,STATUS='IN PROGRESS'
,CSV_dir="&mperef" ,CSV_dir="&mperef"
,PROCESSED_DTTM=&now; ,PROCESSED_DTTM=&now
,reason_txt = symget('submitted_reason_txt');
/* import CSV */ /* import CSV */
@ -300,28 +301,6 @@ create table vars_csv2 as
on a.name=b.name on a.name=b.name
order by a.varnum; order by a.varnum;
/* make sure that the variables we are importing, actually
exist on the target table */
/** edit - extra variables are now simply ignored
%local very_bad_vars;
select name into: very_bad_vars separated by ' '
from vars_csv1
where name not in (select name from vars)
and name ne "_____DELETE__THIS__RECORD_____";
%if %length(&very_bad_vars) > 0 %then %do;
%let msg=%str(WARNING: The following vars are not defined in %trim(
)&libref..&ds, yet they exist in &csv_dir/&ds..csv: &very_bad_vars);
%mpe_loadfail(
status=FAILED
,now=&now
,mperef=&mperef
,reason_txt=%quote(&msg)
,dc_dttmtfmt=&dc_dttmtfmt.
)
%return;
%end;
**/
/* now build input statement */ /* now build input statement */
data final_check; data final_check;
@ -408,7 +387,7 @@ run;
/* get log back */ /* get log back */
proc printto log=&logloc;run; proc printto log=&logloc;run;
data _null_; infile tmp; input; putlog _infile_;run; data _null_; infile tmp; input; putlog _infile_;run;
/* scan log for invalid data warning */ /* scan log for invalid data warnings */
data _null_; data _null_;
infile tmp; infile tmp;
input; input;

View File

@ -26,14 +26,6 @@
%end; %end;
proc sql; proc sql;
insert into &lib..mpe_alerts set
tx_from=0
,tx_to='31DEC9999:23:59:59'dt
,alert_event='*ALL*'
,alert_lib='*ALL*'
,alert_ds='*ALL*'
,alert_user="&sysuserid";
insert into &lib..mpe_column_level_security set insert into &lib..mpe_column_level_security set
tx_from=0 tx_from=0
,tx_to='31DEC9999:23:59:59'dt ,tx_to='31DEC9999:23:59:59'dt
@ -1874,6 +1866,16 @@ insert into &lib..MPE_VALIDATIONS set
,rule_value="services/validations/columns_in_libds" ,rule_value="services/validations/columns_in_libds"
,rule_active=1 ,rule_active=1
,tx_to='31DEC5999:23:59:59'dt; ,tx_to='31DEC5999:23:59:59'dt;
/* test softselect on numeric var (should be ordered numerically) */
insert into &lib..MPE_VALIDATIONS set
tx_from=0
,base_lib="&lib"
,base_ds="MPE_X_TEST"
,base_col="SOME_BESTNUM"
,rule_type='SOFTSELECT'
,rule_value="&lib..MPE_X_TEST.SOME_BESTNUM"
,rule_active=1
,tx_to='31DEC5999:23:59:59'dt;
insert into &lib..MPE_VALIDATIONS set insert into &lib..MPE_VALIDATIONS set
tx_from=0 tx_from=0
,base_lib="&lib" ,base_lib="&lib"

View File

@ -33,7 +33,7 @@
,CLOSE_VARS= /* provide close vars to override defaults */ ,CLOSE_VARS= /* provide close vars to override defaults */
,dclib=NOTPROVIDED ,dclib=NOTPROVIDED
,mdebug=0 ,mdebug=0
,dc_dttmtfmt=E8601DT26.6 ,dc_dttmtfmt=%sysfunc(datetime())
); );
%local lib ds nobs; %local lib ds nobs;
@ -219,7 +219,7 @@ run;
) )
%end; %end;
%else %do; %else %do;
%put WARNING: LOADTYPE &LOADTYPE not supported; %put %str(WARN)ING: LOADTYPE &LOADTYPE not supported;
%let syscc=4; %let syscc=4;
%mp_abort(msg=LOADTYPE &LOADTYPE not supported,mac=mpe_targetloader.sas) %mp_abort(msg=LOADTYPE &LOADTYPE not supported,mac=mpe_targetloader.sas)
%end; %end;

View File

@ -33,7 +33,8 @@
dc_activation_key /* extracted in dc_getsettings */ dc_activation_key /* extracted in dc_getsettings */
dc_locale /* extracted in dc_getsettings */ dc_locale /* extracted in dc_getsettings */
dc_dttmtfmt /* can be overridden in dc_getsettings */ dc_dttmtfmt /* can be overridden in dc_getsettings */
_debug _debug /* automatic variable when provided in URL */
sasjs_mdebug /* used to show extra info when _debug is enabled */
; ;
%if &mpeinit=1 %then %return; %if &mpeinit=1 %then %return;
@ -110,4 +111,9 @@ run;
,msg=%str(Problem during compilation or with STP precode (&syswarningtext)) ,msg=%str(Problem during compilation or with STP precode (&syswarningtext))
) )
%if "&_debug"="2477" or "&_debug"="fields,log,trace" or "&_debug"="131"
%then %do;
%let sasjs_mdebug=1;
%end;
%mend mpeinit; %mend mpeinit;

View File

@ -233,7 +233,7 @@
"streamWeb": true, "streamWeb": true,
"streamWebFolder": "web", "streamWebFolder": "web",
"webSourcePath": "../client/dist", "webSourcePath": "../client/dist",
"streamServiceName": "DataController", "streamServiceName": "DataController2",
"streamLogo": "favicon.ico", "streamLogo": "favicon.ico",
"assetPaths": [] "assetPaths": []
} }

View File

@ -59,6 +59,7 @@ data _null_;
call symputx('LOAD_REF',TABLE); call symputx('LOAD_REF',TABLE);
/* DIFFTIME is when the DIFF was generated on the frontend */ /* DIFFTIME is when the DIFF was generated on the frontend */
call symputx('DIFFTIME',DIFFTIME); call symputx('DIFFTIME',DIFFTIME);
putlog (_all_)(=);
run; run;
%global action is_err err_msg msg; %global action is_err err_msg msg;
@ -190,7 +191,7 @@ run;
and REVIEW_STATUS_ID ne "SUBMITTED"; and REVIEW_STATUS_ID ne "SUBMITTED";
%let authcheck=%mf_getattrn(work.authAPP,NLOBS); %let authcheck=%mf_getattrn(work.authAPP,NLOBS);
%if &authcheck=0 or &prev_upload_check=1 %then %do; %if &authcheck=0 or &prev_upload_check=1 %then %do;
%put WARNING: authcheck=&authcheck prev_upload_check=&prev_upload_check; %put %str(WARN)ING: &=authcheck &=prev_upload_check;
data apPARAMS; data apPARAMS;
AUTHORISED=&authcheck; AUTHORISED=&authcheck;
PREV_UPLOAD_CHECK=&prev_upload_check; PREV_UPLOAD_CHECK=&prev_upload_check;

View File

@ -34,12 +34,17 @@ data work.jsdata;
SOME_DATETIME=put(dttm2,datetime19.); SOME_DATETIME=put(dttm2,datetime19.);
some_time=put(tm2,time.); some_time=put(tm2,time.);
drop dt2 dttm2 tm2; drop dt2 dttm2 tm2;
_____DELETE__THIS__RECORD_____='No';
if _n_=1 then do; if _n_=1 then do;
primary_key_field=sum(&maxpk,1); _____DELETE__THIS__RECORD_____='Yes';
some_char=' leadingblanks';
some_num=._;
output; output;
_____DELETE__THIS__RECORD_____='No';
some_char=' leadingblanks';
some_bestnum=._;
/* modify 1 record and add 4 more */
do primary_key_field=&maxpk to (&maxpk+5);
some_num=ranuni(0);
output;
end;
end; end;
else stop; else stop;
run; run;
@ -52,7 +57,7 @@ run;
) )
%let status=0; %let status=0;
data work.sasparams; data _null_;
set web1.sasparams; set web1.sasparams;
putlog (_all_)(=); putlog (_all_)(=);
if status='SUCCESS' then call symputx('status',1); if status='SUCCESS' then call symputx('status',1);
@ -91,7 +96,7 @@ data _null_;
/* the JSON libname engine removes leading blanks!!!! */ /* the JSON libname engine removes leading blanks!!!! */
if index(_infile_,' leadingblanks') then call symputx('leadcheck',1); if index(_infile_,' leadingblanks') then call symputx('leadcheck',1);
/* there is no clean way to send a special missing - so is sent as string */ /* there is no clean way to send a special missing - so is sent as string */
if index(_infile_,',"SOME_NUM":"_"') then call symputx('speshcheck',1); if index(_infile_,',"SOME_BESTNUM":"_"') then call symputx('speshcheck',1);
run; run;
@ -103,3 +108,32 @@ run;
iftrue=(&leadcheck=1), iftrue=(&leadcheck=1),
desc=Checking special characters were applied desc=Checking special characters were applied
) )
/* Now actually approve the table */
data work.sascontroltable;
ACTION='APPROVE_TABLE';
TABLE="&dsid";
/* difftime is numeric for approve action */
DIFFTIME="%sysfunc(datetime())";
output;
stop;
run;
%mx_testservice(&appLoc/services/auditors/postdata,
viyacontext=&defaultcontext,
inputdatasets=work.sascontroltable,
outlib=web3,
outref=wb3,
mdebug=&sasjs_mdebug
)
%let status=0;
data _null_;
set web3.apparams;
putlog (_all_)(=);
if response='SUCCESS!' then call symputx('status',1);
run;
%mp_assert(
iftrue=(&status=1 and &syscc=0),
desc=Checking successful submission
)

View File

@ -41,6 +41,9 @@
<h5> xl_rules </h5> <h5> xl_rules </h5>
<h5> query </h5> <h5> query </h5>
<h5> versions </h5>
history of DC versions for this particular table
<h4> SAS Macros </h4> <h4> SAS Macros </h4>
@li dc_assignlib.sas @li dc_assignlib.sas
@ -63,6 +66,7 @@
@li mpe_columnlevelsecurity.sas @li mpe_columnlevelsecurity.sas
@li mpe_dsmeta.sas @li mpe_dsmeta.sas
@li mpe_getlabels.sas @li mpe_getlabels.sas
@li mpe_getversions.sas
@li mpe_filtermaster.sas @li mpe_filtermaster.sas
@li mpe_runhook.sas @li mpe_runhook.sas
@ -631,9 +635,14 @@ create table dqdata as
select distinct "&&base_col&x" as base_col length=32 select distinct "&&base_col&x" as base_col length=32
,"&source" as rule_value length=74 ,"&source" as rule_value length=74
,cats(&col) as rule_data length=1000 ,cats(&col) as rule_data length=1000
,0 as selectbox_order ,&col as tmp_order
from &lib..&ds from &lib..&ds
order by 1; order by tmp_order;
/* ensure both numerics and char vals are ordered correctly */
data work.dqdata&x (drop=tmp_order);
set work.dqdata&x;
selectbox_order=_n_;
run;
%mp_abort(iftrue= (&syscc ne 0) %mp_abort(iftrue= (&syscc ne 0)
,mac=&_program ,mac=&_program
,msg=%str(syscc=&syscc when selecting &&base_col&x from &orig_libds) ,msg=%str(syscc=&syscc when selecting &&base_col&x from &orig_libds)
@ -667,6 +676,13 @@ run;
%mpe_dsmeta(&libds, outds=dsmeta) %mpe_dsmeta(&libds, outds=dsmeta)
%mpe_getversions(&mpelib,
%scan(&orig_libds,1,.),
%scan(&orig_libds,2,.),
outds=versions
)
/* send to the client */ /* send to the client */
%webout(OPEN) %webout(OPEN)
%webout(OBJ,approvers) %webout(OBJ,approvers)
@ -678,6 +694,7 @@ run;
%webout(OBJ,query) %webout(OBJ,query)
%webout(OBJ,sasdata1,fmt=N,missing=STRING,showmeta=YES,dslabel=sasdata) %webout(OBJ,sasdata1,fmt=N,missing=STRING,showmeta=YES,dslabel=sasdata)
%webout(OBJ,sasparams) %webout(OBJ,sasparams)
%webout(OBJ,versions)
%webout(OBJ,xl_rules) %webout(OBJ,xl_rules)
%webout(CLOSE) %webout(CLOSE)

View File

@ -0,0 +1,157 @@
/**
@file restore.sas
@brief Restores a data version
@details Only applies if the history is stored in the audit table
<h4> SAS Macros </h4>
@li dc_assignlib.sas
@li mf_nobs.sas
@li mp_abort.sas
@li mp_ds2csv.sas
@li mp_stripdiffs.sas
@li mpeinit.sas
@li mpe_checkrestore.sas
@li mpe_loader.sas
<h4> Service Inputs </h4>
<h5> restore_in </h5>
|LOAD_REF:$32|
|---|
|DCXXXXXX|
@version 9.2
@author 4GL Apps Ltd
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
and may not be re-distributed or re-sold without the express permission of
4GL Apps Ltd.
**/
%mpeinit()
%let loadref=;
data _null_;
set work.restore_in;
call symputx('loadref',load_ref);
run;
/**
* Check if user has basic access permission to RESTORE the table
*/
%put checking access;
%global allow_restore reason;
%mpe_checkrestore(&loadref,outresult=ALLOW_RESTORE,outreason=REASON)
%mp_abort(iftrue= (&ALLOW_RESTORE ne YES)
,mac=&_program..sas
,msg=%str(Cannot restore because: &reason)
)
/* grab the base DS */
proc sql noprint;
select cats(base_lib,'.',base_ds) into: tgtds
from &mpelib..mpe_submit
where TABLE_ID="&loadref";
/* find the audit table */
select coalescec(audit_libds,"&mpelib..MPE_AUDIT"), loadtype, var_txto
into: difftable, :loadtype, :txto
from &mpelib..MPE_TABLES
where libref="%scan(&tgtds,1,.)"
& dsn="%scan(&tgtds,2,.)"
& &dc_dttmtfmt<tx_to;
%mp_abort(iftrue= ("&difftable"="0")
,mac=&_program..sas
,msg=%str(No audit table configured for &tgtds)
)
/* assign the library */
%dc_assignlib(READ,%scan(&tgtds,1,.))
/* if the target is an SCD2 table, create a view to get current snapshot */
%let fltr=%sysfunc(ifc(&loadtype=TXTEMPORAL,&dc_dttmtfmt<&txto,1=1));
/* extract the differences to be applied */
%mp_stripdiffs(&tgtds,&loadref,&difftable
,outds=work.mp_stripdiffs
,filtervar=fltr
,mdebug=&sasjs_mdebug
)
/* abort if any issues */
%mp_abort(iftrue= (&syscc>0)
,mac=&_program..sas
,msg=%str(syscc=&syscc after stripdiffs)
)
%mp_abort(iftrue= (%mf_nobs(work.mp_stripdiffs)=0)
,mac=&_program..sas
,msg=%str(THERE ARE NO DIFFERENCES TO APPLY)
)
/* create a new load ref */
%let mperef=DC%left(%sysfunc(datetime(),B8601DT19.3))_%substr(
%sysfunc(ranuni(0)),3,6)_%substr(%str(&sysjobid ),1,4);
/* Create package folder */
%let dir=&mpelocapprovals/&mperef;
%mf_mkdir(&dir)
options notes mprint;
libname approve "&dir";
/* take copy of macvars */
data _null_;
file "&dir/macvars.sas";
set sashelp.vmacro;
where scope='GLOBAL';
put '%let ' name '=' value ';';
run;
/* copy the diffs dataset */
data approve.jsdset;
length _____DELETE__THIS__RECORD_____ $3;
if 0 then call missing(_____DELETE__THIS__RECORD_____);
set work.mp_stripdiffs;
run;
/* export to csv */
%mp_ds2csv(approve.jsdset
,dlm=COMMA
,outfile="&dir/%trim(&tgtds).csv"
,outencoding="UTF-8"
,headerformat=NAME
,termstr=CRLF
)
%mp_abort(iftrue= (&syscc ne 0)
,mac=&_program
,msg=%str(syscc=&syscc when writing the CSV)
)
%mpe_loader(mperef=&mperef
,submitted_reason_txt=Restoring &loadref
,dc_dttmtfmt=&dc_dttmtfmt
)
%mp_abort(mode=INCLUDE)
%mp_abort(
iftrue=(%sysfunc(fileexist(%sysfunc(pathname(work))/mf_abort.error))=1)
,mac=&_program..sas
,msg=%str(mf_abort.error=1)
)
%mp_abort(iftrue= (&syscc ne 0)
,mac=&_program..sas
,msg=%str(syscc=&syscc)
)
/* send relevant SUCCESS values */
data work.restore_out;
loadref="&mperef";
run;
%webout(OPEN)
%webout(OBJ,restore_out)
%webout(CLOSE)

View File

@ -0,0 +1,109 @@
/**
@file
@brief testing restore process
<h4> SAS Macros </h4>
@li mf_getuniquefileref.sas
@li mx_testservice.sas
@li mp_assert.sas
**/
%let _program=&appLoc/services/editors/restore;
/* take a snapshot of the table for later comparison */
proc sort data=&mpelib..mpe_x_test out=work.origds;
by primary_key_field;
run;
/* run an update */
%mx_testservice(&appLoc/tests/services/auditors/postdata.test.1,
viyacontext=&defaultcontext
)
/* grab the loadref for later reversion */
%let loadref=0;
data APPROVE1;
set &mpelib..mpe_submit end=last;
if last then call symputx('loadref',table_id);
run;
/* run another update for good measure */
%mx_testservice(&appLoc/tests/services/auditors/postdata.test.1,
viyacontext=&defaultcontext
)
/* now we are ready to revert */
data work.restore_in;
load_ref="&loadref";
output;
stop;
run;
%mx_testservice(&_program,
viyacontext=&defaultcontext,
inputdatasets=work.restore_in,
outlib=web1,
mdebug=&sasjs_mdebug
)
/* check for success */
%let loadref=0;
data work.restore_out;
set web1.restore_out;
putlog (_all_)(=);
call symputx('newref',loadref);
run;
%mp_assert(
iftrue=(&newref ne 0),
desc=Checking successful submission of a reversion,
outds=work.test_results
)
/* approve the reversion */
data work.sascontroltable;
ACTION='APPROVE_TABLE';
TABLE="&newref";
/* difftime is numeric for approve action */
DIFFTIME="%sysfunc(datetime())";
output;
stop;
run;
%mx_testservice(&appLoc/services/auditors/postdata,
viyacontext=&defaultcontext,
inputdatasets=work.sascontroltable,
outlib=web3,
outref=wb3,
mdebug=&sasjs_mdebug
)
%let status=0;
data _null_;
set web3.apparams;
putlog (_all_)(=);
if response='SUCCESS!' then call symputx('status',1);
run;
%mp_assert(
iftrue=(&status=1 and &syscc=0),
desc=Checking successful submission of reversion
)
/* compare snapshot with latest data */
proc sort data=&mpelib..mpe_x_test out=work.compareds;
by primary_key_field;
run;
proc compare base=work.origds compare=work.compareds
out=work.resultds outnoequal;
run;
data _null_;
set work.resultds;
if &sasjs_mdebug=1 then putlog (_all_)(=);
if _n_>10 then stop;
run;
%mp_assert(
iftrue=(&sysinfo le 41),
desc=Checking compare of MPE_X_TEST,
outds=work.test_results
)

View File

@ -5,8 +5,27 @@
<h4> SAS Macros </h4> <h4> SAS Macros </h4>
@li mf_getengine.sas @li mf_getengine.sas
@li dc_assignlib.sas
@li mp_abort.sas @li mp_abort.sas
@li mpe_checkrestore.sas
<h4> Service Inputs </h4>
<h5> sascontroltable </h5>
@li table table ID or LOAD_REF used to uniquely identify a staged change
<h4> Service Outputs </h4>
<h5> work.jsparams </h5>
Mainly sourced from MPE_SUBMIT plus some extra cols:
@li LIB_ENGINE Library engine
@li allow_restore YES if a user can restore, else NO
@li REASON reason why a restore is / is no possible
<h4> Data Inputs </h4>
@li MPE_AUDIT
@li MPE_COLUMN_LEVEL_SECURITY
@li MPE_ROW_LEVEL_SECURITY
@li MPE_SUBMIT
@version 9.2 @version 9.2
@author 4GL Apps Ltd @author 4GL Apps Ltd
@ -24,10 +43,6 @@ data _null_;
call symputx('table',table); call symputx('table',table);
run; run;
%dc_assignlib(WRITE,%scan(&table,1,.))
%let max_ver_dttm=0;
data APPROVE1; data APPROVE1;
set &mpelib..mpe_submit set &mpelib..mpe_submit
(rename=(SUBMITTED_ON_DTTM=submitted_on REVIEWED_ON_DTTM=REVIEWED_ON)); (rename=(SUBMITTED_ON_DTTM=submitted_on REVIEWED_ON_DTTM=REVIEWED_ON));
@ -39,9 +54,18 @@ data APPROVE1;
SUBMITTED_ON_DTTM=put(submitted_on,datetime19.); SUBMITTED_ON_DTTM=put(submitted_on,datetime19.);
run; run;
data jsParams; /**
* Check if user has basic access permission to RESTORE the table
*/
%put checking access;
%global allow_restore reason;
%mpe_checkrestore(&table,outresult=ALLOW_RESTORE,outreason=REASON)
data work.jsParams;
set approve1; set approve1;
LIB_ENGINE="%mf_getEngine(&base_lib)"; LIB_ENGINE="%mf_getEngine(&base_lib)";
allow_restore="&allow_restore";
REASON="&reason";
run; run;
%mp_abort(iftrue= (&syscc ne 0) %mp_abort(iftrue= (&syscc ne 0)

View File

@ -0,0 +1,97 @@
/**
@file
@brief testing getchangeinfo service
<h4> SAS Macros </h4>
@li mp_assertcolvals.sas
@li mf_getuniquefileref.sas
**/
%let _program=&appLoc/services/public/getchangeinfo;
/**
* First part - stage some data (for diffing)
*/
data work.sascontroltable;
action='LOAD';
message="getdiffs prep";
libds="&dclib..MPE_X_TEST";
output;
stop;
run;
proc sql noprint;
select max(primary_key_field) into: maxpk from &dclib..mpe_x_test;
data work.jsdata;
set &dclib..mpe_x_test(rename=(
some_date=dt2 SOME_DATETIME=dttm2 SOME_TIME=tm2)
);
/* for now, the adapter sends these as strings */
some_date=put(dt2,date9.);
SOME_DATETIME=put(dttm2,datetime19.);
some_time=put(tm2,time.);
drop dt2 dttm2 tm2;
_____DELETE__THIS__RECORD_____='No';
if _n_=1 then do;
primary_key_field=sum(&maxpk,1);
some_char=' leadingblanks';
some_num=._;
output;
end;
else if _n_<3 then do;
SOME_NUM=ranuni(0);
end;
else stop;
run;
%mx_testservice(&appLoc/services/editors/stagedata,
viyacontext=&defaultcontext,
inputdatasets=work.jsdata work.sascontroltable,
outlib=web1,
mdebug=&sasjs_mdebug
)
%let status=0;
data work.sasparams;
set web1.sasparams;
putlog (_all_)(=);
if status='SUCCESS' then call symputx('status',1);
call symputx('dsid',dsid);
run;
%mp_assert(
iftrue=(&status=1 and &syscc=0),
desc=Checking successful submission
)
/* now call getchangeinfo */
%let f3=%mf_getuniquefileref();
data _null_;
file &f3 termstr=crlf;
put 'TABLE:$43.';
put "&dsid";
run;
%mp_testservice(&_program,
viyacontext=&defaultcontext,
inputfiles=&f3:sascontroltable,
outlib=web3,
mdebug=&sasjs_mdebug
)
data work.jsparams;
set web3.jsparams;
putlog (_all_)(=);
call symputx('ALLOW_RESTORE',ALLOW_RESTORE);
run;
%mp_assert(
iftrue=(&syscc=0),
desc=Checking successful execution
)
%mp_assert(
iftrue=(%mf_nobs(work.jsparams)=1),
desc=Checking data was returned
)
%mp_assert(
iftrue=(&ALLOW_RESTORE=NO),
desc=Checking admin user cannot restore - as table was not approved
)

View File

@ -0,0 +1,48 @@
/**
@file getversion.sas
@brief get a specific (previous) version of a particular table
@details Used to fetch a version of a table as at a previous point in time
Delivered as part of this issue: https://git.datacontroller.io/dc/dc/issues/84
<h4> Service Inputs </h4>
<h5> getversion_input </h5>
|LIBREF:$char8.|DS:$char32.|TS: 8.|
|---|---|---|
|SOMELIB|SOMEDS|1341344.804|
<h4> Service Outputs </h4>
<h5> work.getversion_output </h5>
The data for a particular version
<h4> SAS Macros </h4>
@li mf_getuser.sas
@li mpeinit.sas
@li mpe_getvars.sas
@version 9.2
@author 4GL Apps Ltd
**/
%mpeinit()
%global LIBREF DS;
/* load parameters */
%mpe_getvars(getversion_input, getversion_input)
%mp_abort(iftrue= (&syscc ne 0 )
,mac=&_program
,msg=%str(Issue on startup)
)
/* todo */
%webout(OPEN)
%webout(OBJ,getversion_output)
%webout(CLOSE)
%mpeterm()

View File

@ -31,6 +31,9 @@
@li TABLEURI @li TABLEURI
@li VARS @li VARS
<h5> versions </h5>
history of DC versions for this particular table
<h5> viewdata </h5> <h5> viewdata </h5>
The raw data from the target table. The raw data from the target table.
@ -51,6 +54,7 @@
@li mp_validatecol.sas @li mp_validatecol.sas
@li mpe_columnlevelsecurity.sas @li mpe_columnlevelsecurity.sas
@li mpe_dsmeta.sas @li mpe_dsmeta.sas
@li mpe_getversions.sas
@li mpe_filtermaster.sas @li mpe_filtermaster.sas
@ -81,7 +85,7 @@
data work.intest; data work.intest;
length libds $41 filter_rk 8. searchval $100 searchtype $4; length libds $41 filter_rk 8. searchval $100 searchtype $4;
set work.SASCONTROLTABLE; set work.SASCONTROLTABLE;
call symputx('orig_libds',libds);
/* validate filter_rk */ /* validate filter_rk */
if filter_rk le 0 then filter_rk=-1; if filter_rk le 0 then filter_rk=-1;
@ -353,12 +357,19 @@ run;
%mpe_dsmeta(&libds, outds=dsmeta) %mpe_dsmeta(&libds, outds=dsmeta)
%mpe_getversions(&mpelib,
%scan(&orig_libds,1,.),
%scan(&orig_libds,2,.),
outds=versions
)
%webout(OPEN) %webout(OPEN)
%webout(OBJ,cls_rules) %webout(OBJ,cls_rules)
%webout(OBJ,cols) %webout(OBJ,cols)
%webout(OBJ,dsmeta) %webout(OBJ,dsmeta)
%webout(OBJ,query) %webout(OBJ,query)
%webout(OBJ,sasparams) %webout(OBJ,sasparams)
%webout(OBJ,versions)
%webout(OBJ,viewData2,fmt=Y,missing=STRING,showmeta=YES,dslabel=viewdata) %webout(OBJ,viewData2,fmt=Y,missing=STRING,showmeta=YES,dslabel=viewdata)
%webout(CLOSE) %webout(CLOSE)

View File

@ -32,7 +32,7 @@ data groups
a=1; a=1;
grpassn=metadata_getnasn(uri,"IdentityGroups",a,groupuri); grpassn=metadata_getnasn(uri,"IdentityGroups",a,groupuri);
if grpassn in (-3,-4) then do; if grpassn in (-3,-4) then do;
putlog "WARNING: No groups found for "; putlog "%str(WARN)ING: No groups found for ";
end; end;
else do while (grpassn > 0); else do while (grpassn > 0);
rc=metadata_getattr(groupuri, "Name", groupname); rc=metadata_getattr(groupuri, "Name", groupname);

View File

@ -9,12 +9,15 @@
@li mf_getplatform.sas @li mf_getplatform.sas
@li mpeinit2.sas @li mpeinit2.sas
@li mp_abort.sas @li mp_abort.sas
@li mp_init.sas
@li mp_testservice.sas @li mp_testservice.sas
REMOVE THAT LAST MACRO REMOVE THAT LAST MACRO
**/ **/
%mp_init()
%let syscc=0; %let syscc=0;
%global apploc _program dclib defaultcontext _debug sasjs_mdebug dc_dttmtfmt; %global apploc _program dclib defaultcontext _debug sasjs_mdebug dc_dttmtfmt;