Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8010d4c0c | ||
| a57b49c936 | |||
| a84ba41ea9 | |||
| dc200646f7 | |||
| e273e870ef | |||
|
|
6fc34aca00 | ||
| f97ac70678 | |||
| 6ceb681463 | |||
| 716ee6eba0 | |||
| f6b0f6b0cd | |||
| 737a652ff0 | |||
| 2995e5c9dc | |||
| 338c7a2e41 | |||
| ad27358deb | |||
| 495754816c | |||
|
|
96f2518af9 | ||
|
|
280bdeeb1b | ||
| 46cdeb0bab | |||
| d41f88f8bf | |||
| 815d6e97a8 | |||
| 4e35aefe41 | |||
| ca84915e43 | |||
|
|
31cc7e9e4d | ||
| 4ec107705e | |||
|
|
7740d2ac86 | ||
|
|
8c2aeacc85 | ||
|
|
8b8e8aec15 | ||
|
|
6ac3f660e9 | ||
| 7ee576a9c1 | |||
| f4c8699aaf | |||
|
|
4273ca6e5c | ||
|
|
4ba043b77e | ||
| 0169415ea2 | |||
| 86791dbaca | |||
| d5b58a3cbd | |||
| 3d8281d27e | |||
| b1a014c7bc | |||
| 505d0af2b3 | |||
| ece6bd1d78 | |||
| 8dc18b155a | |||
| aecd597687 | |||
| c99f106bae | |||
| d2fc7ae6fe | |||
|
|
c1c1d0055a | ||
| f37ec82d39 | |||
| 6e9e30e0f0 | |||
| 990ddb5cd3 | |||
| c6ebbb48bb | |||
| 95cc0b1c91 | |||
| 81b282f1f1 | |||
| 8c5b357dd2 | |||
| a13b2cbfd2 | |||
| 19617c2285 | |||
| f9794a973f | |||
| 65efe62d19 | |||
| 683ddcaf53 | |||
|
|
113e0bbc3c | ||
| 2af97e40bf | |||
|
|
83cbe3aece | ||
| ceac1ba614 | |||
|
|
765fdbdf9d | ||
|
|
43ae73c5f3 | ||
|
|
e57a0de8a9 | ||
|
|
3de491105b | ||
|
|
2fe690e962 | ||
|
|
b826d37086 | ||
|
|
bfbfd55fe7 | ||
|
|
15f38efd52 | ||
|
|
5d25681485 | ||
|
|
d26df376f8 | ||
| cff3fb3bad | |||
|
|
fb3c49aa8b | ||
|
|
af1657e226 | ||
|
|
d7c7302c12 | ||
| 26ce95f7c1 | |||
|
|
4924df2ef3 | ||
| 2e141a5d52 | |||
|
|
cb1978bcaf | ||
|
|
387f5122f1 | ||
|
|
db5887de21 | ||
| fe24d9bcbd | |||
|
|
6c6b1cbf46 | ||
|
|
4d65c9c999 | ||
| 4417279275 | |||
|
|
365f12996d | ||
|
|
ef1015f33b | ||
| b43dfb5cf4 | |||
|
|
225e693d1f |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.15.1
|
node-version: 24.5.0
|
||||||
|
|
||||||
- name: Install Google Chrome
|
- name: Install Google Chrome
|
||||||
run: |
|
run: |
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.15.1
|
node-version: 24.5.0
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.15.1]
|
node-version: [24.5.0]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -98,4 +98,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: Lighthouse results
|
name: Lighthouse results
|
||||||
path: client/lighthouse-reports
|
path: client/lighthouse-reports
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.14.0
|
node-version: 24.5.0
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
touch client/.npmrc
|
touch client/.npmrc
|
||||||
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
||||||
|
|
||||||
- name: Install Chrome for Angular tests
|
- name: Install Chrome for Angular tests
|
||||||
run: |
|
run: |
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
npm ci
|
npm ci
|
||||||
|
|
||||||
- name: Check audit
|
- name: Check audit
|
||||||
# Audit should fail and stop the CI if critical vulnerability found
|
# Audit should fail and stop the CI if critical vulnerability found
|
||||||
run: |
|
run: |
|
||||||
npm audit --audit-level=critical --omit=dev
|
npm audit --audit-level=critical --omit=dev
|
||||||
cd ./sas
|
cd ./sas
|
||||||
@@ -68,12 +68,12 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.14.0
|
node-version: 24.5.0
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
touch client/.npmrc
|
touch client/.npmrc
|
||||||
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
||||||
|
|
||||||
- run: apt-get update
|
- run: apt-get update
|
||||||
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||||
@@ -158,7 +158,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.14.0
|
node-version: 24.5.0
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
@@ -228,6 +228,8 @@ jobs:
|
|||||||
cp sasjs/utils/favicon.ico ../client/dist/favicon.ico
|
cp sasjs/utils/favicon.ico ../client/dist/favicon.ico
|
||||||
sasjs c -t server
|
sasjs c -t server
|
||||||
rm -rf sasjsbuild/tests
|
rm -rf sasjsbuild/tests
|
||||||
|
server_apploc="/Public/app/dc"
|
||||||
|
sed -i "s|apploc=\"[^\"]*\"|apploc=\"${server_apploc}\"|g" sasjsbuild/services/web/index.html
|
||||||
sasjs b -t server
|
sasjs b -t server
|
||||||
cp sasjsbuild/server.json.zip ./sasjs_server.json.zip
|
cp sasjsbuild/server.json.zip ./sasjs_server.json.zip
|
||||||
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,6 +14,7 @@ client/documentation
|
|||||||
client/**/sheet-crypto.tgz
|
client/**/sheet-crypto.tgz
|
||||||
client/.nx
|
client/.nx
|
||||||
client/libraries/sheet-crypto.tgz
|
client/libraries/sheet-crypto.tgz
|
||||||
|
client/lighthouse-reports
|
||||||
cypress.env.json
|
cypress.env.json
|
||||||
sasjsbuild
|
sasjsbuild
|
||||||
sasjsresults
|
sasjsresults
|
||||||
|
|||||||
101
CHANGELOG.md
101
CHANGELOG.md
@@ -1,3 +1,104 @@
|
|||||||
|
# [7.4.0](https://git.datacontroller.io/dc/dc/compare/v7.3.0...v7.4.0) (2026-02-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* cli bump for mf_getscheme support ([a84ba41](https://git.datacontroller.io/dc/dc/commit/a84ba41ea9f0c97ae24f0a572b8cf5ec200f2132))
|
||||||
|
* missing upcase on SNOW section, plus local sasjs target ([dc20064](https://git.datacontroller.io/dc/dc/commit/dc200646f7df2fd1910841f392c314532aae7581))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* SAS code changes for snowflake support ([e273e87](https://git.datacontroller.io/dc/dc/commit/e273e870efbf7875db869b760f2c7b1f39d571ae))
|
||||||
|
|
||||||
|
# [7.3.0](https://git.datacontroller.io/dc/dc/compare/v7.2.8...v7.3.0) (2026-02-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bump xlsx, add crypto-shim ([8dc18b1](https://git.datacontroller.io/dc/dc/commit/8dc18b155abfc20fd0b043e0d70bbbc17e6b6811))
|
||||||
|
* correctly applying deletes on viya, also ([46cdeb0](https://git.datacontroller.io/dc/dc/commit/46cdeb0babee6870553a41877cbe85204e7099c4))
|
||||||
|
* crypto module requirement for sheetjs/crypto package ([505d0af](https://git.datacontroller.io/dc/dc/commit/505d0af2b3b3c1c79c65045dcaffc263e0f8e796))
|
||||||
|
* disable parsing excel in web worker beacuse it breaks in the stream apps ([280bdee](https://git.datacontroller.io/dc/dc/commit/280bdeeb1b82f00689f46c68a3cde3f2d24bc18f))
|
||||||
|
* Display all contexts when installing DC on Viya ([d41f88f](https://git.datacontroller.io/dc/dc/commit/d41f88f8bf5bb2c725ee3edba085e8961ab8c727))
|
||||||
|
* **edit:** use cellValidation keys and hotDataSchema to fill in defaults on add row ([4957548](https://git.datacontroller.io/dc/dc/commit/495754816c0e757b8f8b1c0ad51246dc7b65d957))
|
||||||
|
* enabling closeouts for UPDATE in CAS tables ([8b8e8ae](https://git.datacontroller.io/dc/dc/commit/8b8e8aec159ff2f50cfa4683bcd7a25aabb75bf8))
|
||||||
|
* enabling rollback when the table has formatted values ([815d6e9](https://git.datacontroller.io/dc/dc/commit/815d6e97a8e304d79d48cc949ba126e02a318dc1))
|
||||||
|
* improvements to validations ([6ceb681](https://git.datacontroller.io/dc/dc/commit/6ceb6814633691b6d4ac2cb898cfb75e9d609102))
|
||||||
|
* remove IE checks and conditions ([ece6bd1](https://git.datacontroller.io/dc/dc/commit/ece6bd1d787d722531334fc4f1396a94cf6d92ec))
|
||||||
|
* updates to demodata to enable auto CAS promote ([7740d2a](https://git.datacontroller.io/dc/dc/commit/7740d2ac8694295b33b40a30603d8239818896f5))
|
||||||
|
* upgrade angular core and compiler ([aecd597](https://git.datacontroller.io/dc/dc/commit/aecd5976875a7c01189248c5f5aa3478b28c1ab2))
|
||||||
|
* using fcopy instead of binary copy for file upload, for Viya 2026 compatibility ([716ee6e](https://git.datacontroller.io/dc/dc/commit/716ee6eba0a28f4f0a7a96b0719caf15da9b6e78))
|
||||||
|
* **viewer:** search causing blank Handsontable ([338c7a2](https://git.datacontroller.io/dc/dc/commit/338c7a2e418c47e34331bd04718cd816f978837c)), closes [#206](https://git.datacontroller.io/dc/dc/issues/206)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* adding demo data job ([8c2aeac](https://git.datacontroller.io/dc/dc/commit/8c2aeacc85da5c106c356709cefcb412ed0a71db))
|
||||||
|
* **dq rules:** notnull validation when invalid cell, will auto populate a default value ([96f2518](https://git.datacontroller.io/dc/dc/commit/96f2518af9e547956be5862a1322d9ab8e07369b))
|
||||||
|
|
||||||
|
## [7.2.8](https://git.datacontroller.io/dc/dc/compare/v7.2.7...v7.2.8) (2026-02-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bump adapter version ([f4c8699](https://git.datacontroller.io/dc/dc/commit/f4c8699aaf0b1e01b447296978a4f6dedc8903f9))
|
||||||
|
|
||||||
|
## [7.2.7](https://git.datacontroller.io/dc/dc/compare/v7.2.6...v7.2.7) (2026-02-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* dclib not found error in getchangeinfo job ([86791db](https://git.datacontroller.io/dc/dc/commit/86791dbaca39034a19bf8f34efbddf898c57f2f7))
|
||||||
|
|
||||||
|
## [7.2.6](https://git.datacontroller.io/dc/dc/compare/v7.2.5...v7.2.6) (2026-01-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **deps:** update angular and moment ([8c5b357](https://git.datacontroller.io/dc/dc/commit/8c5b357dd286db331a6dcdeb3fd499fe3b634288))
|
||||||
|
|
||||||
|
## [7.2.5](https://git.datacontroller.io/dc/dc/compare/v7.2.4...v7.2.5) (2025-12-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* (build) rebuilt package-lock files ([bfbfd55](https://git.datacontroller.io/dc/dc/commit/bfbfd55fe7e2dff3ce707763a2c7939ff365318b))
|
||||||
|
* (deps) bump @sasjs/cli and @sasjs/core ([d7c7302](https://git.datacontroller.io/dc/dc/commit/d7c7302c12ac60f355ab9b3b1b461fcf7d0719b8))
|
||||||
|
* (deps) bumped @sasjs/core, @sasjs/cli, @sasjs/utils and @sasjs/adapter ([af1657e](https://git.datacontroller.io/dc/dc/commit/af1657e226a4efd22cc87401a3850c4a665c2680))
|
||||||
|
* configurable audit table on restore check ([26ce95f](https://git.datacontroller.io/dc/dc/commit/26ce95f7c1d2260f81c240cd6b058db154d997e4)), closes [#193](https://git.datacontroller.io/dc/dc/issues/193)
|
||||||
|
* improved testing ([fb3c49a](https://git.datacontroller.io/dc/dc/commit/fb3c49aa8bfdc6acf2ae3034b885010dcdce32a6))
|
||||||
|
* output values to intended macro variables ([43ae73c](https://git.datacontroller.io/dc/dc/commit/43ae73c5f3ad919394201f54984b61bb2a52fcfe))
|
||||||
|
|
||||||
|
## [7.2.4](https://git.datacontroller.io/dc/dc/compare/v7.2.3...v7.2.4) (2025-10-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* ensure reload after applying licence key ([cb1978b](https://git.datacontroller.io/dc/dc/commit/cb1978bcaf23b0bf45b5d3b78b9707fd4e48a5f4))
|
||||||
|
* snyk report security patches ([387f512](https://git.datacontroller.io/dc/dc/commit/387f5122f1ea6dff55d23c9223f17737283a94d3))
|
||||||
|
|
||||||
|
## [7.2.3](https://git.datacontroller.io/dc/dc/compare/v7.2.2...v7.2.3) (2025-10-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* opening second table in viewer throws an error ([6c6b1cb](https://git.datacontroller.io/dc/dc/commit/6c6b1cbf460e5291ec746af017e764b894fff8d5))
|
||||||
|
|
||||||
|
## [7.2.2](https://git.datacontroller.io/dc/dc/compare/v7.2.1...v7.2.2) (2025-09-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* jsrsasign, @sasjs/cli bump ([365f129](https://git.datacontroller.io/dc/dc/commit/365f12996db3ef50a4f4f099d5af15696c43bb42))
|
||||||
|
|
||||||
|
## [7.2.1](https://git.datacontroller.io/dc/dc/compare/v7.2.0...v7.2.1) (2025-08-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* removing localhost from index.html ([225e693](https://git.datacontroller.io/dc/dc/commit/225e693d1fd4381f2b8ce42fecb508f0a9e9dad8))
|
||||||
|
|
||||||
# [7.2.0](https://git.datacontroller.io/dc/dc/compare/v7.1.1...v7.2.0) (2025-08-08)
|
# [7.2.0](https://git.datacontroller.io/dc/dc/compare/v7.1.1...v7.2.0) (2025-08-08)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,8 @@
|
|||||||
"zone.js",
|
"zone.js",
|
||||||
"text-encoding",
|
"text-encoding",
|
||||||
"crypto-js/md5",
|
"crypto-js/md5",
|
||||||
|
"crypto-js/sha1",
|
||||||
|
"crypto-js/sha512",
|
||||||
"buffer",
|
"buffer",
|
||||||
"numbro",
|
"numbro",
|
||||||
"@clr/icons",
|
"@clr/icons",
|
||||||
@@ -51,26 +53,22 @@
|
|||||||
"base64-arraybuffer",
|
"base64-arraybuffer",
|
||||||
"@handsontable/formulajs"
|
"@handsontable/formulajs"
|
||||||
],
|
],
|
||||||
"polyfills": [
|
"polyfills": ["src/polyfills.ts", "zone.js"],
|
||||||
"src/polyfills.ts",
|
|
||||||
"zone.js"
|
|
||||||
],
|
|
||||||
"outputPath": "dist",
|
"outputPath": "dist",
|
||||||
"resourcesOutputPath": "images",
|
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"main": "src/main.ts",
|
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/images"
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "src/images",
|
||||||
|
"output": "images"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": ["src/styles.scss"],
|
||||||
"src/styles.scss"
|
"scripts": ["node_modules/marked/marked.min.js"],
|
||||||
],
|
"webWorkerTsConfig": "tsconfig.worker.json",
|
||||||
"scripts": [
|
"main": "src/main.ts"
|
||||||
"node_modules/marked/marked.min.js"
|
|
||||||
],
|
|
||||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -103,9 +101,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"vendorChunk": true,
|
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"buildOptimizer": false,
|
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
"namedChunks": true
|
"namedChunks": true
|
||||||
@@ -134,20 +130,11 @@
|
|||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
"options": {
|
"options": {
|
||||||
"polyfills": [
|
"polyfills": ["src/polyfills.ts", "zone.js", "zone.js/testing"],
|
||||||
"src/polyfills.ts",
|
|
||||||
"zone.js",
|
|
||||||
"zone.js/testing"
|
|
||||||
],
|
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": ["src/favicon.ico", "src/assets"],
|
||||||
"src/favicon.ico",
|
"styles": ["src/styles.scss"],
|
||||||
"src/assets"
|
|
||||||
],
|
|
||||||
"styles": [
|
|
||||||
"src/styles.scss"
|
|
||||||
],
|
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"karmaConfig": "karma.conf.js",
|
"karmaConfig": "karma.conf.js",
|
||||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||||
@@ -156,10 +143,7 @@
|
|||||||
"lint": {
|
"lint": {
|
||||||
"builder": "@angular-eslint/builder:lint",
|
"builder": "@angular-eslint/builder:lint",
|
||||||
"options": {
|
"options": {
|
||||||
"lintFilePatterns": [
|
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
|
||||||
"src/**/*.ts",
|
|
||||||
"src/**/*.html"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -309,6 +309,83 @@ context('excel tests: ', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('22 | Uploads password protected Excel and unlocks with correct password', (done) => {
|
||||||
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||||
|
|
||||||
|
cy.get('.buttonBar button:last-child')
|
||||||
|
.should('exist')
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
cy.get('input[type="file"]#file-upload')
|
||||||
|
.attachFile(`/${fixturePath}/regular_excel_password.xlsx`)
|
||||||
|
.then(() => {
|
||||||
|
// Wait for password modal to appear
|
||||||
|
cy.get('#filePasswordInput', { timeout: 10000 })
|
||||||
|
.should('be.visible')
|
||||||
|
.type('123123')
|
||||||
|
|
||||||
|
// Click Unlock button
|
||||||
|
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
|
||||||
|
|
||||||
|
// Click away the overlay
|
||||||
|
cy.get('.modal-footer .btn.btn-primary', { timeout: 5000 }).click()
|
||||||
|
|
||||||
|
// Verify file loads successfully
|
||||||
|
cy.get('.btn-upload-preview', { timeout: 60000 })
|
||||||
|
.should('be.visible')
|
||||||
|
.then(() => {
|
||||||
|
submitExcel()
|
||||||
|
rejectExcel(done)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('23 | Uploads password protected Excel and handles wrong password', (done) => {
|
||||||
|
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||||
|
|
||||||
|
cy.get('.buttonBar button:last-child')
|
||||||
|
.should('exist')
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
cy.get('input[type="file"]#file-upload')
|
||||||
|
.attachFile(`/${fixturePath}/regular_excel_password.xlsx`)
|
||||||
|
.then(() => {
|
||||||
|
// First attempt: Enter wrong password
|
||||||
|
cy.get('#filePasswordInput', { timeout: 10000 })
|
||||||
|
.should('be.visible')
|
||||||
|
.type('wrongpassword')
|
||||||
|
|
||||||
|
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
|
||||||
|
|
||||||
|
// Verify error message appears
|
||||||
|
cy.get('.modal-footer .color-red', { timeout: 10000 })
|
||||||
|
.should('be.visible')
|
||||||
|
.should('contain', "Sorry that didn't work, try again.")
|
||||||
|
|
||||||
|
// Modal should still be open for retry
|
||||||
|
cy.get('#filePasswordInput')
|
||||||
|
.should('be.visible')
|
||||||
|
.clear()
|
||||||
|
.type('123123')
|
||||||
|
|
||||||
|
// Second attempt: Enter correct password
|
||||||
|
cy.get('.btn.btn-success-outline').should('not.be.disabled').click()
|
||||||
|
|
||||||
|
// Click away the overlay
|
||||||
|
cy.get('.modal-footer .btn.btn-primary', { timeout: 5000 }).click()
|
||||||
|
|
||||||
|
// Verify file loads successfully
|
||||||
|
cy.get('.btn-upload-preview', { timeout: 60000 })
|
||||||
|
.should('be.visible')
|
||||||
|
.then(() => {
|
||||||
|
submitExcel()
|
||||||
|
rejectExcel(done)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Large files break Cypress
|
// Large files break Cypress
|
||||||
|
|
||||||
// it ('? | Uploads Excel with size of 5MB', (done) => {
|
// it ('? | Uploads Excel with size of 5MB', (done) => {
|
||||||
|
|||||||
BIN
client/cypress/fixtures/excels/regular_excel_password.xlsx
Normal file
BIN
client/cypress/fixtures/excels/regular_excel_password.xlsx
Normal file
Binary file not shown.
BIN
client/libraries/xlsx-0.20.3.tgz
Normal file
BIN
client/libraries/xlsx-0.20.3.tgz
Normal file
Binary file not shown.
@@ -10,7 +10,7 @@ const check = (cwd) => {
|
|||||||
onlyAllow:
|
onlyAllow:
|
||||||
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
|
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
|
||||||
excludePackages:
|
excludePackages:
|
||||||
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;handsontable@16.0.1;hyperformula@2.7.1;hyperformula@3.0.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1'
|
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;handsontable@^16.0.1;handsontable@16.2.0;hyperformula@2.7.1;hyperformula@3.0.0;hyperformula@3.1.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1'
|
||||||
},
|
},
|
||||||
(error, json) => {
|
(error, json) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ module.exports = {
|
|||||||
ci: {
|
ci: {
|
||||||
collect: {
|
collect: {
|
||||||
settings: {
|
settings: {
|
||||||
preset: "desktop",
|
preset: 'desktop',
|
||||||
chromeFlags: "--no-sandbox --disable-dev-shm-usage"
|
chromeFlags: '--no-sandbox --disable-dev-shm-usage'
|
||||||
},
|
},
|
||||||
url: [
|
url: [
|
||||||
'http://localhost:5000/AppStream/clickme/#/home/tables',
|
'http://localhost:5000/AppStream/clickme/#/home/tables',
|
||||||
@@ -37,6 +37,10 @@ module.exports = {
|
|||||||
{ minScore: 0.4, aggregationMethod: 'median' }
|
{ minScore: 0.4, aggregationMethod: 'median' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
upload: {
|
||||||
|
target: 'filesystem',
|
||||||
|
outputDir: './lighthouse-reports'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11707
client/package-lock.json
generated
11707
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -32,26 +32,27 @@
|
|||||||
"compodoc:build": "compodoc -p tsconfig.doc.json --name 'Data Controller Client'",
|
"compodoc:build": "compodoc -p tsconfig.doc.json --name 'Data Controller Client'",
|
||||||
"compodoc:build-and-serve": "compodoc -p tsconfig.doc.json -s --name 'Data Controller Client'",
|
"compodoc:build-and-serve": "compodoc -p tsconfig.doc.json -s --name 'Data Controller Client'",
|
||||||
"compodoc:serve": "compodoc -s --name 'Data Controller Client'",
|
"compodoc:serve": "compodoc -s --name 'Data Controller Client'",
|
||||||
"lighthouse": "lhci autorun"
|
"lighthouse": "lhci autorun",
|
||||||
|
"ng": "ng"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^17.3.3",
|
"@angular/animations": "^19.2.18",
|
||||||
"@angular/cdk": "^17.3.3",
|
"@angular/cdk": "^19.2.19",
|
||||||
"@angular/common": "^17.3.3",
|
"@angular/common": "^19.2.18",
|
||||||
"@angular/compiler": "^17.3.3",
|
"@angular/compiler": "^19.2.18",
|
||||||
"@angular/core": "^17.3.3",
|
"@angular/core": "^19.2.18",
|
||||||
"@angular/forms": "^17.3.3",
|
"@angular/forms": "^19.2.18",
|
||||||
"@angular/platform-browser": "^17.3.3",
|
"@angular/platform-browser": "^19.2.18",
|
||||||
"@angular/platform-browser-dynamic": "^17.3.3",
|
"@angular/platform-browser-dynamic": "^19.2.18",
|
||||||
"@angular/router": "^17.3.3",
|
"@angular/router": "^19.2.18",
|
||||||
"@cds/core": "^6.15.1",
|
"@cds/core": "^6.15.1",
|
||||||
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
|
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
|
||||||
"@clr/icons": "^13.0.2",
|
"@clr/icons": "^13.0.2",
|
||||||
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
|
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
|
||||||
"@handsontable/angular-wrapper": "16.0.1",
|
"@handsontable/angular-wrapper": "16.0.1",
|
||||||
"@sasjs/adapter": "^4.12.2",
|
"@sasjs/adapter": "^4.16.3",
|
||||||
"@sasjs/utils": "^3.4.0",
|
"@sasjs/utils": "^3.5.3",
|
||||||
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
||||||
"@types/d3-graphviz": "^2.6.7",
|
"@types/d3-graphviz": "^2.6.7",
|
||||||
"@types/text-encoding": "0.0.35",
|
"@types/text-encoding": "0.0.35",
|
||||||
@@ -66,9 +67,9 @@
|
|||||||
"hyperformula": "^2.5.0",
|
"hyperformula": "^2.5.0",
|
||||||
"iconv-lite": "^0.5.0",
|
"iconv-lite": "^0.5.0",
|
||||||
"jquery-datetimepicker": "^2.5.21",
|
"jquery-datetimepicker": "^2.5.21",
|
||||||
"jsrsasign": "^10.2.0",
|
"jsrsasign": "^11.1.0",
|
||||||
"marked": "^5.0.0",
|
"marked": "^5.0.0",
|
||||||
"moment": "^2.26.0",
|
"moment": "^2.30.1",
|
||||||
"ngx-clipboard": "^16.0.0",
|
"ngx-clipboard": "^16.0.0",
|
||||||
"ngx-json-viewer": "file:libraries/ngx-json-viewer-3.2.1.tgz",
|
"ngx-json-viewer": "file:libraries/ngx-json-viewer-3.2.1.tgz",
|
||||||
"nodejs": "0.0.0",
|
"nodejs": "0.0.0",
|
||||||
@@ -81,18 +82,18 @@
|
|||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"vm": "^0.1.0",
|
"vm": "^0.1.0",
|
||||||
"webpack": "^5.91.0",
|
"webpack": "^5.91.0",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "file:libraries/xlsx-0.20.3.tgz",
|
||||||
"zone.js": "~0.14.4"
|
"zone.js": "~0.15.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^17.3.3",
|
"@angular-devkit/build-angular": "^19.2.19",
|
||||||
"@angular-eslint/builder": "17.3.0",
|
"@angular-eslint/builder": "19.8.1",
|
||||||
"@angular-eslint/eslint-plugin": "17.3.0",
|
"@angular-eslint/eslint-plugin": "19.8.1",
|
||||||
"@angular-eslint/eslint-plugin-template": "17.3.0",
|
"@angular-eslint/eslint-plugin-template": "19.8.1",
|
||||||
"@angular-eslint/schematics": "17.3.0",
|
"@angular-eslint/schematics": "19.8.1",
|
||||||
"@angular-eslint/template-parser": "17.3.0",
|
"@angular-eslint/template-parser": "19.8.1",
|
||||||
"@angular/cli": "^17.3.3",
|
"@angular/cli": "^19.2.19",
|
||||||
"@angular/compiler-cli": "^17.3.3",
|
"@angular/compiler-cli": "^19.2.18",
|
||||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||||
"@compodoc/compodoc": "^1.1.21",
|
"@compodoc/compodoc": "^1.1.21",
|
||||||
"@cypress/webpack-preprocessor": "^5.17.1",
|
"@cypress/webpack-preprocessor": "^5.17.1",
|
||||||
@@ -128,7 +129,7 @@
|
|||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"ts-loader": "^9.2.8",
|
"ts-loader": "^9.2.8",
|
||||||
"ts-node": "^3.3.0",
|
"ts-node": "^3.3.0",
|
||||||
"typescript": "~5.4.4",
|
"typescript": "~5.8.3",
|
||||||
"wait-on": "^6.0.1",
|
"wait-on": "^6.0.1",
|
||||||
"watch": "^1.0.2"
|
"watch": "^1.0.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Location } from '@angular/common'
|
|||||||
import '@clr/icons'
|
import '@clr/icons'
|
||||||
import '@clr/icons/shapes/all-shapes'
|
import '@clr/icons/shapes/all-shapes'
|
||||||
import { globals } from './_globals'
|
import { globals } from './_globals'
|
||||||
import * as moment from 'moment'
|
import moment from 'moment'
|
||||||
import { EventService } from './services/event.service'
|
import { EventService } from './services/event.service'
|
||||||
import { AppService } from './services/app.service'
|
import { AppService } from './services/app.service'
|
||||||
import { InfoModal } from './models/InfoModal'
|
import { InfoModal } from './models/InfoModal'
|
||||||
@@ -42,7 +42,8 @@ ClarityIcons.addIcons(
|
|||||||
selector: 'my-app',
|
selector: 'my-app',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.scss'],
|
styleUrls: ['./app.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
private dcAdapterSettings: DcAdapterSettings | undefined
|
private dcAdapterSettings: DcAdapterSettings | undefined
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HttpClientModule } from '@angular/common/http'
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { BrowserModule } from '@angular/platform-browser'
|
import { BrowserModule } from '@angular/platform-browser'
|
||||||
@@ -36,12 +36,12 @@ import { AppSettingsService } from './services/app-settings.service'
|
|||||||
InfoModalComponent,
|
InfoModalComponent,
|
||||||
ViyaApiExplorerComponent
|
ViyaApiExplorerComponent
|
||||||
],
|
],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
HttpClientModule,
|
|
||||||
ROUTING,
|
ROUTING,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
ClarityModule,
|
ClarityModule,
|
||||||
@@ -50,7 +50,12 @@ import { AppSettingsService } from './services/app-settings.service'
|
|||||||
DirectivesModule,
|
DirectivesModule,
|
||||||
NgxJsonViewerModule
|
NgxJsonViewerModule
|
||||||
],
|
],
|
||||||
providers: [AppService, SasStoreService, LicensingGuard, AppSettingsService],
|
providers: [
|
||||||
bootstrap: [AppComponent]
|
AppService,
|
||||||
|
SasStoreService,
|
||||||
|
LicensingGuard,
|
||||||
|
AppSettingsService,
|
||||||
|
provideHttpClient(withInterceptorsFromDi())
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { DcAdapterSettings } from '../models/DcAdapterSettings'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class DeployComponent implements OnInit {
|
export class DeployComponent implements OnInit {
|
||||||
public step: number = 0
|
public step: number = 0
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ import {
|
|||||||
selector: 'app-automatic-deploy',
|
selector: 'app-automatic-deploy',
|
||||||
templateUrl: './automatic.component.html',
|
templateUrl: './automatic.component.html',
|
||||||
styleUrls: ['./automatic.component.scss'],
|
styleUrls: ['./automatic.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class AutomaticComponent implements OnInit {
|
export class AutomaticComponent implements OnInit {
|
||||||
@Input() sasJs!: SASjs
|
@Input() sasJs!: SASjs
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import { SasService } from 'src/app/services/sas.service'
|
|||||||
selector: 'app-manual-deploy',
|
selector: 'app-manual-deploy',
|
||||||
templateUrl: './manual.component.html',
|
templateUrl: './manual.component.html',
|
||||||
styleUrls: ['./manual.component.scss'],
|
styleUrls: ['./manual.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ManualComponent implements OnInit {
|
export class ManualComponent implements OnInit {
|
||||||
@Input() sasJs!: SASjs
|
@Input() sasJs!: SASjs
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import { SasjsService } from 'src/app/services/sasjs.service'
|
|||||||
selector: 'app-sasjs-configurator',
|
selector: 'app-sasjs-configurator',
|
||||||
templateUrl: './sasjs-configurator.component.html',
|
templateUrl: './sasjs-configurator.component.html',
|
||||||
styleUrls: ['./sasjs-configurator.component.scss'],
|
styleUrls: ['./sasjs-configurator.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class SasjsConfiguratorComponent implements OnInit {
|
export class SasjsConfiguratorComponent implements OnInit {
|
||||||
@Input() sasJs!: SASjs
|
@Input() sasJs!: SASjs
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[appDragNdrop]'
|
selector: '[appDragNdrop]',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class DragNdropDirective {
|
export class DragNdropDirective {
|
||||||
@HostBinding('class.fileover') fileOver: boolean = false
|
@HostBinding('class.fileover') fileOver: boolean = false
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
import { FileUploader } from '../models/FileUploader.class'
|
import { FileUploader } from '../models/FileUploader.class'
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[appFileDrop]'
|
selector: '[appFileDrop]',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class FileDropDirective {
|
export class FileDropDirective {
|
||||||
@Input() uploader?: FileUploader
|
@Input() uploader?: FileUploader
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
import { FileUploader } from '../models/FileUploader.class'
|
import { FileUploader } from '../models/FileUploader.class'
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[appFileSelect]'
|
selector: '[appFileSelect]',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class FileSelectDirective {
|
export class FileSelectDirective {
|
||||||
@Input() uploader?: FileUploader
|
@Input() uploader?: FileUploader
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'
|
|||||||
* Calling functions in html is bad for performance
|
* Calling functions in html is bad for performance
|
||||||
*/
|
*/
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[ngVar]'
|
selector: '[ngVar]',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class NgVarDirective {
|
export class NgVarDirective {
|
||||||
@Input()
|
@Input()
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Directive, HostListener } from '@angular/core'
|
import { Directive, HostListener } from '@angular/core'
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[appStealFocus]'
|
selector: '[appStealFocus]',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class StealFocusDirective {
|
export class StealFocusDirective {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { HelperService } from 'src/app/services/helper.service'
|
|||||||
import { SasStoreService } from 'src/app/services/sas-store.service'
|
import { SasStoreService } from 'src/app/services/sas-store.service'
|
||||||
import { DcValidator } from 'src/app/shared/dc-validator/dc-validator'
|
import { DcValidator } from 'src/app/shared/dc-validator/dc-validator'
|
||||||
import { DcValidation } from 'src/app/shared/dc-validator/models/dc-validation.model'
|
import { DcValidation } from 'src/app/shared/dc-validator/models/dc-validation.model'
|
||||||
|
import { isEmpty } from 'src/app/shared/dc-validator/utils/isEmpty'
|
||||||
import {
|
import {
|
||||||
EditRecordDropdownChangeEvent,
|
EditRecordDropdownChangeEvent,
|
||||||
EditRecordInputFocusedEvent
|
EditRecordInputFocusedEvent
|
||||||
@@ -24,7 +25,8 @@ import { EditRecordModal } from '../../models/EditRecordModal'
|
|||||||
selector: 'app-edit-record',
|
selector: 'app-edit-record',
|
||||||
templateUrl: './edit-record.component.html',
|
templateUrl: './edit-record.component.html',
|
||||||
styleUrls: ['./edit-record.component.scss'],
|
styleUrls: ['./edit-record.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class EditRecordComponent implements OnInit {
|
export class EditRecordComponent implements OnInit {
|
||||||
@Input() currentRecord!: EditRecordModal
|
@Input() currentRecord!: EditRecordModal
|
||||||
@@ -145,23 +147,63 @@ export class EditRecordComponent implements OnInit {
|
|||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
async recordInputChange(event: any, colName: string) {
|
async recordInputChange(event: any, colName: string): Promise<void> {
|
||||||
const colRules = this.currentRecordValidator?.getRule(colName)
|
const colRules = this.currentRecordValidator?.getRule(colName)
|
||||||
const value = event.target.value
|
const value = event.target.value
|
||||||
|
|
||||||
this.helperService.debounceCall(300, () => {
|
this.helperService.debounceCall(300, () => {
|
||||||
this.validateRecordCol(colRules, value).then((valid: boolean) => {
|
this.validateRecordCol(colRules, value).then((valid: boolean) => {
|
||||||
const index = this.currentRecordInvalidCols.indexOf(colName)
|
this.updateValidationState(colName, valid)
|
||||||
|
|
||||||
if (valid) {
|
if (!valid) {
|
||||||
if (index > -1) this.currentRecordInvalidCols.splice(index, 1)
|
this.tryAutoPopulateNotNull(event, colName, colRules, value)
|
||||||
} else {
|
|
||||||
if (index < 0) this.currentRecordInvalidCols.push(colName)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the invalid columns list based on validation result
|
||||||
|
*/
|
||||||
|
private updateValidationState(colName: string, valid: boolean): void {
|
||||||
|
const index = this.currentRecordInvalidCols.indexOf(colName)
|
||||||
|
|
||||||
|
if (valid && index > -1) {
|
||||||
|
this.currentRecordInvalidCols.splice(index, 1)
|
||||||
|
} else if (!valid && index < 0) {
|
||||||
|
this.currentRecordInvalidCols.push(colName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-populates NOTNULL default value when the field is empty and has a default
|
||||||
|
*/
|
||||||
|
private tryAutoPopulateNotNull(
|
||||||
|
event: any,
|
||||||
|
colName: string,
|
||||||
|
colRules: DcValidation | undefined,
|
||||||
|
value: any
|
||||||
|
): void {
|
||||||
|
if (
|
||||||
|
!isEmpty(value) ||
|
||||||
|
!this.currentRecordValidator ||
|
||||||
|
!this.currentRecord
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValue =
|
||||||
|
this.currentRecordValidator.getNotNullDefaultValue(colName)
|
||||||
|
if (defaultValue === undefined) return
|
||||||
|
|
||||||
|
this.currentRecord[colName] = defaultValue
|
||||||
|
event.target.value = defaultValue
|
||||||
|
|
||||||
|
this.validateRecordCol(colRules, defaultValue).then((isValid: boolean) => {
|
||||||
|
this.updateValidationState(colName, isValid)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onNextRecordClick() {
|
onNextRecordClick() {
|
||||||
this.onNextRecord.emit()
|
this.onNextRecord.emit()
|
||||||
}
|
}
|
||||||
@@ -171,23 +213,8 @@ export class EditRecordComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public copyToClip(text: string) {
|
public copyToClip(text: string) {
|
||||||
const modalElement = document.querySelector('#recordModalRef .modal-title')
|
navigator.clipboard.writeText(text)
|
||||||
|
this.generatedRecordUrl = text
|
||||||
if (modalElement) {
|
|
||||||
const selBox = document.createElement('textarea')
|
|
||||||
selBox.style.position = 'fixed'
|
|
||||||
selBox.style.left = '0'
|
|
||||||
selBox.style.top = '0'
|
|
||||||
selBox.style.opacity = '0'
|
|
||||||
selBox.style.zIndex = '5000'
|
|
||||||
selBox.value = text
|
|
||||||
modalElement.appendChild(selBox)
|
|
||||||
selBox.focus()
|
|
||||||
selBox.select()
|
|
||||||
document.execCommand('copy')
|
|
||||||
modalElement.removeChild(selBox)
|
|
||||||
this.generatedRecordUrl = text
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateEditRecordUrl() {
|
async generateEditRecordUrl() {
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
|
|||||||
selector: 'app-upload-stater',
|
selector: 'app-upload-stater',
|
||||||
templateUrl: './upload-stater.component.html',
|
templateUrl: './upload-stater.component.html',
|
||||||
styleUrls: ['./upload-stater.component.scss'],
|
styleUrls: ['./upload-stater.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class UploadStaterComponent implements OnInit {
|
export class UploadStaterComponent implements OnInit {
|
||||||
public statesList: string[] = [] //States appended to be displayed
|
public statesList: string[] = [] //States appended to be displayed
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { Col } from '../shared/dc-validator/models/col.model'
|
|||||||
import { DcValidation } from '../shared/dc-validator/models/dc-validation.model'
|
import { DcValidation } from '../shared/dc-validator/models/dc-validation.model'
|
||||||
import { DQRule } from '../shared/dc-validator/models/dq-rules.model'
|
import { DQRule } from '../shared/dc-validator/models/dq-rules.model'
|
||||||
import { getHotDataSchema } from '../shared/dc-validator/utils/getHotDataSchema'
|
import { getHotDataSchema } from '../shared/dc-validator/utils/getHotDataSchema'
|
||||||
|
import { isEmpty } from '../shared/dc-validator/utils/isEmpty'
|
||||||
import { globals } from '../_globals'
|
import { globals } from '../_globals'
|
||||||
import { UploadStaterComponent } from './components/upload-stater/upload-stater.component'
|
import { UploadStaterComponent } from './components/upload-stater/upload-stater.component'
|
||||||
import { DynamicExtendedCellValidation } from './models/dynamicExtendedCellValidation'
|
import { DynamicExtendedCellValidation } from './models/dynamicExtendedCellValidation'
|
||||||
@@ -70,7 +71,8 @@ import { ParseResult } from '../models/ParseResult.interface'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
@ViewChildren('uploadStater')
|
@ViewChildren('uploadStater')
|
||||||
@@ -1044,12 +1046,16 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new empty row object with proper structure
|
* Creates a new empty row object with proper structure.
|
||||||
|
* Columns with NOTNULL DQ rules are pre-populated with their RULE_VALUE.
|
||||||
*/
|
*/
|
||||||
private createEmptyRow(): any {
|
private createEmptyRow(): any {
|
||||||
const newRow: any = {}
|
const newRow: any = {}
|
||||||
this.headerColumns.forEach((col: string) => {
|
this.cellValidation.forEach((rule: any) => {
|
||||||
newRow[col] = ''
|
const dataKey = rule.data
|
||||||
|
newRow[dataKey] = this.hotDataSchema.hasOwnProperty(dataKey)
|
||||||
|
? this.hotDataSchema[dataKey]
|
||||||
|
: ''
|
||||||
})
|
})
|
||||||
newRow['noLinkOption'] = true
|
newRow['noLinkOption'] = true
|
||||||
return newRow
|
return newRow
|
||||||
@@ -2675,13 +2681,14 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
// Note: this.headerColumns and this.columnHeader contains same data
|
// Note: this.headerColumns and this.columnHeader contains same data
|
||||||
// need to resolve redundancy
|
// need to resolve redundancy
|
||||||
|
|
||||||
// default schema
|
// default schema - includes NOTNULL defaults from DQ rules
|
||||||
for (let i = 0; i < this.headerColumns.length; i++) {
|
for (let i = 0; i < this.headerColumns.length; i++) {
|
||||||
const colType = this.cellValidation[i].type
|
const colType = this.cellValidation[i].type
|
||||||
|
|
||||||
this.hotDataSchema[this.cellValidation[i].data] = getHotDataSchema(
|
this.hotDataSchema[this.cellValidation[i].data] = getHotDataSchema(
|
||||||
colType,
|
colType,
|
||||||
this.cellValidation[i]
|
this.cellValidation[i],
|
||||||
|
this.dcValidator?.getDqDetails()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2986,6 +2993,29 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Auto-populate NOTNULL default when validation fails due to empty value
|
||||||
|
hot.addHook(
|
||||||
|
'afterValidate',
|
||||||
|
(isValid: boolean, value: any, row: number, prop: string | number) => {
|
||||||
|
if (isValid || !isEmpty(value)) return
|
||||||
|
|
||||||
|
const colName =
|
||||||
|
typeof prop === 'string'
|
||||||
|
? prop
|
||||||
|
: (hot.colToProp(prop as number) as string)
|
||||||
|
|
||||||
|
const defaultValue = this.dcValidator?.getNotNullDefaultValue(colName)
|
||||||
|
if (defaultValue === undefined) return
|
||||||
|
|
||||||
|
// Auto-populate using setTimeout to avoid modifying during validation
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isEmpty(hot.getDataAtRowProp(row, colName))) {
|
||||||
|
hot.setDataAtRowProp(row, colName, defaultValue, 'autoPopulate')
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
hot.addHook('beforePaste', (data: any, cords: any) => {
|
hot.addHook('beforePaste', (data: any, cords: any) => {
|
||||||
const startCol = cords[0].startCol
|
const startCol = cords[0].startCol
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class GroupComponent implements OnInit {
|
export class GroupComponent implements OnInit {
|
||||||
public groups: Array<any> | undefined
|
public groups: Array<any> | undefined
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import { LicenceService } from '../services/licence.service'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class HomeComponent implements AfterContentInit {
|
export class HomeComponent implements AfterContentInit {
|
||||||
public treeNodeLibraries: Array<any> | null = null
|
public treeNodeLibraries: Array<any> | null = null
|
||||||
|
|||||||
@@ -1,178 +1,182 @@
|
|||||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core'
|
import { Component, OnInit, ViewEncapsulation } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AppService, LicenceService, SasService } from '../services'
|
import { AppService, LicenceService, SasService } from '../services'
|
||||||
import { LicenseKeyData } from '../models/LicenseKeyData'
|
import { LicenseKeyData } from '../models/LicenseKeyData'
|
||||||
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
|
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
|
||||||
|
|
||||||
enum LicenseActions {
|
enum LicenseActions {
|
||||||
key = 'key',
|
key = 'key',
|
||||||
register = 'register',
|
register = 'register',
|
||||||
limit = 'limit',
|
limit = 'limit',
|
||||||
update = 'update'
|
update = 'update'
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-licensing',
|
selector: 'app-licensing',
|
||||||
templateUrl: './licensing.component.html',
|
templateUrl: './licensing.component.html',
|
||||||
styleUrls: ['./licensing.component.scss'],
|
styleUrls: ['./licensing.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
})
|
standalone: false
|
||||||
export class LicensingComponent implements OnInit {
|
})
|
||||||
public action: LicenseActions | null = null
|
export class LicensingComponent implements OnInit {
|
||||||
|
public action: LicenseActions | null = null
|
||||||
public licenseErrors: { [key: string]: string } = {
|
|
||||||
missing: `Licence key is missing - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
|
public licenseErrors: { [key: string]: string } = {
|
||||||
expired: `Licence key is expired - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
|
missing: `Licence key is missing - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
|
||||||
invalid: `Licence key is invalid - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
|
expired: `Licence key is expired - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
|
||||||
missmatch: `Your SYSSITE (below) is not found in the licence key - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`
|
invalid: `Licence key is invalid - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`,
|
||||||
}
|
missmatch: `Your SYSSITE (below) is not found in the licence key - please contact <a class="color-green" href="mailto: support@datacontroller.io">support@datacontroller.io</a> and enter valid keys below.`
|
||||||
|
}
|
||||||
public keyError: string | undefined
|
|
||||||
public errorDetails: string | undefined
|
public keyError: string | undefined
|
||||||
public missmatchedKey: string | undefined
|
public errorDetails: string | undefined
|
||||||
public licenceKeyValue: string = ''
|
public missmatchedKey: string | undefined
|
||||||
public activationKeyValue: string = ''
|
public licenceKeyValue: string = ''
|
||||||
|
public activationKeyValue: string = ''
|
||||||
public applyingKeys: boolean = false
|
|
||||||
|
public applyingKeys: boolean = false
|
||||||
public syssite = this.appService.syssite
|
|
||||||
public currentLicenceKey = this.licenceService.licenceKey
|
public syssite = this.appService.syssite
|
||||||
public currentActivationKey = this.licenceService.activationKey
|
public currentLicenceKey = this.licenceService.licenceKey
|
||||||
public isAppFreeTier = this.licenceService.isAppFreeTier
|
public currentActivationKey = this.licenceService.activationKey
|
||||||
public userCountLimitation = this.licenceService.userCountLimitation
|
public isAppFreeTier = this.licenceService.isAppFreeTier
|
||||||
|
public userCountLimitation = this.licenceService.userCountLimitation
|
||||||
public licenseKeyData: LicenseKeyData | null = null
|
|
||||||
|
public licenseKeyData: LicenseKeyData | null = null
|
||||||
public inputType: 'file' | 'paste' = 'file'
|
|
||||||
public licenceFileError: string | undefined
|
public inputType: 'file' | 'paste' = 'file'
|
||||||
public licenceFileLoading: boolean = false
|
public licenceFileError: string | undefined
|
||||||
public licencefile: { filename: string } = {
|
public licenceFileLoading: boolean = false
|
||||||
filename: ''
|
public licencefile: { filename: string } = {
|
||||||
}
|
filename: ''
|
||||||
|
}
|
||||||
constructor(
|
|
||||||
private route: ActivatedRoute,
|
constructor(
|
||||||
private licenceService: LicenceService,
|
private route: ActivatedRoute,
|
||||||
private sasService: SasService,
|
private router: Router,
|
||||||
private appService: AppService
|
private licenceService: LicenceService,
|
||||||
) {}
|
private sasService: SasService,
|
||||||
|
private appService: AppService
|
||||||
ngOnInit(): void {
|
) {}
|
||||||
this.licenceKeyValue = this.currentLicenceKey || ''
|
|
||||||
this.activationKeyValue = this.currentActivationKey || ''
|
ngOnInit(): void {
|
||||||
|
this.licenceKeyValue = this.currentLicenceKey || ''
|
||||||
this.route.queryParams.subscribe((queryParams: any) => {
|
this.activationKeyValue = this.currentActivationKey || ''
|
||||||
this.keyError = queryParams.error
|
|
||||||
this.missmatchedKey = queryParams.missmatchId
|
this.route.queryParams.subscribe((queryParams: any) => {
|
||||||
|
this.keyError = queryParams.error
|
||||||
if (queryParams.details) {
|
this.missmatchedKey = queryParams.missmatchId
|
||||||
this.errorDetails = atob(queryParams.details)
|
|
||||||
}
|
if (queryParams.details) {
|
||||||
})
|
this.errorDetails = atob(queryParams.details)
|
||||||
|
}
|
||||||
this.route.params.subscribe((params: any) => {
|
})
|
||||||
let actionInUrl = params.action
|
|
||||||
|
this.route.params.subscribe((params: any) => {
|
||||||
if (actionInUrl) {
|
let actionInUrl = params.action
|
||||||
if (Object.values(LicenseActions).includes(actionInUrl)) {
|
|
||||||
this.action = actionInUrl
|
if (actionInUrl) {
|
||||||
}
|
if (Object.values(LicenseActions).includes(actionInUrl)) {
|
||||||
}
|
this.action = actionInUrl
|
||||||
})
|
}
|
||||||
|
}
|
||||||
this.licenseKeyData = this.licenceService.getLicenseKeyData()
|
})
|
||||||
}
|
|
||||||
|
this.licenseKeyData = this.licenceService.getLicenseKeyData()
|
||||||
public trimKeys() {
|
}
|
||||||
this.licenceKeyValue = this.licenceKeyValue.trim()
|
|
||||||
this.activationKeyValue = this.activationKeyValue.trim()
|
public trimKeys() {
|
||||||
}
|
this.licenceKeyValue = this.licenceKeyValue.trim()
|
||||||
|
this.activationKeyValue = this.activationKeyValue.trim()
|
||||||
public copySyssite(copyIconRef: any, copyTooltip: any, syssite: string[]) {
|
}
|
||||||
const syssiteString = syssite.join('\n')
|
|
||||||
|
public copySyssite(copyIconRef: any, copyTooltip: any, syssite: string[]) {
|
||||||
navigator.clipboard.writeText(syssiteString).then(() => {
|
const syssiteString = syssite.join('\n')
|
||||||
copyIconRef.setAttribute('shape', 'check')
|
|
||||||
copyIconRef.setAttribute('class', 'is-success')
|
navigator.clipboard.writeText(syssiteString).then(() => {
|
||||||
copyTooltip.innerText = 'Copied!'
|
copyIconRef.setAttribute('shape', 'check')
|
||||||
|
copyIconRef.setAttribute('class', 'is-success')
|
||||||
setTimeout(() => {
|
copyTooltip.innerText = 'Copied!'
|
||||||
copyIconRef.setAttribute('shape', 'copy')
|
|
||||||
copyIconRef.removeAttribute('class')
|
setTimeout(() => {
|
||||||
copyTooltip.innerText = 'Copy to clipboard'
|
copyIconRef.setAttribute('shape', 'copy')
|
||||||
}, 1000)
|
copyIconRef.removeAttribute('class')
|
||||||
})
|
copyTooltip.innerText = 'Copy to clipboard'
|
||||||
}
|
}, 1000)
|
||||||
|
})
|
||||||
public applyKeys() {
|
}
|
||||||
this.applyingKeys = true
|
|
||||||
|
public applyKeys() {
|
||||||
let table = {
|
this.applyingKeys = true
|
||||||
keyupload: [
|
|
||||||
{
|
let table = {
|
||||||
ACTIVATION_KEY: this.activationKeyValue,
|
keyupload: [
|
||||||
LICENCE_KEY: this.licenceKeyValue
|
{
|
||||||
}
|
ACTIVATION_KEY: this.activationKeyValue,
|
||||||
]
|
LICENCE_KEY: this.licenceKeyValue
|
||||||
}
|
}
|
||||||
|
]
|
||||||
this.sasService
|
}
|
||||||
.request('admin/registerkey', table)
|
|
||||||
.then((res: RequestWrapperResponse) => {
|
this.sasService
|
||||||
if (
|
.request('admin/registerkey', table)
|
||||||
res.adapterResponse.return &&
|
.then((res: RequestWrapperResponse) => {
|
||||||
res.adapterResponse.return[0] &&
|
if (
|
||||||
res.adapterResponse.return[0].MSG === 'SUCCESS'
|
res.adapterResponse.return &&
|
||||||
) {
|
res.adapterResponse.return[0] &&
|
||||||
location.replace(location.href.split('#')[0])
|
res.adapterResponse.return[0].MSG === 'SUCCESS'
|
||||||
}
|
) {
|
||||||
})
|
this.router.navigateByUrl('/').then(() => {
|
||||||
.finally(() => {
|
window.location.reload()
|
||||||
this.applyingKeys = false
|
})
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
.finally(() => {
|
||||||
public onFileCapture(event: any, dropped = false) {
|
this.applyingKeys = false
|
||||||
let file = dropped ? event[0] : event.target.files[0]
|
})
|
||||||
this.licencefile.filename = file.name
|
}
|
||||||
|
|
||||||
if (!file) return
|
public onFileCapture(event: any, dropped = false) {
|
||||||
|
let file = dropped ? event[0] : event.target.files[0]
|
||||||
this.licenceFileLoading = true
|
this.licencefile.filename = file.name
|
||||||
|
|
||||||
const reader = new FileReader()
|
if (!file) return
|
||||||
|
|
||||||
reader.onload = (evt) => {
|
this.licenceFileLoading = true
|
||||||
this.licenceFileError = 'Error reading file.'
|
|
||||||
|
const reader = new FileReader()
|
||||||
if (!evt || !evt.target) return
|
|
||||||
if (evt.target.readyState != 2) return
|
reader.onload = (evt) => {
|
||||||
if (evt.target.error) return
|
this.licenceFileError = 'Error reading file.'
|
||||||
if (!evt.target.result) return
|
|
||||||
|
if (!evt || !evt.target) return
|
||||||
this.licenceFileLoading = false
|
if (evt.target.readyState != 2) return
|
||||||
this.licenceFileError = undefined
|
if (evt.target.error) return
|
||||||
const fileArr = evt.target.result.toString().split('\n')
|
if (!evt.target.result) return
|
||||||
this.activationKeyValue = fileArr[1]
|
|
||||||
this.licenceKeyValue = fileArr[0]
|
this.licenceFileLoading = false
|
||||||
}
|
this.licenceFileError = undefined
|
||||||
|
const fileArr = evt.target.result.toString().split('\n')
|
||||||
reader.readAsText(file)
|
this.activationKeyValue = fileArr[1]
|
||||||
}
|
this.licenceKeyValue = fileArr[0]
|
||||||
|
}
|
||||||
public switchType(type: 'paste' | 'file') {
|
|
||||||
this.inputType = type
|
reader.readAsText(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
get disableApplyButton(): boolean {
|
public switchType(type: 'paste' | 'file') {
|
||||||
if (this.licenceKeyValue.length < 1 || this.activationKeyValue.length < 1)
|
this.inputType = type
|
||||||
return true
|
}
|
||||||
if (
|
|
||||||
this.licenceKeyValue === this.currentLicenceKey &&
|
get disableApplyButton(): boolean {
|
||||||
this.activationKeyValue === this.currentActivationKey
|
if (this.licenceKeyValue.length < 1 || this.activationKeyValue.length < 1)
|
||||||
)
|
return true
|
||||||
return true
|
if (
|
||||||
|
this.licenceKeyValue === this.currentLicenceKey &&
|
||||||
return false
|
this.activationKeyValue === this.currentActivationKey
|
||||||
}
|
)
|
||||||
}
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -239,13 +239,7 @@
|
|||||||
|
|
||||||
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
||||||
<div (click)="downloadSVG()" clrDropdownItem>SVG</div>
|
<div (click)="downloadSVG()" clrDropdownItem>SVG</div>
|
||||||
<div
|
<div (click)="downloadPNG()" clrDropdownItem>PNG</div>
|
||||||
*ngIf="!helperService.isMicrosoft"
|
|
||||||
(click)="downloadPNG()"
|
|
||||||
clrDropdownItem
|
|
||||||
>
|
|
||||||
PNG
|
|
||||||
</div>
|
|
||||||
<div (click)="downloadDot()" clrDropdownItem>Dot</div>
|
<div (click)="downloadDot()" clrDropdownItem>Dot</div>
|
||||||
<div *ngIf="flatdata" (click)="downloadCSV()" clrDropdownItem>
|
<div *ngIf="flatdata" (click)="downloadCSV()" clrDropdownItem>
|
||||||
CSV
|
CSV
|
||||||
@@ -366,13 +360,7 @@
|
|||||||
|
|
||||||
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
||||||
<div (click)="renderToDownload('SVG')" clrDropdownItem>SVG</div>
|
<div (click)="renderToDownload('SVG')" clrDropdownItem>SVG</div>
|
||||||
<div
|
<div (click)="renderToDownload('PNG')" clrDropdownItem>PNG</div>
|
||||||
*ngIf="!helperService.isMicrosoft"
|
|
||||||
(click)="renderToDownload('PNG')"
|
|
||||||
clrDropdownItem
|
|
||||||
>
|
|
||||||
PNG
|
|
||||||
</div>
|
|
||||||
<div (click)="downloadDot(); cancelRenderingGraph()" clrDropdownItem>
|
<div (click)="downloadDot(); cancelRenderingGraph()" clrDropdownItem>
|
||||||
Dot
|
Dot
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ const moment = require('moment')
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class LineageComponent {
|
export class LineageComponent {
|
||||||
public switchFlag: boolean = false
|
public switchFlag: boolean = false
|
||||||
@@ -746,28 +747,13 @@ export class LineageComponent {
|
|||||||
return URL.createObjectURL(svg_blob)
|
return URL.createObjectURL(svg_blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSVGBlob() {
|
|
||||||
let svg: any = document.getElementById('graph')
|
|
||||||
let serializer = new XMLSerializer()
|
|
||||||
let svg_blob = new Blob([serializer.serializeToString(svg)], {
|
|
||||||
type: 'image/svg+xml'
|
|
||||||
})
|
|
||||||
return svg_blob
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadSVG() {
|
downloadSVG() {
|
||||||
d3Viz.graphviz('#graph').resetZoom()
|
d3Viz.graphviz('#graph').resetZoom()
|
||||||
|
|
||||||
if (navigator.appVersion.toString().indexOf('.NET') > 0) {
|
let downloadLink = document.createElement('a')
|
||||||
window.navigator.msSaveBlob(this.getSVGBlob(), this.constructName('svg'))
|
downloadLink.href = this.getSVGURL()
|
||||||
} else {
|
downloadLink.download = this.constructName('svg')
|
||||||
let downloadLink = document.createElement('a')
|
downloadLink.click()
|
||||||
downloadLink.href = this.getSVGURL()
|
|
||||||
downloadLink.download = this.constructName('svg')
|
|
||||||
document.body.appendChild(downloadLink)
|
|
||||||
downloadLink.click()
|
|
||||||
document.body.removeChild(downloadLink)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadPNG() {
|
async downloadPNG() {
|
||||||
@@ -795,16 +781,11 @@ export class LineageComponent {
|
|||||||
var a = document.createElement('a')
|
var a = document.createElement('a')
|
||||||
var blob = new Blob([csvArray], { type: 'text/csv' })
|
var blob = new Blob([csvArray], { type: 'text/csv' })
|
||||||
|
|
||||||
if (navigator.appVersion.toString().indexOf('.NET') > 0) {
|
var url = window.URL.createObjectURL(blob)
|
||||||
window.navigator.msSaveBlob(blob, this.constructName('csv'))
|
a.href = url
|
||||||
} else {
|
a.download = this.constructName('csv')
|
||||||
var url = window.URL.createObjectURL(blob)
|
a.click()
|
||||||
a.href = url
|
window.URL.revokeObjectURL(url)
|
||||||
a.download = this.constructName('csv')
|
|
||||||
a.click()
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
a.remove()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDotUrl() {
|
private getDotUrl() {
|
||||||
@@ -813,23 +794,11 @@ export class LineageComponent {
|
|||||||
return window.URL.createObjectURL(dot_blob)
|
return window.URL.createObjectURL(dot_blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDotBlob() {
|
|
||||||
let data = this.vizInput
|
|
||||||
let dot_blob = new Blob([data], { type: 'text/plain' })
|
|
||||||
return dot_blob
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadDot() {
|
downloadDot() {
|
||||||
if (navigator.appVersion.toString().indexOf('.NET') > 0) {
|
let downloadLink = document.createElement('a')
|
||||||
window.navigator.msSaveBlob(this.getDotBlob(), this.constructName('txt'))
|
downloadLink.href = this.getDotUrl()
|
||||||
} else {
|
downloadLink.download = this.constructName('txt')
|
||||||
let downloadLink = document.createElement('a')
|
downloadLink.click()
|
||||||
downloadLink.href = this.getDotUrl()
|
|
||||||
downloadLink.download = this.constructName('txt')
|
|
||||||
document.body.appendChild(downloadLink)
|
|
||||||
downloadLink.click()
|
|
||||||
document.body.removeChild(downloadLink)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public showSvg() {
|
public showSvg() {
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ class ValueFilter implements ClrDatagridStringFilterInterface<MetaData> {
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class MetadataComponent implements OnInit {
|
export class MetadataComponent implements OnInit {
|
||||||
metaDataList: Array<any> | undefined
|
metaDataList: Array<any> | undefined
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ enum FileLoadingState {
|
|||||||
selector: 'app-multi-dataset',
|
selector: 'app-multi-dataset',
|
||||||
templateUrl: './multi-dataset.component.html',
|
templateUrl: './multi-dataset.component.html',
|
||||||
styleUrls: ['./multi-dataset.component.scss'],
|
styleUrls: ['./multi-dataset.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class MultiDatasetComponent implements OnInit, AfterViewInit {
|
export class MultiDatasetComponent implements OnInit, AfterViewInit {
|
||||||
@HostBinding('class.content-container') contentContainerClass = true
|
@HostBinding('class.content-container') contentContainerClass = true
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class NotFoundComponent implements OnInit {
|
export class NotFoundComponent implements OnInit {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { Pipe, PipeTransform } from '@angular/core'
|
|||||||
import { bytesToSize } from '@sasjs/utils/utils/bytesToSize'
|
import { bytesToSize } from '@sasjs/utils/utils/bytesToSize'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'convertSize'
|
name: 'convertSize',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ConvertSizePipe implements PipeTransform {
|
export class ConvertSizePipe implements PipeTransform {
|
||||||
transform(bytes: string | number, ...args: string[]): string {
|
transform(bytes: string | number, ...args: string[]): string {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
import * as moment from 'moment'
|
import moment from 'moment'
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'dateTimeFormatter'
|
name: 'dateTimeFormatter',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class DateTimeFormatterPipe implements PipeTransform {
|
export class DateTimeFormatterPipe implements PipeTransform {
|
||||||
transform(value: Date | string, type: string): string {
|
transform(value: Date | string, type: string): string {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'linkinze'
|
name: 'linkinze',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class LinkinzePipe implements PipeTransform {
|
export class LinkinzePipe implements PipeTransform {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { Pipe, PipeTransform } from '@angular/core'
|
|||||||
import { HelperService } from '../services/helper.service'
|
import { HelperService } from '../services/helper.service'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'sasToJsDate'
|
name: 'sasToJsDate',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class sasToJsDatePipe implements PipeTransform {
|
export class sasToJsDatePipe implements PipeTransform {
|
||||||
constructor(private helperService: HelperService) {}
|
constructor(private helperService: HelperService) {}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'pkSpaceSeparate'
|
name: 'pkSpaceSeparate',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class PkSpaceSeparatePipe implements PipeTransform {
|
export class PkSpaceSeparatePipe implements PipeTransform {
|
||||||
transform(value: string): string {
|
transform(value: string): string {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'prettyjson'
|
name: 'prettyjson',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class PrettyjsonPipe implements PipeTransform {
|
export class PrettyjsonPipe implements PipeTransform {
|
||||||
transform(rawJson: any): string {
|
transform(rawJson: any): string {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { Pipe, PipeTransform } from '@angular/core'
|
|||||||
import { HelperService } from '../services/helper.service'
|
import { HelperService } from '../services/helper.service'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'secondsParser'
|
name: 'secondsParser',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class SecondsParserPipe implements PipeTransform {
|
export class SecondsParserPipe implements PipeTransform {
|
||||||
constructor(private helperService: HelperService) {}
|
constructor(private helperService: HelperService) {}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'thousandSeparator'
|
name: 'thousandSeparator',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ThousandSeparatorPipe implements PipeTransform {
|
export class ThousandSeparatorPipe implements PipeTransform {
|
||||||
transform(value: string | number, separator?: string): string {
|
transform(value: string | number, separator?: string): string {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Pipe, PipeTransform } from '@angular/core'
|
import { Pipe, PipeTransform } from '@angular/core'
|
||||||
|
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'toNumber'
|
name: 'toNumber',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ToNumberPipe implements PipeTransform {
|
export class ToNumberPipe implements PipeTransform {
|
||||||
transform(value: string | number): number {
|
transform(value: string | number): number {
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ registerLocaleData(localeEnGB)
|
|||||||
templateUrl: './query.component.html',
|
templateUrl: './query.component.html',
|
||||||
styleUrls: ['./query.component.scss'],
|
styleUrls: ['./query.component.scss'],
|
||||||
providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }],
|
providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class QueryComponent
|
export class QueryComponent
|
||||||
implements AfterViewInit, AfterContentInit, OnDestroy
|
implements AfterViewInit, AfterContentInit, OnDestroy
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ interface ChangesObj {
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
|
export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
|
||||||
private _detailsSub: Subscription | undefined
|
private _detailsSub: Subscription | undefined
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ class SubmittedFilter implements ClrDatagridStringFilterInterface<ApproveData> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubmitReasonFilter
|
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<ApproveData> {
|
||||||
implements ClrDatagridStringFilterInterface<ApproveData>
|
|
||||||
{
|
|
||||||
accepts(data: ApproveData, search: string): boolean {
|
accepts(data: ApproveData, search: string): boolean {
|
||||||
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
}
|
}
|
||||||
@@ -53,7 +51,8 @@ class SubmitReasonFilter
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ApproveComponent implements OnInit {
|
export class ApproveComponent implements OnInit {
|
||||||
public approveList: Array<ApproveData> | undefined
|
public approveList: Array<ApproveData> | undefined
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ class SubmitterFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubmitReasonFilter
|
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||||
implements ClrDatagridStringFilterInterface<HistoryData>
|
|
||||||
{
|
|
||||||
accepts(data: HistoryData, search: string): boolean {
|
accepts(data: HistoryData, search: string): boolean {
|
||||||
return data.submittedReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
return data.submittedReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
}
|
}
|
||||||
@@ -65,7 +63,8 @@ class ReviewedFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class HistoryComponent implements OnInit {
|
export class HistoryComponent implements OnInit {
|
||||||
public history: Array<any> = []
|
public history: Array<any> = []
|
||||||
|
|||||||
@@ -17,17 +17,13 @@ interface SubmitterData {
|
|||||||
approver: string
|
approver: string
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubmittedFilter
|
class SubmittedFilter implements ClrDatagridStringFilterInterface<SubmitterData> {
|
||||||
implements ClrDatagridStringFilterInterface<SubmitterData>
|
|
||||||
{
|
|
||||||
accepts(data: SubmitterData, search: string): boolean {
|
accepts(data: SubmitterData, search: string): boolean {
|
||||||
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubmitReasonFilter
|
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<SubmitterData> {
|
||||||
implements ClrDatagridStringFilterInterface<SubmitterData>
|
|
||||||
{
|
|
||||||
accepts(data: SubmitterData, search: string): boolean {
|
accepts(data: SubmitterData, search: string): boolean {
|
||||||
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
}
|
}
|
||||||
@@ -40,7 +36,8 @@ class SubmitReasonFilter
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class SubmitterComponent implements OnInit, AfterViewInit {
|
export class SubmitterComponent implements OnInit, AfterViewInit {
|
||||||
public remained: number = 0
|
public remained: number = 0
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RoleComponent implements OnInit {
|
export class RoleComponent implements OnInit {
|
||||||
public roles: Array<any> | undefined
|
public roles: Array<any> | undefined
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class HomeRouteComponent implements OnInit, OnDestroy {
|
export class HomeRouteComponent implements OnInit, OnDestroy {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { Component, OnInit, OnDestroy } from '@angular/core'
|
|||||||
templateUrl: './multi-dataset-route.component.html',
|
templateUrl: './multi-dataset-route.component.html',
|
||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
}
|
},
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class MultiDatasetRouteComponent implements OnInit, OnDestroy {
|
export class MultiDatasetRouteComponent implements OnInit, OnDestroy {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ReviewRouteComponent implements OnInit {
|
export class ReviewRouteComponent implements OnInit {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class UsernavRouteComponent implements OnInit {
|
export class UsernavRouteComponent implements OnInit {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ViewRouteComponent implements OnInit, OnDestroy {
|
export class ViewRouteComponent implements OnInit, OnDestroy {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Component, OnInit, OnDestroy, ViewEncapsulation } from '@angular/core'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class XLMapRouteComponent implements OnInit, OnDestroy {
|
export class XLMapRouteComponent implements OnInit, OnDestroy {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import cloneDeep from 'lodash-es/cloneDeep'
|
import cloneDeep from 'lodash-es/cloneDeep'
|
||||||
import * as CryptoMD5 from 'crypto-js/md5'
|
import CryptoMD5 from 'crypto-js/md5'
|
||||||
import { SasService } from './sas.service'
|
import { SasService } from './sas.service'
|
||||||
|
|
||||||
const librariesToShow = 50
|
const librariesToShow = 50
|
||||||
@@ -11,12 +11,8 @@ const librariesToShow = 50
|
|||||||
export class HelperService {
|
export class HelperService {
|
||||||
public shownLibraries: number = librariesToShow
|
public shownLibraries: number = librariesToShow
|
||||||
public loadMoreCount: number = librariesToShow
|
public loadMoreCount: number = librariesToShow
|
||||||
public isMicrosoft: boolean = false
|
|
||||||
|
|
||||||
constructor(private sasService: SasService) {
|
constructor(private sasService: SasService) {}
|
||||||
this.isMicrosoft = this.isIEorEDGE()
|
|
||||||
console.log('Is IE or Edge?', this.isMicrosoft)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a JavaScript date object to a SAS Date or Datetime, given the logic below:
|
* Converts a JavaScript date object to a SAS Date or Datetime, given the logic below:
|
||||||
@@ -215,32 +211,6 @@ export class HelperService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public isIEorEDGE() {
|
|
||||||
var ua = window.navigator.userAgent
|
|
||||||
|
|
||||||
var msie = ua.indexOf('MSIE ')
|
|
||||||
if (msie > 0) {
|
|
||||||
// IE 10 or older => return version number
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var trident = ua.indexOf('Trident/')
|
|
||||||
if (trident > 0) {
|
|
||||||
// IE 11 => return version number
|
|
||||||
var rv = ua.indexOf('rv:')
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var edge = ua.indexOf('Edge/')
|
|
||||||
if (edge > 0) {
|
|
||||||
// Edge (IE 12+) => return version number
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// other browser
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
public convertObjectsToArray(
|
public convertObjectsToArray(
|
||||||
objectArray: Array<object>,
|
objectArray: Array<object>,
|
||||||
deepClone: boolean = false
|
deepClone: boolean = false
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'
|
|||||||
import { BehaviorSubject } from 'rxjs'
|
import { BehaviorSubject } from 'rxjs'
|
||||||
import { LicenseKeyData } from '../models/LicenseKeyData'
|
import { LicenseKeyData } from '../models/LicenseKeyData'
|
||||||
import { SasService } from './sas.service'
|
import { SasService } from './sas.service'
|
||||||
import * as moment from 'moment'
|
import moment from 'moment'
|
||||||
import * as base64Converter from 'base64-arraybuffer'
|
import * as base64Converter from 'base64-arraybuffer'
|
||||||
import * as encoding from 'text-encoding'
|
import * as encoding from 'text-encoding'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
|
|||||||
@@ -120,9 +120,12 @@ export class SasViyaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getComputeContexts(): Observable<ViyaComputeContexts> {
|
getComputeContexts(): Observable<ViyaComputeContexts> {
|
||||||
return this.get<ViyaComputeContexts>(`${this.serverUrl}/compute/contexts`, {
|
return this.get<ViyaComputeContexts>(
|
||||||
withCredentials: true
|
`${this.serverUrl}/compute/contexts?limit=1000`,
|
||||||
})
|
{
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getComputeContextById(id: string): Observable<ComputeContextDetails> {
|
getComputeContextById(id: string): Observable<ComputeContextDetails> {
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import { AbortDetails, InfoModal } from '../../models/InfoModal'
|
|||||||
selector: 'app-info-modal',
|
selector: 'app-info-modal',
|
||||||
templateUrl: './info-modal.component.html',
|
templateUrl: './info-modal.component.html',
|
||||||
styleUrls: ['./info-modal.component.scss'],
|
styleUrls: ['./info-modal.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class InfoModalComponent implements OnInit {
|
export class InfoModalComponent implements OnInit {
|
||||||
@Output() onConfirmModalClick: EventEmitter<any> = new EventEmitter()
|
@Output() onConfirmModalClick: EventEmitter<any> = new EventEmitter()
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { AlertsService } from './alerts.service'
|
|||||||
selector: 'app-alerts',
|
selector: 'app-alerts',
|
||||||
templateUrl: './alerts.component.html',
|
templateUrl: './alerts.component.html',
|
||||||
styleUrls: ['./alerts.component.scss'],
|
styleUrls: ['./alerts.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class AlertsComponent implements OnInit {
|
export class AlertsComponent implements OnInit {
|
||||||
public alerts: Array<Alert> = []
|
public alerts: Array<Alert> = []
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export type OnLoadingMoreEvent = {
|
|||||||
selector: 'app-autocomplete',
|
selector: 'app-autocomplete',
|
||||||
templateUrl: './autocomplete.component.html',
|
templateUrl: './autocomplete.component.html',
|
||||||
styleUrls: ['./autocomplete.component.scss'],
|
styleUrls: ['./autocomplete.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class AutocompleteComponent implements OnInit, AfterViewInit {
|
export class AutocompleteComponent implements OnInit, AfterViewInit {
|
||||||
@ViewChild('input') inputElement: any
|
@ViewChild('input') inputElement: any
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'
|
|||||||
selector: 'contact-link',
|
selector: 'contact-link',
|
||||||
templateUrl: './contact-link.component.html',
|
templateUrl: './contact-link.component.html',
|
||||||
styleUrls: ['./contact-link.component.scss'],
|
styleUrls: ['./contact-link.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ContactLinkComponent implements OnInit {
|
export class ContactLinkComponent implements OnInit {
|
||||||
@Input() classes: string = ''
|
@Input() classes: string = ''
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { Tab } from './models/dsmeta-groupped.model'
|
|||||||
selector: 'app-dataset-info',
|
selector: 'app-dataset-info',
|
||||||
templateUrl: './dataset-info.component.html',
|
templateUrl: './dataset-info.component.html',
|
||||||
styleUrls: ['./dataset-info.component.scss'],
|
styleUrls: ['./dataset-info.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class DatasetInfoComponent implements OnInit, OnChanges {
|
export class DatasetInfoComponent implements OnInit, OnChanges {
|
||||||
@Input() open: boolean = false
|
@Input() open: boolean = false
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import { TableClickEmitter } from './models/TableClickEmitter'
|
|||||||
selector: 'dc-tree',
|
selector: 'dc-tree',
|
||||||
templateUrl: './dc-tree.component.html',
|
templateUrl: './dc-tree.component.html',
|
||||||
styleUrls: ['./dc-tree.component.scss'],
|
styleUrls: ['./dc-tree.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class DcTreeComponent implements OnInit, AfterViewInit, OnChanges {
|
export class DcTreeComponent implements OnInit, AfterViewInit, OnChanges {
|
||||||
// REFACTOR NOTICE
|
// REFACTOR NOTICE
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from './models/dc-validation.model'
|
} from './models/dc-validation.model'
|
||||||
import { DQRule, DQRuleTypes } from './models/dq-rules.model'
|
import { DQRule, DQRuleTypes } from './models/dq-rules.model'
|
||||||
import { getDqDataCols } from './utils/getDqDataCols'
|
import { getDqDataCols } from './utils/getDqDataCols'
|
||||||
|
import { getNotNullDefault } from './utils/getNotNullDefault'
|
||||||
import { mergeColsRules } from './utils/mergeColsRules'
|
import { mergeColsRules } from './utils/mergeColsRules'
|
||||||
import { parseColType } from './utils/parseColType'
|
import { parseColType } from './utils/parseColType'
|
||||||
import { dqValidate } from './validations/dq-validation'
|
import { dqValidate } from './validations/dq-validation'
|
||||||
@@ -133,6 +134,19 @@ export class DcValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the RULE_VALUE for a NOTNULL rule on the given column.
|
||||||
|
* Used for auto-populating default values when cells are empty.
|
||||||
|
* Converts to number for numeric columns.
|
||||||
|
*
|
||||||
|
* @param col column name
|
||||||
|
* @returns RULE_VALUE (string or number) if NOTNULL rule exists, otherwise undefined
|
||||||
|
*/
|
||||||
|
getNotNullDefaultValue(col: string): string | number | undefined {
|
||||||
|
const colRule = this.getRule(col)
|
||||||
|
return getNotNullDefault(col, this.dqrules, colRule?.type)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves dropdown source for given dc validation rule
|
* Retrieves dropdown source for given dc validation rule
|
||||||
* The values comes from MPE_SELECTBOX table
|
* The values comes from MPE_SELECTBOX table
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import Handsontable from 'handsontable'
|
import Handsontable from 'handsontable'
|
||||||
import Core from 'handsontable/core'
|
import Core from 'handsontable/core'
|
||||||
|
|
||||||
export class CustomAutocompleteEditor extends Handsontable.editors
|
export class CustomAutocompleteEditor
|
||||||
.AutocompleteEditor {
|
extends Handsontable.editors.AutocompleteEditor
|
||||||
|
{
|
||||||
constructor(instance: Core) {
|
constructor(instance: Core) {
|
||||||
super(instance)
|
super(instance)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export interface DcColumnSettings {
|
|||||||
export interface DcValidation extends HotColumnSettings, DcColumnSettings {}
|
export interface DcValidation extends HotColumnSettings, DcColumnSettings {}
|
||||||
|
|
||||||
export interface DcValidationRuleUpdate
|
export interface DcValidationRuleUpdate
|
||||||
extends Handsontable.ColumnSettings,
|
extends Handsontable.ColumnSettings, DcColumnSettings {
|
||||||
DcColumnSettings {
|
|
||||||
data?: string
|
data?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DQRule } from '../models/dq-rules.model'
|
||||||
import { getHotDataSchema } from '../utils/getHotDataSchema'
|
import { getHotDataSchema } from '../utils/getHotDataSchema'
|
||||||
|
|
||||||
describe('DC Validator - hot data schema', () => {
|
describe('DC Validator - hot data schema', () => {
|
||||||
@@ -8,4 +9,58 @@ describe('DC Validator - hot data schema', () => {
|
|||||||
).toEqual(1)
|
).toEqual(1)
|
||||||
expect(getHotDataSchema('missing')).toEqual('')
|
expect(getHotDataSchema('missing')).toEqual('')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('NOTNULL defaults', () => {
|
||||||
|
const dqRules: DQRule[] = [
|
||||||
|
{
|
||||||
|
BASE_COL: 'TEXT_COL',
|
||||||
|
RULE_TYPE: 'NOTNULL',
|
||||||
|
RULE_VALUE: 'default_text',
|
||||||
|
X: 1
|
||||||
|
},
|
||||||
|
{ BASE_COL: 'NUM_COL', RULE_TYPE: 'NOTNULL', RULE_VALUE: '42', X: 1 }
|
||||||
|
]
|
||||||
|
|
||||||
|
it('should return NOTNULL default for text column', () => {
|
||||||
|
expect(getHotDataSchema('text', { data: 'TEXT_COL' }, dqRules)).toEqual(
|
||||||
|
'default_text'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return NOTNULL default as number for numeric column', () => {
|
||||||
|
expect(getHotDataSchema('numeric', { data: 'NUM_COL' }, dqRules)).toEqual(
|
||||||
|
42
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fall back to type default when no NOTNULL rule exists', () => {
|
||||||
|
expect(
|
||||||
|
getHotDataSchema('numeric', { data: 'OTHER_COL' }, dqRules)
|
||||||
|
).toEqual('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should prioritize NOTNULL over autocomplete first option', () => {
|
||||||
|
const rulesWithAutocomplete: DQRule[] = [
|
||||||
|
{
|
||||||
|
BASE_COL: 'SELECT_COL',
|
||||||
|
RULE_TYPE: 'NOTNULL',
|
||||||
|
RULE_VALUE: 'priority_value',
|
||||||
|
X: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BASE_COL: 'SELECT_COL',
|
||||||
|
RULE_TYPE: 'HARDSELECT',
|
||||||
|
RULE_VALUE: 'ignored',
|
||||||
|
X: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
expect(
|
||||||
|
getHotDataSchema(
|
||||||
|
'autocomplete',
|
||||||
|
{ data: 'SELECT_COL', source: ['first', 'second'] },
|
||||||
|
rulesWithAutocomplete
|
||||||
|
)
|
||||||
|
).toEqual('priority_value')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { DQRule } from '../models/dq-rules.model'
|
||||||
|
import { getNotNullDefault } from '../utils/getNotNullDefault'
|
||||||
|
|
||||||
|
describe('DC Validator - getNotNullDefault', () => {
|
||||||
|
const dqRules: DQRule[] = [
|
||||||
|
{
|
||||||
|
BASE_COL: 'TEXT_COL',
|
||||||
|
RULE_TYPE: 'NOTNULL',
|
||||||
|
RULE_VALUE: 'default_text',
|
||||||
|
X: 1
|
||||||
|
},
|
||||||
|
{ BASE_COL: 'NUM_COL', RULE_TYPE: 'NOTNULL', RULE_VALUE: '42', X: 1 },
|
||||||
|
{ BASE_COL: 'EMPTY_RULE', RULE_TYPE: 'NOTNULL', RULE_VALUE: ' ', X: 1 },
|
||||||
|
{
|
||||||
|
BASE_COL: 'OTHER_COL',
|
||||||
|
RULE_TYPE: 'HARDSELECT',
|
||||||
|
RULE_VALUE: 'some_value',
|
||||||
|
X: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
it('should return string value for text columns', () => {
|
||||||
|
expect(getNotNullDefault('TEXT_COL', dqRules, 'text')).toEqual(
|
||||||
|
'default_text'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return number for numeric columns when RULE_VALUE is numeric', () => {
|
||||||
|
expect(getNotNullDefault('NUM_COL', dqRules, 'numeric')).toEqual(42)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return string for numeric columns when RULE_VALUE is not numeric', () => {
|
||||||
|
const rulesWithNonNumeric: DQRule[] = [
|
||||||
|
{
|
||||||
|
BASE_COL: 'NUM_COL',
|
||||||
|
RULE_TYPE: 'NOTNULL',
|
||||||
|
RULE_VALUE: 'not_a_number',
|
||||||
|
X: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
expect(
|
||||||
|
getNotNullDefault('NUM_COL', rulesWithNonNumeric, 'numeric')
|
||||||
|
).toEqual('not_a_number')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined for empty RULE_VALUE', () => {
|
||||||
|
expect(getNotNullDefault('EMPTY_RULE', dqRules, 'text')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined for columns without NOTNULL rule', () => {
|
||||||
|
expect(getNotNullDefault('OTHER_COL', dqRules, 'text')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined for non-existent columns', () => {
|
||||||
|
expect(getNotNullDefault('MISSING_COL', dqRules, 'text')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined for empty dqRules array', () => {
|
||||||
|
expect(getNotNullDefault('TEXT_COL', [], 'text')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return string when colType is undefined', () => {
|
||||||
|
expect(getNotNullDefault('NUM_COL', dqRules, undefined)).toEqual('42')
|
||||||
|
})
|
||||||
|
})
|
||||||
39
client/src/app/shared/dc-validator/tests/isEmpty.spec.ts
Normal file
39
client/src/app/shared/dc-validator/tests/isEmpty.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { isEmpty } from '../utils/isEmpty'
|
||||||
|
|
||||||
|
describe('DC Validator - isEmpty', () => {
|
||||||
|
it('should return true for null', () => {
|
||||||
|
expect(isEmpty(null)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for undefined', () => {
|
||||||
|
expect(isEmpty(undefined)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for empty string', () => {
|
||||||
|
expect(isEmpty('')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for whitespace-only string', () => {
|
||||||
|
expect(isEmpty(' ')).toBe(true)
|
||||||
|
expect(isEmpty('\t\n')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for non-empty string', () => {
|
||||||
|
expect(isEmpty('hello')).toBe(false)
|
||||||
|
expect(isEmpty(' hello ')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for number zero', () => {
|
||||||
|
expect(isEmpty(0)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for non-zero numbers', () => {
|
||||||
|
expect(isEmpty(42)).toBe(false)
|
||||||
|
expect(isEmpty(-1)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for boolean values', () => {
|
||||||
|
expect(isEmpty(true)).toBe(false)
|
||||||
|
expect(isEmpty(false)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { DcValidation } from '../models/dc-validation.model'
|
import { DcValidation } from '../models/dc-validation.model'
|
||||||
|
import { DQRule } from '../models/dq-rules.model'
|
||||||
|
import { getNotNullDefault } from './getNotNullDefault'
|
||||||
|
|
||||||
const schemaTypeMap: { [key: string]: any } = {
|
const schemaTypeMap: { [key: string]: any } = {
|
||||||
numeric: '',
|
numeric: '',
|
||||||
@@ -7,14 +9,25 @@ const schemaTypeMap: { [key: string]: any } = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema defines the default values for given types. For example when new row is added.
|
* Schema defines the default values for given types. For example when new row is added.
|
||||||
|
* Priority: NOTNULL RULE_VALUE > autocomplete first option > type default
|
||||||
*/
|
*/
|
||||||
export const getHotDataSchema = (
|
export function getHotDataSchema(
|
||||||
type: string | undefined,
|
type: string | undefined,
|
||||||
cellValidation?: DcValidation
|
cellValidation?: DcValidation,
|
||||||
): any => {
|
dqRules?: DQRule[]
|
||||||
|
): any {
|
||||||
|
// Check for NOTNULL default first
|
||||||
|
if (dqRules && cellValidation?.data) {
|
||||||
|
const defaultValue = getNotNullDefault(cellValidation.data, dqRules, type)
|
||||||
|
if (defaultValue !== undefined) {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!type) return schemaTypeMap.default
|
if (!type) return schemaTypeMap.default
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case 'dropdown':
|
||||||
case 'autocomplete': {
|
case 'autocomplete': {
|
||||||
return cellValidation && cellValidation.source
|
return cellValidation && cellValidation.source
|
||||||
? (cellValidation.source as string[] | number[])[0]
|
? (cellValidation.source as string[] | number[])[0]
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { DQRule } from '../models/dq-rules.model'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the NOTNULL default value for a column from DQ rules.
|
||||||
|
* Converts to number for numeric columns based on colType parameter.
|
||||||
|
*
|
||||||
|
* @param colName column name to look up
|
||||||
|
* @param dqRules array of DQ rules
|
||||||
|
* @param colType column type (e.g., 'numeric', 'text')
|
||||||
|
* @returns default value (string or number) if NOTNULL rule exists with non-empty value, otherwise undefined
|
||||||
|
*/
|
||||||
|
export function getNotNullDefault(
|
||||||
|
colName: string,
|
||||||
|
dqRules: DQRule[],
|
||||||
|
colType?: string
|
||||||
|
): string | number | undefined {
|
||||||
|
const notNullRule = dqRules.find(
|
||||||
|
(rule: DQRule) => rule.BASE_COL === colName && rule.RULE_TYPE === 'NOTNULL'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!notNullRule?.RULE_VALUE || notNullRule.RULE_VALUE.trim().length === 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colType === 'numeric' && !isNaN(Number(notNullRule.RULE_VALUE))) {
|
||||||
|
return Number(notNullRule.RULE_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
return notNullRule.RULE_VALUE
|
||||||
|
}
|
||||||
8
client/src/app/shared/dc-validator/utils/isEmpty.ts
Normal file
8
client/src/app/shared/dc-validator/utils/isEmpty.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Checks if a value is considered empty for NOTNULL validation purposes.
|
||||||
|
* A value is empty if it's null, undefined, or a string that is blank after trimming.
|
||||||
|
*/
|
||||||
|
export function isEmpty(value: unknown): boolean {
|
||||||
|
if (value === null || value === undefined) return true
|
||||||
|
return value.toString().trim().length === 0
|
||||||
|
}
|
||||||
@@ -7,7 +7,8 @@ import { Options } from './models/options.interface'
|
|||||||
selector: 'app-excel-password-modal',
|
selector: 'app-excel-password-modal',
|
||||||
styleUrls: ['./excel-password-modal.component.scss'],
|
styleUrls: ['./excel-password-modal.component.scss'],
|
||||||
templateUrl: './excel-password-modal.component.html',
|
templateUrl: './excel-password-modal.component.html',
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ExcelPasswordModalComponent {
|
export class ExcelPasswordModalComponent {
|
||||||
options$: Observable<Options> = this.excelPasswordModalService.optionsSubject$
|
options$: Observable<Options> = this.excelPasswordModalService.optionsSubject$
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { Service } from '../service.interface'
|
|||||||
selector: 'app-loading-indicator',
|
selector: 'app-loading-indicator',
|
||||||
templateUrl: './loading-indicator.component.html',
|
templateUrl: './loading-indicator.component.html',
|
||||||
styleUrls: ['./loading-indicator.component.scss'],
|
styleUrls: ['./loading-indicator.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class LoadingIndicatorComponent implements OnInit, OnDestroy {
|
export class LoadingIndicatorComponent implements OnInit, OnDestroy {
|
||||||
public loading: boolean = false
|
public loading: boolean = false
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ interface User {
|
|||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
styleUrls: ['./login.component.scss'],
|
styleUrls: ['./login.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit, OnDestroy {
|
export class LoginComponent implements OnInit, OnDestroy {
|
||||||
private _subscription: Subscription = new Subscription()
|
private _subscription: Subscription = new Subscription()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
ViewEncapsulation
|
ViewEncapsulation
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { SASjsRequest } from '@sasjs/adapter'
|
import { SASjsRequest } from '@sasjs/adapter'
|
||||||
import * as moment from 'moment'
|
import moment from 'moment'
|
||||||
import { HelperService } from 'src/app/services/helper.service'
|
import { HelperService } from 'src/app/services/helper.service'
|
||||||
import { LoggerService } from '../../services/logger.service'
|
import { LoggerService } from '../../services/logger.service'
|
||||||
import { SasService } from '../../services/sas.service'
|
import { SasService } from '../../services/sas.service'
|
||||||
@@ -23,7 +23,8 @@ interface SASjsRequestExtended extends SASjsRequest {
|
|||||||
selector: 'app-requests-modal',
|
selector: 'app-requests-modal',
|
||||||
templateUrl: './requests-modal.component.html',
|
templateUrl: './requests-modal.component.html',
|
||||||
styleUrls: ['./requests-modal.component.scss'],
|
styleUrls: ['./requests-modal.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RequestsModalComponent implements OnInit {
|
export class RequestsModalComponent implements OnInit {
|
||||||
private _opened: boolean = false
|
private _opened: boolean = false
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import { globals } from '../../_globals'
|
|||||||
selector: 'app-sidebar',
|
selector: 'app-sidebar',
|
||||||
templateUrl: './sidebar.component.html',
|
templateUrl: './sidebar.component.html',
|
||||||
styleUrls: ['./sidebar.component.scss'],
|
styleUrls: ['./sidebar.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class SidebarComponent implements OnInit {
|
export class SidebarComponent implements OnInit {
|
||||||
@ViewChild('sidebarNav') sidebarNav!: ElementRef
|
@ViewChild('sidebarNav') sidebarNav!: ElementRef
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { OnLoadingMoreEvent } from '../autocomplete/autocomplete.component'
|
|||||||
selector: 'app-soft-select',
|
selector: 'app-soft-select',
|
||||||
templateUrl: './soft-select.component.html',
|
templateUrl: './soft-select.component.html',
|
||||||
styleUrls: ['./soft-select.component.scss'],
|
styleUrls: ['./soft-select.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class SoftSelectComponent implements OnInit, OnChanges {
|
export class SoftSelectComponent implements OnInit, OnChanges {
|
||||||
@Input() inputId: string = ''
|
@Input() inputId: string = ''
|
||||||
|
|||||||
@@ -511,6 +511,21 @@ export class SpreadsheetUtil {
|
|||||||
return resolve(XLSX.read(data, opts))
|
return resolve(XLSX.read(data, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TEMPORARILY DISABLED: Web Worker for XLSX parsing
|
||||||
|
// Worker is disabled because Angular/webpack bundles it as a separate chunk
|
||||||
|
// with a numeric filename (e.g., 411.hash.js). In SAS9/Viya streaming
|
||||||
|
// environments, all JS files need to be served through SASJobExecution
|
||||||
|
// with _program= parameter, but our post-build processor can't reliably
|
||||||
|
// find and replace the worker chunk reference in the minified output.
|
||||||
|
// FIX: Add "namedChunks": true to production config in angular.json
|
||||||
|
// (under projects.datacontroller.architect.build.configurations.production)
|
||||||
|
// This will output worker as "spreadsheet-worker.hash.js" instead of
|
||||||
|
// numeric ID, making it findable by post-processor.
|
||||||
|
// Trade-off: UI may briefly freeze when parsing large Excel files.
|
||||||
|
|
||||||
|
return resolve(XLSX.read(data, opts))
|
||||||
|
|
||||||
|
/*
|
||||||
if (typeof Worker === 'undefined') {
|
if (typeof Worker === 'undefined') {
|
||||||
console.info(
|
console.info(
|
||||||
'Not using worker to parse the XLSX - no Worker available in this environment'
|
'Not using worker to parse the XLSX - no Worker available in this environment'
|
||||||
@@ -551,6 +566,7 @@ export class SpreadsheetUtil {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
return resolve(XLSX.read(data, opts))
|
return resolve(XLSX.read(data, opts))
|
||||||
}, 600 * 1000) // 10 minutes
|
}, 600 * 1000) // 10 minutes
|
||||||
|
*/
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { RequestWrapperResponse } from 'src/app/models/request-wrapper/RequestWr
|
|||||||
selector: 'app-terms',
|
selector: 'app-terms',
|
||||||
templateUrl: './terms.component.html',
|
templateUrl: './terms.component.html',
|
||||||
styleUrls: ['./terms.component.scss'],
|
styleUrls: ['./terms.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class TermsComponent implements OnInit, AfterViewInit {
|
export class TermsComponent implements OnInit, AfterViewInit {
|
||||||
@ViewChild('markdownCard') markdownCard!: ElementRef
|
@ViewChild('markdownCard') markdownCard!: ElementRef
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import { globals } from 'src/app/_globals'
|
|||||||
selector: 'app-header-actions',
|
selector: 'app-header-actions',
|
||||||
templateUrl: './header-actions.component.html',
|
templateUrl: './header-actions.component.html',
|
||||||
styleUrls: ['./header-actions.component.scss'],
|
styleUrls: ['./header-actions.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class HeaderActions implements OnInit, OnDestroy {
|
export class HeaderActions implements OnInit, OnDestroy {
|
||||||
public userName: string = 'Not logged in'
|
public userName: string = 'Not logged in'
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ import { Viewbox } from './models/viewbox.model'
|
|||||||
selector: 'app-viewboxes',
|
selector: 'app-viewboxes',
|
||||||
templateUrl: './viewboxes.component.html',
|
templateUrl: './viewboxes.component.html',
|
||||||
styleUrls: ['./viewboxes.component.scss'],
|
styleUrls: ['./viewboxes.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
@ViewChildren('resizeBox') resizeBoxQuery!: QueryList<ElementRef> //make query list, handle multiple
|
@ViewChildren('resizeBox') resizeBoxQuery!: QueryList<ElementRef> //make query list, handle multiple
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
* We use normal version of the XLSX (SheetJS)
|
* We use normal version of the XLSX (SheetJS)
|
||||||
* Because at the moment "@sheet/crypto" can't work in the Web Worker environment
|
* Because at the moment "@sheet/crypto" can't work in the Web Worker environment
|
||||||
* Because of the missing "global" variable.
|
* Because of the missing "global" variable.
|
||||||
|
*
|
||||||
|
* Version bumped to v0.20.3 (`libraries/xlsx-0.20.3.tgz`)
|
||||||
|
* @see https://cdn.sheetjs.com/
|
||||||
*/
|
*/
|
||||||
import * as XLSX from 'xlsx'
|
import * as XLSX from 'xlsx'
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class StageComponent implements OnInit, AfterViewInit {
|
export class StageComponent implements OnInit, AfterViewInit {
|
||||||
public table_id: any
|
public table_id: any
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class SystemComponent implements OnInit {
|
export class SystemComponent implements OnInit {
|
||||||
appInfo: AppInfo = {
|
appInfo: AppInfo = {
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class UserComponent implements OnInit {
|
export class UserComponent implements OnInit {
|
||||||
public users: Array<any> | undefined
|
public users: Array<any> | undefined
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapper
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ViewerComponent
|
export class ViewerComponent
|
||||||
implements AfterContentInit, AfterViewInit, OnDestroy
|
implements AfterContentInit, AfterViewInit, OnDestroy
|
||||||
@@ -157,15 +158,26 @@ export class ViewerComponent
|
|||||||
return ' '
|
return ' '
|
||||||
},
|
},
|
||||||
afterGetColHeader: (col: number, th: any, headerLevel: number) => {
|
afterGetColHeader: (col: number, th: any, headerLevel: number) => {
|
||||||
const column = this.hotInstance?.colToProp(col) as string
|
// CRITICAL: Prevent "colToProp method cannot be called because this Handsontable instance has been destroyed" error
|
||||||
|
// This callback can be triggered even after the instance is destroyed during rapid table switching
|
||||||
|
if (!this.hotInstance || this.hotInstance.isDestroyed) {
|
||||||
|
// Graceful fallback: apply only dark mode styling when instance is unavailable
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// header columns styling - primary keys
|
try {
|
||||||
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
const column = this.hotInstance.colToProp(col) as string
|
||||||
|
|
||||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
||||||
|
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||||
|
|
||||||
// Dark mode
|
// Apply dark mode styling to all headers
|
||||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
} catch (error) {
|
||||||
|
// Safety net: if colToProp() fails, still apply basic styling
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
rowHeaderWidth: 15,
|
rowHeaderWidth: 15,
|
||||||
rowHeights: 20,
|
rowHeights: 20,
|
||||||
@@ -199,12 +211,21 @@ export class ViewerComponent
|
|||||||
let colInfo: DataFormat | undefined
|
let colInfo: DataFormat | undefined
|
||||||
let textInfo = 'No info found'
|
let textInfo = 'No info found'
|
||||||
|
|
||||||
if (this.hotInstance) {
|
if (
|
||||||
const hotSelected: [number, number, number, number][] =
|
this.hotInstance &&
|
||||||
this.hotInstance.getSelected() || []
|
!this.hotInstance.isDestroyed &&
|
||||||
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
|
!this.isTableSwitching
|
||||||
const colName = this.hotInstance?.colToProp(selectedCol)
|
) {
|
||||||
colInfo = this.$dataFormats?.vars[colName]
|
try {
|
||||||
|
const hotSelected: [number, number, number, number][] =
|
||||||
|
this.hotInstance.getSelected() || []
|
||||||
|
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
|
||||||
|
const colName = this.hotInstance.colToProp(selectedCol)
|
||||||
|
colInfo = this.$dataFormats?.vars[colName]
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors during table switching
|
||||||
|
colInfo = undefined
|
||||||
|
}
|
||||||
|
|
||||||
if (colInfo)
|
if (colInfo)
|
||||||
textInfo = `LABEL: ${colInfo?.label}<br>TYPE: ${colInfo?.type}<br>LENGTH: ${colInfo?.length}<br>FORMAT: ${colInfo?.format}`
|
textInfo = `LABEL: ${colInfo?.label}<br>TYPE: ${colInfo?.type}<br>LENGTH: ${colInfo?.length}<br>FORMAT: ${colInfo?.format}`
|
||||||
@@ -224,6 +245,13 @@ export class ViewerComponent
|
|||||||
private hotInstance: Handsontable | null = null
|
private hotInstance: Handsontable | null = null
|
||||||
public hotInstanceClickListener: boolean = false
|
public hotInstanceClickListener: boolean = false
|
||||||
|
|
||||||
|
// Race condition prevention for rapid table switching
|
||||||
|
private isTableSwitching: boolean = false
|
||||||
|
private switchingTimeout: any = null
|
||||||
|
|
||||||
|
// Prevents duplicate setupHot() calls within short time windows
|
||||||
|
private lastSetupTime: number = 0
|
||||||
|
|
||||||
public viewboxOpen: boolean = false
|
public viewboxOpen: boolean = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -505,17 +533,7 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public copyToClip() {
|
public copyToClip() {
|
||||||
let selBox = document.createElement('textarea')
|
navigator.clipboard.writeText(this.webQueryText)
|
||||||
selBox.style.position = 'fixed'
|
|
||||||
selBox.style.left = '0'
|
|
||||||
selBox.style.top = '0'
|
|
||||||
selBox.style.opacity = '0'
|
|
||||||
selBox.value = this.webQueryText
|
|
||||||
document.body.appendChild(selBox)
|
|
||||||
selBox.focus()
|
|
||||||
selBox.select()
|
|
||||||
document.execCommand('copy')
|
|
||||||
document.body.removeChild(selBox)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public goToViewer() {
|
public goToViewer() {
|
||||||
@@ -599,10 +617,24 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onTableClick(libTable: any, library: any) {
|
public onTableClick(libTable: any, library: any) {
|
||||||
this.lib = library.LIBRARYREF
|
// OPTIMIZATION: Prevent race conditions and destroyed instance errors during rapid table switching
|
||||||
this.table = libTable
|
if (this.isTableSwitching) {
|
||||||
this.selectLibTable(libTable)
|
return
|
||||||
this.viewData(0)
|
}
|
||||||
|
|
||||||
|
// Clear any existing timeout to prevent stale operations
|
||||||
|
if (this.switchingTimeout) {
|
||||||
|
clearTimeout(this.switchingTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PERFORMANCE: Debounce table switches to prevent rapid successive calls
|
||||||
|
// This ensures only the final table selection is processed
|
||||||
|
this.switchingTimeout = setTimeout(() => {
|
||||||
|
this.lib = library.LIBRARYREF
|
||||||
|
this.table = libTable
|
||||||
|
this.selectLibTable(libTable)
|
||||||
|
this.viewData(0)
|
||||||
|
}, 50) // 50ms debounce - fast enough for good UX, slow enough to prevent issues
|
||||||
}
|
}
|
||||||
|
|
||||||
public async selectTable(lib: string, initial?: boolean, library?: any) {
|
public async selectTable(lib: string, initial?: boolean, library?: any) {
|
||||||
@@ -721,6 +753,10 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async viewData(filter_pk: number) {
|
public async viewData(filter_pk: number) {
|
||||||
|
// CRITICAL: Set switching flag to prevent concurrent operations and race conditions
|
||||||
|
// This prevents callbacks from accessing destroyed instances during table switching
|
||||||
|
this.isTableSwitching = true
|
||||||
|
|
||||||
this.loadingTableView = true
|
this.loadingTableView = true
|
||||||
|
|
||||||
let libDataset: any
|
let libDataset: any
|
||||||
@@ -961,14 +997,25 @@ export class ViewerComponent
|
|||||||
|
|
||||||
this.loadingTableView = false
|
this.loadingTableView = false
|
||||||
|
|
||||||
//If we try to setup hot when no data is returned it errors `isDestoryed`.
|
// Setup Handsontable after async operations complete
|
||||||
//That is intorduced by HOT update
|
// Original issue: setupHot() called before API responses populated headerPks array
|
||||||
if (!this.noData && !this.noDataReqErr && libDataset) this.setupHot()
|
// Solution: Delay ensures both API paths (lines 328 & 886) have chance to set headerPks
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.noData && !this.noDataReqErr && libDataset) {
|
||||||
|
this.setupHot()
|
||||||
|
}
|
||||||
|
}, 50) // Optimized from 100ms - fast enough for API completion, slow enough to prevent race conditions
|
||||||
|
|
||||||
|
// RACE CONDITION PREVENTION: Reset switching flag after setup completion
|
||||||
|
// This allows new table switches after current operation finishes
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isTableSwitching = false
|
||||||
|
}, 300) // Optimized from 700ms to match reduced setup times
|
||||||
|
|
||||||
// Fix ARIA accessibility issues after data loading
|
// Fix ARIA accessibility issues after data loading
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.fixAriaAccessibility()
|
this.fixAriaAccessibility()
|
||||||
}, 1500)
|
}, 500)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area.
|
* This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area.
|
||||||
@@ -988,7 +1035,7 @@ export class ViewerComponent
|
|||||||
})
|
})
|
||||||
this.hotInstanceClickListener = true
|
this.hotInstanceClickListener = true
|
||||||
}
|
}
|
||||||
}, 2000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1116,51 +1163,135 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CRITICAL CLEANUP (workaround needed for HOT version 16 and above): Safely destroys Handsontable instances
|
||||||
|
*
|
||||||
|
* Purpose: Prevents "instance destroyed" errors and memory leaks during table switching
|
||||||
|
*
|
||||||
|
* Called from:
|
||||||
|
* - ngOnDestroy() - component cleanup
|
||||||
|
*
|
||||||
|
* Safety features:
|
||||||
|
* - Checks if instance exists and is not already destroyed
|
||||||
|
* - Try-catch prevents errors during destruction
|
||||||
|
* - Sets instance to null to prevent stale references
|
||||||
|
*/
|
||||||
|
private cleanupHotInstance() {
|
||||||
|
if (this.hotInstance && !this.hotInstance.isDestroyed) {
|
||||||
|
try {
|
||||||
|
this.hotInstance.destroy()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error destroying Handsontable instance:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.hotInstance = null
|
||||||
|
this.hooksAttached = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PERFORMANCE: Configures Handsontable with enhanced error handling (workaround needed for HOT version 16 and above)
|
||||||
|
*
|
||||||
|
* 1. Duplicate call prevention (500ms window)
|
||||||
|
* 2. Multiple validation checks to prevent race conditions
|
||||||
|
* 3. Forced render for immediate primary key styling
|
||||||
|
*
|
||||||
|
* Instance lifecycle is managed by Angular's hot-table component via [data] and [settings] bindings.
|
||||||
|
* This method only applies additional config that can't go through bindings (hooks, PK styling).
|
||||||
|
*/
|
||||||
private setupHot() {
|
private setupHot() {
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - this.lastSetupTime < 500) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.lastSetupTime = now
|
||||||
|
|
||||||
|
// VALIDATION: Don't setup if we're currently switching tables or still loading
|
||||||
|
if (this.loadingTableView || !this.libDataset) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.hotInstance || this.hotInstance.isDestroyed) {
|
||||||
|
this.hotInstance = this.hotTableComponent?.hotInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hotInstance && !this.hotInstance.isDestroyed) {
|
||||||
|
this.configureHotInstance()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance not ready yet — Angular may still be creating the component
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.loadingTableView && this.libDataset) {
|
if (this.isTableSwitching || this.loadingTableView || !this.libDataset) {
|
||||||
this.hotInstance = this.hotTableComponent?.hotInstance
|
return
|
||||||
|
|
||||||
if (this.hotInstance) {
|
|
||||||
this.hotInstance.updateSettings({
|
|
||||||
height: this.hotTable.height,
|
|
||||||
modifyColWidth: (width: any, col: any) => {
|
|
||||||
if (width > 500) return 500
|
|
||||||
else return width
|
|
||||||
},
|
|
||||||
afterGetColHeader: (col: number, th: any) => {
|
|
||||||
const column = this.hotInstance?.colToProp(col) as string
|
|
||||||
|
|
||||||
// header columns styling - primary keys
|
|
||||||
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
|
||||||
|
|
||||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
|
||||||
|
|
||||||
// Dark mode
|
|
||||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add hooks for accessibility fixes
|
|
||||||
this.hotInstance.addHook('afterRender', () => {
|
|
||||||
// Fix ARIA accessibility issues after each render
|
|
||||||
this.fixAriaAccessibility()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.hotInstance.addHook('afterChange', () => {
|
|
||||||
// Fix ARIA accessibility issues after any data change
|
|
||||||
setTimeout(() => {
|
|
||||||
this.fixAriaAccessibility()
|
|
||||||
}, 50)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix ARIA accessibility issues after table setup
|
this.hotInstance = this.hotTableComponent?.hotInstance
|
||||||
setTimeout(() => {
|
this.configureHotInstance()
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
private hooksAttached = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies settings that can't go through Angular [settings] binding:
|
||||||
|
* - Primary key column header styling
|
||||||
|
* - Column width cap
|
||||||
|
* - ARIA accessibility hooks (attached once per instance)
|
||||||
|
*/
|
||||||
|
private configureHotInstance() {
|
||||||
|
if (!this.hotInstance || this.hotInstance.isDestroyed) return
|
||||||
|
|
||||||
|
this.hotInstance.updateSettings({
|
||||||
|
height: this.hotTable.height,
|
||||||
|
modifyColWidth: (width: any, col: any) => {
|
||||||
|
if (width > 500) return 500
|
||||||
|
else return width
|
||||||
|
},
|
||||||
|
afterGetColHeader: (col: number, th: any) => {
|
||||||
|
// CRITICAL: Same safety checks as initial callback to prevent destroyed instance errors
|
||||||
|
if (!this.hotInstance || this.hotInstance.isDestroyed) {
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const column = this.hotInstance.colToProp(col) as string
|
||||||
|
|
||||||
|
// PRIMARY KEY STYLING: Apply special styling to PK columns (populated from API response)
|
||||||
|
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
||||||
|
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||||
|
|
||||||
|
// DARK MODE: Apply to all headers
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
} catch (error) {
|
||||||
|
// SAFETY NET: Ensure basic styling is always applied
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add hooks for accessibility fixes
|
||||||
|
// Hooks are attached once per instance to avoid accumulating duplicate listeners
|
||||||
|
if (!this.hooksAttached) {
|
||||||
|
this.hotInstance.addHook('afterRender', () => {
|
||||||
|
// Fix ARIA accessibility issues after each render
|
||||||
this.fixAriaAccessibility()
|
this.fixAriaAccessibility()
|
||||||
}, 500)
|
})
|
||||||
}, 1000)
|
|
||||||
|
this.hotInstance.addHook('afterChange', () => {
|
||||||
|
// Fix ARIA accessibility issues after any data change
|
||||||
|
setTimeout(() => {
|
||||||
|
this.fixAriaAccessibility()
|
||||||
|
}, 50)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.hooksAttached = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force immediate render to apply primary key styling
|
||||||
|
// Without this, styling would wait for ~2 seconds to be applied
|
||||||
|
// (workaround needed for HOT version 16 and above)
|
||||||
|
this.hotInstance.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadWithParameters() {
|
async loadWithParameters() {
|
||||||
@@ -1233,13 +1364,27 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
// Clean up the MutationObserver
|
// Proper component destruction to prevent memory leaks and errors
|
||||||
|
|
||||||
|
// Prevent any new operations during cleanup
|
||||||
|
this.isTableSwitching = true
|
||||||
|
|
||||||
|
// Clear any pending debounced table switches
|
||||||
|
if (this.switchingTimeout) {
|
||||||
|
clearTimeout(this.switchingTimeout)
|
||||||
|
this.switchingTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safely destroy Handsontable instance
|
||||||
|
this.cleanupHotInstance()
|
||||||
|
|
||||||
|
// Clean up ARIA accessibility observers
|
||||||
if (this.ariaObserver) {
|
if (this.ariaObserver) {
|
||||||
this.ariaObserver.disconnect()
|
this.ariaObserver.disconnect()
|
||||||
this.ariaObserver = undefined
|
this.ariaObserver = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up the interval
|
// Clear ARIA check intervals
|
||||||
if (this.ariaCheckInterval) {
|
if (this.ariaCheckInterval) {
|
||||||
clearInterval(this.ariaCheckInterval)
|
clearInterval(this.ariaCheckInterval)
|
||||||
this.ariaCheckInterval = undefined
|
this.ariaCheckInterval = undefined
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { ViyaApis } from './models/viya-apis.models'
|
|||||||
host: {
|
host: {
|
||||||
class: 'content-container'
|
class: 'content-container'
|
||||||
},
|
},
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ViyaApiExplorerComponent implements OnInit {
|
export class ViyaApiExplorerComponent implements OnInit {
|
||||||
collections: ViyaApis = {}
|
collections: ViyaApis = {}
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ enum Tabs {
|
|||||||
selector: 'app-xlmap',
|
selector: 'app-xlmap',
|
||||||
templateUrl: './xlmap.component.html',
|
templateUrl: './xlmap.component.html',
|
||||||
styleUrls: ['./xlmap.component.scss'],
|
styleUrls: ['./xlmap.component.scss'],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
|
export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
|
||||||
@HostBinding('class.content-container') contentContainerClass = true
|
@HostBinding('class.content-container') contentContainerClass = true
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<sasjs
|
<sasjs
|
||||||
serverUrl="http://localhost:5000"
|
serverUrl=""
|
||||||
appLoc="/Public/app/devtest"
|
appLoc="/Public/app/devtest"
|
||||||
serverType="SASJS"
|
serverType="SASJS"
|
||||||
loginMechanism="Redirected"
|
loginMechanism="Redirected"
|
||||||
|
|||||||
@@ -10,12 +10,6 @@
|
|||||||
"outDir": "./app",
|
"outDir": "./app",
|
||||||
"types": []
|
"types": []
|
||||||
},
|
},
|
||||||
"files": [
|
"files": ["src/polyfills.ts", "src/main.ts", "src/app/app.d.ts"],
|
||||||
"src/polyfills.ts",
|
"include": ["src/**/*.d.ts"]
|
||||||
"src/main.ts",
|
}
|
||||||
"src/app/app.d.ts"
|
|
||||||
],
|
|
||||||
"include": [
|
|
||||||
"src/**/*.d.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,55 +1,37 @@
|
|||||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
{
|
{
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "",
|
"baseUrl": "",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"declaration": false,
|
"declaration": false,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"lib": [
|
"lib": ["ES2022", "dom"],
|
||||||
"ES2022",
|
"skipLibCheck": true,
|
||||||
"dom"
|
"module": "ES2022",
|
||||||
],
|
"importHelpers": true,
|
||||||
"skipLibCheck": true,
|
"moduleResolution": "node",
|
||||||
"module": "ES2022",
|
"sourceMap": true,
|
||||||
"importHelpers": true,
|
"resolveJsonModule": true,
|
||||||
"moduleResolution": "node",
|
"target": "ES2022",
|
||||||
"sourceMap": true,
|
"paths": {
|
||||||
"resolveJsonModule": true,
|
"crypto": ["./node_modules/crypto-browserify"],
|
||||||
"target": "ES2022",
|
"stream": ["./node_modules/stream-browserify"],
|
||||||
"paths": {
|
"assert": ["./node_modules/assert"],
|
||||||
"crypto": [
|
"http": ["./node_modules/stream-http"],
|
||||||
"./node_modules/crypto-browserify"
|
"https": ["./node_modules/https-browserify"],
|
||||||
],
|
"os": ["./node_modules/os-browserify"]
|
||||||
"stream": [
|
|
||||||
"./node_modules/stream-browserify"
|
|
||||||
],
|
|
||||||
"assert": [
|
|
||||||
"./node_modules/assert"
|
|
||||||
],
|
|
||||||
"http": [
|
|
||||||
"./node_modules/stream-http"
|
|
||||||
],
|
|
||||||
"https": [
|
|
||||||
"./node_modules/https-browserify"
|
|
||||||
],
|
|
||||||
"os": [
|
|
||||||
"./node_modules/os-browserify"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"useDefineForClassFields": false
|
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"useDefineForClassFields": false
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
},
|
||||||
"strictInjectionParameters": true,
|
"angularCompilerOptions": {
|
||||||
"strictInputAccessModifiers": true,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
"strictTemplates": true,
|
"strictInjectionParameters": true,
|
||||||
},
|
"strictInputAccessModifiers": true,
|
||||||
"exclude": [
|
"strictTemplates": true
|
||||||
"cypress/**/*.ts",
|
},
|
||||||
"cypress.config.ts"
|
"exclude": ["cypress/**/*.ts", "cypress.config.ts"]
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,8 @@
|
|||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./out-tsc/spec",
|
"outDir": "./out-tsc/spec",
|
||||||
"types": [
|
"types": ["jasmine"]
|
||||||
"jasmine"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": ["src/polyfills.ts"],
|
||||||
"src/polyfills.ts"
|
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
|
||||||
],
|
|
||||||
"include": [
|
|
||||||
"src/**/*.spec.ts",
|
|
||||||
"src/**/*.d.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
559
package-lock.json
generated
559
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dcfrontend",
|
"name": "dcfrontend",
|
||||||
"version": "7.2.0",
|
"version": "7.4.0",
|
||||||
"description": "Data Controller",
|
"description": "Data Controller",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@saithodev/semantic-release-gitea": "^2.1.0",
|
"@saithodev/semantic-release-gitea": "^2.1.0",
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"@semantic-release/npm": "11.0.0",
|
"@semantic-release/npm": "11.0.0",
|
||||||
"@semantic-release/release-notes-generator": "^11.0.4",
|
"@semantic-release/release-notes-generator": "^11.0.4",
|
||||||
"commit-and-tag-version": "^11.2.2",
|
"commit-and-tag-version": "^11.2.2",
|
||||||
"prettier": "3.6.2"
|
"prettier": "^3.7.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install": "cd client && npm i && cd ../sas && npm i",
|
"install": "cd client && npm i && cd ../sas && npm i",
|
||||||
@@ -32,6 +32,5 @@
|
|||||||
"//": [
|
"//": [
|
||||||
"Readme",
|
"Readme",
|
||||||
"We must set private: true so that semantic-release/npm plugin will update the package.json version but not try to release it as NPM package"
|
"We must set private: true so that semantic-release/npm plugin will update the package.json version but not try to release it as NPM package"
|
||||||
],
|
]
|
||||||
"dependencies": {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"fromjs": [
|
"fromjs": [
|
||||||
{
|
{
|
||||||
"ADMIN": "DCDEFAULT",
|
"ADMIN": "DCDEFAULT",
|
||||||
"DCPATH": "/tmp/mihajlo/dcserverfrs"
|
"DCPATH": "/tmp/dcdata"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
938
sas/package-lock.json
generated
938
sas/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user