Compare commits
245 Commits
ca281b70c9
...
v6.7.0
Author | SHA1 | Date | |
---|---|---|---|
96066c66cb | |||
7997b77158 | |||
d1966bcdc5 | |||
7b5bbe024d | |||
4d84f15aca | |||
928937daab | |||
3bd8d247e5 | |||
cf6c9dd5f2 | |||
ff55cbbaad | |||
3eda4e2c58 | |||
c3af97ef57 | |||
31d4e5c727 | |||
fbbcf90956 | |||
f522038b8d | |||
ace599b39f | |||
963562621d | |||
5171d07441 | |||
9a0b9573d5 | |||
4733311ef3 | |||
432450a15b | |||
47638becc0 | |||
bdd3a95685 | |||
38601346a5 | |||
dc3a6ae6a1 | |||
f668b1e7f7 | |||
eb1c09d790 | |||
9bf324c74b | |||
f13e909478 | |||
6a0fe287dd | |||
5a48f2e6e3 | |||
6565834ad4 | |||
837821fd01 | |||
cff5989559 | |||
60510a4d68 | |||
2b54034973 | |||
347b0f9065 | |||
eac0104d7a | |||
1c8e4604de | |||
e9624635ed | |||
f9beda1ddb | |||
53400de110 | |||
cf37ddab22 | |||
625af199f4 | |||
56e9217f4b | |||
86f1af7926 | |||
7737f8455d | |||
b0f1677fcc | |||
4406e0d4b4 | |||
cf19381060 | |||
802d8a3b08 | |||
2a852496e9 | |||
4653097225 | |||
8afee29e02 | |||
233eca39ef | |||
1a96bb1233 | |||
93702c63dc | |||
df065562d1 | |||
802c99adf9 | |||
482c7455f5 | |||
731b96dccc | |||
9550ae4d11 | |||
2d6e747db9 | |||
fd94945466 | |||
d3b0c09332 | |||
01915a2db9 | |||
51b043b6d2 | |||
c144fd8087 | |||
12b15df78c | |||
d6ecd12cea | |||
1c3d498da6 | |||
d75e10aef5 | |||
f0f9d85558 | |||
86f3411896 | |||
6daef39268 | |||
7d1720a360 | |||
b11a4884b4 | |||
50696bb926 | |||
d67d4e2f86 | |||
2f01c4d251 | |||
9ffa30ab74 | |||
5d93346b52 | |||
39762b36c6 | |||
e40ebdff05 | |||
8d12d9e51e | |||
23708c9aae | |||
c86fba9dc7 | |||
e747e6e4e7 | |||
5aec024242 | |||
b473b198a6 | |||
516e5a2062 | |||
fb3abbe491 | |||
3e009f3037 | |||
e63d304953 | |||
3cd90c2d47 | |||
a485c3b787 | |||
2702bb3c84 | |||
56264ecc69 | |||
cc4535245c | |||
6ae31de1dd | |||
2d4d068413 | |||
271543a446 | |||
8f796aec36 | |||
6eb1aa85d2 | |||
ac59b77ad5 | |||
3efccc4cf3 | |||
8cbcd18f4b | |||
6bb2378790 | |||
e7d0ffe8c0 | |||
ab89600c73 | |||
830e3816a0 | |||
dadac4f13f | |||
1de48a49af | |||
687a1e1cb5 | |||
665a04f5c5 | |||
fdb18d242b | |||
ec173da4ce | |||
bb35cc15d2 | |||
181f52eaea | |||
fc7c8101ed | |||
a347603fe0 | |||
09022c995f | |||
3609943f30 | |||
a1d308ea07 | |||
5579db0eaf | |||
3a3e488b23 | |||
0a82ec0a70 | |||
bc1d89218e | |||
817b9adeac | |||
a7aa42a59b | |||
34f239036d | |||
91f128c2fe | |||
a00ebea692 | |||
c27cdab3fc | |||
d7da2d7890 | |||
76f0fd4232 | |||
008b45ad17 | |||
4d49263816 | |||
6a2482e5c6 | |||
b5c3fb2af4 | |||
fa2c8eb839 | |||
f3e82b4ee2 | |||
ba67248155 | |||
a6d962bfaa | |||
95cddb52d4 | |||
5a5118d775 | |||
1b1cdd7a4b | |||
a9ddf7f7dd | |||
b54b3f1778 | |||
349a63c591 | |||
293d33912f | |||
357b9849e7 | |||
0c8a9eef32 | |||
112b1d0da4 | |||
a05007416a | |||
9f7dd55583 | |||
11b06f6416 | |||
adb7eb7755 | |||
b776b80728 | |||
73a149ea7b | |||
ef8784093b | |||
b30c788e3d | |||
23899bdff3 | |||
8bd0dd22c2 | |||
c55b00c74f | |||
c895f509b0 | |||
5968915331 | |||
44ffc082f6 | |||
b716ae5675 | |||
01a0b59494 | |||
8ebc3da0bb | |||
133577a4fa | |||
a19615db41 | |||
32b212a6bf | |||
00ec4529cd | |||
102d03888f | |||
9f8247320e | |||
ef871de30e | |||
b3a15ce26b | |||
270695aec2 | |||
ad7392a326 | |||
92a50a42e2 | |||
a3a8856d8c | |||
150c19b1b0 | |||
f04c51ee4e | |||
c4338bf957 | |||
5b06f4ede8 | |||
e7ab2cc956 | |||
5ebf8a66f7 | |||
3d4e886b9b | |||
a00d31caf3 | |||
40fe707287 | |||
8296be01ba | |||
dbeb003292 | |||
f048501c48 | |||
498350b3f3 | |||
91e82c9c65 | |||
24067ea82b | |||
aa7deddba0 | |||
b2d13203d1 | |||
19c1092b5b | |||
94ab949df8 | |||
9eb2451c2f | |||
6e521bfa3e | |||
239720fe0c | |||
8571e01e44 | |||
6f482ec6d9 | |||
5e30dc0f89 | |||
3d76d12c86 | |||
93f1b81d70 | |||
e2b65ddd82 | |||
b64bbe91d4 | |||
1f2ce55f24 | |||
921157da9e | |||
413acf7d05 | |||
725f75aa74 | |||
c64ab8a577 | |||
1154c99e0a | |||
5bcdef77b8 | |||
e4a0089102 | |||
ba022d8a35 | |||
c8ac859d2e | |||
d6fd72e880 | |||
61cc360c85 | |||
08e39c4fca | |||
8c2ee441fc | |||
0a7d23c763 | |||
7b54fff26e | |||
cd3e0f614b | |||
81c0aec202 | |||
3193bdd720 | |||
b9a12454e1 | |||
01a857f7c6 | |||
2bb2eee80e | |||
c054ea500d | |||
d7f8201246 | |||
622cfcc6fe | |||
303240e4d2 | |||
a8b849aede | |||
52d4b3eefc | |||
908d2761f2 | |||
7c98ad8c5b | |||
5bb55e6484 | |||
e48e47bc63 | |||
bba2a6cc9d | |||
b036cc2abe |
@ -1,5 +1,5 @@
|
|||||||
name: Build
|
name: Build
|
||||||
run-name: Running Lint Check
|
run-name: Running Lint Check and Licence checker on Pull Request
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@ -18,4 +18,20 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NPMRC: ${{ secrets.NPMRC}}
|
NPMRC: ${{ secrets.NPMRC}}
|
||||||
|
|
||||||
- run: npm run lint:check
|
- name: Lint check
|
||||||
|
run: npm run lint:check
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
cd client
|
||||||
|
npm ci
|
||||||
|
# Install sheet
|
||||||
|
wget ${{ secrets.SHEETLINK }}
|
||||||
|
mv ${{ secrets.SHEETNAME }} ${{ secrets.SHEETNAME }}.tgz
|
||||||
|
npm i ${{ secrets.SHEETNAME }}.tgz
|
||||||
|
# End
|
||||||
|
|
||||||
|
- name: Licence checker
|
||||||
|
run: |
|
||||||
|
cd client
|
||||||
|
npm run license-checker
|
@ -1,223 +0,0 @@
|
|||||||
name: Test
|
|
||||||
run-name: Building and testing development branch
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- development
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
Build-production-and-ng-test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
|
|
||||||
- name: Write .npmrc file
|
|
||||||
run: |
|
|
||||||
touch client/.npmrc
|
|
||||||
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
|
||||||
|
|
||||||
- name: Install Chrome for Angular tests
|
|
||||||
run: apt-get update
|
|
||||||
run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
|
||||||
run: apt install -y ./google-chrome*.deb;
|
|
||||||
run: export CHROME_BIN=/usr/bin/google-chrome
|
|
||||||
|
|
||||||
- name: Write cypress credentials
|
|
||||||
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Check audit
|
|
||||||
# Audit should fail and stop the CI if critical vulnerability found
|
|
||||||
run: |
|
|
||||||
npm audit --audit-level=critical
|
|
||||||
cd ./sas
|
|
||||||
npm audit --audit-level=critical
|
|
||||||
cd ./client
|
|
||||||
npm audit --audit-level=critical
|
|
||||||
|
|
||||||
- name: Angular Tests
|
|
||||||
run: |
|
|
||||||
npm test -- --no-watch --no-progress --browsers=ChromeHeadlessCI
|
|
||||||
|
|
||||||
- name: Angular Production Build
|
|
||||||
run: |
|
|
||||||
npm run postinstall
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
Build-and-test-development:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
|
|
||||||
- name: Write .npmrc file
|
|
||||||
run: |
|
|
||||||
touch client/.npmrc
|
|
||||||
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
|
||||||
|
|
||||||
- run: apt-get update
|
|
||||||
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
|
||||||
- run: apt install -y ./google-chrome*.deb;
|
|
||||||
- run: export CHROME_BIN=/usr/bin/google-chrome
|
|
||||||
- run: apt-get update -y
|
|
||||||
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
|
|
||||||
- run: apt -y install jq
|
|
||||||
|
|
||||||
- name: Write cypress credentials
|
|
||||||
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
# Install pm2 and prepare SASJS server
|
|
||||||
- run: npm i -g pm2
|
|
||||||
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
|
||||||
- run: unzip linux.zip
|
|
||||||
- run: touch .env
|
|
||||||
- run: echo RUN_TIMES=js >> .env
|
|
||||||
- run: echo NODE_PATH=node >> .env
|
|
||||||
- run: echo CORS=enable >> .env
|
|
||||||
- run: echo WHITELIST=http://localhost:4200 >> .env
|
|
||||||
- run: cat .env
|
|
||||||
- run: pm2 start api-linux --wait-ready
|
|
||||||
|
|
||||||
- name: Deploy mocked services
|
|
||||||
run: |
|
|
||||||
cd ./sas/mocks/sasjs
|
|
||||||
npm install -g @sasjs/cli
|
|
||||||
npm install -g replace-in-files-cli
|
|
||||||
sasjs cbd -t server-ci
|
|
||||||
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
|
|
||||||
|
|
||||||
- name: Install ZIP
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install zip
|
|
||||||
|
|
||||||
- name: Prepare and run frontend and cypress
|
|
||||||
run: |
|
|
||||||
cd ./client
|
|
||||||
mv ./cypress.env.example.json ./cypress.env.json
|
|
||||||
replace-in-files --regex='"username".*' --replacement='"username":"'${{ secrets.CYPRESS_USERNAME_SASJS }}'",' ./cypress.env.json
|
|
||||||
replace-in-files --regex='"password".*' --replacement='"password":"'${{ secrets.CYPRESS_PWD_SASJS }}'" ' ./cypress.env.json
|
|
||||||
cat ./cypress.env.json
|
|
||||||
npm run postinstall
|
|
||||||
# Prepare index.html to SASJS local
|
|
||||||
replace-in-files --regex='serverUrl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./src/index.html
|
|
||||||
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/Public/app/devtest"' ./src/index.html
|
|
||||||
replace-in-files --regex='serverType=".*?"' --replacement='serverType="SASJS"' ./src/index.html
|
|
||||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
|
||||||
cat ./cypress.config.ts
|
|
||||||
# Start frontend and run cypress
|
|
||||||
npm start & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
|
|
||||||
|
|
||||||
- name: Zip Cypress videos
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
zip -r cypress-videos ./client/cypress/videos
|
|
||||||
|
|
||||||
- name: Cypress videos artifacts
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: cypress-videos.zip
|
|
||||||
path: cypress-videos.zip
|
|
||||||
|
|
||||||
Build-and-test-development-latest-adapter:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18
|
|
||||||
|
|
||||||
- name: Write .npmrc file
|
|
||||||
run: echo "$NPMRC" > client/.npmrc
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
NPMRC: ${{ secrets.NPMRC}}
|
|
||||||
|
|
||||||
- run: apt-get update
|
|
||||||
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
|
||||||
- run: apt install -y ./google-chrome*.deb;
|
|
||||||
- run: export CHROME_BIN=/usr/bin/google-chrome
|
|
||||||
- run: apt-get update -y
|
|
||||||
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
|
|
||||||
- run: apt -y install jq
|
|
||||||
|
|
||||||
- name: Write cypress credentials
|
|
||||||
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
# Install pm2 and prepare SASJS server
|
|
||||||
- run: npm i -g pm2
|
|
||||||
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
|
||||||
- run: unzip linux.zip
|
|
||||||
- run: touch .env
|
|
||||||
- run: echo RUN_TIMES=js >> .env
|
|
||||||
- run: echo NODE_PATH=node >> .env
|
|
||||||
- run: echo CORS=enable >> .env
|
|
||||||
- run: echo WHITELIST=http://localhost:4200 >> .env
|
|
||||||
- run: cat .env
|
|
||||||
- run: pm2 start api-linux --wait-ready
|
|
||||||
|
|
||||||
- name: Deploy mocked services
|
|
||||||
run: |
|
|
||||||
cd ./sas/mocks/sasjs
|
|
||||||
npm install -g @sasjs/cli
|
|
||||||
npm install -g replace-in-files-cli
|
|
||||||
sasjs cbd -t server-ci
|
|
||||||
|
|
||||||
- name: Install ZIP
|
|
||||||
run: |
|
|
||||||
apt-get update
|
|
||||||
apt-get install zip
|
|
||||||
|
|
||||||
- name: Prepare and run frontend and cypress
|
|
||||||
run: |
|
|
||||||
cd ./client
|
|
||||||
mv ./cypress.env.example.json ./cypress.env.json
|
|
||||||
replace-in-files --regex='"username".*' --replacement='"username":"'${{ secrets.CYPRESS_USERNAME_SASJS }}'",' ./cypress.env.json
|
|
||||||
replace-in-files --regex='"password".*' --replacement='"password":"'${{ secrets.CYPRESS_PWD_SASJS }}'" ' ./cypress.env.json
|
|
||||||
cat ./cypress.env.json
|
|
||||||
npm run postinstall
|
|
||||||
npm install @sasjs/adapter@latest
|
|
||||||
# Prepare index.html to SASJS local
|
|
||||||
replace-in-files --regex='serverUrl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./src/index.html
|
|
||||||
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/Public/app/devtest"' ./src/index.html
|
|
||||||
replace-in-files --regex='serverType=".*?"' --replacement='serverType="SASJS"' ./src/index.html
|
|
||||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
|
||||||
cat ./cypress.config.ts
|
|
||||||
# Start frontend and run cypress
|
|
||||||
npm start & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
|
|
||||||
|
|
||||||
- name: Zip Cypress videos
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
zip -r cypress-videos ./client/cypress/videos
|
|
||||||
|
|
||||||
- name: Cypress videos artifacts
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: cypress-videos-latest-adapter.zip
|
|
||||||
path: cypress-videos.zip
|
|
@ -1,19 +1,171 @@
|
|||||||
name: Release
|
name: Release
|
||||||
run-name: Releasing DC
|
run-name: Testing and Releasing DC
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
Build-production-and-ng-test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
|
|
||||||
|
- name: Write .npmrc file
|
||||||
|
run: |
|
||||||
|
touch client/.npmrc
|
||||||
|
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
||||||
|
|
||||||
|
- name: Install Chrome for Angular tests
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||||
|
apt install -y ./google-chrome*.deb;
|
||||||
|
export CHROME_BIN=/usr/bin/google-chrome
|
||||||
|
|
||||||
|
- name: Write cypress credentials
|
||||||
|
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
cd client
|
||||||
|
# Install sheet
|
||||||
|
wget ${{ secrets.SHEETLINK }}
|
||||||
|
mv ${{ secrets.SHEETNAME }} ${{ secrets.SHEETNAME }}.tgz
|
||||||
|
npm i ${{ secrets.SHEETNAME }}.tgz
|
||||||
|
# End
|
||||||
|
|
||||||
|
- name: Check audit
|
||||||
|
# Audit should fail and stop the CI if critical vulnerability found
|
||||||
|
run: |
|
||||||
|
npm audit --audit-level=critical --omit=dev
|
||||||
|
cd ./sas
|
||||||
|
npm audit --audit-level=critical --omit=dev
|
||||||
|
cd ../client
|
||||||
|
npm audit --audit-level=critical --omit=dev
|
||||||
|
|
||||||
|
- name: Angular Tests
|
||||||
|
run: |
|
||||||
|
cd client
|
||||||
|
npm test -- --no-watch --no-progress --browsers=ChromeHeadlessCI
|
||||||
|
|
||||||
|
- name: Angular Production Build
|
||||||
|
run: |
|
||||||
|
cd client
|
||||||
|
npm run postinstall
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
Build-and-test-development:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: Build-production-and-ng-test
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
|
||||||
|
- name: Write .npmrc file
|
||||||
|
run: |
|
||||||
|
touch client/.npmrc
|
||||||
|
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
||||||
|
|
||||||
|
- run: apt-get update
|
||||||
|
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||||
|
- run: apt install -y ./google-chrome*.deb;
|
||||||
|
- run: export CHROME_BIN=/usr/bin/google-chrome
|
||||||
|
- run: apt-get update -y
|
||||||
|
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
|
||||||
|
- run: apt -y install jq
|
||||||
|
|
||||||
|
- name: Write cypress credentials
|
||||||
|
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
cd client
|
||||||
|
# Install sheet
|
||||||
|
wget ${{ secrets.SHEETLINK }}
|
||||||
|
mv ${{ secrets.SHEETNAME }} ${{ secrets.SHEETNAME }}.tgz
|
||||||
|
npm i ${{ secrets.SHEETNAME }}.tgz
|
||||||
|
# End
|
||||||
|
|
||||||
|
# Install pm2 and prepare SASJS server
|
||||||
|
- run: npm i -g pm2
|
||||||
|
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||||
|
- run: unzip linux.zip
|
||||||
|
- run: touch .env
|
||||||
|
- run: echo RUN_TIMES=js >> .env
|
||||||
|
- run: echo NODE_PATH=node >> .env
|
||||||
|
- run: echo CORS=enable >> .env
|
||||||
|
- run: echo WHITELIST=http://localhost:4200 >> .env
|
||||||
|
- run: cat .env
|
||||||
|
- run: pm2 start api-linux --wait-ready
|
||||||
|
|
||||||
|
- name: Deploy mocked services
|
||||||
|
run: |
|
||||||
|
cd ./sas/mocks/sasjs
|
||||||
|
npm install -g @sasjs/cli
|
||||||
|
npm install -g replace-in-files-cli
|
||||||
|
sasjs cbd -t server-ci
|
||||||
|
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
|
||||||
|
|
||||||
|
- name: Install ZIP
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install zip
|
||||||
|
|
||||||
|
- name: Prepare and run frontend and cypress
|
||||||
|
run: |
|
||||||
|
cd ./client
|
||||||
|
mv ./cypress.env.example.json ./cypress.env.json
|
||||||
|
replace-in-files --regex='"username".*' --replacement='"username":"'${{ secrets.CYPRESS_USERNAME_SASJS }}'",' ./cypress.env.json
|
||||||
|
replace-in-files --regex='"password".*' --replacement='"password":"'${{ secrets.CYPRESS_PWD_SASJS }}'" ' ./cypress.env.json
|
||||||
|
cat ./cypress.env.json
|
||||||
|
npm run postinstall
|
||||||
|
# Prepare index.html to SASJS local
|
||||||
|
replace-in-files --regex='serverUrl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./src/index.html
|
||||||
|
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/Public/app/devtest"' ./src/index.html
|
||||||
|
replace-in-files --regex='serverType=".*?"' --replacement='serverType="SASJS"' ./src/index.html
|
||||||
|
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
||||||
|
cat ./cypress.config.ts
|
||||||
|
# Start frontend and run cypress
|
||||||
|
npm start & npx wait-on http://localhost:4200 && npx cypress run --browser chrome --spec "cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts"
|
||||||
|
|
||||||
|
- name: Zip Cypress videos
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
zip -r cypress-videos ./client/cypress/videos
|
||||||
|
|
||||||
|
- name: Add cypress videos artifacts
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: cypress-videos.zip
|
||||||
|
path: cypress-videos.zip
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [Build-production-and-ng-test, Build-and-test-development]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
echo "$NPMRC" > client/.npmrc
|
echo "$NPMRC" > client/.npmrc
|
||||||
@ -30,11 +182,16 @@ jobs:
|
|||||||
npm i -g @sasjs/cli
|
npm i -g @sasjs/cli
|
||||||
# jq is used to parse the release JSON
|
# jq is used to parse the release JSON
|
||||||
apt-get install jq -y
|
apt-get install jq -y
|
||||||
|
# doxygen is used for the SASJS docs
|
||||||
|
apt-get update
|
||||||
|
apt-get install doxygen -y
|
||||||
|
|
||||||
- name: Create Empty Release (assets are posted later)
|
- name: Create Empty Release (assets are posted later)
|
||||||
run: |
|
run: |
|
||||||
npm i
|
npm i
|
||||||
npm i -g semantic-release
|
npm i -g semantic-release
|
||||||
|
# We do a semantic-release DRY RUN to make the job fail if there are no changes to release
|
||||||
|
GITEA_TOKEN=${{ secrets.RELEASE_TOKEN }} GITEA_URL=https://git.datacontroller.io semantic-release --dry-run | grep -q "There are no relevant changes, so no new version is released." && exit 1
|
||||||
GITEA_TOKEN=${{ secrets.RELEASE_TOKEN }} GITEA_URL=https://git.datacontroller.io semantic-release
|
GITEA_TOKEN=${{ secrets.RELEASE_TOKEN }} GITEA_URL=https://git.datacontroller.io semantic-release
|
||||||
|
|
||||||
- name: Frontend Build
|
- name: Frontend Build
|
||||||
@ -42,13 +199,18 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd client
|
cd client
|
||||||
npm ci
|
npm ci
|
||||||
|
# Install sheet
|
||||||
|
wget ${{ secrets.SHEETLINK }}
|
||||||
|
mv ${{ secrets.SHEETNAME }} ${{ secrets.SHEETNAME }}.tgz
|
||||||
|
npm i ${{ secrets.SHEETNAME }}.tgz
|
||||||
|
# End
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
- name: Build SAS9 EBI Release
|
- name: Build SAS9 EBI Release
|
||||||
description: Compile SAS 9 services, remove tests & create deployment program
|
description: Compile SAS 9 services, remove tests & create deployment program
|
||||||
run: |
|
run: |
|
||||||
cd sas
|
cd sas
|
||||||
npm ci
|
npm i
|
||||||
sasjs c -t sas9
|
sasjs c -t sas9
|
||||||
rm -rf sasjsbuild/tests
|
rm -rf sasjsbuild/tests
|
||||||
sasjs b -t sas9
|
sasjs b -t sas9
|
||||||
@ -96,15 +258,22 @@ jobs:
|
|||||||
- name: Release Typedoc
|
- name: Release Typedoc
|
||||||
run: |
|
run: |
|
||||||
cd client
|
cd client
|
||||||
|
npm -g install cloudron-surfer
|
||||||
npm run compodoc:build
|
npm run compodoc:build
|
||||||
surfer put --token ${{ secrets.TSDOC_TOKEN }} --server tsdoc.datacontroller.io documentation/* /
|
surfer put --token ${{ secrets.TSDOC_TOKEN }} --server webdoc.datacontroller.io documentation/* /
|
||||||
|
|
||||||
|
- name: Release code.datacontroller.io
|
||||||
|
run: |
|
||||||
|
cd sas
|
||||||
|
sasjs doc
|
||||||
|
surfer put --token ${{ secrets.CODE_DATACONTROLLER_IO }} --server code.datacontroller.io sasjsbuild/sasdocs/* /
|
||||||
|
|
||||||
- name: Upload assets to release
|
- name: Upload assets to release
|
||||||
run: |
|
run: |
|
||||||
RELEASE_ID=`curl -k 'https://git.datacontroller.io/api/v1/repos/dc/dc/releases/latest?access_token=${{ secrets.RELEASE_TOKEN }}' | jq -r '.id'`
|
RELEASE_ID=`curl -k 'https://git.datacontroller.io/api/v1/repos/dc/dc/releases/latest?access_token=${{ secrets.RELEASE_TOKEN }}' | jq -r '.id'`
|
||||||
RELEASE_BODY=`curl -k 'https://git.datacontroller.io/api/v1/repos/dc/dc/releases/latest?access_token=${{ secrets.RELEASE_TOKEN }}' | jq -r '.body'`
|
RELEASE_BODY=`curl -k 'https://git.datacontroller.io/api/v1/repos/dc/dc/releases/latest?access_token=${{ secrets.RELEASE_TOKEN }}' | jq -r '.body'`
|
||||||
# Update body
|
# Update body
|
||||||
curl --data '{"draft": true,"body":"'"$RELEASE_BODY\n\nFor installation instructions, please visit https://docs.datacontroller.io/"'"}' -X PATCH --header 'Content-Type: application/json' -k https://git.datacontroller.io/api/v1/repos/dc/dc/releases/$RELEASE_ID?access_token=${{ secrets.RELEASE_TOKEN }}
|
curl --data '{"draft": false,"body":"'"$RELEASE_BODY\n\nFor installation instructions, please visit https://docs.datacontroller.io/"'"}' -X PATCH --header 'Content-Type: application/json' -k https://git.datacontroller.io/api/v1/repos/dc/dc/releases/$RELEASE_ID?access_token=${{ secrets.RELEASE_TOKEN }}
|
||||||
# Upload assets
|
# Upload assets
|
||||||
URL="https://git.datacontroller.io/api/v1/repos/dc/dc/releases/$RELEASE_ID/assets?access_token=${{ secrets.RELEASE_TOKEN }}"
|
URL="https://git.datacontroller.io/api/v1/repos/dc/dc/releases/$RELEASE_ID/assets?access_token=${{ secrets.RELEASE_TOKEN }}"
|
||||||
curl -k $URL -F attachment=@frontend.zip
|
curl -k $URL -F attachment=@frontend.zip
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,7 @@ client/src/environments/version.ts
|
|||||||
client/cypress/screenshots
|
client/cypress/screenshots
|
||||||
client/cypress/results
|
client/cypress/results
|
||||||
client/cypress/videos
|
client/cypress/videos
|
||||||
|
client/documentation
|
||||||
cypress.env.json
|
cypress.env.json
|
||||||
sasjsbuild
|
sasjsbuild
|
||||||
sasjsresults
|
sasjsresults
|
||||||
|
@ -6,11 +6,13 @@
|
|||||||
"@semantic-release/commit-analyzer",
|
"@semantic-release/commit-analyzer",
|
||||||
"@semantic-release/release-notes-generator",
|
"@semantic-release/release-notes-generator",
|
||||||
"@semantic-release/changelog",
|
"@semantic-release/changelog",
|
||||||
|
"@semantic-release/npm",
|
||||||
[
|
[
|
||||||
"@semantic-release/git",
|
"@semantic-release/git",
|
||||||
{
|
{
|
||||||
"assets": [
|
"assets": [
|
||||||
"CHANGELOG.md"
|
"CHANGELOG.md",
|
||||||
|
"package.json"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
35
.vscode/settings.json
vendored
35
.vscode/settings.json
vendored
@ -1,18 +1,19 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"SYSERRORTEXT",
|
"Licence",
|
||||||
"SYSWARNINGTEXT"
|
"SYSERRORTEXT",
|
||||||
],
|
"SYSWARNINGTEXT",
|
||||||
"editor.rulers": [
|
"xlmaprules",
|
||||||
80
|
"xlmaps"
|
||||||
],
|
],
|
||||||
"files.trimTrailingWhitespace": true,
|
"editor.rulers": [80],
|
||||||
"[markdown]": {
|
"files.trimTrailingWhitespace": true,
|
||||||
"files.trimTrailingWhitespace": false
|
"[markdown]": {
|
||||||
},
|
"files.trimTrailingWhitespace": false
|
||||||
"workbench.colorCustomizations": {
|
},
|
||||||
"titleBar.activeForeground": "#ebe8e8",
|
"workbench.colorCustomizations": {
|
||||||
"titleBar.activeBackground": "#95ff0053",
|
"titleBar.activeForeground": "#ebe8e8",
|
||||||
},
|
"titleBar.activeBackground": "#95ff0053"
|
||||||
"terminal.integrated.wordSeparators": " ()[]{}',\"`─‘’"
|
},
|
||||||
}
|
"terminal.integrated.wordSeparators": " ()[]{}',\"`─‘’"
|
||||||
|
}
|
||||||
|
203
CHANGELOG.md
203
CHANGELOG.md
@ -1,3 +1,206 @@
|
|||||||
|
# [6.7.0](https://git.datacontroller.io/dc/dc/compare/v6.6.4...v6.7.0) (2024-04-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* numeric values in hot dropdown aligned right ([9635626](https://git.datacontroller.io/dc/dc/commit/963562621ddf0e8d24a29a8481c5e6da1b040708))
|
||||||
|
|
||||||
|
## [6.6.4](https://git.datacontroller.io/dc/dc/compare/v6.6.3...v6.6.4) (2024-04-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* ordering SOFTSELECT numerically in dropdown ([f522038](https://git.datacontroller.io/dc/dc/commit/f522038b8ddb1da14b8adbf8346d0a4539a94cc8)), closes [#85](https://git.datacontroller.io/dc/dc/issues/85)
|
||||||
|
* reverting col ([fbbcf90](https://git.datacontroller.io/dc/dc/commit/fbbcf90956bf538b032b0107c07b8576d20353b9))
|
||||||
|
* typo ([31d4e5c](https://git.datacontroller.io/dc/dc/commit/31d4e5c727f790d428fb2ea8da60dca929561805))
|
||||||
|
|
||||||
|
## [6.6.3](https://git.datacontroller.io/dc/dc/compare/v6.6.2...v6.6.3) (2024-02-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* allow empty clause value when NE or CONTAINS ([432450a](https://git.datacontroller.io/dc/dc/commit/432450a15b51a269821ba1d430854f5d1dd04703))
|
||||||
|
|
||||||
|
## [6.6.2](https://git.datacontroller.io/dc/dc/compare/v6.6.1...v6.6.2) (2024-02-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* excel with commas getting wrapped in quotes ([3860134](https://git.datacontroller.io/dc/dc/commit/38601346a529cfe3787bb286a639e0293c365020))
|
||||||
|
|
||||||
|
## [6.6.1](https://git.datacontroller.io/dc/dc/compare/v6.6.0...v6.6.1) (2024-02-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **client:** bumped @sasjs/adapter with fixed redirected login ([eb1c09d](https://git.datacontroller.io/dc/dc/commit/eb1c09d7909ba07faf763da261545dc1efaec1b3))
|
||||||
|
|
||||||
|
# [6.6.0](https://git.datacontroller.io/dc/dc/compare/v6.5.2...v6.6.0) (2024-02-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* adjust the col numbers in extracted data ([cff5989](https://git.datacontroller.io/dc/dc/commit/cff598955930d2581349e5c6e8b2dd3f9ac96b4c))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* extra table metadata for [#75](https://git.datacontroller.io/dc/dc/issues/75) ([837821f](https://git.datacontroller.io/dc/dc/commit/837821fd01477d340524dfdaf8dd3d3758cf3095))
|
||||||
|
* show dsnote on hover title ([6565834](https://git.datacontroller.io/dc/dc/commit/6565834ad4089ecf2de39967e6ed6f217ee4a0a5))
|
||||||
|
|
||||||
|
## [6.5.2](https://git.datacontroller.io/dc/dc/compare/v6.5.1...v6.5.2) (2024-02-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* ordering mpe_selectbox data by the data values after selectbox_order ([2b54034](https://git.datacontroller.io/dc/dc/commit/2b5403497317632a4be8a00f21455c036f1e6461))
|
||||||
|
|
||||||
|
## [6.5.1](https://git.datacontroller.io/dc/dc/compare/v6.5.0...v6.5.1) (2024-02-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* ensuring submitter email can be pulled from mpe_emails ([eac0104](https://git.datacontroller.io/dc/dc/commit/eac0104d7aebaf98ff1d1c504c1ce3b25d4a0ce8))
|
||||||
|
|
||||||
|
# [6.5.0](https://git.datacontroller.io/dc/dc/compare/v6.4.0...v6.5.0) (2024-01-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* filtering by reference to Variables as well as Values ([6eb1aa8](https://git.datacontroller.io/dc/dc/commit/6eb1aa85d29294d63e6af377e622fbed7fd1fab8))
|
||||||
|
|
||||||
|
# [6.4.0](https://git.datacontroller.io/dc/dc/compare/v6.3.1...v6.4.0) (2024-01-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add dcLib to globals ([5d93346](https://git.datacontroller.io/dc/dc/commit/5d93346b52eda27c2829770e96686a713296d373))
|
||||||
|
* add service to get xlmap rules and fixed interface name ([9ffa30a](https://git.datacontroller.io/dc/dc/commit/9ffa30ab747f5b62acbd452431a5e6e440afcb80))
|
||||||
|
* increasing length of mpe_excel_map cols to ([2d4d068](https://git.datacontroller.io/dc/dc/commit/2d4d068413dcdac98581f08939e74bde65b73428))
|
||||||
|
* providing info on mapids to FE ([fd94945](https://git.datacontroller.io/dc/dc/commit/fd94945466c1a797ddc89815258a65624a9cb0cf))
|
||||||
|
* removing tables from EDIT menu that are in xlmaps ([9550ae4](https://git.datacontroller.io/dc/dc/commit/9550ae4d1154a0272f8a2427ac9d2afdfd699c96))
|
||||||
|
* removing XLMAP_TARGETLIBDS from mpe_xlmaps_rules table ([93702c6](https://git.datacontroller.io/dc/dc/commit/93702c63dc280cdba1e46f0fd8fe0deaec879611))
|
||||||
|
* renaming TABLE macvar to LOAD_REF in postdata.sas ([01915a2](https://git.datacontroller.io/dc/dc/commit/01915a2db9a4dfb94e4e8213e2c32181da36d349))
|
||||||
|
* reverting xlmap in getdata change ([2d6e747](https://git.datacontroller.io/dc/dc/commit/2d6e747db9b84e9fb0dfcf9102a2f7dd2cb51891))
|
||||||
|
* update edit tab to load ([516e5a2](https://git.datacontroller.io/dc/dc/commit/516e5a206216f79ab1dce9f4eab0d31115743160))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* adding ability to define the target table for excel maps ([c86fba9](https://git.datacontroller.io/dc/dc/commit/c86fba9dc75ddc6033132f469ad1c31b9131b12e))
|
||||||
|
* adding ismap attribute to getdata response (and fixing test) ([2702bb3](https://git.datacontroller.io/dc/dc/commit/2702bb3c84c45903def1aa2b8cc20a6dd080281b))
|
||||||
|
* Complex Excel Uploads ([cf19381](https://git.datacontroller.io/dc/dc/commit/cf193810606f287b8d6f864c4eb64d43c5ab5f3c)), closes [#69](https://git.datacontroller.io/dc/dc/issues/69)
|
||||||
|
* Create Tables / Files dropdown under load tab ([b473b19](https://git.datacontroller.io/dc/dc/commit/b473b198a61f468dff74cd8e64692e7847084a80))
|
||||||
|
* display list of maps in sidebar ([5aec024](https://git.datacontroller.io/dc/dc/commit/5aec0242429942f8a989b5fb79f8d3865e9de01a))
|
||||||
|
* implemented the logic for xlmap component ([50696bb](https://git.datacontroller.io/dc/dc/commit/50696bb926dd00472db65a008771a4b6352871be))
|
||||||
|
* model changes for [#69](https://git.datacontroller.io/dc/dc/issues/69) ([271543a](https://git.datacontroller.io/dc/dc/commit/271543a446a2116718f99f0540e3cd911f9f5fe7))
|
||||||
|
* new getxlmaps service to return rules for a particular xlmap_id ([56264ec](https://git.datacontroller.io/dc/dc/commit/56264ecc6908bf6c8e3e666dfeba7068d6195df8))
|
||||||
|
* validating the excel map after stage (adding load-ref) ([a485c3b](https://git.datacontroller.io/dc/dc/commit/a485c3b78724a36f7bacb264fb02140cc62d6512))
|
||||||
|
|
||||||
|
## [6.3.1](https://git.datacontroller.io/dc/dc/compare/v6.3.0...v6.3.1) (2024-01-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* enabling excel uploads to tables with retained keys, also adding more validation to MPE_TABLES updates ([3efccc4](https://git.datacontroller.io/dc/dc/commit/3efccc4cf3752763d049836724f2491c287f65db))
|
||||||
|
|
||||||
|
# [6.3.0](https://git.datacontroller.io/dc/dc/compare/v6.2.8...v6.3.0) (2023-12-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* viewer row handle ([dadac4f](https://git.datacontroller.io/dc/dc/commit/dadac4f13f85b5446198b6340cad28844defc94d))
|
||||||
|
|
||||||
|
## [6.2.8](https://git.datacontroller.io/dc/dc/compare/v6.2.7...v6.2.8) (2023-12-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bumping sasjs/core to fix mp_loadformat issue ([a1d308e](https://git.datacontroller.io/dc/dc/commit/a1d308ea078786b27bf7ec940d018fc657d4c398))
|
||||||
|
* new logic for -fc suffix. Closes [#63](https://git.datacontroller.io/dc/dc/issues/63) ([5579db0](https://git.datacontroller.io/dc/dc/commit/5579db0eafc668b1bc310099b7cc3062e0598fc4))
|
||||||
|
|
||||||
|
## [6.2.7](https://git.datacontroller.io/dc/dc/compare/v6.2.6...v6.2.7) (2023-11-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **audit:** updated crypto-js (hashing rows in dynamic cell validation) ([a7aa42a](https://git.datacontroller.io/dc/dc/commit/a7aa42a59b71597399924b8d2d06010c806321f3))
|
||||||
|
* missing dependency and avoiding label length limit issue ([91f128c](https://git.datacontroller.io/dc/dc/commit/91f128c2fead1e4f72267d689e67f49ec9a2ab35))
|
||||||
|
|
||||||
|
## [6.2.6](https://git.datacontroller.io/dc/dc/compare/v6.2.5...v6.2.6) (2023-10-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bumping core to address mm_assigndirectlib issue ([c27cdab](https://git.datacontroller.io/dc/dc/commit/c27cdab3fccbde814a29424d0344173a73ea816c))
|
||||||
|
|
||||||
|
## [6.2.5](https://git.datacontroller.io/dc/dc/compare/v6.2.4...v6.2.5) (2023-10-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* enabling AUTHDOMAIN in MM_ASSIGNDIRECTLIB ([008b45a](https://git.datacontroller.io/dc/dc/commit/008b45ad175ec0e6026f5ef3bc210470226e328f))
|
||||||
|
|
||||||
|
## [6.2.4](https://git.datacontroller.io/dc/dc/compare/v6.2.3...v6.2.4) (2023-10-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Enable display of metadata-only tables. Closes [#56](https://git.datacontroller.io/dc/dc/issues/56) ([f3e82b4](https://git.datacontroller.io/dc/dc/commit/f3e82b4ee2a9c1c851f812ac60e9eaf05f91a0f9))
|
||||||
|
|
||||||
|
## [6.2.3](https://git.datacontroller.io/dc/dc/compare/v6.2.2...v6.2.3) (2023-10-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bumping core library to avoid non-ascii char in mp_validatecols.sas. [#50](https://git.datacontroller.io/dc/dc/issues/50) ([11b06f6](https://git.datacontroller.io/dc/dc/commit/11b06f6416300b6d70b1570c415d5a5c004976db))
|
||||||
|
* removing copyright symbol from mpe_alerts macro. [#50](https://git.datacontroller.io/dc/dc/issues/50) ([adb7eb7](https://git.datacontroller.io/dc/dc/commit/adb7eb77550c68a2dab15a6ff358129820e9b612))
|
||||||
|
|
||||||
|
## [6.2.2](https://git.datacontroller.io/dc/dc/compare/v6.2.1...v6.2.2) (2023-10-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* updated SheetJS (crypto) to the latest ([8bd0dd2](https://git.datacontroller.io/dc/dc/commit/8bd0dd22c258911672303869e4df893a98e93575))
|
||||||
|
|
||||||
|
## [6.2.1](https://git.datacontroller.io/dc/dc/compare/v6.2.0...v6.2.1) (2023-10-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* approve, history and submit pages grouped in review module ([e056ece](https://git.datacontroller.io/dc/dc/commit/e056ece2234ef6aab050f6a5b1f8de633b163d91))
|
||||||
|
* closes [#39](https://git.datacontroller.io/dc/dc/issues/39) upcase issue in MPE_SECURITY ([a00d31c](https://git.datacontroller.io/dc/dc/commit/a00d31caf3c5634cd61a4700fb175e76856edbb6))
|
||||||
|
* handsontable v13 ([6f482ec](https://git.datacontroller.io/dc/dc/commit/6f482ec6d909907a304ef9975262889e2370035f))
|
||||||
|
* latest adapter ([5e30dc0](https://git.datacontroller.io/dc/dc/commit/5e30dc0f892fab2af41f4ea56e30f27ec3b3912e))
|
||||||
|
* sasjs/cli and sasjs/core updated to the latest ([8571e01](https://git.datacontroller.io/dc/dc/commit/8571e01e44a8cb6df9d150d271c34bb75bffdf31))
|
||||||
|
* updating editors/stagedata to address issues in particular viya configurations as described in issue [#33](https://git.datacontroller.io/dc/dc/issues/33) ([94ab949](https://git.datacontroller.io/dc/dc/commit/94ab949df8c75072525751a2156b7a32c2e641dc))
|
||||||
|
* updating logic for REPLACE loadtype ([1f2ce55](https://git.datacontroller.io/dc/dc/commit/1f2ce55f249f4af56f0cacdec47e69246cd47431))
|
||||||
|
|
||||||
|
# [6.2.0](https://git.datacontroller.io/dc/dc/compare/v6.1.0...v6.2.0) (2023-08-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* re-enabling full REPLACE uploads ([08e39c4](https://git.datacontroller.io/dc/dc/commit/08e39c4fca570406f9aad3d907cb04596421d074))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* support for European numeric formats ([e48e47b](https://git.datacontroller.io/dc/dc/commit/e48e47bc635452b59e107b235e597c26e748875e))
|
||||||
|
|
||||||
|
# [6.1.0](https://git.datacontroller.io/dc/dc/compare/v6.0.0...v6.1.0) (2023-07-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* missing mf_existds dependency in bitemporal_dataloader ([5ce1701](https://git.datacontroller.io/dc/dc/commit/5ce1701657136f2cf792441412230513ff52e7e8))
|
||||||
|
* reducing audit data volumes. Closes [#4](https://git.datacontroller.io/dc/dc/issues/4) ([54fe701](https://git.datacontroller.io/dc/dc/commit/54fe7013b1a25be228eeb2aba3553f6219952de6))
|
||||||
|
* release script, excel upload duplicate primary keys, cypress fix ([2f79487](https://git.datacontroller.io/dc/dc/commit/2f79487aeaf6268b027a8fa52fcdaae2b449de3d))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* full format deletion, closes [#2](https://git.datacontroller.io/dc/dc/issues/2) ([8dc40bd](https://git.datacontroller.io/dc/dc/commit/8dc40bdd4e3a7ad5c1e6582b4130f24bc445eb77))
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
||||||
|
@ -53,6 +53,17 @@ npm run lint:fix
|
|||||||
Typedoc is used for generating typescript documentation based on the code.
|
Typedoc is used for generating typescript documentation based on the code.
|
||||||
That part is automated and beign done as a part of CI job.
|
That part is automated and beign done as a part of CI job.
|
||||||
|
|
||||||
|
# Release
|
||||||
|
Release is automated as a part of CI job. Workflow file: `.gitea/workflows/release.yaml`.
|
||||||
|
It will run automatically when branch merged to the `main` branch.
|
||||||
|
IMPORTANT!
|
||||||
|
If release job fails, after it has been created empty release and a tag, we must not re-run the relase job until we removed the newly create GIT TAG and RELEASE.
|
||||||
|
To remove the git tag run:
|
||||||
|
```
|
||||||
|
git push -d origin vX.X.X
|
||||||
|
```
|
||||||
|
To remove the release, you need to do it with repo administration over at [https://git.datacontroller.io/dc/dc](https://git.datacontroller.io/dc/dc)
|
||||||
|
|
||||||
# Troubleshooting
|
# Troubleshooting
|
||||||
|
|
||||||
## Makedata service "could not create directory" error
|
## Makedata service "could not create directory" error
|
||||||
|
@ -10,7 +10,7 @@ const check = (cwd) => {
|
|||||||
onlyAllow:
|
onlyAllow:
|
||||||
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
|
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
|
||||||
excludePackages:
|
excludePackages:
|
||||||
'@cds/city@1.1.0;@handsontable/angular@13.0.0;handsontable@13.0.0;hyperformula@2.5.0;jackspeak@2.2.0;path-scurry@1.7.0'
|
'@cds/city@1.1.0;@handsontable/angular@13.1.0;handsontable@13.1.0;hyperformula@2.5.0;jackspeak@2.2.0;path-scurry@1.7.0'
|
||||||
},
|
},
|
||||||
(error, json) => {
|
(error, json) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
3817
client/package-lock.json
generated
3817
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -48,19 +48,18 @@
|
|||||||
"@clr/angular": "^13.17.0",
|
"@clr/angular": "^13.17.0",
|
||||||
"@clr/icons": "^13.0.2",
|
"@clr/icons": "^13.0.2",
|
||||||
"@clr/ui": "^13.17.0",
|
"@clr/ui": "^13.17.0",
|
||||||
"@handsontable/angular": "^13.0.0",
|
"@handsontable/angular": "^13.1.0",
|
||||||
"@sasjs/adapter": "4.3.6",
|
"@sasjs/adapter": "4.10.2",
|
||||||
"@sasjs/utils": "^3.3.0",
|
"@sasjs/utils": "^3.4.0",
|
||||||
"@sheet/crypto": "1.20211122.1",
|
|
||||||
"@types/d3-graphviz": "^2.6.7",
|
"@types/d3-graphviz": "^2.6.7",
|
||||||
"@types/text-encoding": "0.0.35",
|
"@types/text-encoding": "0.0.35",
|
||||||
"base64-arraybuffer": "^0.2.0",
|
"base64-arraybuffer": "^0.2.0",
|
||||||
"buffer": "^5.4.3",
|
"buffer": "^5.4.3",
|
||||||
"crypto-browserify": "3.12.0",
|
"crypto-browserify": "3.12.0",
|
||||||
"crypto-js": "^3.3.0",
|
"crypto-js": "^4.2.0",
|
||||||
"d3-graphviz": "^5.0.2",
|
"d3-graphviz": "^5.0.2",
|
||||||
"fs-extra": "^7.0.1",
|
"fs-extra": "^7.0.1",
|
||||||
"handsontable": "^13.0.0",
|
"handsontable": "^13.1.0",
|
||||||
"https-browserify": "1.0.0",
|
"https-browserify": "1.0.0",
|
||||||
"hyperformula": "^2.5.0",
|
"hyperformula": "^2.5.0",
|
||||||
"iconv-lite": "^0.5.0",
|
"iconv-lite": "^0.5.0",
|
||||||
@ -71,7 +70,6 @@
|
|||||||
"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",
|
||||||
"numbro": "^2.1.1",
|
|
||||||
"os-browserify": "0.3.0",
|
"os-browserify": "0.3.0",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
"save-svg-as-png": "^1.4.17",
|
"save-svg-as-png": "^1.4.17",
|
||||||
@ -94,7 +92,7 @@
|
|||||||
"@compodoc/compodoc": "^1.1.21",
|
"@compodoc/compodoc": "^1.1.21",
|
||||||
"@cypress/webpack-preprocessor": "^5.17.1",
|
"@cypress/webpack-preprocessor": "^5.17.1",
|
||||||
"@types/core-js": "^2.5.5",
|
"@types/core-js": "^2.5.5",
|
||||||
"@types/crypto-js": "^4.0.1",
|
"@types/crypto-js": "^4.2.1",
|
||||||
"@types/es6-shim": "^0.31.39",
|
"@types/es6-shim": "^0.31.39",
|
||||||
"@types/jasmine": "~3.6.0",
|
"@types/jasmine": "~3.6.0",
|
||||||
"@types/lodash-es": "^4.17.3",
|
"@types/lodash-es": "^4.17.3",
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { QueryClause } from './models/TableData'
|
import { QueryClause } from './models/TableData'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtering cache info, to be reused when filtering modal is re-open
|
||||||
|
*/
|
||||||
interface FilterCache {
|
interface FilterCache {
|
||||||
cols: any[]
|
cols: any[]
|
||||||
vals: any[]
|
vals: any[]
|
||||||
@ -10,12 +13,18 @@ interface FilterCache {
|
|||||||
query: QueryClause[]
|
query: QueryClause[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtering cache info in the open viewboxes, to be reused when filtering modal is re-open
|
||||||
|
*/
|
||||||
interface ViewboxCache {
|
interface ViewboxCache {
|
||||||
[key: number]: {
|
[key: number]: {
|
||||||
filter: FilterCache
|
filter: FilterCache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial values when no cached values stored
|
||||||
|
*/
|
||||||
export const initFilter: { filter: FilterCache } = {
|
export const initFilter: { filter: FilterCache } = {
|
||||||
filter: {
|
filter: {
|
||||||
cols: <any[]>[],
|
cols: <any[]>[],
|
||||||
@ -28,8 +37,23 @@ export const initFilter: { filter: FilterCache } = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface XLMapListItem {
|
||||||
|
id: string
|
||||||
|
description: string
|
||||||
|
targetDS: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached filtering values across whole app (editor, viewer, viewboxes)
|
||||||
|
* Cached lineage libraries, tables
|
||||||
|
* Cached metadata tree
|
||||||
|
* Cached usernav tree
|
||||||
|
* Cached viyaApi collections, search and selected endpoint
|
||||||
|
*/
|
||||||
export const globals: {
|
export const globals: {
|
||||||
rootParam: string
|
rootParam: string
|
||||||
|
dcLib: string
|
||||||
|
xlmaps: XLMapListItem[]
|
||||||
editor: any
|
editor: any
|
||||||
viewer: any
|
viewer: any
|
||||||
viewboxes: ViewboxCache
|
viewboxes: ViewboxCache
|
||||||
@ -41,11 +65,13 @@ export const globals: {
|
|||||||
[key: string]: any
|
[key: string]: any
|
||||||
} = {
|
} = {
|
||||||
rootParam: <string>'',
|
rootParam: <string>'',
|
||||||
|
dcLib: '',
|
||||||
|
xlmaps: [],
|
||||||
editor: {
|
editor: {
|
||||||
startupSet: <boolean>false,
|
startupSet: <boolean>false,
|
||||||
treeNodeLibraries: <any[] | null>[],
|
treeNodeLibraries: <any[] | null>[],
|
||||||
libsAndTables: <any[]>[],
|
libsAndTables: <any[]>[],
|
||||||
libraries: <String[] | undefined>[],
|
libraries: <string[] | undefined>[],
|
||||||
library: <string>'',
|
library: <string>'',
|
||||||
table: <string>'',
|
table: <string>'',
|
||||||
filter: <FilterCache>{
|
filter: <FilterCache>{
|
||||||
|
@ -168,7 +168,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<clr-dropdown-menu *clrIfOpen clrPosition="bottom-left">
|
<clr-dropdown-menu *clrIfOpen clrPosition="bottom-left">
|
||||||
<a [routerLink]="['/view']" clrDropdownItem>VIEW</a>
|
<a [routerLink]="['/view']" clrDropdownItem>VIEW</a>
|
||||||
<a [routerLink]="['/home']" clrDropdownItem>EDIT</a>
|
<a [routerLink]="['/home']" clrDropdownItem>LOAD</a>
|
||||||
<a [routerLink]="['/review/submitted']" clrDropdownItem>REVIEW</a>
|
<a [routerLink]="['/review/submitted']" clrDropdownItem>REVIEW</a>
|
||||||
</clr-dropdown-menu>
|
</clr-dropdown-menu>
|
||||||
</clr-dropdown>
|
</clr-dropdown>
|
||||||
@ -189,7 +189,7 @@
|
|||||||
router.url.includes('edit-record') ||
|
router.url.includes('edit-record') ||
|
||||||
router.url.includes('home')
|
router.url.includes('home')
|
||||||
"
|
"
|
||||||
>EDIT</a
|
>LOAD</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
[routerLink]="['/review/submitted']"
|
[routerLink]="['/review/submitted']"
|
||||||
|
@ -57,24 +57,15 @@ export class AppComponent {
|
|||||||
private elementRef: ElementRef
|
private elementRef: ElementRef
|
||||||
) {
|
) {
|
||||||
this.parseDcAdapterSettings()
|
this.parseDcAdapterSettings()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints app info in the console such as:
|
||||||
|
* - Adapter versions
|
||||||
|
* - App version
|
||||||
|
* - Build timestamp
|
||||||
|
*
|
||||||
|
*/
|
||||||
;(window as any).appinfo = () => {
|
;(window as any).appinfo = () => {
|
||||||
const licenseKeyData = this.licenceService.getLicenseKeyData()
|
|
||||||
|
|
||||||
if (licenseKeyData) {
|
|
||||||
const expiry_date = moment(
|
|
||||||
licenseKeyData.valid_until,
|
|
||||||
'YYYY-MM-DD'
|
|
||||||
).startOf('day')
|
|
||||||
const current_date = moment().startOf('day')
|
|
||||||
const daysToExpiry = expiry_date.diff(current_date, 'days')
|
|
||||||
|
|
||||||
licenseKeyData.valid_until += ` (${daysToExpiry} ${
|
|
||||||
daysToExpiry === 1 ? 'day' : 'days'
|
|
||||||
} remaining)`
|
|
||||||
|
|
||||||
if (isNaN(daysToExpiry)) licenseKeyData.valid_until = 'Unlimited'
|
|
||||||
}
|
|
||||||
|
|
||||||
console.table({
|
console.table({
|
||||||
'Adapter version': VERSION.adapterVersion || 'n/a',
|
'Adapter version': VERSION.adapterVersion || 'n/a',
|
||||||
'App version': (VERSION.tag || '').replace('v', ''),
|
'App version': (VERSION.tag || '').replace('v', ''),
|
||||||
@ -87,7 +78,12 @@ export class AppComponent {
|
|||||||
|
|
||||||
this.subscribeToLicenseEvents()
|
this.subscribeToLicenseEvents()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches git tag ang git hash from `version.ts` file
|
||||||
|
* It's placed in the user drop down.
|
||||||
|
*/
|
||||||
this.commitVer = (VERSION.tag || '').replace('v', '') + '.' + VERSION.hash
|
this.commitVer = (VERSION.tag || '').replace('v', '') + '.' + VERSION.hash
|
||||||
|
|
||||||
router.events.subscribe((val) => {
|
router.events.subscribe((val) => {
|
||||||
this.routeUrl = this.router.url
|
this.routeUrl = this.router.url
|
||||||
|
|
||||||
@ -127,8 +123,10 @@ export class AppComponent {
|
|||||||
this.subscribeToAppActive()
|
this.subscribeToAppActive()
|
||||||
this.subscribeToDemoLimitModal()
|
this.subscribeToDemoLimitModal()
|
||||||
|
|
||||||
/* In Viya streaming apps, content is served within an iframe. This code
|
/**
|
||||||
makes that iframe "full screen" so it looks like a regular window. */
|
* In Viya streaming apps, content is served within an iframe. This code
|
||||||
|
* makes that iframe "full screen" so it looks like a regular window.
|
||||||
|
*/
|
||||||
if (window.frameElement) {
|
if (window.frameElement) {
|
||||||
window.frameElement.setAttribute(
|
window.frameElement.setAttribute(
|
||||||
'style',
|
'style',
|
||||||
@ -143,6 +141,9 @@ export class AppComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses adapter settings that are found in the <sasjs> tag inside index.html
|
||||||
|
*/
|
||||||
private parseDcAdapterSettings() {
|
private parseDcAdapterSettings() {
|
||||||
const sasjsElement = document.querySelector('sasjs')
|
const sasjsElement = document.querySelector('sasjs')
|
||||||
|
|
||||||
@ -180,9 +181,14 @@ export class AppComponent {
|
|||||||
this.appService.sasServiceInit()
|
this.appService.sasServiceInit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens licence page with the active licence problem
|
||||||
|
* Problem details are encoded in the url
|
||||||
|
*/
|
||||||
public licenceProblemDetails(url: string) {
|
public licenceProblemDetails(url: string) {
|
||||||
this.router.navigateByUrl(url)
|
this.router.navigateByUrl(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on string provided we return true, false or null
|
* Based on string provided we return true, false or null
|
||||||
* True -> Compute API
|
* True -> Compute API
|
||||||
@ -199,6 +205,12 @@ export class AppComponent {
|
|||||||
return value === 'true' || false
|
return value === 'true' || false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for an `demo limit` event that will show the `Feature locked modal`
|
||||||
|
* For exmaple when in editor upload feature is not enabled
|
||||||
|
* When user tries to upload the excel, editor component will trgger this event
|
||||||
|
* And stop the execution of file upload code.
|
||||||
|
*/
|
||||||
public subscribeToDemoLimitModal() {
|
public subscribeToDemoLimitModal() {
|
||||||
this.eventService.onDemoLimitModalShow.subscribe((featureName: string) => {
|
this.eventService.onDemoLimitModalShow.subscribe((featureName: string) => {
|
||||||
this.demoLimitNotice = {
|
this.demoLimitNotice = {
|
||||||
@ -208,6 +220,10 @@ export class AppComponent {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for licence events so banner can be displayed.
|
||||||
|
* App is free tier, licence will expire, is expired or is invalid
|
||||||
|
*/
|
||||||
public subscribeToLicenseEvents() {
|
public subscribeToLicenseEvents() {
|
||||||
this.licenceService.isAppFreeTier.subscribe((isAppFreeTier: boolean) => {
|
this.licenceService.isAppFreeTier.subscribe((isAppFreeTier: boolean) => {
|
||||||
this.freeTierBanner = isAppFreeTier
|
this.freeTierBanner = isAppFreeTier
|
||||||
@ -227,6 +243,10 @@ export class AppComponent {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for an event that will activate od deactivate full application.
|
||||||
|
* Based on licence key prcoessing result
|
||||||
|
*/
|
||||||
public subscribeToAppActive() {
|
public subscribeToAppActive() {
|
||||||
this.licenceService.isAppActivated.subscribe((value: any) => {
|
this.licenceService.isAppActivated.subscribe((value: any) => {
|
||||||
this.appActive = value
|
this.appActive = value
|
||||||
@ -248,31 +268,51 @@ export class AppComponent {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When startupservice request is finished with valid response, this event will
|
||||||
|
* make sure loading screen is gone.
|
||||||
|
*/
|
||||||
public subscribeToStartupData() {
|
public subscribeToStartupData() {
|
||||||
this.eventService.onStartupDataLoaded.subscribe(() => {
|
this.eventService.onStartupDataLoaded.subscribe(() => {
|
||||||
this.startupDataLoaded = true
|
this.startupDataLoaded = true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens requests modal when requested from event service
|
||||||
|
*/
|
||||||
public subscribeToRequestsModal() {
|
public subscribeToRequestsModal() {
|
||||||
this.eventService.onRequestsModalOpen.subscribe((value: boolean) => {
|
this.eventService.onRequestsModalOpen.subscribe((value: boolean) => {
|
||||||
this.requestsModal = true
|
this.requestsModal = true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes abort modal with matching ID (there could be multiple abort modals open)
|
||||||
|
*/
|
||||||
public closeAbortModal(abortId: number) {
|
public closeAbortModal(abortId: number) {
|
||||||
let abortIndex = this.sasjsAborts.findIndex((abort) => abort.id === abortId)
|
let abortIndex = this.sasjsAborts.findIndex((abort) => abort.id === abortId)
|
||||||
this.sasjsAborts.splice(abortIndex, 1)
|
this.sasjsAborts.splice(abortIndex, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles sidebar when requested from event service
|
||||||
|
*/
|
||||||
public toggleSidebar() {
|
public toggleSidebar() {
|
||||||
this.eventService.toggleSidebar()
|
this.eventService.toggleSidebar()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not current route includes the route from param
|
||||||
|
* @param route route to check
|
||||||
|
*/
|
||||||
public isMainRoute(route: string): boolean {
|
public isMainRoute(route: string): boolean {
|
||||||
return this.router.url.includes(route)
|
return this.router.url.includes(route)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a page for updating the licence.
|
||||||
|
*/
|
||||||
public openLicencingPage() {
|
public openLicencingPage() {
|
||||||
this.router.navigateByUrl('/licensing/update')
|
this.router.navigateByUrl('/licensing/update')
|
||||||
}
|
}
|
||||||
|
2
client/src/app/app.d.ts
vendored
2
client/src/app/app.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
declare module 'save-svg-as-png'
|
declare module 'save-svg-as-png'
|
||||||
|
declare module 'numbro/dist/languages.min'
|
||||||
declare interface Navigator {
|
declare interface Navigator {
|
||||||
msSaveBlob: (blob: any, defaultName?: string) => boolean
|
msSaveBlob: (blob: any, defaultName?: string) => boolean
|
||||||
}
|
}
|
||||||
|
@ -11,27 +11,16 @@ import { NotFoundComponent } from './not-found/not-found.component'
|
|||||||
|
|
||||||
import { SasStoreService } from './services/sas-store.service'
|
import { SasStoreService } from './services/sas-store.service'
|
||||||
import { SharedModule } from './shared/shared.module'
|
import { SharedModule } from './shared/shared.module'
|
||||||
// import { EditorComponent } from './editor/editor.component'
|
|
||||||
import { AppSharedModule } from './app-shared.module'
|
import { AppSharedModule } from './app-shared.module'
|
||||||
import { DeployComponent } from './deploy/deploy.component'
|
|
||||||
import { AutomaticComponent } from './deploy/sections/automatic/automatic.component'
|
|
||||||
import { ManualComponent } from './deploy/sections/manual/manual.component'
|
|
||||||
import { SasjsConfiguratorComponent } from './deploy/sections/sasjs-configurator/sasjs-configurator.component'
|
|
||||||
import { GroupComponent } from './group/group.component'
|
|
||||||
import { LicensingComponent } from './licensing/licensing.component'
|
|
||||||
import { LineageComponent } from './lineage/lineage.component'
|
|
||||||
import { MetadataComponent } from './metadata/metadata.component'
|
|
||||||
import { PipesModule } from './pipes/pipes.module'
|
import { PipesModule } from './pipes/pipes.module'
|
||||||
import { RoleComponent } from './role/role.component'
|
|
||||||
import { ReviewRouteComponent } from './routes/review-route/review-route.component'
|
import { ReviewRouteComponent } from './routes/review-route/review-route.component'
|
||||||
import { LicensingGuard } from './routes/licensing.guard'
|
import { LicensingGuard } from './routes/licensing.guard'
|
||||||
import { UsernavRouteComponent } from './routes/usernav-route/usernav-route.component'
|
import { UsernavRouteComponent } from './routes/usernav-route/usernav-route.component'
|
||||||
import { AppService } from './services/app.service'
|
import { AppService } from './services/app.service'
|
||||||
import { InfoModalComponent } from './shared/abort-modal/info-modal.component'
|
import { InfoModalComponent } from './shared/abort-modal/info-modal.component'
|
||||||
import { RequestsModalComponent } from './shared/requests-modal/requests-modal.component'
|
import { RequestsModalComponent } from './shared/requests-modal/requests-modal.component'
|
||||||
import { UserComponent } from './user/user.component'
|
|
||||||
import { HomeModule } from './home/home.module'
|
import { HomeModule } from './home/home.module'
|
||||||
import { SystemComponent } from './system/system.component'
|
|
||||||
import { DirectivesModule } from './directives/directives.module'
|
import { DirectivesModule } from './directives/directives.module'
|
||||||
import { ViyaApiExplorerComponent } from './viya-api-explorer/viya-api-explorer.component'
|
import { ViyaApiExplorerComponent } from './viya-api-explorer/viya-api-explorer.component'
|
||||||
import { NgxJsonViewerModule } from 'ngx-json-viewer'
|
import { NgxJsonViewerModule } from 'ngx-json-viewer'
|
||||||
@ -40,22 +29,11 @@ import { NgxJsonViewerModule } from 'ngx-json-viewer'
|
|||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
NotFoundComponent,
|
NotFoundComponent,
|
||||||
LineageComponent,
|
|
||||||
ReviewRouteComponent,
|
ReviewRouteComponent,
|
||||||
ReviewRouteComponent,
|
ReviewRouteComponent,
|
||||||
MetadataComponent,
|
|
||||||
UsernavRouteComponent,
|
UsernavRouteComponent,
|
||||||
UserComponent,
|
|
||||||
GroupComponent,
|
|
||||||
RoleComponent,
|
|
||||||
RequestsModalComponent,
|
RequestsModalComponent,
|
||||||
DeployComponent,
|
|
||||||
InfoModalComponent,
|
InfoModalComponent,
|
||||||
LicensingComponent,
|
|
||||||
ManualComponent,
|
|
||||||
AutomaticComponent,
|
|
||||||
SasjsConfiguratorComponent,
|
|
||||||
SystemComponent,
|
|
||||||
ViyaApiExplorerComponent
|
ViyaApiExplorerComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -4,21 +4,23 @@
|
|||||||
* The full license information can be found in LICENSE in the root directory of this project.
|
* The full license information can be found in LICENSE in the root directory of this project.
|
||||||
*/
|
*/
|
||||||
import { ModuleWithProviders } from '@angular/core'
|
import { ModuleWithProviders } from '@angular/core'
|
||||||
import { Routes, RouterModule } from '@angular/router'
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
|
|
||||||
import { HomeComponent } from './home/home.component'
|
|
||||||
import { NotFoundComponent } from './not-found/not-found.component'
|
import { NotFoundComponent } from './not-found/not-found.component'
|
||||||
|
|
||||||
import { ReviewRouteComponent } from './routes/review-route/review-route.component'
|
import { DeployModule } from './deploy/deploy.module'
|
||||||
import { DeployComponent } from './deploy/deploy.component'
|
|
||||||
import { LicensingComponent } from './licensing/licensing.component'
|
|
||||||
import { LicensingGuard } from './routes/licensing.guard'
|
|
||||||
import { StageModule } from './stage/stage.module'
|
|
||||||
import { EditorModule } from './editor/editor.module'
|
import { EditorModule } from './editor/editor.module'
|
||||||
import { ViewerModule } from './viewer/viewer.module'
|
import { HomeModule } from './home/home.module'
|
||||||
import { SystemComponent } from './system/system.component'
|
import { LicensingModule } from './licensing/licensing.module'
|
||||||
import { ReviewModule } from './review/review.module'
|
import { ReviewModule } from './review/review.module'
|
||||||
|
import { ReviewRouteComponent } from './routes/review-route/review-route.component'
|
||||||
|
import { StageModule } from './stage/stage.module'
|
||||||
|
import { SystemModule } from './system/system.module'
|
||||||
|
import { ViewerModule } from './viewer/viewer.module'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defining routes
|
||||||
|
*/
|
||||||
export const ROUTES: Routes = [
|
export const ROUTES: Routes = [
|
||||||
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
||||||
{
|
{
|
||||||
@ -26,6 +28,9 @@ export const ROUTES: Routes = [
|
|||||||
loadChildren: () => ViewerModule
|
loadChildren: () => ViewerModule
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Load review module (approve, history, submitted)
|
||||||
|
*/
|
||||||
path: 'review',
|
path: 'review',
|
||||||
component: ReviewRouteComponent,
|
component: ReviewRouteComponent,
|
||||||
children: [
|
children: [
|
||||||
@ -37,13 +42,14 @@ export const ROUTES: Routes = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'licensing/:action',
|
path: 'licensing',
|
||||||
component: LicensingComponent,
|
loadChildren: () => LicensingModule
|
||||||
canActivate: [LicensingGuard],
|
|
||||||
canDeactivate: [LicensingGuard]
|
|
||||||
},
|
},
|
||||||
{ path: 'home', component: HomeComponent },
|
{ path: 'home', loadChildren: () => HomeModule },
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Load editor module with subroutes
|
||||||
|
*/
|
||||||
path: 'editor',
|
path: 'editor',
|
||||||
loadChildren: () => EditorModule
|
loadChildren: () => EditorModule
|
||||||
},
|
},
|
||||||
@ -51,12 +57,20 @@ export const ROUTES: Routes = [
|
|||||||
path: 'stage',
|
path: 'stage',
|
||||||
loadChildren: () => StageModule
|
loadChildren: () => StageModule
|
||||||
},
|
},
|
||||||
{ path: 'system', component: SystemComponent },
|
{
|
||||||
{ path: 'deploy', component: DeployComponent },
|
path: 'system',
|
||||||
{ path: 'deploy/manualdeploy', component: DeployComponent },
|
loadChildren: () => SystemModule
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'deploy',
|
||||||
|
loadChildren: () => DeployModule
|
||||||
|
},
|
||||||
{ path: '**', component: NotFoundComponent }
|
{ path: '**', component: NotFoundComponent }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exporting routes
|
||||||
|
*/
|
||||||
export const ROUTING: ModuleWithProviders<RouterModule> = RouterModule.forRoot(
|
export const ROUTING: ModuleWithProviders<RouterModule> = RouterModule.forRoot(
|
||||||
ROUTES,
|
ROUTES,
|
||||||
{ useHash: true }
|
{ useHash: true }
|
||||||
|
14
client/src/app/deploy/deploy-routing.module.ts
Normal file
14
client/src/app/deploy/deploy-routing.module.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
|
import { DeployComponent } from './deploy.component'
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: '', component: DeployComponent },
|
||||||
|
{ path: 'manualdeploy', component: DeployComponent }
|
||||||
|
]
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class DeployRoutingModule {}
|
@ -78,6 +78,9 @@ export class DeployComponent implements OnInit {
|
|||||||
this.setDeployDefaults()
|
this.setDeployDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setting default values used for deploy request
|
||||||
|
*/
|
||||||
public setDeployDefaults() {
|
public setDeployDefaults() {
|
||||||
this.dcPath = this.dcAdapterSettings?.dcPath || ''
|
this.dcPath = this.dcAdapterSettings?.dcPath || ''
|
||||||
this.selectedAdminGroup = this.dcAdapterSettings?.adminGroup || ''
|
this.selectedAdminGroup = this.dcAdapterSettings?.adminGroup || ''
|
||||||
@ -86,6 +89,9 @@ export class DeployComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepting terms of service shows next screen
|
||||||
|
*/
|
||||||
public termsAgreeChange() {
|
public termsAgreeChange() {
|
||||||
if (!this.autodeploy) {
|
if (!this.autodeploy) {
|
||||||
this.getAdminGroups()
|
this.getAdminGroups()
|
||||||
@ -94,6 +100,9 @@ export class DeployComponent implements OnInit {
|
|||||||
this.step++
|
this.step++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches admin groups from VIYA to be selected for a backend deploy
|
||||||
|
*/
|
||||||
public getAdminGroups() {
|
public getAdminGroups() {
|
||||||
fetch(
|
fetch(
|
||||||
this.sasJsConfig.serverUrl + '/identities/groups?sortBy=name&limit=5000',
|
this.sasJsConfig.serverUrl + '/identities/groups?sortBy=name&limit=5000',
|
||||||
|
20
client/src/app/deploy/deploy.module.ts
Normal file
20
client/src/app/deploy/deploy.module.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { DeployComponent } from './deploy.component'
|
||||||
|
import { AutomaticComponent } from './sections/automatic/automatic.component'
|
||||||
|
import { ManualComponent } from './sections/manual/manual.component'
|
||||||
|
import { SasjsConfiguratorComponent } from './sections/sasjs-configurator/sasjs-configurator.component'
|
||||||
|
import { ClarityModule } from '@clr/angular'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import { DeployRoutingModule } from './deploy-routing.module'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
DeployComponent,
|
||||||
|
AutomaticComponent,
|
||||||
|
ManualComponent,
|
||||||
|
SasjsConfiguratorComponent
|
||||||
|
],
|
||||||
|
imports: [CommonModule, FormsModule, ClarityModule, DeployRoutingModule]
|
||||||
|
})
|
||||||
|
export class DeployModule {}
|
@ -55,6 +55,13 @@ export class AutomaticComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {}
|
ngOnInit(): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes sas.json file to deploy the backend
|
||||||
|
* Method will first try to run the `auto deploy`
|
||||||
|
* If that fails the rest of the code is ignored.
|
||||||
|
* If request is successfull, method will continue to try
|
||||||
|
* to create database if checkbox is toggled on
|
||||||
|
*/
|
||||||
public async executeJson() {
|
public async executeJson() {
|
||||||
this.autodeploying = true
|
this.autodeploying = true
|
||||||
this.isSubmittingJson = true
|
this.isSubmittingJson = true
|
||||||
@ -99,6 +106,9 @@ export class AutomaticComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the `makedata` request sending the ADMIN and DCPATH values
|
||||||
|
*/
|
||||||
public createDatabase() {
|
public createDatabase() {
|
||||||
let data = {
|
let data = {
|
||||||
fromjs: [
|
fromjs: [
|
||||||
|
@ -52,6 +52,9 @@ export class ManualComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {}
|
ngOnInit(): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: Remove
|
||||||
|
*/
|
||||||
public async executableContext() {
|
public async executableContext() {
|
||||||
// getExecutableContexts now need AuthConfig parameter which we don't have on web
|
// getExecutableContexts now need AuthConfig parameter which we don't have on web
|
||||||
// this.contextsLoading = true
|
// this.contextsLoading = true
|
||||||
@ -65,10 +68,16 @@ export class ManualComponent implements OnInit {
|
|||||||
// this.contextsLoading = false
|
// this.contextsLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes sas.json file attached to the input
|
||||||
|
*/
|
||||||
public clearUploadInput(event: Event) {
|
public clearUploadInput(event: Event) {
|
||||||
this.deployService.clearUploadInput(event)
|
this.deployService.clearUploadInput(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads attached SAS file to be sent to sas for execution (backend deploy)
|
||||||
|
*/
|
||||||
public onSasFileChange(event: any) {
|
public onSasFileChange(event: any) {
|
||||||
this.preloadedFile = false
|
this.preloadedFile = false
|
||||||
|
|
||||||
@ -93,12 +102,18 @@ export class ManualComponent implements OnInit {
|
|||||||
fileReader.readAsText(file)
|
fileReader.readAsText(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads attached JSON file to be sent to sas for execution (backend deploy)
|
||||||
|
*/
|
||||||
public async onJsonFileChange(event: any) {
|
public async onJsonFileChange(event: any) {
|
||||||
let file = event.target.files[0]
|
let file = event.target.files[0]
|
||||||
|
|
||||||
this.jsonFile = await this.deployService.readFile(file)
|
this.jsonFile = await this.deployService.readFile(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appending precode lines to the attached sas or json file for backend deploy
|
||||||
|
*/
|
||||||
public addPrecodeLines() {
|
public addPrecodeLines() {
|
||||||
let headerLines = [
|
let headerLines = [
|
||||||
`%let context=${this.selectedContext};`,
|
`%let context=${this.selectedContext};`,
|
||||||
@ -110,6 +125,9 @@ export class ManualComponent implements OnInit {
|
|||||||
this.linesOfCode.unshift(...headerLines)
|
this.linesOfCode.unshift(...headerLines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloadng file with precode included
|
||||||
|
*/
|
||||||
public downloadSasPrecodeFile() {
|
public downloadSasPrecodeFile() {
|
||||||
let linesAsText = this.linesOfCode.join('\n')
|
let linesAsText = this.linesOfCode.join('\n')
|
||||||
let filename = this.fileName.split('.')[0]
|
let filename = this.fileName.split('.')[0]
|
||||||
@ -117,6 +135,9 @@ export class ManualComponent implements OnInit {
|
|||||||
this.downloadFile(linesAsText, filename, 'sas')
|
this.downloadFile(linesAsText, filename, 'sas')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for downloading log and repsonse as a file
|
||||||
|
*/
|
||||||
public downloadFile(
|
public downloadFile(
|
||||||
content: any,
|
content: any,
|
||||||
filename: string,
|
filename: string,
|
||||||
@ -125,10 +146,17 @@ export class ManualComponent implements OnInit {
|
|||||||
this.deployService.downloadFile(content, filename, extension)
|
this.deployService.downloadFile(content, filename, extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saving dcpath to localstorage
|
||||||
|
* FIXME: maybe it'snot necessary
|
||||||
|
*/
|
||||||
public saveDcPath() {
|
public saveDcPath() {
|
||||||
localStorage.setItem('deploy_dc_loc', this.dcPath)
|
localStorage.setItem('deploy_dc_loc', this.dcPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send sas.json to be executed (deploying backend)
|
||||||
|
*/
|
||||||
public async executeJson() {
|
public async executeJson() {
|
||||||
this.isSubmittingJson = true
|
this.isSubmittingJson = true
|
||||||
|
|
||||||
@ -162,6 +190,9 @@ export class ManualComponent implements OnInit {
|
|||||||
this.isSubmittingJson = false
|
this.isSubmittingJson = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send sas file to be executed (deploying backend)
|
||||||
|
*/
|
||||||
public async executeSAS() {
|
public async executeSAS() {
|
||||||
this.executingScript = true
|
this.executingScript = true
|
||||||
this.jobLog = ''
|
this.jobLog = ''
|
||||||
@ -194,6 +225,10 @@ export class ManualComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Running makedata service
|
||||||
|
* @param newTab open and run in new tab
|
||||||
|
*/
|
||||||
public createDatabase(newTab: boolean = true) {
|
public createDatabase(newTab: boolean = true) {
|
||||||
if (newTab) {
|
if (newTab) {
|
||||||
let url =
|
let url =
|
||||||
|
@ -5,7 +5,6 @@ import { ServerType } from '@sasjs/utils/types/serverType'
|
|||||||
import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings'
|
import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings'
|
||||||
import { SASGroup } from 'src/app/models/sas/public-getgroups.model'
|
import { SASGroup } from 'src/app/models/sas/public-getgroups.model'
|
||||||
import { SASjsApiServerInfo } from 'src/app/models/sasjs-api/SASjsApiServerInfo.model'
|
import { SASjsApiServerInfo } from 'src/app/models/sasjs-api/SASjsApiServerInfo.model'
|
||||||
import { HelperService } from 'src/app/services/helper.service'
|
|
||||||
import { SasService } from 'src/app/services/sas.service'
|
import { SasService } from 'src/app/services/sas.service'
|
||||||
import { SasjsService } from 'src/app/services/sasjs.service'
|
import { SasjsService } from 'src/app/services/sasjs.service'
|
||||||
|
|
||||||
@ -51,6 +50,9 @@ export class SasjsConfiguratorComponent implements OnInit {
|
|||||||
this.getServerInfo()
|
this.getServerInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fethes the sasjs server instance info
|
||||||
|
*/
|
||||||
getServerInfo() {
|
getServerInfo() {
|
||||||
this.sasjsService
|
this.sasjsService
|
||||||
.getServerInfo()
|
.getServerInfo()
|
||||||
@ -59,6 +61,9 @@ export class SasjsConfiguratorComponent implements OnInit {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches user groups from the `usernav/usergroupsbymember` service
|
||||||
|
*/
|
||||||
getUserGroups() {
|
getUserGroups() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
||||||
@ -99,6 +104,9 @@ export class SasjsConfiguratorComponent implements OnInit {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creating database
|
||||||
|
*/
|
||||||
makeData() {
|
makeData() {
|
||||||
// const _debug = "&_debug=131"; //debug on
|
// const _debug = "&_debug=131"; //debug on
|
||||||
const _debug = ' ' //debug off
|
const _debug = ' ' //debug off
|
||||||
|
@ -14,7 +14,9 @@ export class DragNdropDirective {
|
|||||||
@Output() fileDropped = new EventEmitter<any>()
|
@Output() fileDropped = new EventEmitter<any>()
|
||||||
@Output() fileDraggedOver = new EventEmitter<any>()
|
@Output() fileDraggedOver = new EventEmitter<any>()
|
||||||
|
|
||||||
// Dragover listener
|
/**
|
||||||
|
* Dragover listener
|
||||||
|
*/
|
||||||
@HostListener('dragover', ['$event'])
|
@HostListener('dragover', ['$event'])
|
||||||
onDragOver(event: any) {
|
onDragOver(event: any) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@ -26,7 +28,9 @@ export class DragNdropDirective {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dragleave listener
|
/**
|
||||||
|
* Dragleave listener
|
||||||
|
*/
|
||||||
@HostListener('dragleave', ['$event'])
|
@HostListener('dragleave', ['$event'])
|
||||||
public onDragLeave(event: any) {
|
public onDragLeave(event: any) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@ -34,7 +38,9 @@ export class DragNdropDirective {
|
|||||||
this.fileOver = false
|
this.fileOver = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop listener
|
/**
|
||||||
|
* Drop listener
|
||||||
|
*/
|
||||||
@HostListener('drop', ['$event'])
|
@HostListener('drop', ['$event'])
|
||||||
public ondrop(event: any) {
|
public ondrop(event: any) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@ -48,6 +54,9 @@ export class DragNdropDirective {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks wether dragging element contain files
|
||||||
|
*/
|
||||||
private containsFiles(event: any) {
|
private containsFiles(event: any) {
|
||||||
if (event && event.dataTransfer && event.dataTransfer.types) {
|
if (event && event.dataTransfer && event.dataTransfer.types) {
|
||||||
for (let i = 0; i < event.dataTransfer.types.length; i++) {
|
for (let i = 0; i < event.dataTransfer.types.length; i++) {
|
||||||
|
@ -22,6 +22,9 @@ export class FileDropDirective {
|
|||||||
this.element = element
|
this.element = element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dragging element drop event
|
||||||
|
*/
|
||||||
@HostListener('drop', ['$event'])
|
@HostListener('drop', ['$event'])
|
||||||
onDrop(event: DragEvent): void {
|
onDrop(event: DragEvent): void {
|
||||||
this._preventAndStop(event)
|
this._preventAndStop(event)
|
||||||
@ -39,6 +42,9 @@ export class FileDropDirective {
|
|||||||
this.fileDrop.emit(fileList)
|
this.fileDrop.emit(fileList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dragging element drag over event
|
||||||
|
*/
|
||||||
@HostListener('dragover', ['$event'])
|
@HostListener('dragover', ['$event'])
|
||||||
onDragOver(event: DragEvent): void {
|
onDragOver(event: DragEvent): void {
|
||||||
this._preventAndStop(event)
|
this._preventAndStop(event)
|
||||||
@ -59,6 +65,10 @@ export class FileDropDirective {
|
|||||||
this.fileOver.emit(false)
|
this.fileOver.emit(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent propagation trough elements and stop default behavior
|
||||||
|
* For particular event
|
||||||
|
*/
|
||||||
protected _preventAndStop(event: MouseEvent): void {
|
protected _preventAndStop(event: MouseEvent): void {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
@ -21,6 +21,9 @@ export class FileSelectDirective {
|
|||||||
this.element = element
|
this.element = element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if files exist in the input after input change
|
||||||
|
*/
|
||||||
isEmptyAfterSelection(): boolean {
|
isEmptyAfterSelection(): boolean {
|
||||||
return !!this.element.nativeElement.attributes.multiple
|
return !!this.element.nativeElement.attributes.multiple
|
||||||
}
|
}
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
export interface RowValidation {
|
|
||||||
valid: boolean
|
|
||||||
invalidError: string
|
|
||||||
rowNumber?: number
|
|
||||||
colName?: string
|
|
||||||
value?: string
|
|
||||||
}
|
|
@ -24,8 +24,8 @@
|
|||||||
generatedRecordUrl
|
generatedRecordUrl
|
||||||
? 'copy to clipboard'
|
? 'copy to clipboard'
|
||||||
: generateEditRecordUrlLoading
|
: generateEditRecordUrlLoading
|
||||||
? 'Generating url...'
|
? 'Generating url...'
|
||||||
: 'Link to this record'
|
: 'Link to this record'
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -112,7 +112,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
*ngIf="
|
*ngIf="
|
||||||
['autocomplete'].includes(
|
['autocomplete', 'autocomplete.custom'].includes(
|
||||||
$any(currentRecordValidator?.getRule(col.key)?.editor)
|
$any(currentRecordValidator?.getRule(col.key)?.editor)
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@ -163,7 +163,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
*ngIf="
|
*ngIf="
|
||||||
['autocomplete'].includes(
|
['autocomplete', 'autocomplete.custom'].includes(
|
||||||
$any(currentRecordValidator?.getRule(col.key)?.editor)
|
$any(currentRecordValidator?.getRule(col.key)?.editor)
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
@ -59,6 +59,12 @@ export class EditRecordComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {}
|
ngOnInit(): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs native HOT validator against cell value
|
||||||
|
* @param cellValidation column rules
|
||||||
|
* @param cellValue value in the cell that is beign validated
|
||||||
|
* @returns Promise boolean - wether valid or invalid
|
||||||
|
*/
|
||||||
async validateRecordCol(
|
async validateRecordCol(
|
||||||
cellValidation: any,
|
cellValidation: any,
|
||||||
cellValue: any
|
cellValue: any
|
||||||
@ -74,6 +80,12 @@ export class EditRecordComponent implements OnInit {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when date field in the record change
|
||||||
|
* Function will parse date and format to string
|
||||||
|
* @param date picker value
|
||||||
|
* @param colKey column name (key)
|
||||||
|
*/
|
||||||
recordDateChange(date: Date, colKey: string) {
|
recordDateChange(date: Date, colKey: string) {
|
||||||
let cellValidation = this.currentRecordValidator?.getRule(colKey)
|
let cellValidation = this.currentRecordValidator?.getRule(colKey)
|
||||||
let format = cellValidation ? cellValidation.dateFormat : ''
|
let format = cellValidation ? cellValidation.dateFormat : ''
|
||||||
@ -82,24 +94,38 @@ export class EditRecordComponent implements OnInit {
|
|||||||
this.currentRecord[colKey] = moment(date).format(format)
|
this.currentRecord[colKey] = moment(date).format(format)
|
||||||
}
|
}
|
||||||
|
|
||||||
isRecordModalInvalid(): boolean {
|
/**
|
||||||
return this.currentRecordInvalidCols.length > 0
|
* Close edit record modal and apply changes by emitting output event
|
||||||
}
|
*/
|
||||||
|
|
||||||
confirmRecordEdit() {
|
confirmRecordEdit() {
|
||||||
if (this.currentRecordInvalidCols.length < 1) {
|
if (this.currentRecordInvalidCols.length < 1) {
|
||||||
this.onRecordChange.emit(this.currentRecord)
|
this.onRecordChange.emit(this.currentRecord)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close edit record modal without applying the changes
|
||||||
|
*/
|
||||||
closeRecordEdit() {
|
closeRecordEdit() {
|
||||||
this.onRecordEditClose.emit()
|
this.onRecordEditClose.emit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitting output event when dropdown (autocomplete) input in any col change
|
||||||
|
* @param colName column name (key)
|
||||||
|
* @param col column index
|
||||||
|
*/
|
||||||
onRecordDropdownChange(colName: string, col: number) {
|
onRecordDropdownChange(colName: string, col: number) {
|
||||||
this.onRecordDropdownChanged.emit({ colName, col })
|
this.onRecordDropdownChanged.emit({ colName, col })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitting output event when input is focused (clicked on) so we can run a `dynamic cell validation`
|
||||||
|
* Since that bit must be run from the parent component (editor.component)
|
||||||
|
* Result is then applied in the `cellValidation` variable and automatically updated in this component.
|
||||||
|
* @param event input event
|
||||||
|
* @param colName column name (key)
|
||||||
|
*/
|
||||||
onRecordInputFocus(event: any, colName: number) {
|
onRecordInputFocus(event: any, colName: number) {
|
||||||
this.onRecordInputFocused.emit({ event, colName })
|
this.onRecordInputFocused.emit({ event, colName })
|
||||||
}
|
}
|
||||||
|
@ -186,24 +186,37 @@
|
|||||||
} as libdsParsed"
|
} as libdsParsed"
|
||||||
class="editor-title text-center mt-0-i"
|
class="editor-title text-center mt-0-i"
|
||||||
>
|
>
|
||||||
<clr-icon
|
<clr-tooltip>
|
||||||
(click)="datasetInfo = true"
|
<clr-icon
|
||||||
shape="info-circle"
|
clrTooltipTrigger
|
||||||
class="is-highlight cursor-pointer"
|
(click)="datasetInfo = true"
|
||||||
size="24"
|
shape="info-circle"
|
||||||
></clr-icon>
|
class="is-highlight cursor-pointer"
|
||||||
|
size="24"
|
||||||
|
></clr-icon>
|
||||||
|
|
||||||
<clr-icon
|
<clr-icon
|
||||||
*ngIf="libdsParsed.tableName.includes('-FC')"
|
*ngIf="libdsParsed.tableName.includes('-FC')"
|
||||||
shape="bolt"
|
shape="bolt"
|
||||||
class="color-yellow"
|
class="color-yellow"
|
||||||
></clr-icon>
|
></clr-icon>
|
||||||
|
|
||||||
|
<span clrTooltipTrigger>
|
||||||
|
{{ libdsParsed.libName }}.<a
|
||||||
|
class="mr-10"
|
||||||
|
[routerLink]="'/view/data/' + libds!"
|
||||||
|
>{{ libdsParsed.tableName.replace('-FC', '') }}</a
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<clr-tooltip-content
|
||||||
|
clrPosition="bottom-left"
|
||||||
|
clrSize="lg"
|
||||||
|
*clrIfOpen
|
||||||
|
>
|
||||||
|
{{ this.dsNote }}
|
||||||
|
</clr-tooltip-content>
|
||||||
|
</clr-tooltip>
|
||||||
|
|
||||||
{{ libdsParsed.libName }}.<a
|
|
||||||
class="mr-10"
|
|
||||||
[routerLink]="'/view/data/' + libds!"
|
|
||||||
>{{ libdsParsed.tableName.replace('-FC', '') }}</a
|
|
||||||
>
|
|
||||||
<ng-container *ngIf="dataSource">
|
<ng-container *ngIf="dataSource">
|
||||||
<ng-container *ngIf="!zeroFilterRows">
|
<ng-container *ngIf="!zeroFilterRows">
|
||||||
({{ dataSource.length | thousandSeparator: ',' }}
|
({{ dataSource.length | thousandSeparator: ',' }}
|
||||||
@ -280,7 +293,7 @@
|
|||||||
licenceState.value.editor_rows_allowed === 1
|
licenceState.value.editor_rows_allowed === 1
|
||||||
? 'row'
|
? 'row'
|
||||||
: 'rows'
|
: 'rows'
|
||||||
}}, contact support@datacontroller.io</span
|
}}, contact support@datacontroller.io</span
|
||||||
>
|
>
|
||||||
</clr-tooltip-content>
|
</clr-tooltip-content>
|
||||||
</clr-tooltip>
|
</clr-tooltip>
|
||||||
@ -385,7 +398,6 @@
|
|||||||
[class.hidden]="hotTable.hidden"
|
[class.hidden]="hotTable.hidden"
|
||||||
[licenseKey]="hotTable.licenseKey"
|
[licenseKey]="hotTable.licenseKey"
|
||||||
>
|
>
|
||||||
<!--[licenseKey]=""-->
|
|
||||||
</hot-table>
|
</hot-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -418,7 +430,7 @@
|
|||||||
licenceState.value.editor_rows_allowed === 1
|
licenceState.value.editor_rows_allowed === 1
|
||||||
? 'row'
|
? 'row'
|
||||||
: 'rows'
|
: 'rows'
|
||||||
}}, contact support@datacontroller.io</span
|
}}, contact support@datacontroller.io</span
|
||||||
>
|
>
|
||||||
</clr-tooltip-content>
|
</clr-tooltip-content>
|
||||||
</clr-tooltip>
|
</clr-tooltip>
|
||||||
@ -468,7 +480,7 @@
|
|||||||
: 'rows'
|
: 'rows'
|
||||||
}}
|
}}
|
||||||
will be submitted. To remove the restriction, contact
|
will be submitted. To remove the restriction, contact
|
||||||
support@datacontroller.io</span
|
support@datacontroller.io</span
|
||||||
>
|
>
|
||||||
<div *ngIf="tableTrue" class="clr-offset-md-2 clr-col-md-8">
|
<div *ngIf="tableTrue" class="clr-offset-md-2 clr-col-md-8">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -529,7 +541,7 @@
|
|||||||
Due to current licence, only
|
Due to current licence, only
|
||||||
{{ licenceState.value.submit_rows_limit }} rows in a file will
|
{{ licenceState.value.submit_rows_limit }} rows in a file will
|
||||||
be submitted. To remove the restriction, contact
|
be submitted. To remove the restriction, contact
|
||||||
support@datacontroller.io
|
support@datacontroller.io
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
@ -15,7 +15,15 @@ import { Subject, Subscription } from 'rxjs'
|
|||||||
import { SasStoreService } from '../services/sas-store.service'
|
import { SasStoreService } from '../services/sas-store.service'
|
||||||
|
|
||||||
import * as XLSX from '@sheet/crypto'
|
import * as XLSX from '@sheet/crypto'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used in combination with buffer
|
||||||
|
*/
|
||||||
const iconv = require('iconv-lite')
|
const iconv = require('iconv-lite')
|
||||||
|
/**
|
||||||
|
* In combination with `iconv` is used for encoding json data captured with sheet js from excel file into a file again
|
||||||
|
* Which will be send to backend
|
||||||
|
*/
|
||||||
const Buffer = require('buffer/').Buffer
|
const Buffer = require('buffer/').Buffer
|
||||||
type AOA = any[][]
|
type AOA = any[][]
|
||||||
|
|
||||||
@ -30,7 +38,7 @@ import { HotTableInterface } from '../models/HotTable.interface'
|
|||||||
import {
|
import {
|
||||||
$DataFormats,
|
$DataFormats,
|
||||||
DSMeta,
|
DSMeta,
|
||||||
EditorsGetdataServiceResponse
|
EditorsGetDataServiceResponse
|
||||||
} from '../models/sas/editors-getdata.model'
|
} from '../models/sas/editors-getdata.model'
|
||||||
import { DataFormat } from '../models/sas/common/DateFormat'
|
import { DataFormat } from '../models/sas/common/DateFormat'
|
||||||
import SheetInfo from '../models/SheetInfo'
|
import SheetInfo from '../models/SheetInfo'
|
||||||
@ -63,6 +71,8 @@ import {
|
|||||||
} from './utils/renderers.utils'
|
} from './utils/renderers.utils'
|
||||||
import { isStringDecimal, isStringNumber } from './utils/types.utils'
|
import { isStringDecimal, isStringNumber } from './utils/types.utils'
|
||||||
import { LicenceService } from '../services/licence.service'
|
import { LicenceService } from '../services/licence.service'
|
||||||
|
import * as numbro from 'numbro'
|
||||||
|
import * as languages from 'numbro/dist/languages.min'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-editor',
|
selector: 'app-editor',
|
||||||
@ -111,6 +121,7 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
datasetInfo: boolean = false
|
datasetInfo: boolean = false
|
||||||
dsmeta: DSMeta[] = []
|
dsmeta: DSMeta[] = []
|
||||||
|
dsNote = ''
|
||||||
|
|
||||||
viewboxes: boolean = false
|
viewboxes: boolean = false
|
||||||
|
|
||||||
@ -362,12 +373,19 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
private cdf: ChangeDetectorRef,
|
private cdf: ChangeDetectorRef,
|
||||||
private hotRegisterer: HotTableRegisterer
|
private hotRegisterer: HotTableRegisterer
|
||||||
) {
|
) {
|
||||||
|
const lang = languages[window.navigator.language]
|
||||||
|
if (lang)
|
||||||
|
numbro.default.registerLanguage(languages[window.navigator.language])
|
||||||
|
|
||||||
this.hotRegisterer = new HotTableRegisterer()
|
this.hotRegisterer = new HotTableRegisterer()
|
||||||
|
|
||||||
this.parseRestrictions()
|
this.parseRestrictions()
|
||||||
this.setRestrictions()
|
this.setRestrictions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare feature restrictions based on licence key
|
||||||
|
*/
|
||||||
private parseRestrictions() {
|
private parseRestrictions() {
|
||||||
this.restrictions.restrictAddRecord =
|
this.restrictions.restrictAddRecord =
|
||||||
this.licenceState.value.addRecord === false
|
this.licenceState.value.addRecord === false
|
||||||
@ -377,6 +395,10 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
this.licenceState.value.fileUpload === false
|
this.licenceState.value.fileUpload === false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applying prepared restrictions
|
||||||
|
* @param overrideRestrictions can be used to apply and override specific restrictions
|
||||||
|
*/
|
||||||
private setRestrictions(overrideRestrictions?: EditorRestrictions) {
|
private setRestrictions(overrideRestrictions?: EditorRestrictions) {
|
||||||
if (overrideRestrictions) {
|
if (overrideRestrictions) {
|
||||||
this.restrictions = {
|
this.restrictions = {
|
||||||
@ -396,6 +418,9 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disabling add row button based on wether rows limit is present
|
||||||
|
*/
|
||||||
private checkRowLimit() {
|
private checkRowLimit() {
|
||||||
if (this.columnLevelSecurityFlag) return
|
if (this.columnLevelSecurityFlag) return
|
||||||
|
|
||||||
@ -410,12 +435,19 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resetting filter variables
|
||||||
|
*/
|
||||||
public resetFilter() {
|
public resetFilter() {
|
||||||
if (this.queryFilterCompList.first) {
|
if (this.queryFilterCompList.first) {
|
||||||
this.queryFilterCompList.first.resetFilter()
|
this.queryFilterCompList.first.resetFilter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Openning file upload modal
|
||||||
|
* If feature is locked, `feature locked` modal will be shown
|
||||||
|
*/
|
||||||
public onShowUploadModal() {
|
public onShowUploadModal() {
|
||||||
if (this.restrictions.restrictFileUpload) {
|
if (this.restrictions.restrictFileUpload) {
|
||||||
this.eventService.showDemoLimitModal('File Upload')
|
this.eventService.showDemoLimitModal('File Upload')
|
||||||
@ -433,6 +465,10 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
if (!this.uploadPreview) this.showUploadModal = true
|
if (!this.uploadPreview) this.showUploadModal = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by FileDropDirective
|
||||||
|
* @param e true if file is dragged over the drop zone
|
||||||
|
*/
|
||||||
public fileOverBase(e: boolean): void {
|
public fileOverBase(e: boolean): void {
|
||||||
this.hasBaseDropZoneOver = e
|
this.hasBaseDropZoneOver = e
|
||||||
}
|
}
|
||||||
@ -614,6 +650,10 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
return returnObj
|
return returnObj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When excel is password protected we will display the password promppt for user to type password in.
|
||||||
|
* @returns Password user input or undefined if discarded by user
|
||||||
|
*/
|
||||||
public promptExcelPassword(): Promise<string | undefined> {
|
public promptExcelPassword(): Promise<string | undefined> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.filePasswordModal = true
|
this.filePasswordModal = true
|
||||||
@ -639,6 +679,13 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses attached file, to be uploaded
|
||||||
|
* If attached file is CSV it will be send to backend straight away
|
||||||
|
* If attached file is EXCEL it will be displayed in the table, in preview mode
|
||||||
|
* @param event file drop event
|
||||||
|
* @param dropped whether it's dropped or added by browse button
|
||||||
|
*/
|
||||||
public getFileDesc(event: any, dropped: boolean = false) {
|
public getFileDesc(event: any, dropped: boolean = false) {
|
||||||
this.excelUploadState = 'Loading'
|
this.excelUploadState = 'Loading'
|
||||||
this.excelFileParsing = true
|
this.excelFileParsing = true
|
||||||
@ -893,13 +940,30 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
return row.map((col: any, index: number) => {
|
return row.map((col: any, index: number) => {
|
||||||
if (!col && col !== 0) col = ''
|
if (!col && col !== 0) col = ''
|
||||||
|
|
||||||
if (isNaN(col)) {
|
/**
|
||||||
col = col.replace(/"/g, '""')
|
* Keeping this for the reference
|
||||||
|
* Code below used to convert JSON to CSV
|
||||||
|
* now the XLSX is converting to CSV
|
||||||
|
*/
|
||||||
|
// if (isNaN(col)) {
|
||||||
|
// // Match and replace the double quotes, ignore the first and last char
|
||||||
|
// // in case they are double quotes already
|
||||||
|
// col = col.replace(/(?<!^)"(?!$)/g, '""')
|
||||||
|
|
||||||
if (col.search(/,/g) > -1) {
|
// if (col.search(/,/g) > -1 ||
|
||||||
col = '"' + col + '"'
|
// col.search(/\r|\n/g) > -1
|
||||||
}
|
// ) {
|
||||||
}
|
// // Missing quotes at the end
|
||||||
|
// if (col.search(/"$/g) < 0) {
|
||||||
|
// col = col + '"' // So we add them
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Missing quotes at the start
|
||||||
|
// if (col.search(/^"/g) < 0) {
|
||||||
|
// col = '"' + col // So we add them
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
const colName = this.headerShow[index]
|
const colName = this.headerShow[index]
|
||||||
const colRule = this.dcValidator?.getRule(colName)
|
const colRule = this.dcValidator?.getRule(colName)
|
||||||
@ -914,20 +978,30 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
this.data = csvArrayData
|
this.data = csvArrayData
|
||||||
|
|
||||||
let csvContent = csvArrayHeaders.join(',') + '\n'
|
// Apply licence rows limitation if exists, it is only affecting data
|
||||||
// Apply licence rows limitation if exists
|
// which will be send to SAS
|
||||||
csvContent += csvArrayData
|
const strippedCsvArrayData = csvArrayData.slice(
|
||||||
.slice(0, this.licenceState.value.submit_rows_limit)
|
0,
|
||||||
.map((e) => e.join(','))
|
this.licenceState.value.submit_rows_limit
|
||||||
.join('\n')
|
)
|
||||||
|
// To submit to sas service, we need clean version of CSV of file
|
||||||
|
// attached. XLSX will do the parsing and heavy lifting
|
||||||
|
// First we create worksheet of json (data we extracted)
|
||||||
|
let ws = XLSX.utils.json_to_sheet(strippedCsvArrayData, {
|
||||||
|
skipHeader: true
|
||||||
|
})
|
||||||
|
// create CSV to be uploaded from worksheet
|
||||||
|
let csvContentClean = XLSX.utils.sheet_to_csv(ws)
|
||||||
|
// Prepend headers
|
||||||
|
csvContentClean = csvArrayHeaders.join(',') + '\n' + csvContentClean
|
||||||
|
|
||||||
if (this.encoding === 'WLATIN1') {
|
if (this.encoding === 'WLATIN1') {
|
||||||
let encoded = iconv.decode(Buffer.from(csvContent), 'CP-1252')
|
let encoded = iconv.decode(Buffer.from(csvContentClean), 'CP-1252')
|
||||||
let blob = new Blob([encoded], { type: 'application/csv' })
|
let blob = new Blob([encoded], { type: 'application/csv' })
|
||||||
let newCSVFile: File = this.blobToFile(blob, this.filename + '.csv')
|
let newCSVFile: File = this.blobToFile(blob, this.filename + '.csv')
|
||||||
this.uploader.addToQueue([newCSVFile])
|
this.uploader.addToQueue([newCSVFile])
|
||||||
} else {
|
} else {
|
||||||
let blob = new Blob([csvContent], { type: 'application/csv' })
|
let blob = new Blob([csvContentClean], { type: 'application/csv' })
|
||||||
let newCSVFile: File = this.blobToFile(blob, this.filename + '.csv')
|
let newCSVFile: File = this.blobToFile(blob, this.filename + '.csv')
|
||||||
this.uploader.addToQueue([newCSVFile])
|
this.uploader.addToQueue([newCSVFile])
|
||||||
}
|
}
|
||||||
@ -1004,6 +1078,9 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits attached excel file that is in preview mode
|
||||||
|
*/
|
||||||
public submitExcel() {
|
public submitExcel() {
|
||||||
if (this.licenceState.value.submit_rows_limit !== Infinity) {
|
if (this.licenceState.value.submit_rows_limit !== Infinity) {
|
||||||
this.submitLimitNotice = true
|
this.submitLimitNotice = true
|
||||||
@ -1013,6 +1090,9 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
this.getFile()
|
this.getFile()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will run validations and upload all of the pending files that are in the uploader queue
|
||||||
|
*/
|
||||||
public getFile() {
|
public getFile() {
|
||||||
if (this.checkInvalid()) {
|
if (this.checkInvalid()) {
|
||||||
this.eventService.showAbortModal(null, 'Invalid values are present.')
|
this.eventService.showAbortModal(null, 'Invalid values are present.')
|
||||||
@ -1085,6 +1165,9 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After excel file is attached and parsed, this function will display it's content in the HOT table in read only mode
|
||||||
|
*/
|
||||||
public getPendingExcelPreview() {
|
public getPendingExcelPreview() {
|
||||||
this.queryTextSaved = this.queryText
|
this.queryTextSaved = this.queryText
|
||||||
this.queryText = ''
|
this.queryText = ''
|
||||||
@ -1136,46 +1219,12 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
this.excelFileParsing = false
|
this.excelFileParsing = false
|
||||||
this.excelUploadState = null
|
this.excelUploadState = null
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* This is half validation feature to speed up file upload
|
|
||||||
* Currently disabled but will leave it here in case it needs to be re-enabled
|
|
||||||
*/
|
|
||||||
// this.excelUploadState = 'Validating-DQ'
|
|
||||||
|
|
||||||
// this.validateRowsOnPendingExcel(
|
|
||||||
// async (rowValidation: RowValidation | undefined) => {
|
|
||||||
// if (rowValidation) {
|
|
||||||
// this.eventService.showAbortModal(
|
|
||||||
// 'Excel validation',
|
|
||||||
// `Please fix the data and re-submit the file. Invalid data details: <br><br> Row: ${rowValidation.rowNumber} <br> Column: ${rowValidation.colName} <br> Reason: <strong>${rowValidation.invalidError}</strong> <br> Invalid value: ${rowValidation.value}`
|
|
||||||
// )
|
|
||||||
|
|
||||||
// this.excelFileParsing = false
|
|
||||||
// this.excelUploadState = null
|
|
||||||
// } else {
|
|
||||||
// this.excelUploadState = 'Validating-HOT'
|
|
||||||
|
|
||||||
// hot.updateSettings(
|
|
||||||
// {
|
|
||||||
// data: this.dataSource
|
|
||||||
// },
|
|
||||||
// false
|
|
||||||
// )
|
|
||||||
// hot.render()
|
|
||||||
|
|
||||||
// hot.validateCells(() => {
|
|
||||||
// this.showUploadModal = false
|
|
||||||
// this.uploadPreview = true
|
|
||||||
|
|
||||||
// this.excelFileParsing = false
|
|
||||||
// this.excelUploadState = null
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops the attached excel file
|
||||||
|
* @param discardData wheter to discard data parsed from the file or to keep it in the table after dropping a attached excel file
|
||||||
|
*/
|
||||||
public discardPendingExcel(discardData?: boolean) {
|
public discardPendingExcel(discardData?: boolean) {
|
||||||
this.hotInstance.updateSettings({
|
this.hotInstance.updateSettings({
|
||||||
maxRows: this.licenceState.value.editor_rows_allowed
|
maxRows: this.licenceState.value.editor_rows_allowed
|
||||||
@ -1199,6 +1248,10 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops attached excel file, keeps it's data in the DC table
|
||||||
|
* User can now edit the table and submit. Witout the file present.
|
||||||
|
*/
|
||||||
public previewTableEditConfirm() {
|
public previewTableEditConfirm() {
|
||||||
this.discardPendingExcel()
|
this.discardPendingExcel()
|
||||||
this.convertToCorrectTypes(this.dataSource)
|
this.convertToCorrectTypes(this.dataSource)
|
||||||
@ -1903,13 +1956,13 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
if (entry.values.length > 0) {
|
if (entry.values.length > 0) {
|
||||||
hot.setCellMeta(entry.row, entry.col, 'renderer', 'autocomplete')
|
hot.setCellMeta(entry.row, entry.col, 'renderer', 'autocomplete')
|
||||||
hot.setCellMeta(entry.row, entry.col, 'editor', 'autocomplete')
|
hot.setCellMeta(entry.row, entry.col, 'editor', 'autocomplete.custom')
|
||||||
hot.setCellMeta(entry.row, entry.col, 'strict', entry.strict)
|
hot.setCellMeta(entry.row, entry.col, 'strict', entry.strict)
|
||||||
hot.setCellMeta(entry.row, entry.col, 'filter', false)
|
hot.setCellMeta(entry.row, entry.col, 'filter', false)
|
||||||
|
|
||||||
this.currentEditRecordValidator?.updateRule(entry.col, {
|
this.currentEditRecordValidator?.updateRule(entry.col, {
|
||||||
renderer: 'autocomplete',
|
renderer: 'autocomplete',
|
||||||
editor: 'autocomplete',
|
editor: 'autocomplete.custom',
|
||||||
strict: entry.strict,
|
strict: entry.strict,
|
||||||
filter: false
|
filter: false
|
||||||
})
|
})
|
||||||
@ -2004,13 +2057,13 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hot.setCellMeta(row, cellCol, 'renderer', 'autocomplete')
|
hot.setCellMeta(row, cellCol, 'renderer', 'autocomplete')
|
||||||
hot.setCellMeta(row, cellCol, 'editor', 'autocomplete')
|
hot.setCellMeta(row, cellCol, 'editor', 'autocomplete.custom')
|
||||||
hot.setCellMeta(row, cellCol, 'strict', cellValidationEntry.strict)
|
hot.setCellMeta(row, cellCol, 'strict', cellValidationEntry.strict)
|
||||||
hot.setCellMeta(row, cellCol, 'filter', false)
|
hot.setCellMeta(row, cellCol, 'filter', false)
|
||||||
|
|
||||||
this.currentEditRecordValidator?.updateRule(cellCol, {
|
this.currentEditRecordValidator?.updateRule(cellCol, {
|
||||||
renderer: 'autocomplete',
|
renderer: 'autocomplete',
|
||||||
editor: 'autocomplete',
|
editor: 'autocomplete.custom',
|
||||||
strict: cellValidationEntry.strict,
|
strict: cellValidationEntry.strict,
|
||||||
filter: false
|
filter: false
|
||||||
})
|
})
|
||||||
@ -2653,13 +2706,13 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
const strict = this.cellValidationSource[validationSourceIndex].strict
|
const strict = this.cellValidationSource[validationSourceIndex].strict
|
||||||
|
|
||||||
hot.setCellMeta(row, column, 'renderer', 'autocomplete')
|
hot.setCellMeta(row, column, 'renderer', 'autocomplete')
|
||||||
hot.setCellMeta(row, column, 'editor', 'autocomplete')
|
hot.setCellMeta(row, column, 'editor', 'autocomplete.custom')
|
||||||
hot.setCellMeta(row, column, 'strict', strict)
|
hot.setCellMeta(row, column, 'strict', strict)
|
||||||
hot.setCellMeta(row, column, 'filter', false)
|
hot.setCellMeta(row, column, 'filter', false)
|
||||||
|
|
||||||
this.currentEditRecordValidator?.updateRule(column, {
|
this.currentEditRecordValidator?.updateRule(column, {
|
||||||
renderer: 'autocomplete',
|
renderer: 'autocomplete',
|
||||||
editor: 'autocomplete',
|
editor: 'autocomplete.custom',
|
||||||
strict: strict,
|
strict: strict,
|
||||||
filter: false
|
filter: false
|
||||||
})
|
})
|
||||||
@ -2893,6 +2946,37 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function checks if selected hot cell is solo cell selected
|
||||||
|
* and if it is, set the `filter` property based on filter param.
|
||||||
|
*
|
||||||
|
* @param filter
|
||||||
|
*/
|
||||||
|
private setCellFilter(filter: boolean) {
|
||||||
|
const hotSelected = this.hotInstance.getSelected()
|
||||||
|
const selection = hotSelected ? hotSelected[0] : hotSelected
|
||||||
|
|
||||||
|
// When we open a dropdown we want filter disabled so value in cell
|
||||||
|
// don't filter out items, since we want to see them all.
|
||||||
|
// But when we start typing we want to be able to start filtering values
|
||||||
|
// again
|
||||||
|
if (selection) {
|
||||||
|
const startRow = selection[0]
|
||||||
|
const endRow = selection[2]
|
||||||
|
const startCell = selection[1]
|
||||||
|
const endCell = selection[3]
|
||||||
|
|
||||||
|
if (startRow === endRow && startCell === endCell) {
|
||||||
|
const cellMeta = this.hotInstance.getCellMeta(startRow, startCell)
|
||||||
|
|
||||||
|
// If filter is not already set at the value in the param, set it
|
||||||
|
if (cellMeta && cellMeta.filter === !filter) {
|
||||||
|
this.hotInstance.setCellMeta(startRow, startCell, 'filter', filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.licenceService.hot_license_key.subscribe(
|
this.licenceService.hot_license_key.subscribe(
|
||||||
(hot_license_key: string | undefined) => {
|
(hot_license_key: string | undefined) => {
|
||||||
@ -2939,7 +3023,7 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
await this.sasStoreService
|
await this.sasStoreService
|
||||||
.callService(myParams, 'SASControlTable', 'editors/getdata', this.libds)
|
.callService(myParams, 'SASControlTable', 'editors/getdata', this.libds)
|
||||||
.then((res: EditorsGetdataServiceResponse) => {
|
.then((res: EditorsGetDataServiceResponse) => {
|
||||||
this.initSetup(res)
|
this.initSetup(res)
|
||||||
})
|
})
|
||||||
.catch((err: any) => {
|
.catch((err: any) => {
|
||||||
@ -2951,7 +3035,7 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
ngAfterViewInit() {}
|
ngAfterViewInit() {}
|
||||||
|
|
||||||
initSetup(response: EditorsGetdataServiceResponse) {
|
initSetup(response: EditorsGetDataServiceResponse) {
|
||||||
this.hotInstance = this.hotRegisterer.getInstance('hotInstance')
|
this.hotInstance = this.hotRegisterer.getInstance('hotInstance')
|
||||||
|
|
||||||
if (this.getdataError) return
|
if (this.getdataError) return
|
||||||
@ -2961,6 +3045,20 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
this.cols = response.data.cols
|
this.cols = response.data.cols
|
||||||
this.dsmeta = response.data.dsmeta
|
this.dsmeta = response.data.dsmeta
|
||||||
|
|
||||||
|
const notes = this.dsmeta.find((item) => item.NAME === 'NOTES')
|
||||||
|
const longDesc = this.dsmeta.find((item) => item.NAME === 'DD_LONGDESC')
|
||||||
|
const shortDesc = this.dsmeta.find((item) => item.NAME === 'DD_SHORTDESC')
|
||||||
|
|
||||||
|
if (notes && notes.VALUE) {
|
||||||
|
this.dsNote = notes.VALUE
|
||||||
|
} else if (longDesc && longDesc.VALUE) {
|
||||||
|
this.dsNote = longDesc.VALUE
|
||||||
|
} else if (shortDesc && shortDesc.VALUE) {
|
||||||
|
this.dsNote = shortDesc.VALUE
|
||||||
|
} else {
|
||||||
|
this.dsNote = ''
|
||||||
|
}
|
||||||
|
|
||||||
const hot: Handsontable = this.hotInstance
|
const hot: Handsontable = this.hotInstance
|
||||||
|
|
||||||
const approvers: Approver[] = response.data.approvers
|
const approvers: Approver[] = response.data.approvers
|
||||||
@ -3234,28 +3332,16 @@ export class EditorComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
hot.addHook('beforeKeyDown', (e: any) => {
|
hot.addHook('afterBeginEditing', () => {
|
||||||
const hotSelected = this.hotInstance.getSelected()
|
|
||||||
const selection = hotSelected ? hotSelected[0] : hotSelected
|
|
||||||
|
|
||||||
// When we open a dropdown we want filter disabled so value in cell
|
// When we open a dropdown we want filter disabled so value in cell
|
||||||
// don't filter out items, since we want to see them all.
|
// don't filter out items, since we want to see them all.
|
||||||
|
this.setCellFilter(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
hot.addHook('beforeKeyDown', () => {
|
||||||
// When we start typing, we are enabling the filter since we want to find
|
// When we start typing, we are enabling the filter since we want to find
|
||||||
// values faster.
|
// values faster.
|
||||||
if (selection) {
|
this.setCellFilter(true)
|
||||||
const startRow = selection[0]
|
|
||||||
const endRow = selection[2]
|
|
||||||
const startCell = selection[1]
|
|
||||||
const endCell = selection[3]
|
|
||||||
|
|
||||||
if (startRow === endRow && startCell === endCell) {
|
|
||||||
const cellMeta = this.hotInstance.getCellMeta(startRow, startCell)
|
|
||||||
|
|
||||||
if (cellMeta && cellMeta.filter === false) {
|
|
||||||
this.hotInstance.setCellMeta(startRow, startCell, 'filter', true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
hot.addHook('afterChange', (source: any, change: any) => {
|
hot.addHook('afterChange', (source: any, change: any) => {
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { DcValidation } from 'src/app/shared/dc-validator/models/dc-validation.model'
|
import { DcValidation } from 'src/app/shared/dc-validator/models/dc-validation.model'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for DC Validation because we need `noLinkOption` property
|
||||||
|
* to be used as a flag to show/hide button that generates link for the
|
||||||
|
* edit record modal
|
||||||
|
*/
|
||||||
export interface EditRecordModal extends DcValidation {
|
export interface EditRecordModal extends DcValidation {
|
||||||
noLinkOption: boolean
|
noLinkOption: boolean
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
export interface CellValidation {
|
|
||||||
data: string
|
|
||||||
length: number
|
|
||||||
type?: string
|
|
||||||
source: string[]
|
|
||||||
format?: number
|
|
||||||
validator?: any
|
|
||||||
valid?: boolean
|
|
||||||
renderer?: any
|
|
||||||
dateFormat?: string
|
|
||||||
readOnly?: boolean
|
|
||||||
desc?: string
|
|
||||||
correctFormat?: boolean
|
|
||||||
/**
|
|
||||||
* Key for accessing object fields, any type because it can be
|
|
||||||
* any of the types interface have
|
|
||||||
*/
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
export enum ColumnType {
|
|
||||||
string = 'string',
|
|
||||||
number = 'number'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ColumnInterface {
|
|
||||||
id: number | undefined
|
|
||||||
name: string | undefined
|
|
||||||
type: ColumnType | undefined
|
|
||||||
length: number | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Column implements ColumnInterface {
|
|
||||||
public id: number | undefined
|
|
||||||
public name: string | undefined
|
|
||||||
public type: ColumnType | undefined
|
|
||||||
public length: number | undefined
|
|
||||||
public static fromPlainObject(obj: object) {
|
|
||||||
return Object.assign(new Column(), obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(id?: number, name?: string, type?: ColumnType, length?: number) {
|
|
||||||
this.id = id
|
|
||||||
this.name = name
|
|
||||||
this.type = type
|
|
||||||
this.length = length
|
|
||||||
}
|
|
||||||
|
|
||||||
get hsType() {
|
|
||||||
return (
|
|
||||||
(this.type === ColumnType.string && 'text') ||
|
|
||||||
(this.type === ColumnType.number && 'numeric') ||
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Model for the dynamic cell validation in the editor
|
||||||
|
* (sending whole row to the backend service and recieving data for the cell dropdown)
|
||||||
|
*/
|
||||||
export interface DynamicExtendedCellValidation {
|
export interface DynamicExtendedCellValidation {
|
||||||
DISPLAY_INDEX: number
|
DISPLAY_INDEX: number
|
||||||
DISPLAY_TYPE: string
|
DISPLAY_TYPE: string
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Edit record modal - input has been focused event
|
||||||
|
*/
|
||||||
export interface EditRecordInputFocusedEvent {
|
export interface EditRecordInputFocusedEvent {
|
||||||
event: any
|
event: any
|
||||||
colName: number
|
colName: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit record modal - dropdown has been changed event
|
||||||
|
*/
|
||||||
export interface EditRecordDropdownChangeEvent {
|
export interface EditRecordDropdownChangeEvent {
|
||||||
colName: string
|
colName: string
|
||||||
col: number
|
col: number
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Editor restrictions model (based on the licencing)
|
||||||
|
*/
|
||||||
export interface EditorRestrictions {
|
export interface EditorRestrictions {
|
||||||
restrictEditRecord?: boolean // Feature is locked but edit/add record buttons are visible so when user clicks he gets the `locked feature modal`
|
restrictEditRecord?: boolean // Feature is locked but edit/add record buttons are visible so when user clicks he gets the `locked feature modal`
|
||||||
restrictAddRecord?: boolean // Same as editRecord, but for addRecord
|
restrictAddRecord?: boolean // Same as editRecord, but for addRecord
|
||||||
|
@ -1,71 +0,0 @@
|
|||||||
import { Column, ColumnType } from './models/column'
|
|
||||||
|
|
||||||
export enum TableType {
|
|
||||||
INPUT = 'In',
|
|
||||||
OUTPUT = 'Out'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TableInterface {
|
|
||||||
id: number | undefined
|
|
||||||
name: string | undefined
|
|
||||||
data: Array<Object>
|
|
||||||
columns: Array<Column>
|
|
||||||
type: TableType | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Table implements TableInterface {
|
|
||||||
public id: number | undefined
|
|
||||||
public name: string | undefined
|
|
||||||
public data: Array<any>
|
|
||||||
public columns: Array<Column> = []
|
|
||||||
public type: TableType | undefined
|
|
||||||
|
|
||||||
public static fromPlainObject(obj: any) {
|
|
||||||
obj.columns = obj.columns.map((column: object) => {
|
|
||||||
return Column.fromPlainObject(column)
|
|
||||||
})
|
|
||||||
|
|
||||||
return Object.assign(new Table(), obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
id?: number,
|
|
||||||
name?: string,
|
|
||||||
type?: TableType,
|
|
||||||
data?: Array<Object>,
|
|
||||||
columns?: Array<Column>
|
|
||||||
) {
|
|
||||||
this.id = id
|
|
||||||
this.name = name
|
|
||||||
this.type = type
|
|
||||||
|
|
||||||
this.data = data || [{}]
|
|
||||||
this.columns = columns || []
|
|
||||||
}
|
|
||||||
|
|
||||||
public getNextColumnId(): number {
|
|
||||||
let highestIdColumn: any = this.columns.sort(
|
|
||||||
(cA: any, cB: any) => cA.id - cB.id
|
|
||||||
)[this.columns.length - 1]
|
|
||||||
return (highestIdColumn && highestIdColumn.id + 1) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
public addColumn(column: Column) {
|
|
||||||
this.columns.push(column)
|
|
||||||
|
|
||||||
this.data.forEach((row: any) => {
|
|
||||||
if (column.name) {
|
|
||||||
row[column.name] = column.type === ColumnType.string ? '' : null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return column
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeColumn(colInd: any) {
|
|
||||||
this.data.forEach((row) => {
|
|
||||||
delete row[this.columns[colInd].name!]
|
|
||||||
})
|
|
||||||
this.columns.splice(colInd, 1)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Converting date object to the UTC time string
|
||||||
|
*/
|
||||||
export const dateToUtcTime = (date: Date) => {
|
export const dateToUtcTime = (date: Date) => {
|
||||||
let timeStr = ('0' + date.getUTCHours()).slice(-2) + ':'
|
let timeStr = ('0' + date.getUTCHours()).slice(-2) + ':'
|
||||||
timeStr = timeStr + ('0' + date.getUTCMinutes()).slice(-2) + ':'
|
timeStr = timeStr + ('0' + date.getUTCMinutes()).slice(-2) + ':'
|
||||||
@ -5,6 +8,9 @@ export const dateToUtcTime = (date: Date) => {
|
|||||||
return timeStr
|
return timeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts date object to the time string
|
||||||
|
*/
|
||||||
export const dateToTime = (date: Date) => {
|
export const dateToTime = (date: Date) => {
|
||||||
let timeStr = ('0' + date.getHours()).slice(-2) + ':'
|
let timeStr = ('0' + date.getHours()).slice(-2) + ':'
|
||||||
timeStr = timeStr + ('0' + date.getMinutes()).slice(-2) + ':'
|
timeStr = timeStr + ('0' + date.getMinutes()).slice(-2) + ':'
|
||||||
@ -12,6 +18,9 @@ export const dateToTime = (date: Date) => {
|
|||||||
return timeStr
|
return timeStr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts date object to the YYYY-MM-DD
|
||||||
|
*/
|
||||||
export const dateFormat = (date: Date) => {
|
export const dateFormat = (date: Date) => {
|
||||||
return (
|
return (
|
||||||
date.getFullYear() +
|
date.getFullYear() +
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
import { Col } from 'src/app/shared/dc-validator/models/col.model'
|
import { Col } from 'src/app/shared/dc-validator/models/col.model'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts excel date serial number to JS date
|
||||||
|
*/
|
||||||
export const excelDateToJSDate = (serial: number) => {
|
export const excelDateToJSDate = (serial: number) => {
|
||||||
return new Date(Math.round((serial - 25569) * 86400 * 1000))
|
return new Date(Math.round((serial - 25569) * 86400 * 1000))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsing table columns for the HOT in editor
|
||||||
|
* Converts array of objects into array of strings, every string is column name (key)
|
||||||
|
* @param data array of objects (columns data)
|
||||||
|
*/
|
||||||
export const parseTableColumns = (data: Col[]): string[] => {
|
export const parseTableColumns = (data: Col[]): string[] => {
|
||||||
const columns: string[] = []
|
const columns: string[] = []
|
||||||
|
|
||||||
@ -16,6 +24,12 @@ export const parseTableColumns = (data: Col[]): string[] => {
|
|||||||
return columns
|
return columns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures headers that are not found in the current table but is found in the uploaded file data
|
||||||
|
* @param data
|
||||||
|
* @param headers
|
||||||
|
* @returns string array of missing headers
|
||||||
|
*/
|
||||||
export const getMissingHeaders = (data: any, headers: any) => {
|
export const getMissingHeaders = (data: any, headers: any) => {
|
||||||
const missingHeaders: string[] = []
|
const missingHeaders: string[] = []
|
||||||
const remainingHeaders: string[] = []
|
const remainingHeaders: string[] = []
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Custom renderer for HOT cell
|
||||||
|
* Used to show error icon
|
||||||
|
*/
|
||||||
export const errorRenderer = (
|
export const errorRenderer = (
|
||||||
instance: any,
|
instance: any,
|
||||||
td: any,
|
td: any,
|
||||||
@ -14,6 +18,10 @@ export const errorRenderer = (
|
|||||||
return td
|
return td
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom renderer for HOT cell
|
||||||
|
* Used to revert cell back to original state (no spinner, no error)
|
||||||
|
*/
|
||||||
export const noSpinnerRenderer = (
|
export const noSpinnerRenderer = (
|
||||||
instance: any,
|
instance: any,
|
||||||
td: any,
|
td: any,
|
||||||
@ -28,7 +36,11 @@ export const noSpinnerRenderer = (
|
|||||||
return td
|
return td
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spinner shown whilst waiting for SAS to respond
|
/**
|
||||||
|
* Custom renderer for HOT cell
|
||||||
|
* Used to show loading spinner in the cell
|
||||||
|
* (Spinner shown whilst waiting for SAS to respond)
|
||||||
|
*/
|
||||||
export const spinnerRenderer = (
|
export const spinnerRenderer = (
|
||||||
instance: any,
|
instance: any,
|
||||||
td: any,
|
td: any,
|
||||||
|
23
client/src/app/home/home-routing.module.ts
Normal file
23
client/src/app/home/home-routing.module.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
|
import { HomeRouteComponent } from '../routes/home-route/home-route.component'
|
||||||
|
import { HomeComponent } from './home.component'
|
||||||
|
import { XLMapModule } from '../xlmap/xlmap.module'
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: HomeRouteComponent,
|
||||||
|
children: [
|
||||||
|
{ path: '', pathMatch: 'full', redirectTo: 'tables' },
|
||||||
|
{ path: 'tables', component: HomeComponent },
|
||||||
|
{ path: 'files', loadChildren: () => XLMapModule }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class HomeRoutingModule {}
|
@ -100,7 +100,7 @@
|
|||||||
*clrIfOpen
|
*clrIfOpen
|
||||||
>
|
>
|
||||||
<span *ngIf="tableLocked">
|
<span *ngIf="tableLocked">
|
||||||
To unlock all tables, contact support@datacontroller.io
|
To unlock all tables, contact support@datacontroller.io
|
||||||
</span>
|
</span>
|
||||||
</clr-tooltip-content>
|
</clr-tooltip-content>
|
||||||
</clr-tooltip>
|
</clr-tooltip>
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { HomeComponent } from './home.component'
|
import { NgModule } from '@angular/core'
|
||||||
import { ClarityModule } from '@clr/angular'
|
|
||||||
import { FormsModule } from '@angular/forms'
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import { ClarityModule } from '@clr/angular'
|
||||||
import { AppSharedModule } from '../app-shared.module'
|
import { AppSharedModule } from '../app-shared.module'
|
||||||
import { DcTreeModule } from '../shared/dc-tree/dc-tree.module'
|
|
||||||
import { DirectivesModule } from '../directives/directives.module'
|
import { DirectivesModule } from '../directives/directives.module'
|
||||||
|
import { HomeRouteComponent } from '../routes/home-route/home-route.component'
|
||||||
|
import { DcTreeModule } from '../shared/dc-tree/dc-tree.module'
|
||||||
|
import { HomeRoutingModule } from './home-routing.module'
|
||||||
|
import { HomeComponent } from './home.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [HomeComponent],
|
declarations: [HomeComponent, HomeRouteComponent],
|
||||||
imports: [
|
imports: [
|
||||||
|
HomeRoutingModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ClarityModule,
|
ClarityModule,
|
||||||
AppSharedModule,
|
AppSharedModule,
|
||||||
|
19
client/src/app/licensing/licensing-routing.module.ts
Normal file
19
client/src/app/licensing/licensing-routing.module.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
|
import { LicensingGuard } from '../routes/licensing.guard'
|
||||||
|
import { LicensingComponent } from './licensing.component'
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: ':action',
|
||||||
|
component: LicensingComponent,
|
||||||
|
canActivate: [LicensingGuard],
|
||||||
|
canDeactivate: [LicensingGuard]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class LicensingRoutingModule {}
|
20
client/src/app/licensing/licensing.module.ts
Normal file
20
client/src/app/licensing/licensing.module.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
|
||||||
|
import { LicensingRoutingModule } from './licensing-routing.module'
|
||||||
|
import { ClarityModule } from '@clr/angular'
|
||||||
|
import { SharedModule } from '../shared/shared.module'
|
||||||
|
import { LicensingComponent } from './licensing.component'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [LicensingComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ClarityModule,
|
||||||
|
LicensingRoutingModule,
|
||||||
|
SharedModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class LicensingModule {}
|
@ -656,11 +656,10 @@ export class LineageComponent {
|
|||||||
this.flatdata = res.flatdata
|
this.flatdata = res.flatdata
|
||||||
|
|
||||||
if (this.libraryList) {
|
if (this.libraryList) {
|
||||||
let libraryToSelect = this.libraryList.find(
|
let libraryToSelect = this.libraryList.find((library: any) =>
|
||||||
(library: any) =>
|
res.info[0]?.LIBURI?.toUpperCase()?.includes(
|
||||||
res.info[0]?.LIBURI?.toUpperCase()?.includes(
|
library?.LIBRARYID?.toUpperCase()
|
||||||
library?.LIBRARYID?.toUpperCase()
|
)
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
let tableToSelect: any
|
let tableToSelect: any
|
||||||
|
5
client/src/app/models/ErrorBody.ts
Normal file
5
client/src/app/models/ErrorBody.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface ErrorBody {
|
||||||
|
message: string
|
||||||
|
details: any
|
||||||
|
raw: any
|
||||||
|
}
|
@ -6,6 +6,7 @@ export interface FilterClause {
|
|||||||
operators: string[]
|
operators: string[]
|
||||||
type: string
|
type: string
|
||||||
value: any
|
value: any
|
||||||
|
valueVariable: boolean
|
||||||
values: { formatted: string; unformatted: any }[]
|
values: { formatted: string; unformatted: any }[]
|
||||||
variable: string
|
variable: string
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,12 @@ import { DQData, SASParam } from '../TableData'
|
|||||||
import { BaseSASResponse } from './common/BaseSASResponse'
|
import { BaseSASResponse } from './common/BaseSASResponse'
|
||||||
import { DataFormat } from './common/DateFormat'
|
import { DataFormat } from './common/DateFormat'
|
||||||
|
|
||||||
export interface EditorsGetdataServiceResponse {
|
export interface EditorsGetDataServiceResponse {
|
||||||
data: EditorsGetdataSASResponse
|
data: EditorsGetDataSASResponse
|
||||||
libds: string
|
libds: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorsGetdataSASResponse extends BaseSASResponse {
|
export interface EditorsGetDataSASResponse extends BaseSASResponse {
|
||||||
$sasdata: $DataFormats
|
$sasdata: $DataFormats
|
||||||
sasdata: Sasdata[]
|
sasdata: Sasdata[]
|
||||||
sasparams: SASParam[]
|
sasparams: SASParam[]
|
||||||
|
@ -413,7 +413,10 @@
|
|||||||
>
|
>
|
||||||
<app-soft-select
|
<app-soft-select
|
||||||
label="Value"
|
label="Value"
|
||||||
|
[secondLabel]="'Variable'"
|
||||||
|
[emitOnlySelected]="query.valueVariable"
|
||||||
[inputId]="'vals_' + queryIndex + '_' + clauseIndex"
|
[inputId]="'vals_' + queryIndex + '_' + clauseIndex"
|
||||||
|
(selectedLabelChange)="selectedLabelChange($event, query)"
|
||||||
[(value)]="query.value"
|
[(value)]="query.value"
|
||||||
[enableLoadMore]="query.nobs > query.values.length"
|
[enableLoadMore]="query.nobs > query.values.length"
|
||||||
(onInputEvent)="
|
(onInputEvent)="
|
||||||
@ -423,9 +426,19 @@
|
|||||||
onAutocompleteLoadingMore($event, query.variable, queryIndex, clauseIndex)
|
onAutocompleteLoadingMore($event, query.variable, queryIndex, clauseIndex)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<option [value]="column.unformatted" *ngFor="let column of query.values">
|
<div *ngIf="!query.valueVariable">
|
||||||
{{ column.formatted.trim() }}
|
<option [value]="column.unformatted" *ngFor="let column of query.values">
|
||||||
</option>
|
{{ column.formatted.trim() }}
|
||||||
|
</option>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="query.valueVariable">
|
||||||
|
<ng-container *ngFor="let column of cols">
|
||||||
|
<option [value]="column.NAME" *ngIf="column.TYPE === query.type">
|
||||||
|
{{ column.NAME }}
|
||||||
|
</option>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
</app-soft-select>
|
</app-soft-select>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
@ -95,6 +95,7 @@ export class QueryComponent
|
|||||||
variable: null,
|
variable: null,
|
||||||
operator: null,
|
operator: null,
|
||||||
value: null,
|
value: null,
|
||||||
|
valueVariable: false,
|
||||||
startrow: 0,
|
startrow: 0,
|
||||||
rows: 0,
|
rows: 0,
|
||||||
nobs: 0,
|
nobs: 0,
|
||||||
@ -137,6 +138,11 @@ export class QueryComponent
|
|||||||
public whereClause: string | undefined
|
public whereClause: string | undefined
|
||||||
public logicOperators: Array<string> = ['AND', 'OR']
|
public logicOperators: Array<string> = ['AND', 'OR']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporary values array used in pickers
|
||||||
|
* because they need particular format to work
|
||||||
|
* before sending values to backend, values are parsed
|
||||||
|
*/
|
||||||
public queryDateTime: QueryDateTime[] = []
|
public queryDateTime: QueryDateTime[] = []
|
||||||
|
|
||||||
public currentClauseIndex: number = -1
|
public currentClauseIndex: number = -1
|
||||||
@ -158,6 +164,11 @@ export class QueryComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets and sets temporary values selected with DATETIME or TIME picker
|
||||||
|
* Those values are used for picker to work with format it lieks
|
||||||
|
* Later before sending values to backend, values are parsed
|
||||||
|
*/
|
||||||
getQueryDateTime(clauseIndex: number, queryIndex: number): QueryDateTime {
|
getQueryDateTime(clauseIndex: number, queryIndex: number): QueryDateTime {
|
||||||
let existingQueryDateTime = this.queryDateTime.find(
|
let existingQueryDateTime = this.queryDateTime.find(
|
||||||
(x) => x.clauseIndex === clauseIndex && x.queryIndex === queryIndex
|
(x) => x.clauseIndex === clauseIndex && x.queryIndex === queryIndex
|
||||||
@ -178,10 +189,30 @@ export class QueryComponent
|
|||||||
return existingQueryDateTime
|
return existingQueryDateTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When toggling pickers feature we reset the temp picker values array
|
||||||
|
*/
|
||||||
usePickersChange() {
|
usePickersChange() {
|
||||||
this.queryDateTime = []
|
this.queryDateTime = []
|
||||||
|
if (this.usePickers) {
|
||||||
|
this.clauses.queryObj.forEach((queryObj: any) => {
|
||||||
|
queryObj.elements.forEach((element: any) => {
|
||||||
|
const isDateOrTime = ['DATETIME', 'TIME', 'DATE'].includes(
|
||||||
|
element.ddtype
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isDateOrTime && element.valueVariable) {
|
||||||
|
element.value = ''
|
||||||
|
element.valueVariable = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets all variables used for filtering
|
||||||
|
*/
|
||||||
public resetFilter() {
|
public resetFilter() {
|
||||||
this.whereString = undefined
|
this.whereString = undefined
|
||||||
this.whereClause = undefined
|
this.whereClause = undefined
|
||||||
@ -210,6 +241,10 @@ export class QueryComponent
|
|||||||
this.whereClauseFn(true)
|
this.whereClauseFn(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `Globals` are used to store filtering state (variables) as a caching feature
|
||||||
|
* until browser reloads
|
||||||
|
*/
|
||||||
public setToGlobals() {
|
public setToGlobals() {
|
||||||
if (!this.caching) return
|
if (!this.caching) return
|
||||||
|
|
||||||
@ -233,10 +268,12 @@ export class QueryComponent
|
|||||||
get(globals, objPath).filter.libds = this.libds
|
get(globals, objPath).filter.libds = this.libds
|
||||||
}
|
}
|
||||||
get(globals, objPath).filter.clauses = this.clauses
|
get(globals, objPath).filter.clauses = this.clauses
|
||||||
|
|
||||||
console.log('globals', globals)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `Globals` are used to store filtering state (variables) as a caching feature
|
||||||
|
* until browser reloads
|
||||||
|
*/
|
||||||
public getFromGlobals() {
|
public getFromGlobals() {
|
||||||
if (!this.caching) return
|
if (!this.caching) return
|
||||||
|
|
||||||
@ -269,6 +306,11 @@ export class QueryComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets filtering multiple caluses group logic (and / or)
|
||||||
|
*
|
||||||
|
* @param groupLogic to set
|
||||||
|
*/
|
||||||
public setGroupLogic(groupLogic: any) {
|
public setGroupLogic(groupLogic: any) {
|
||||||
this.groupLogic = groupLogic
|
this.groupLogic = groupLogic
|
||||||
this.clauses.groupLogic = groupLogic
|
this.clauses.groupLogic = groupLogic
|
||||||
@ -721,6 +763,12 @@ export class QueryComponent
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public selectedLabelChange(label: string, query: any) {
|
||||||
|
query.valueVariable = label === 'Variable'
|
||||||
|
query.value = ''
|
||||||
|
this.whereClauseFn()
|
||||||
|
}
|
||||||
|
|
||||||
public variableInputChange(
|
public variableInputChange(
|
||||||
queryVariable: any,
|
queryVariable: any,
|
||||||
index: number,
|
index: number,
|
||||||
@ -830,17 +878,25 @@ export class QueryComponent
|
|||||||
*/
|
*/
|
||||||
public hasInvalidCluase(clauses: any): boolean {
|
public hasInvalidCluase(clauses: any): boolean {
|
||||||
for (let clause of clauses) {
|
for (let clause of clauses) {
|
||||||
|
clause['invalidClause'] = false
|
||||||
|
|
||||||
if (
|
if (
|
||||||
clause.variable === null ||
|
clause.value === '' &&
|
||||||
clause.operator === null ||
|
!(clause.operator === 'NE' || clause.operator === 'CONTAINS')
|
||||||
clause.value === null ||
|
) {
|
||||||
clause.value === ''
|
clause['invalidClause'] = true
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
clause.variable === null ||
|
||||||
|
clause.operator === null ||
|
||||||
|
clause.value === null
|
||||||
) {
|
) {
|
||||||
clause['invalidClause'] = true
|
clause['invalidClause'] = true
|
||||||
|
|
||||||
return true
|
return true
|
||||||
} else {
|
|
||||||
clause['invalidClause'] = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@
|
|||||||
>
|
>
|
||||||
To unlock more than
|
To unlock more than
|
||||||
{{ licenceState.value.history_rows_allowed }} records, contact
|
{{ licenceState.value.history_rows_allowed }} records, contact
|
||||||
support@datacontroller.io
|
support@datacontroller.io
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -2,7 +2,12 @@ import { Component, OnInit } from '@angular/core'
|
|||||||
|
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { SASjsConfig } from '@sasjs/adapter'
|
import { SASjsConfig } from '@sasjs/adapter'
|
||||||
import { LicenceService, SasStoreService, EventService, SasService } from 'src/app/services'
|
import {
|
||||||
|
LicenceService,
|
||||||
|
SasStoreService,
|
||||||
|
EventService,
|
||||||
|
SasService
|
||||||
|
} from 'src/app/services'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-history',
|
selector: 'app-history',
|
||||||
|
@ -11,7 +11,7 @@ const ROUTES: Routes = [
|
|||||||
{ path: 'approveDet/:tableId', component: ApproveDetailsComponent },
|
{ path: 'approveDet/:tableId', component: ApproveDetailsComponent },
|
||||||
{ path: 'submitted', component: SubmitterComponent },
|
{ path: 'submitted', component: SubmitterComponent },
|
||||||
{ path: 'submitted/:tableId', component: SubmitterComponent },
|
{ path: 'submitted/:tableId', component: SubmitterComponent },
|
||||||
{ path: 'history', component: HistoryComponent },
|
{ path: 'history', component: HistoryComponent }
|
||||||
]
|
]
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from '@angular/common'
|
||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from '@angular/core'
|
||||||
import { FormsModule } from "@angular/forms";
|
import { FormsModule } from '@angular/forms'
|
||||||
import { ClarityModule } from "@clr/angular";
|
import { ClarityModule } from '@clr/angular'
|
||||||
import { HotTableModule } from "@handsontable/angular";
|
import { HotTableModule } from '@handsontable/angular'
|
||||||
import { DirectivesModule } from "../directives/directives.module";
|
import { DirectivesModule } from '../directives/directives.module'
|
||||||
import { SharedModule } from "../shared/shared.module";
|
import { SharedModule } from '../shared/shared.module'
|
||||||
import { ApproveDetailsComponent } from "./approve-details/approve-details.component";
|
import { ApproveDetailsComponent } from './approve-details/approve-details.component'
|
||||||
import { ApproveComponent } from "./approve/approve.component";
|
import { ApproveComponent } from './approve/approve.component'
|
||||||
import { ReviewRoutingModule } from "./review-routing.module";
|
import { ReviewRoutingModule } from './review-routing.module'
|
||||||
import { SubmitterComponent } from "./submitter/submitter.component";
|
import { SubmitterComponent } from './submitter/submitter.component'
|
||||||
import { HistoryComponent } from "./history/history.component";
|
import { HistoryComponent } from './history/history.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { Component, OnInit } from '@angular/core'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-edit-route',
|
|
||||||
templateUrl: './edit-route.component.html',
|
|
||||||
styleUrls: ['./edit-route.component.scss']
|
|
||||||
})
|
|
||||||
export class EditRouteComponent implements OnInit {
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
ngOnInit() {}
|
|
||||||
}
|
|
17
client/src/app/routes/home-route/home-route.component.ts
Normal file
17
client/src/app/routes/home-route/home-route.component.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Component, OnInit, OnDestroy } from '@angular/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-home-route',
|
||||||
|
templateUrl: './home-route.component.html',
|
||||||
|
styleUrls: ['./home-route.component.scss'],
|
||||||
|
host: {
|
||||||
|
class: 'content-container'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export class HomeRouteComponent implements OnInit, OnDestroy {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
ngOnInit() {}
|
||||||
|
|
||||||
|
ngOnDestroy() {}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
<router-outlet></router-outlet>
|
17
client/src/app/routes/xlmap-route/xlmap-route.component.ts
Normal file
17
client/src/app/routes/xlmap-route/xlmap-route.component.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Component, OnInit, OnDestroy } from '@angular/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-xlmap-route',
|
||||||
|
templateUrl: './xlmap-route.component.html',
|
||||||
|
styleUrls: ['./xlmap-route.component.scss'],
|
||||||
|
host: {
|
||||||
|
class: 'content-container'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export class XLMapRouteComponent implements OnInit, OnDestroy {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
ngOnInit() {}
|
||||||
|
|
||||||
|
ngOnDestroy() {}
|
||||||
|
}
|
@ -74,6 +74,7 @@ export class AppService {
|
|||||||
missingProps.push('Globvars')
|
missingProps.push('Globvars')
|
||||||
if (!res.sasdatasets) missingProps.push('Sasdatasets')
|
if (!res.sasdatasets) missingProps.push('Sasdatasets')
|
||||||
if (!res.saslibs) missingProps.push('Saslibs')
|
if (!res.saslibs) missingProps.push('Saslibs')
|
||||||
|
if (!res.xlmaps) missingProps.push('XLMaps')
|
||||||
|
|
||||||
if (missingProps.length > 0) {
|
if (missingProps.length > 0) {
|
||||||
startupServiceError = true
|
startupServiceError = true
|
||||||
@ -135,10 +136,17 @@ export class AppService {
|
|||||||
globals.editor.libsAndTables = libsAndTables
|
globals.editor.libsAndTables = libsAndTables
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globals.xlmaps = res.xlmaps.map((xlmap: any) => ({
|
||||||
|
id: xlmap[0],
|
||||||
|
description: xlmap[1],
|
||||||
|
targetDS: xlmap[2]
|
||||||
|
}))
|
||||||
globals.editor.treeNodeLibraries = treeNodeLibraries
|
globals.editor.treeNodeLibraries = treeNodeLibraries
|
||||||
globals.editor.libraries = libraries
|
globals.editor.libraries = libraries
|
||||||
globals.editor.startupSet = true
|
globals.editor.startupSet = true
|
||||||
|
|
||||||
|
globals.dcLib = res.globvars[0].DCLIB
|
||||||
|
|
||||||
await this.licenceService.activation(res)
|
await this.licenceService.activation(res)
|
||||||
})
|
})
|
||||||
.catch((err: any) => {
|
.catch((err: any) => {
|
||||||
|
@ -17,6 +17,17 @@ export class HelperService {
|
|||||||
console.log('Is IE or Edge?', this.isMicrosoft)
|
console.log('Is IE or Edge?', this.isMicrosoft)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a JavaScript date object to a SAS Date or Datetime, given the logic below:
|
||||||
|
*
|
||||||
|
* A JS Date contains the number of _milliseconds_ since 01/01/1970
|
||||||
|
* A SAS Date contains the number of _days_ since 01/01/1960
|
||||||
|
* A SAS Datetime contains the number of _seconds_ since 01/01/1960
|
||||||
|
*
|
||||||
|
* @param jsDate JS Date to be converted. The type is instance of `Date`
|
||||||
|
* @param unit Unit in which to return the SAS Date / datetime, eg `sasdate | sasdatetime`
|
||||||
|
* @returns SAS Date value based on `unit` param
|
||||||
|
*/
|
||||||
public convertJsDateToSasDate(
|
public convertJsDateToSasDate(
|
||||||
jsDate: string | Date,
|
jsDate: string | Date,
|
||||||
unit: string = 'days'
|
unit: string = 'days'
|
||||||
@ -63,6 +74,17 @@ export class HelperService {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a SAS Date or Datetime to a JavaScript date object, given the logic below:
|
||||||
|
*
|
||||||
|
* A JS Date contains the number of _milliseconds_ since 01/01/1970
|
||||||
|
* A SAS Date contains the number of _days_ since 01/01/1960
|
||||||
|
* A SAS Datetime contains the number of _seconds_ since 01/01/1960
|
||||||
|
*
|
||||||
|
* @param sasValue SAS Date or Datetime to be converted. The type could be `number` or `string.
|
||||||
|
* @param unit Unit from which to convert the SAS Date / Datetime, eg `sasdate | sasdatetime`
|
||||||
|
* @returns JavaScript Date object
|
||||||
|
*/
|
||||||
public convertSasDaysToJsDate(
|
public convertSasDaysToJsDate(
|
||||||
sasValue: number | string,
|
sasValue: number | string,
|
||||||
unit: string = 'days'
|
unit: string = 'days'
|
||||||
@ -87,6 +109,11 @@ export class HelperService {
|
|||||||
return new Date(msNegativeTenYears + sasValue * msInDay)
|
return new Date(msNegativeTenYears + sasValue * msInDay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param array all elements in the clarity tree
|
||||||
|
* @param arrToFilter sub array in the tree to be filtered for example `tables`
|
||||||
|
*/
|
||||||
public treeOnFilter(array: any, arrToFilter: string) {
|
public treeOnFilter(array: any, arrToFilter: string) {
|
||||||
let search = array['searchString'] ? array['searchString'] : ''
|
let search = array['searchString'] ? array['searchString'] : ''
|
||||||
let arrToFilterArray = arrToFilter.split('.')[0]
|
let arrToFilterArray = arrToFilter.split('.')[0]
|
||||||
|
@ -10,8 +10,8 @@ import { globals } from '../_globals'
|
|||||||
import { FilterClause, FilterGroup, FilterQuery } from '../models/FilterQuery'
|
import { FilterClause, FilterGroup, FilterQuery } from '../models/FilterQuery'
|
||||||
import {
|
import {
|
||||||
$DataFormats,
|
$DataFormats,
|
||||||
EditorsGetdataSASResponse,
|
EditorsGetDataSASResponse,
|
||||||
EditorsGetdataServiceResponse
|
EditorsGetDataServiceResponse
|
||||||
} from '../models/sas/editors-getdata.model'
|
} from '../models/sas/editors-getdata.model'
|
||||||
import { LoggerService } from './logger.service'
|
import { LoggerService } from './logger.service'
|
||||||
import { isSpecialMissing } from '@sasjs/utils/input/validators'
|
import { isSpecialMissing } from '@sasjs/utils/input/validators'
|
||||||
@ -40,6 +40,16 @@ export class SasStoreService {
|
|||||||
private loggerService: LoggerService
|
private loggerService: LoggerService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for making request to service
|
||||||
|
* Should be removed, as it's redundant now
|
||||||
|
* TODO: Refactor to call editors/getdata directly
|
||||||
|
* @param tableData
|
||||||
|
* @param tableName
|
||||||
|
* @param program
|
||||||
|
* @param libds
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
public async callService(
|
public async callService(
|
||||||
tableData: Array<any>,
|
tableData: Array<any>,
|
||||||
tableName: string,
|
tableName: string,
|
||||||
@ -47,19 +57,28 @@ export class SasStoreService {
|
|||||||
libds: string
|
libds: string
|
||||||
) {
|
) {
|
||||||
this.libds = libds
|
this.libds = libds
|
||||||
let tables: any = {}
|
const tables: any = {}
|
||||||
tables[tableName] = [tableData]
|
tables[tableName] = [tableData]
|
||||||
let res: EditorsGetdataSASResponse = await this.sasService.request(
|
const res: EditorsGetDataSASResponse = await this.sasService.request(
|
||||||
program,
|
program,
|
||||||
tables
|
tables
|
||||||
)
|
)
|
||||||
let response: EditorsGetdataServiceResponse = {
|
const response: EditorsGetDataServiceResponse = {
|
||||||
data: res,
|
data: res,
|
||||||
libds: this.libds
|
libds: this.libds
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calling editors/stagedata - saving table data, sending request to backend
|
||||||
|
* @param tableParams params to send to backend
|
||||||
|
* @param tableData data to be updated
|
||||||
|
* @param tableName name of the table to be updated
|
||||||
|
* @param program service against which we send request
|
||||||
|
* @param $dataFormats column data formats recieved from backend, sending it back
|
||||||
|
* @returns adapter.request() response
|
||||||
|
*/
|
||||||
public async updateTable(
|
public async updateTable(
|
||||||
tableParams: any,
|
tableParams: any,
|
||||||
tableData: any,
|
tableData: any,
|
||||||
@ -86,6 +105,13 @@ export class SasStoreService {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sending request to 'approvers/getapprovals' to fetch approvals list
|
||||||
|
* @param tableData sending to backend table data
|
||||||
|
* @param tableName sending to backend table name
|
||||||
|
* @param program service to run request on
|
||||||
|
* @returns HTTP Response
|
||||||
|
*/
|
||||||
public async getApprovals(
|
public async getApprovals(
|
||||||
tableData: any,
|
tableData: any,
|
||||||
tableName: string,
|
tableName: string,
|
||||||
@ -96,6 +122,13 @@ export class SasStoreService {
|
|||||||
let res: any = await this.sasService.request(program, tables)
|
let res: any = await this.sasService.request(program, tables)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interceptor for loading of the submitted details
|
||||||
|
* @param detail submitter
|
||||||
|
* @param index submitter index
|
||||||
|
* @param data submit data
|
||||||
|
*/
|
||||||
public async sendDetails(detail: any, index: any, data: any) {
|
public async sendDetails(detail: any, index: any, data: any) {
|
||||||
let details = Object.assign({ sub: true }, detail)
|
let details = Object.assign({ sub: true }, detail)
|
||||||
let subData = data[index]
|
let subData = data[index]
|
||||||
@ -105,11 +138,20 @@ export class SasStoreService {
|
|||||||
}
|
}
|
||||||
this.submittDetail.next(allData)
|
this.submittDetail.next(allData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns All submits
|
||||||
|
*/
|
||||||
public async getSubmitts() {
|
public async getSubmitts() {
|
||||||
let res: any = await this.sasService.request('editors/getsubmits', null)
|
let res: any = await this.sasService.request('editors/getsubmits', null)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns All libraries
|
||||||
|
*/
|
||||||
public async viewLibs() {
|
public async viewLibs() {
|
||||||
return this.sasService.request('public/viewlibs', null)
|
return this.sasService.request('public/viewlibs', null)
|
||||||
}
|
}
|
||||||
@ -167,6 +209,14 @@ export class SasStoreService {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getXLMapRules(id: string) {
|
||||||
|
const tables = {
|
||||||
|
getxlmaps_in: [{ XLMAP_ID: id }]
|
||||||
|
}
|
||||||
|
const res: any = await this.sasService.request('editors/getxlmaps', tables)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
public async getDetails(tableData: any, tableName: string, program: string) {
|
public async getDetails(tableData: any, tableName: string, program: string) {
|
||||||
let tables: any = {}
|
let tables: any = {}
|
||||||
tables[tableName] = [tableData]
|
tables[tableName] = [tableData]
|
||||||
@ -366,14 +416,18 @@ export class SasStoreService {
|
|||||||
for (let index = 0; index < clauses.queryObj.length; index++) {
|
for (let index = 0; index < clauses.queryObj.length; index++) {
|
||||||
let string = ''
|
let string = ''
|
||||||
let clause = clauses.queryObj[index]
|
let clause = clauses.queryObj[index]
|
||||||
|
|
||||||
for (let ind = 0; ind < clause.elements.length; ind++) {
|
for (let ind = 0; ind < clause.elements.length; ind++) {
|
||||||
let query = clause.elements[ind]
|
let query = clause.elements[ind]
|
||||||
|
|
||||||
if (ind < clause.elements.length - 1) {
|
if (ind < clause.elements.length - 1) {
|
||||||
opr = clause.clauseLogic
|
opr = clause.clauseLogic
|
||||||
} else {
|
} else {
|
||||||
opr = ''
|
opr = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
let val: any
|
let val: any
|
||||||
|
|
||||||
for (let k = 0; k < query.values.length; k++) {
|
for (let k = 0; k < query.values.length; k++) {
|
||||||
if (
|
if (
|
||||||
typeof query.value === 'string' &&
|
typeof query.value === 'string' &&
|
||||||
@ -444,6 +498,8 @@ export class SasStoreService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let type = query.type
|
let type = query.type
|
||||||
|
//if the value is variable, omit quotes in the 'where' string
|
||||||
|
const isValueVariable = query.valueVariable
|
||||||
let variable = query.variable === null ? '' : query.variable
|
let variable = query.variable === null ? '' : query.variable
|
||||||
let oper = query.operator === null ? '' : query.operator
|
let oper = query.operator === null ? '' : query.operator
|
||||||
// let value = val === null ? "''" : val;
|
// let value = val === null ? "''" : val;
|
||||||
@ -457,10 +513,14 @@ export class SasStoreService {
|
|||||||
if (type === 'char' && oper !== 'IN' && oper !== 'NOT IN') {
|
if (type === 'char' && oper !== 'IN' && oper !== 'NOT IN') {
|
||||||
if (typeof value === 'undefined') {
|
if (typeof value === 'undefined') {
|
||||||
value = ''
|
value = ''
|
||||||
value = " '" + value + "' "
|
|
||||||
} else {
|
|
||||||
value = " '" + value + "' "
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isValueVariable) {
|
||||||
|
value = ' ' + value + ' ' //without quotes, with spaces
|
||||||
|
} else {
|
||||||
|
value = " '" + value + "' " //with quotes and spaces
|
||||||
|
}
|
||||||
|
|
||||||
string = string + ' ' + variable + ' ' + oper + value + opr
|
string = string + ' ' + variable + ' ' + oper + value + opr
|
||||||
} else {
|
} else {
|
||||||
if (type === 'num' && typeof value === 'undefined') {
|
if (type === 'num' && typeof value === 'undefined') {
|
||||||
@ -554,7 +614,7 @@ export class SasStoreService {
|
|||||||
rawValue = '.'
|
rawValue = '.'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (filterClause.type === 'char') {
|
if (filterClause.type === 'char' && !filterClause.valueVariable) {
|
||||||
rawValue = `'${filterClause.value.replace(/'/g, "''")}'`
|
rawValue = `'${filterClause.value.replace(/'/g, "''")}'`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import { DcAdapterSettings } from '../models/DcAdapterSettings'
|
|||||||
import { AppStoreService } from './app-store.service'
|
import { AppStoreService } from './app-store.service'
|
||||||
import { LoggerService } from './logger.service'
|
import { LoggerService } from './logger.service'
|
||||||
import { RequestWrapperOptions } from '../models/RequestWrapperOptions'
|
import { RequestWrapperOptions } from '../models/RequestWrapperOptions'
|
||||||
|
import { ErrorBody } from '../models/ErrorBody'
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -39,6 +40,11 @@ export class SasService {
|
|||||||
private router: Router
|
private router: Router
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as `setup` function in the sasjs.service, this is the constructor replacement.
|
||||||
|
* This function is being called by `app.service`.
|
||||||
|
* Because of timing and dependency issues
|
||||||
|
*/
|
||||||
public sasServiceInit() {
|
public sasServiceInit() {
|
||||||
this.dcAdapterSettings = this.appStoreService.getDcAdapterSettings()
|
this.dcAdapterSettings = this.appStoreService.getDcAdapterSettings()
|
||||||
|
|
||||||
@ -80,6 +86,16 @@ export class SasService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runing a backend request against a service.
|
||||||
|
* Function also handles the displaying of success or error modals.
|
||||||
|
*
|
||||||
|
* @param url service to run reuqest against
|
||||||
|
* @param data to be sent to backend service
|
||||||
|
* @param config additional parameters to force eg. { debug: false }
|
||||||
|
* @param wrapperOptions used to suppress error or success abort modals after request is finished
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
public request(
|
public request(
|
||||||
url: string,
|
url: string,
|
||||||
data: any,
|
data: any,
|
||||||
@ -197,6 +213,14 @@ export class SasService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a file to the backend, using the adapter upload function.
|
||||||
|
*
|
||||||
|
* @param sasService Service to which the file will be sent
|
||||||
|
* @param files Files to be sent
|
||||||
|
* @param params Aditional parameters eg. { debug: false }
|
||||||
|
* @returns HTTP Response
|
||||||
|
*/
|
||||||
public uploadFile(sasService: string, files: UploadFile[], params: any) {
|
public uploadFile(sasService: string, files: UploadFile[], params: any) {
|
||||||
return this.sasjsAdapter.uploadFile(sasService, files, params)
|
return this.sasjsAdapter.uploadFile(sasService, files, params)
|
||||||
}
|
}
|
||||||
@ -487,9 +511,3 @@ export class SasService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorBody {
|
|
||||||
message: string
|
|
||||||
details: any
|
|
||||||
raw: any
|
|
||||||
}
|
|
||||||
|
@ -25,6 +25,12 @@ export class SasjsService {
|
|||||||
private appStoreService: AppStoreService
|
private appStoreService: AppStoreService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is replacing the constructor.
|
||||||
|
* The reason for this is timing issues, other services eg. sas.service, app-store.service
|
||||||
|
* must be initialized before this bit of code is executed.
|
||||||
|
* This function is being called by `sas.service`
|
||||||
|
*/
|
||||||
setup() {
|
setup() {
|
||||||
const adapterConfig = this.appStoreService.getDcAdapterSettings()
|
const adapterConfig = this.appStoreService.getDcAdapterSettings()
|
||||||
|
|
||||||
@ -32,10 +38,18 @@ export class SasjsService {
|
|||||||
this.driveUrl = `${this.url}/drive`
|
this.driveUrl = `${this.url}/drive`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns Sasjs/server information
|
||||||
|
*/
|
||||||
getServerInfo(): Observable<SASjsApiServerInfo> {
|
getServerInfo(): Observable<SASjsApiServerInfo> {
|
||||||
return this.http.get<SASjsApiServerInfo>(`${this.url}/info`)
|
return this.http.get<SASjsApiServerInfo>(`${this.url}/info`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets file contents on a given path
|
||||||
|
* @param filePath path to the file
|
||||||
|
*/
|
||||||
getFileFromDrive(filePath: string) {
|
getFileFromDrive(filePath: string) {
|
||||||
return this.http.get(
|
return this.http.get(
|
||||||
`${this.driveUrl}/file/?_filePath=${filePath}`,
|
`${this.driveUrl}/file/?_filePath=${filePath}`,
|
||||||
@ -43,6 +57,11 @@ export class SasjsService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets folder contents on a given path
|
||||||
|
* @param folderPath path to the folder
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
getFolderContentsFromDrive(
|
getFolderContentsFromDrive(
|
||||||
folderPath: string
|
folderPath: string
|
||||||
): Observable<SASjsApiDriveFolderContents> {
|
): Observable<SASjsApiDriveFolderContents> {
|
||||||
|
@ -41,6 +41,14 @@ export class InfoModalComponent implements OnInit {
|
|||||||
this.data = newData
|
this.data = newData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wheter or not to show the `Open configurator button`
|
||||||
|
* Button used for navigating to the `configuration` page
|
||||||
|
* Only for SAS9
|
||||||
|
* @param sasService backend service that caused this info modal to be shown
|
||||||
|
* Decision is made based on that service path
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
showConfiguratorButton(sasService: string | null) {
|
showConfiguratorButton(sasService: string | null) {
|
||||||
const sasjsConfig = this.sasService.getSasjsConfig()
|
const sasjsConfig = this.sasService.getSasjsConfig()
|
||||||
|
|
||||||
@ -54,6 +62,9 @@ export class InfoModalComponent implements OnInit {
|
|||||||
this.onConfirmModalClick.emit()
|
this.onConfirmModalClick.emit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only on SAS9, opening a backend configurator/deploy page
|
||||||
|
*/
|
||||||
openConfigurator() {
|
openConfigurator() {
|
||||||
this.eventService.startupDataLoaded()
|
this.eventService.startupDataLoaded()
|
||||||
this.router.navigateByUrl('/deploy')
|
this.router.navigateByUrl('/deploy')
|
||||||
|
@ -2,5 +2,5 @@
|
|||||||
[ngClass]="classes"
|
[ngClass]="classes"
|
||||||
[class.unset]="classes !== ''"
|
[class.unset]="classes !== ''"
|
||||||
href="mailto:support@datacontroller.io?subject=Licence"
|
href="mailto:support@datacontroller.io?subject=Licence"
|
||||||
>support@datacontroller.io</a
|
>support@datacontroller.io</a
|
||||||
>
|
>
|
||||||
|
@ -106,7 +106,7 @@
|
|||||||
*clrIfOpen
|
*clrIfOpen
|
||||||
>
|
>
|
||||||
<span *ngIf="tableLocked">
|
<span *ngIf="tableLocked">
|
||||||
To unlock all tables, contact support@datacontroller.io
|
To unlock all tables, contact support@datacontroller.io
|
||||||
</span>
|
</span>
|
||||||
</clr-tooltip-content>
|
</clr-tooltip-content>
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@ 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'
|
||||||
import { specialMissingNumericValidator } from './validations/hot-custom-validators'
|
import { specialMissingNumericValidator } from './validations/hot-custom-validators'
|
||||||
|
import { applyNumericFormats } from './utils/applyNumericFormats'
|
||||||
|
import { CustomAutocompleteEditor } from './editors/numericAutocomplete'
|
||||||
|
|
||||||
export class DcValidator {
|
export class DcValidator {
|
||||||
private rules: DcValidation[] = []
|
private rules: DcValidation[] = []
|
||||||
@ -37,10 +39,13 @@ export class DcValidator {
|
|||||||
dqData: DQData[],
|
dqData: DQData[],
|
||||||
hotInstance?: Handsontable
|
hotInstance?: Handsontable
|
||||||
) {
|
) {
|
||||||
|
this.registerCustomEditors()
|
||||||
|
|
||||||
this.sasparams = sasparams
|
this.sasparams = sasparams
|
||||||
this.hotInstance = hotInstance
|
this.hotInstance = hotInstance
|
||||||
this.rules = parseColType(sasparams.COLTYPE)
|
this.rules = parseColType(sasparams.COLTYPE)
|
||||||
this.rules = mergeColsRules(cols, this.rules, $dataFormats)
|
this.rules = mergeColsRules(cols, this.rules, $dataFormats)
|
||||||
|
this.rules = applyNumericFormats(this.rules)
|
||||||
this.dqrules = dqRules
|
this.dqrules = dqRules
|
||||||
this.dqdata = dqData
|
this.dqdata = dqData
|
||||||
this.primaryKeys = sasparams.PK.split(' ')
|
this.primaryKeys = sasparams.PK.split(' ')
|
||||||
@ -49,6 +54,13 @@ export class DcValidator {
|
|||||||
this.setupValidations()
|
this.setupValidations()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerCustomEditors() {
|
||||||
|
Handsontable.editors.registerEditor(
|
||||||
|
'autocomplete.custom',
|
||||||
|
CustomAutocompleteEditor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
getRules(): DcValidation[] {
|
getRules(): DcValidation[] {
|
||||||
return this.rules
|
return this.rules
|
||||||
}
|
}
|
||||||
@ -260,6 +272,7 @@ export class DcValidator {
|
|||||||
if (source.length > 0) {
|
if (source.length > 0) {
|
||||||
this.rules[i].source = source
|
this.rules[i].source = source
|
||||||
this.rules[i].type = 'autocomplete'
|
this.rules[i].type = 'autocomplete'
|
||||||
|
this.rules[i].editor = 'autocomplete.custom'
|
||||||
this.rules[i].filter = false
|
this.rules[i].filter = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,7 +326,10 @@ export class DcValidator {
|
|||||||
|
|
||||||
// Because of dynamic cell validation, that will change the type of cell to dropdown
|
// Because of dynamic cell validation, that will change the type of cell to dropdown
|
||||||
// `rules[i].colType` could be different type (eg. numeric). So we check if current cell is dropdown, to call HOT native dropdown validator
|
// `rules[i].colType` could be different type (eg. numeric). So we check if current cell is dropdown, to call HOT native dropdown validator
|
||||||
if (this.editor === 'autocomplete') {
|
if (
|
||||||
|
this.editor === 'autocomplete' ||
|
||||||
|
this.editor === 'autocomplete.custom'
|
||||||
|
) {
|
||||||
self
|
self
|
||||||
.getHandsontableValidator('autocomplete')
|
.getHandsontableValidator('autocomplete')
|
||||||
.call(this, value, (valid: boolean) => {
|
.call(this, value, (valid: boolean) => {
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
import Handsontable from 'handsontable'
|
||||||
|
import Core from 'handsontable/core'
|
||||||
|
|
||||||
|
export class CustomAutocompleteEditor extends Handsontable.editors
|
||||||
|
.AutocompleteEditor {
|
||||||
|
constructor(instance: Core) {
|
||||||
|
super(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
createElements() {
|
||||||
|
super.createElements()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listbox open
|
||||||
|
open(event?: Event | undefined): void {
|
||||||
|
super.open(event)
|
||||||
|
|
||||||
|
if (this.isCellNumeric()) {
|
||||||
|
this.htContainer.classList.add('numericListbox')
|
||||||
|
} else {
|
||||||
|
this.htContainer.classList.remove('numericListbox')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isCellNumeric() {
|
||||||
|
return this.cellProperties?.className?.includes('htNumeric')
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { DcValidation } from '../models/dc-validation.model'
|
||||||
|
import * as languages from 'numbro/dist/languages.min'
|
||||||
|
/**
|
||||||
|
* Applying the numeric formats based on the browser locale/language
|
||||||
|
* So that correct decimal separators are applied.
|
||||||
|
* For example european format (thousand dot, decimal comma): 1.000,00
|
||||||
|
*
|
||||||
|
* @param rules Cell Validation rules to be updated
|
||||||
|
* Those rules are passed in the `columns` property Of handsontable settings.
|
||||||
|
*/
|
||||||
|
export const applyNumericFormats = (rules: DcValidation[]): DcValidation[] => {
|
||||||
|
const lang = languages[window.navigator.language]
|
||||||
|
|
||||||
|
if (!lang) return rules
|
||||||
|
|
||||||
|
for (let rule of rules) {
|
||||||
|
if (rule.type === 'numeric')
|
||||||
|
rule.numericFormat = {
|
||||||
|
pattern: '0,0',
|
||||||
|
culture: window.navigator.language // use this for EUR (German),
|
||||||
|
// more cultures available on http://numbrojs.com/languages.html
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules
|
||||||
|
}
|
@ -6,7 +6,8 @@ import { DcValidation } from '../models/dc-validation.model'
|
|||||||
* Merging old validation params from sasparams with cols params
|
* Merging old validation params from sasparams with cols params
|
||||||
* @param sasparams sasparams coming from SAS
|
* @param sasparams sasparams coming from SAS
|
||||||
* @param cols cols coming from SAS
|
* @param cols cols coming from SAS
|
||||||
* @param rules rules to be updated
|
* @param rules Cell Validation rules to be updated
|
||||||
|
* Those rules are passed in the `columns` property Of handsontable settings.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const mergeColsRules = (
|
export const mergeColsRules = (
|
||||||
|
@ -107,7 +107,29 @@
|
|||||||
</clr-tab-content>
|
</clr-tab-content>
|
||||||
</clr-tab>
|
</clr-tab>
|
||||||
</clr-tabs>
|
</clr-tabs>
|
||||||
<p *ngIf="isMainRoute('home')" class="page-title">Edit</p>
|
|
||||||
|
<div
|
||||||
|
*ngIf="isMainRoute('home')"
|
||||||
|
class="d-flex justify-content-center sub-dropdown"
|
||||||
|
>
|
||||||
|
<clr-dropdown>
|
||||||
|
<button class="dropdown-toggle btn btn-link" clrDropdownTrigger>
|
||||||
|
{{ getSubPage() }}
|
||||||
|
<clr-icon shape="caret down"></clr-icon>
|
||||||
|
</button>
|
||||||
|
<clr-dropdown-menu *clrIfOpen>
|
||||||
|
<a
|
||||||
|
clrVerticalNavLink
|
||||||
|
routerLink="/home/tables"
|
||||||
|
routerLinkActive="active"
|
||||||
|
>Tables</a
|
||||||
|
>
|
||||||
|
<a clrVerticalNavLink routerLink="/home/files" routerLinkActive="active"
|
||||||
|
>Files</a
|
||||||
|
>
|
||||||
|
</clr-dropdown-menu>
|
||||||
|
</clr-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="nav-divider"></div>
|
<div class="nav-divider"></div>
|
||||||
|
|
||||||
|
@ -1,4 +1,22 @@
|
|||||||
<label *ngIf="label" class="clr-control-label">{{ label }}</label>
|
<label
|
||||||
|
*ngIf="label"
|
||||||
|
[class.secondLabelActive]="secondLabel && secondLabel.length > 0"
|
||||||
|
class="clr-control-label"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
(click)="onChangeLabel('first')"
|
||||||
|
[class.value-type-selected]="labelSelected === 'first'"
|
||||||
|
>{{ label }}</span
|
||||||
|
>
|
||||||
|
<ng-container *ngIf="secondLabel">
|
||||||
|
/
|
||||||
|
<span
|
||||||
|
(click)="onChangeLabel('second')"
|
||||||
|
[class.value-type-selected]="labelSelected === 'second'"
|
||||||
|
>{{ secondLabel }}</span
|
||||||
|
>
|
||||||
|
</ng-container>
|
||||||
|
</label>
|
||||||
<ng-container [ngSwitch]="type">
|
<ng-container [ngSwitch]="type">
|
||||||
<ng-container *ngSwitchCase="'date'">
|
<ng-container *ngSwitchCase="'date'">
|
||||||
<clr-date-container>
|
<clr-date-container>
|
||||||
|
@ -28,4 +28,12 @@ clr-date-container {
|
|||||||
margin-top: -5px;
|
margin-top: -5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label.secondLabelActive span {
|
||||||
|
&:not(.value-type-selected) {
|
||||||
|
text-decoration: line-through;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
}
|
}
|
@ -18,6 +18,7 @@ import { OnLoadingMoreEvent } from '../autocomplete/autocomplete.component'
|
|||||||
export class SoftSelectComponent implements OnInit, OnChanges {
|
export class SoftSelectComponent implements OnInit, OnChanges {
|
||||||
@Input() inputId: string = ''
|
@Input() inputId: string = ''
|
||||||
@Input() label: string | undefined
|
@Input() label: string | undefined
|
||||||
|
@Input() secondLabel: string | undefined
|
||||||
@Input() value: Date | string | null = ''
|
@Input() value: Date | string | null = ''
|
||||||
@Input() disabled: boolean = false
|
@Input() disabled: boolean = false
|
||||||
@Input() type: string = 'text'
|
@Input() type: string = 'text'
|
||||||
@ -30,20 +31,24 @@ export class SoftSelectComponent implements OnInit, OnChanges {
|
|||||||
@Output() focusinInput: EventEmitter<any> = new EventEmitter()
|
@Output() focusinInput: EventEmitter<any> = new EventEmitter()
|
||||||
@Output() onAutocompleteLoadingMore: EventEmitter<OnLoadingMoreEvent> =
|
@Output() onAutocompleteLoadingMore: EventEmitter<OnLoadingMoreEvent> =
|
||||||
new EventEmitter()
|
new EventEmitter()
|
||||||
|
@Output() selectedLabelChange: EventEmitter<string> = new EventEmitter()
|
||||||
|
|
||||||
@ViewChild('input') inputElement: any
|
@ViewChild('input') inputElement: any
|
||||||
|
|
||||||
temp: Date | string | null = ''
|
temp: Date | string | null = ''
|
||||||
inputFocused: boolean = false
|
inputFocused: boolean = false
|
||||||
|
|
||||||
|
labelSelected: LabelTypes = 'first'
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if (
|
if (
|
||||||
changes.value &&
|
changes.value &&
|
||||||
changes.value.currentValue !== changes.value.previousValue
|
changes.value.currentValue !== changes.value.previousValue
|
||||||
)
|
) {
|
||||||
this.valueChange.emit(changes.value.currentValue)
|
this.valueChange.emit(changes.value.currentValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {}
|
ngOnInit(): void {}
|
||||||
@ -85,4 +90,14 @@ export class SoftSelectComponent implements OnInit, OnChanges {
|
|||||||
onFocusinInput(event: any) {
|
onFocusinInput(event: any) {
|
||||||
this.focusinInput.emit(event)
|
this.focusinInput.emit(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChangeLabel(label: LabelTypes) {
|
||||||
|
this.labelSelected = label
|
||||||
|
|
||||||
|
const selectedLabelText = label === 'first' ? this.label : this.secondLabel
|
||||||
|
|
||||||
|
this.selectedLabelChange.emit(selectedLabelText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LabelTypes = 'first' | 'second'
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
class="licence-notice"
|
class="licence-notice"
|
||||||
>To unlock more then {{ licenceState.value.viewbox_limit }}
|
>To unlock more then {{ licenceState.value.viewbox_limit }}
|
||||||
{{ licenceState.value.viewbox_limit === 1 ? 'viewbox' : 'viewboxes' }},
|
{{ licenceState.value.viewbox_limit === 1 ? 'viewbox' : 'viewboxes' }},
|
||||||
contact support@datacontroller.io</span
|
contact support@datacontroller.io</span
|
||||||
>
|
>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import { EventService } from '../services/event.service'
|
|||||||
import { AppService } from '../services/app.service'
|
import { AppService } from '../services/app.service'
|
||||||
import { HotTableInterface } from '../models/HotTable.interface'
|
import { HotTableInterface } from '../models/HotTable.interface'
|
||||||
import { LicenceService } from '../services/licence.service'
|
import { LicenceService } from '../services/licence.service'
|
||||||
|
import { globals } from '../_globals'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-stage',
|
selector: 'app-stage',
|
||||||
@ -55,7 +56,15 @@ export class StageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public goBack() {
|
public goBack() {
|
||||||
this.route.navigateByUrl('/editor/' + this.tableDetails.BASE_TABLE)
|
const xlmap = globals.xlmaps.find(
|
||||||
|
(xlmap) => xlmap.targetDS === this.tableDetails.BASE_TABLE
|
||||||
|
)
|
||||||
|
if (xlmap) {
|
||||||
|
const id = this.hotTable.data[0].XLMAP_ID
|
||||||
|
this.route.navigateByUrl('/home/files/' + id)
|
||||||
|
} else {
|
||||||
|
this.route.navigateByUrl('/editor/' + this.tableDetails.BASE_TABLE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public download(id: any) {
|
public download(id: any) {
|
||||||
|
11
client/src/app/system/system-routing.module.ts
Normal file
11
client/src/app/system/system-routing.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
|
import { SystemComponent } from './system.component'
|
||||||
|
|
||||||
|
const routes: Routes = [{ path: '', component: SystemComponent }]
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class SystemRoutingModule {}
|
12
client/src/app/system/system.module.ts
Normal file
12
client/src/app/system/system.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
|
||||||
|
import { SystemRoutingModule } from './system-routing.module'
|
||||||
|
import { SystemComponent } from './system.component'
|
||||||
|
import { ClarityModule } from '@clr/angular'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [SystemComponent],
|
||||||
|
imports: [CommonModule, SystemRoutingModule, ClarityModule]
|
||||||
|
})
|
||||||
|
export class SystemModule {}
|
@ -105,7 +105,7 @@
|
|||||||
*clrIfOpen
|
*clrIfOpen
|
||||||
>
|
>
|
||||||
<span *ngIf="tableLocked">
|
<span *ngIf="tableLocked">
|
||||||
To unlock all tables, contact support@datacontroller.io
|
To unlock all tables, contact support@datacontroller.io
|
||||||
</span>
|
</span>
|
||||||
</clr-tooltip-content>
|
</clr-tooltip-content>
|
||||||
</clr-tooltip>
|
</clr-tooltip>
|
||||||
@ -358,36 +358,49 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="title-col clr-col-auto clr-flex-column clr-flex-sm-row">
|
<div class="title-col clr-col-auto clr-flex-column clr-flex-sm-row">
|
||||||
<clr-icon
|
|
||||||
(click)="datasetInfo = true"
|
|
||||||
shape="info-circle"
|
|
||||||
class="is-highlight cursor-pointer"
|
|
||||||
size="24"
|
|
||||||
></clr-icon>
|
|
||||||
|
|
||||||
<clr-icon
|
|
||||||
*ngIf="tableTitle?.includes('-FC')"
|
|
||||||
shape="bolt"
|
|
||||||
class="color-yellow mt-5 mr-5"
|
|
||||||
></clr-icon>
|
|
||||||
|
|
||||||
<h3
|
<h3
|
||||||
*ngIf="tableTitle && tableTitle.length > 0"
|
class="viewerTitle clr-flex-column d-flex clr-flex-sm-row clr-align-items-center clr-justify-content-center"
|
||||||
class="viewerTitle clr-flex-column d-flex clr-flex-sm-row clr-align-items-center"
|
|
||||||
>
|
>
|
||||||
{{ tableTitle?.replace('-FC', '') }}
|
<clr-tooltip class="d-flex">
|
||||||
|
<clr-icon
|
||||||
|
clrTooltipTrigger
|
||||||
|
(click)="datasetInfo = true"
|
||||||
|
shape="info-circle"
|
||||||
|
class="is-highlight cursor-pointer"
|
||||||
|
size="24"
|
||||||
|
></clr-icon>
|
||||||
|
|
||||||
<span *ngIf="numberOfRows !== null">
|
<clr-icon
|
||||||
({{ numberOfRows | thousandSeparator: ',' }}
|
*ngIf="tableTitle?.includes('-FC')"
|
||||||
{{ numberOfRows! === 1 ? 'row' : 'rows' }}, {{ filterCols.length
|
shape="bolt"
|
||||||
}}{{ filterCols.length === 1 ? ' col' : ' cols' }})
|
class="color-yellow mt-5 mr-5"
|
||||||
</span>
|
></clr-icon>
|
||||||
|
|
||||||
<clr-icon
|
<span clrTooltipTrigger *ngIf="tableTitle && tableTitle.length > 0">
|
||||||
(click)="reloadTableData()"
|
{{ tableTitle?.replace('-FC', '') }}
|
||||||
class="refresh-table"
|
</span>
|
||||||
shape="refresh"
|
<clr-tooltip-content
|
||||||
></clr-icon>
|
clrPosition="bottom-left"
|
||||||
|
clrSize="lg"
|
||||||
|
*clrIfOpen
|
||||||
|
>
|
||||||
|
{{ this.dsNote }}
|
||||||
|
</clr-tooltip-content>
|
||||||
|
</clr-tooltip>
|
||||||
|
|
||||||
|
<ng-container *ngIf="tableTitle && tableTitle.length > 0">
|
||||||
|
<span *ngIf="numberOfRows !== null">
|
||||||
|
({{ numberOfRows | thousandSeparator: ',' }}
|
||||||
|
{{ numberOfRows! === 1 ? 'row' : 'rows' }}, {{ filterCols.length
|
||||||
|
}}{{ filterCols.length === 1 ? ' col' : ' cols' }})
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<clr-icon
|
||||||
|
(click)="reloadTableData()"
|
||||||
|
class="refresh-table"
|
||||||
|
shape="refresh"
|
||||||
|
></clr-icon>
|
||||||
|
</ng-container>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -630,6 +643,9 @@
|
|||||||
[cells]="hotTable.cells"
|
[cells]="hotTable.cells"
|
||||||
[maxRows]="hotTable.maxRows"
|
[maxRows]="hotTable.maxRows"
|
||||||
[manualColumnResize]="true"
|
[manualColumnResize]="true"
|
||||||
|
[rowHeaders]="hotTable.rowHeaders"
|
||||||
|
[rowHeaderWidth]="hotTable.rowHeaderWidth"
|
||||||
|
[rowHeights]="hotTable.rowHeights"
|
||||||
[licenseKey]="hotTable.licenseKey"
|
[licenseKey]="hotTable.licenseKey"
|
||||||
>
|
>
|
||||||
</hot-table>
|
</hot-table>
|
||||||
|
@ -95,6 +95,7 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
|||||||
public $dataFormats: $DataFormats | null = null
|
public $dataFormats: $DataFormats | null = null
|
||||||
public datasetInfo: boolean = false
|
public datasetInfo: boolean = false
|
||||||
public dsmeta: DSMeta[] = []
|
public dsmeta: DSMeta[] = []
|
||||||
|
public dsNote = ''
|
||||||
|
|
||||||
public licenceState = this.licenceService.licenceState
|
public licenceState = this.licenceService.licenceState
|
||||||
public Infinity = Infinity
|
public Infinity = Infinity
|
||||||
@ -108,6 +109,11 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
|||||||
settings: {},
|
settings: {},
|
||||||
afterGetColHeader: undefined,
|
afterGetColHeader: undefined,
|
||||||
licenseKey: undefined,
|
licenseKey: undefined,
|
||||||
|
rowHeaders: (index: number) => {
|
||||||
|
return ' '
|
||||||
|
},
|
||||||
|
rowHeaderWidth: 15,
|
||||||
|
rowHeights: 20,
|
||||||
contextMenu: ['copy_with_column_headers', 'copy_column_headers_only'],
|
contextMenu: ['copy_with_column_headers', 'copy_column_headers_only'],
|
||||||
copyPaste: {
|
copyPaste: {
|
||||||
copyColumnHeaders: true,
|
copyColumnHeaders: true,
|
||||||
@ -201,16 +207,28 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open viewboxes modal
|
||||||
|
*/
|
||||||
public newViewbox() {
|
public newViewbox() {
|
||||||
this.viewboxOpen = true
|
this.viewboxOpen = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resetting filter variables
|
||||||
|
*/
|
||||||
public resetFilter() {
|
public resetFilter() {
|
||||||
if (this.queryFilterCompList.first) {
|
if (this.queryFilterCompList.first) {
|
||||||
this.queryFilterCompList.first.resetFilter()
|
this.queryFilterCompList.first.resetFilter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searching table against particular string, data is comming from backend.
|
||||||
|
* There is also a toggle that will search for a numeric values
|
||||||
|
*
|
||||||
|
* @param inputElement input from which search string is captured
|
||||||
|
*/
|
||||||
public async searchTable(inputElement: any) {
|
public async searchTable(inputElement: any) {
|
||||||
this.searchLoading = true
|
this.searchLoading = true
|
||||||
|
|
||||||
@ -229,6 +247,7 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
|||||||
this.hotTable.data = res.viewdata
|
this.hotTable.data = res.viewdata
|
||||||
this.$dataFormats = res.$viewdata
|
this.$dataFormats = res.$viewdata
|
||||||
this.dsmeta = res.dsmeta
|
this.dsmeta = res.dsmeta
|
||||||
|
this.setDSNote()
|
||||||
this.numberOfRows = res.sasparams[0].NOBS
|
this.numberOfRows = res.sasparams[0].NOBS
|
||||||
this.queryText = res.sasparams[0].FILTER_TEXT
|
this.queryText = res.sasparams[0].FILTER_TEXT
|
||||||
this.headerPks = res.sasparams[0].PK_FIELDS.split(' ')
|
this.headerPks = res.sasparams[0].PK_FIELDS.split(' ')
|
||||||
@ -249,6 +268,9 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
|||||||
this.searchLoading = false
|
this.searchLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re sending request to backend and re-setting data in the HOT
|
||||||
|
*/
|
||||||
public reloadTableData() {
|
public reloadTableData() {
|
||||||
this.viewData(this.urlFilterPk || 0)
|
this.viewData(this.urlFilterPk || 0)
|
||||||
}
|
}
|
||||||
@ -278,6 +300,9 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: Should be removed, not used
|
||||||
|
*/
|
||||||
public filterFn(input: string) {
|
public filterFn(input: string) {
|
||||||
let libraries = this.libraries
|
let libraries = this.libraries
|
||||||
this.libraries = libraries.filter(
|
this.libraries = libraries.filter(
|
||||||
@ -286,6 +311,9 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads file from backend, against `getrawdata` service, link is created and open in new tab
|
||||||
|
*/
|
||||||
public downloadData() {
|
public downloadData() {
|
||||||
let storage = this.sasjsConfig.serverUrl
|
let storage = this.sasjsConfig.serverUrl
|
||||||
let metaData = this.sasjsConfig.appLoc
|
let metaData = this.sasjsConfig.appLoc
|
||||||
@ -320,6 +348,9 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
|||||||
this.openDownload = false
|
this.openDownload = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads file from backend, against `getddl` service, link is created and open in new tab
|
||||||
|
*/
|
||||||
public downloadDDL() {
|
public downloadDDL() {
|
||||||
let libref = this.lib
|
let libref = this.lib
|
||||||
let ds = this.table
|
let ds = this.table
|
||||||
@ -346,15 +377,27 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
|||||||
this.openDownload = false
|
this.openDownload = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When clicked on textarea in the Web Query Modal, this function will
|
||||||
|
* select all text inside.
|
||||||
|
* @param evt textarea which contains the web query text
|
||||||
|
*/
|
||||||
public onCliCommandFocus(evt: any): void {
|
public onCliCommandFocus(evt: any): void {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
evt.target.select()
|
evt.target.select()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the edit page of a viewing table
|
||||||
|
*/
|
||||||
public editTable() {
|
public editTable() {
|
||||||
this.router.navigateByUrl('/editor/' + this.libTab)
|
this.router.navigateByUrl('/editor/' + this.libTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to show/hide the edit table button
|
||||||
|
* @returns Wheter currently viewed table is edtiable
|
||||||
|
*/
|
||||||
public tableEditExists() {
|
public tableEditExists() {
|
||||||
let editTables: any = {}
|
let editTables: any = {}
|
||||||
editTables = globals.editor.libsAndTables
|
editTables = globals.editor.libsAndTables
|
||||||
@ -367,12 +410,18 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
|||||||
return editTables[currentLibrary].includes(currentTable)
|
return editTables[currentLibrary].includes(currentTable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the lineage of a viewing table
|
||||||
|
*/
|
||||||
public goToLineage() {
|
public goToLineage() {
|
||||||
let routeUri = this.tableuri!.split('\\')[1]
|
let routeUri = this.tableuri!.split('\\')[1]
|
||||||
let lineageUrl = `/view/lineage/${routeUri}/REVERSE`
|
let lineageUrl = `/view/lineage/${routeUri}/REVERSE`
|
||||||
this.router.navigateByUrl(lineageUrl)
|
this.router.navigateByUrl(lineageUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays web query modal
|
||||||
|
*/
|
||||||
public showWebQuery() {
|
public showWebQuery() {
|
||||||
this.webQuery = true
|
this.webQuery = true
|
||||||
let filter_pk: number
|
let filter_pk: number
|
||||||
@ -756,6 +805,7 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
|||||||
this.hotTable.data = res.viewdata
|
this.hotTable.data = res.viewdata
|
||||||
this.$dataFormats = res.$viewdata
|
this.$dataFormats = res.$viewdata
|
||||||
this.dsmeta = res.dsmeta
|
this.dsmeta = res.dsmeta
|
||||||
|
this.setDSNote()
|
||||||
this.queryText = res.sasparams[0].FILTER_TEXT
|
this.queryText = res.sasparams[0].FILTER_TEXT
|
||||||
let columns: any[] = []
|
let columns: any[] = []
|
||||||
let colArr = []
|
let colArr = []
|
||||||
@ -969,6 +1019,22 @@ export class ViewerComponent implements AfterContentInit, AfterViewInit {
|
|||||||
this.sasStoreService.removeClause()
|
this.sasStoreService.removeClause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setDSNote() {
|
||||||
|
const notes = this.dsmeta.find((item) => item.NAME === 'NOTES')
|
||||||
|
const longDesc = this.dsmeta.find((item) => item.NAME === 'DD_LONGDESC')
|
||||||
|
const shortDesc = this.dsmeta.find((item) => item.NAME === 'DD_SHORTDESC')
|
||||||
|
|
||||||
|
if (notes && notes.VALUE) {
|
||||||
|
this.dsNote = notes.VALUE
|
||||||
|
} else if (longDesc && longDesc.VALUE) {
|
||||||
|
this.dsNote = longDesc.VALUE
|
||||||
|
} else if (shortDesc && shortDesc.VALUE) {
|
||||||
|
this.dsNote = shortDesc.VALUE
|
||||||
|
} else {
|
||||||
|
this.dsNote = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private setupHot() {
|
private setupHot() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.loadingTableView && this.libDataset) {
|
if (!this.loadingTableView && this.libDataset) {
|
||||||
|
@ -13,9 +13,22 @@ import { SharedModule } from '../shared/shared.module'
|
|||||||
import { ViewboxesModule } from '../shared/viewboxes/viewboxes.module'
|
import { ViewboxesModule } from '../shared/viewboxes/viewboxes.module'
|
||||||
import { QueryModule } from '../query/query.module'
|
import { QueryModule } from '../query/query.module'
|
||||||
import { DirectivesModule } from '../directives/directives.module'
|
import { DirectivesModule } from '../directives/directives.module'
|
||||||
|
import { UserComponent } from '../user/user.component'
|
||||||
|
import { RoleComponent } from '../role/role.component'
|
||||||
|
import { GroupComponent } from '../group/group.component'
|
||||||
|
import { LineageComponent } from '../lineage/lineage.component'
|
||||||
|
import { MetadataComponent } from '../metadata/metadata.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [ViewerComponent, ViewRouteComponent],
|
declarations: [
|
||||||
|
ViewerComponent,
|
||||||
|
ViewRouteComponent,
|
||||||
|
UserComponent,
|
||||||
|
RoleComponent,
|
||||||
|
GroupComponent,
|
||||||
|
LineageComponent,
|
||||||
|
MetadataComponent
|
||||||
|
],
|
||||||
imports: [
|
imports: [
|
||||||
ViewboxesModule,
|
ViewboxesModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
159
client/src/app/xlmap/tests/xl.utils.spec.ts
Normal file
159
client/src/app/xlmap/tests/xl.utils.spec.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import {
|
||||||
|
extractRowAndCol,
|
||||||
|
getCellAddress,
|
||||||
|
getFinishingCell,
|
||||||
|
isBlankRow
|
||||||
|
} from '../utils/xl.utils'
|
||||||
|
|
||||||
|
describe('isBlankRow', () => {
|
||||||
|
it('should return true for a blank row', () => {
|
||||||
|
const blankRow = { __rowNum__: 1 }
|
||||||
|
expect(isBlankRow(blankRow)).toBeTrue()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for a non-blank row', () => {
|
||||||
|
const nonBlankRow = {
|
||||||
|
B: 3,
|
||||||
|
C: 'some value',
|
||||||
|
D: -203
|
||||||
|
}
|
||||||
|
expect(isBlankRow(nonBlankRow)).toBeFalse()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('extractRowAndCol', () => {
|
||||||
|
it('should extract row and column from "MATCH F R[2]C[0]: CASH BALANCE"', () => {
|
||||||
|
const input = 'MATCH F R[2]C[0]: CASH BALANCE'
|
||||||
|
const result = extractRowAndCol(input)
|
||||||
|
expect(result).toEqual({ row: 2, column: 0 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should extract row and column from "RELATIVE R[10]C[6]"', () => {
|
||||||
|
const input = 'RELATIVE R[10]C[6]'
|
||||||
|
const result = extractRowAndCol(input)
|
||||||
|
expect(result).toEqual({ row: 10, column: 6 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return null for invalid input', () => {
|
||||||
|
const invalidInput = 'INVALID INPUT'
|
||||||
|
const result = extractRowAndCol(invalidInput)
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getCellAddress', () => {
|
||||||
|
const arrayOfObjects = [
|
||||||
|
{ A: 'valueA1', B: 'valueB1' },
|
||||||
|
{ A: 'valueA2', B: 'valueB2' }
|
||||||
|
]
|
||||||
|
|
||||||
|
it('should convert "ABSOLUTE D8" to A1-style address', () => {
|
||||||
|
const input = 'ABSOLUTE D8'
|
||||||
|
const result = getCellAddress(input, arrayOfObjects)
|
||||||
|
expect(result).toBe('D8')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert "RELATIVE R[10]C[6]" to A1-style address', () => {
|
||||||
|
const input = 'RELATIVE R[10]C[6]'
|
||||||
|
const result = getCellAddress(input, arrayOfObjects)
|
||||||
|
expect(result).toBe('F10')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert "MATCH 1 R[0]C[0]:valueA1" to A1-style address', () => {
|
||||||
|
const input = 'MATCH 1 R[0]C[0]:valueA1'
|
||||||
|
const result = getCellAddress(input, arrayOfObjects)
|
||||||
|
expect(result).toBe('A1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert "MATCH A R[0]C[0]:valueA1" to A1-style address', () => {
|
||||||
|
const input = 'MATCH A R[0]C[0]:valueA1'
|
||||||
|
const result = getCellAddress(input, arrayOfObjects)
|
||||||
|
expect(result).toBe('A1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert "MATCH 1 R[1]C[0]:valueA1" to A1-style address', () => {
|
||||||
|
const input = 'MATCH 1 R[1]C[0]:valueA1'
|
||||||
|
const result = getCellAddress(input, arrayOfObjects)
|
||||||
|
expect(result).toBe('A2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert "MATCH A R[0]C[1]:valueA1" to A1-style address', () => {
|
||||||
|
const input = 'MATCH A R[0]C[1]:valueA1'
|
||||||
|
const result = getCellAddress(input, arrayOfObjects)
|
||||||
|
expect(result).toBe('B1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert "MATCH 1 R[1]C[1]:valueA1" to A1-style address', () => {
|
||||||
|
const input = 'MATCH 1 R[1]C[1]:valueA1'
|
||||||
|
const result = getCellAddress(input, arrayOfObjects)
|
||||||
|
expect(result).toBe('B2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert "MATCH A R[1]C[1]:valueA1" to A1-style address', () => {
|
||||||
|
const input = 'MATCH A R[1]C[1]:valueA1'
|
||||||
|
const result = getCellAddress(input, arrayOfObjects)
|
||||||
|
expect(result).toBe('B2')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getFinishingCell', () => {
|
||||||
|
const arrayOfObjects = [
|
||||||
|
{ A: 'valueA1', B: 'valueB1' },
|
||||||
|
{ A: 'valueA2', B: 'valueB2' },
|
||||||
|
{ A: 'valueA3', B: 'valueB3' },
|
||||||
|
{ B: 'valueB4' },
|
||||||
|
{ A: 'valueA5' },
|
||||||
|
{ A: 'valueA6', B: 'valueB6' },
|
||||||
|
{},
|
||||||
|
{ A: 'valueA8' }
|
||||||
|
]
|
||||||
|
|
||||||
|
it('should return the start cell if finish is an empty string', () => {
|
||||||
|
const start = 'A1'
|
||||||
|
const finish = ''
|
||||||
|
const result = getFinishingCell(start, finish, arrayOfObjects)
|
||||||
|
expect(result).toBe(start)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert "ABSOLUTE D8" to A1-style address', () => {
|
||||||
|
const start = 'A1'
|
||||||
|
const finish = 'ABSOLUTE D8'
|
||||||
|
const result = getFinishingCell(start, finish, arrayOfObjects)
|
||||||
|
expect(result).toBe('D8')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert "RELATIVE R[2]C[1]" to A1-style address', () => {
|
||||||
|
const start = 'A1'
|
||||||
|
const finish = 'RELATIVE R[2]C[1]'
|
||||||
|
const result = getFinishingCell(start, finish, arrayOfObjects)
|
||||||
|
expect(result).toBe('B3')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert "MATCH A R[0]C[1]:valueA1" to A1-style address', () => {
|
||||||
|
const start = 'A1'
|
||||||
|
const finish = 'MATCH A R[0]C[1]:valueA1'
|
||||||
|
const result = getFinishingCell(start, finish, arrayOfObjects)
|
||||||
|
expect(result).toBe('B1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert "MATCH 1 R[4]C[0]:valueB1" to A1-style address', () => {
|
||||||
|
const start = 'A1'
|
||||||
|
const finish = 'MATCH 1 R[4]C[0]:valueB1'
|
||||||
|
const result = getFinishingCell(start, finish, arrayOfObjects)
|
||||||
|
expect(result).toBe('B5')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert "LASTDOWN" to A1-style address of the last non-blank cell in column A', () => {
|
||||||
|
const start = 'A1'
|
||||||
|
const finish = 'LASTDOWN'
|
||||||
|
const result = getFinishingCell(start, finish, arrayOfObjects)
|
||||||
|
expect(result).toBe('A3')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should convert "BLANKROW" to A1-style address of the last row with blank cells', () => {
|
||||||
|
const start = 'A1'
|
||||||
|
const finish = 'BLANKROW'
|
||||||
|
const result = getFinishingCell(start, finish, arrayOfObjects)
|
||||||
|
expect(result).toBe('B6')
|
||||||
|
})
|
||||||
|
})
|
31
client/src/app/xlmap/utils/file.utils.ts
Normal file
31
client/src/app/xlmap/utils/file.utils.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export const blobToFile = (blob: Blob, fileName: string): File => {
|
||||||
|
const file = new File([blob], fileName, {
|
||||||
|
lastModified: new Date().getTime()
|
||||||
|
})
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an array of bytes (Uint8Array) to a binary string.
|
||||||
|
* @param {Uint8Array} res - The array of bytes to convert.
|
||||||
|
* @returns {string} The binary string representation of the array of bytes.
|
||||||
|
*/
|
||||||
|
export const byteArrayToBinaryString = (res: Uint8Array): string => {
|
||||||
|
// Create a Uint8Array from the input array (if it's not already)
|
||||||
|
const bytes = new Uint8Array(res)
|
||||||
|
|
||||||
|
// Initialize an empty string to store the binary representation
|
||||||
|
let binary = ''
|
||||||
|
|
||||||
|
// Get the length of the byte array
|
||||||
|
const length = bytes.byteLength
|
||||||
|
|
||||||
|
// Iterate through each byte in the array
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
// Convert each byte to its binary representation and append to the string
|
||||||
|
binary += String.fromCharCode(bytes[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the binary string
|
||||||
|
return binary
|
||||||
|
}
|
225
client/src/app/xlmap/utils/xl.utils.ts
Normal file
225
client/src/app/xlmap/utils/xl.utils.ts
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import * as XLSX from '@sheet/crypto'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an excel row is blank or not
|
||||||
|
*
|
||||||
|
* @param row object is of shape {[key: string]: any}
|
||||||
|
*/
|
||||||
|
export const isBlankRow = (row: any) => {
|
||||||
|
for (const key in row) {
|
||||||
|
if (key !== '__rowNum__') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts row and column number from xlmap rule.
|
||||||
|
*
|
||||||
|
* Input string should be in form of
|
||||||
|
* either "MATCH F R[2]C[0]: CASH BALANCE" or "RELATIVE R[10]C[6]"
|
||||||
|
*/
|
||||||
|
export const extractRowAndCol = (str: string) => {
|
||||||
|
// Regular expression to match and capture the values inside square brackets
|
||||||
|
const regex = /R\[(\d+)\]C\[(\d+)\]/
|
||||||
|
|
||||||
|
// Match the regular expression against the input string
|
||||||
|
const match = str.match(regex)
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract values from the match groups
|
||||||
|
const row = parseInt(match[1], 10)
|
||||||
|
const column = parseInt(match[2], 10)
|
||||||
|
|
||||||
|
return {
|
||||||
|
row,
|
||||||
|
column
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an A1-Style excel cell address from xlmap rule.
|
||||||
|
*
|
||||||
|
* Expect "ABSOLUTE D8" or "RELATIVE R[10]C[6]" or
|
||||||
|
* "MATCH C R[0]C[4]:Common Equity Tier 1 (CET1)" kinds of string as rule input
|
||||||
|
*/
|
||||||
|
export const getCellAddress = (rule: string, arrayOfObjects: any[]) => {
|
||||||
|
if (rule.startsWith('ABSOLUTE ')) {
|
||||||
|
rule = rule.replace('ABSOLUTE ', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.startsWith('RELATIVE ')) {
|
||||||
|
const rowAndCol = extractRowAndCol(rule)
|
||||||
|
|
||||||
|
if (rowAndCol) {
|
||||||
|
const { row, column } = rowAndCol
|
||||||
|
|
||||||
|
// Generate an A1-Style address string from a SheetJS cell address
|
||||||
|
// Spreadsheet applications typically display ordinal row numbers,
|
||||||
|
// where 1 is the first row, 2 is the second row, etc. The numbering starts at 1.
|
||||||
|
// SheetJS follows JavaScript counting conventions,
|
||||||
|
// where 0 is the first row, 1 is the second row, etc. The numbering starts at 0.
|
||||||
|
// Therefore, we have to subtract 1 from row and column to match SheetJS indexing convention
|
||||||
|
rule = XLSX.utils.encode_cell({ r: row - 1, c: column - 1 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.startsWith('MATCH ')) {
|
||||||
|
let targetValue = ''
|
||||||
|
|
||||||
|
// using a regular expression to match "C[x]:" and extract the value after it
|
||||||
|
const match = rule.match(/C\[\d+\]:(.+)/)
|
||||||
|
|
||||||
|
// Check if there is a match
|
||||||
|
if (match) {
|
||||||
|
// Extract the value after "C[x]:"
|
||||||
|
targetValue = match[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the string by spaces to get target row/column
|
||||||
|
const splittedArray = rule.split(' ')
|
||||||
|
|
||||||
|
// Extract the second word
|
||||||
|
const secondWord = splittedArray[1]
|
||||||
|
|
||||||
|
let targetColumn = ''
|
||||||
|
let targetRow = -1
|
||||||
|
let cellAddress = ''
|
||||||
|
|
||||||
|
// Check if the secondWord is a number
|
||||||
|
if (!isNaN(Number(secondWord))) {
|
||||||
|
targetRow = parseInt(secondWord)
|
||||||
|
} else {
|
||||||
|
targetColumn = secondWord
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetRow !== -1) {
|
||||||
|
// sheetJS index starts from 0,
|
||||||
|
// therefore, decremented 1 to make it correct row address for js array
|
||||||
|
const row = arrayOfObjects[targetRow - 1]
|
||||||
|
for (const col in row) {
|
||||||
|
if (col !== '__rowNum__' && row[col] === targetValue) {
|
||||||
|
cellAddress = col + targetRow
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < arrayOfObjects.length; i++) {
|
||||||
|
const row = arrayOfObjects[i]
|
||||||
|
if (row[targetColumn] === targetValue) {
|
||||||
|
// sheetJS index starts from 0,
|
||||||
|
// therefore, incremented 1 to make it correct row address
|
||||||
|
const rowIndex = i + 1
|
||||||
|
cellAddress = targetColumn + rowIndex
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts A1 cell address to 0-indexed form
|
||||||
|
const matchedCellAddress = XLSX.utils.decode_cell(cellAddress)
|
||||||
|
|
||||||
|
// extract number of rows and columns that we have to move
|
||||||
|
// from matched cell to reach target cell
|
||||||
|
const rowAndCol = extractRowAndCol(rule)
|
||||||
|
|
||||||
|
if (rowAndCol) {
|
||||||
|
const { row, column } = rowAndCol
|
||||||
|
|
||||||
|
// Converts 0-indexed cell address to A1 form
|
||||||
|
rule = XLSX.utils.encode_cell({
|
||||||
|
r: matchedCellAddress.r + row,
|
||||||
|
c: matchedCellAddress.c + column
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an A1-Style excel cell address for last cell
|
||||||
|
*
|
||||||
|
* @param start A1 style excel cell address
|
||||||
|
* @param finish XLMAP_FINISH attribute of xlmap rule
|
||||||
|
* @param arrayOfObjects an array of row objects
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getFinishingCell = (
|
||||||
|
start: string,
|
||||||
|
finish: string,
|
||||||
|
arrayOfObjects: any[]
|
||||||
|
) => {
|
||||||
|
// in this case an individual cell would be extracted
|
||||||
|
if (finish === '') {
|
||||||
|
return start
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finish.startsWith('ABSOLUTE ')) {
|
||||||
|
finish = finish.replace('ABSOLUTE ', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finish.startsWith('RELATIVE ')) {
|
||||||
|
const rowAndCol = extractRowAndCol(finish)
|
||||||
|
if (rowAndCol) {
|
||||||
|
const { row, column } = rowAndCol
|
||||||
|
|
||||||
|
const { r, c } = XLSX.utils.decode_cell(start)
|
||||||
|
|
||||||
|
// finish is relative to starting point
|
||||||
|
// therefore, we need to add extracted row and columns
|
||||||
|
// in starting cell address to get actual finishing cell
|
||||||
|
finish = XLSX.utils.encode_cell({ r: r + row, c: c + column })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finish.startsWith('MATCH ')) {
|
||||||
|
finish = getCellAddress(finish, arrayOfObjects)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finish === 'LASTDOWN') {
|
||||||
|
const { r, c } = XLSX.utils.decode_cell(start)
|
||||||
|
const colName = XLSX.utils.encode_col(c)
|
||||||
|
let lastNonBlank = r
|
||||||
|
for (let i = r + 1; i < arrayOfObjects.length; i++) {
|
||||||
|
const row = arrayOfObjects[i]
|
||||||
|
if (!row[colName]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lastNonBlank = i
|
||||||
|
}
|
||||||
|
finish = colName + (lastNonBlank + 1) // excel numbering starts from 1. So incremented 1 to 0 based index
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finish === 'BLANKROW') {
|
||||||
|
const { r } = XLSX.utils.decode_cell(start)
|
||||||
|
let lastNonBlankRow = r
|
||||||
|
for (let i = r + 1; i < arrayOfObjects.length; i++) {
|
||||||
|
const row = arrayOfObjects[i]
|
||||||
|
if (isBlankRow(row)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lastNonBlankRow = i
|
||||||
|
}
|
||||||
|
const row = arrayOfObjects[lastNonBlankRow]
|
||||||
|
|
||||||
|
// Get the keys of the object (excluding '__rowNum__')
|
||||||
|
const keys = Object.keys(row).filter((key) => key !== '__rowNum__')
|
||||||
|
|
||||||
|
// Finding last column in a row
|
||||||
|
// Find the key with the highest alphanumeric value (assumes keys are letters)
|
||||||
|
const lastColumn = keys.reduce(
|
||||||
|
(maxKey, currentKey) => (currentKey > maxKey ? currentKey : maxKey),
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|
||||||
|
// make finishing cell address in A1 style
|
||||||
|
finish = lastColumn + (lastNonBlankRow + 1) // excel numbering starts from 1. So incremented 1 to 0 based index
|
||||||
|
}
|
||||||
|
|
||||||
|
return finish
|
||||||
|
}
|
22
client/src/app/xlmap/xlmap-routing.module.ts
Normal file
22
client/src/app/xlmap/xlmap-routing.module.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
|
|
||||||
|
import { XLMapComponent } from '../xlmap/xlmap.component'
|
||||||
|
import { XLMapRouteComponent } from '../routes/xlmap-route/xlmap-route.component'
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: XLMapRouteComponent,
|
||||||
|
children: [
|
||||||
|
{ path: '', component: XLMapComponent },
|
||||||
|
{ path: ':id', component: XLMapComponent }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class XLMapRoutingModule {}
|
252
client/src/app/xlmap/xlmap.component.html
Normal file
252
client/src/app/xlmap/xlmap.component.html
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
<app-sidebar>
|
||||||
|
<div *ngIf="xlmapsLoading" class="my-10-mx-auto text-center">
|
||||||
|
<clr-spinner clrMedium></clr-spinner>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<clr-tree>
|
||||||
|
<clr-tree-node class="search-node">
|
||||||
|
<div class="tree-search-wrapper">
|
||||||
|
<input
|
||||||
|
clrInput
|
||||||
|
#searchXLMapTreeInput
|
||||||
|
placeholder="Filter by Id"
|
||||||
|
name="input"
|
||||||
|
[(ngModel)]="searchString"
|
||||||
|
(keyup)="xlmapListOnFilter()"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<clr-icon
|
||||||
|
*ngIf="searchXLMapTreeInput.value.length < 1"
|
||||||
|
shape="search"
|
||||||
|
></clr-icon>
|
||||||
|
<clr-icon
|
||||||
|
*ngIf="searchXLMapTreeInput.value.length > 0"
|
||||||
|
(click)="searchString = ''; xlmapListOnFilter()"
|
||||||
|
shape="times"
|
||||||
|
></clr-icon>
|
||||||
|
</div>
|
||||||
|
</clr-tree-node>
|
||||||
|
|
||||||
|
<ng-container *ngFor="let xlmap of xlmaps">
|
||||||
|
<clr-tree-node>
|
||||||
|
<button
|
||||||
|
(click)="xlmapOnClick(xlmap)"
|
||||||
|
class="clr-treenode-link"
|
||||||
|
[class.table-active]="isActiveXLMap(xlmap.id)"
|
||||||
|
>
|
||||||
|
<clr-icon shape="file"></clr-icon>
|
||||||
|
{{ xlmap.id }}
|
||||||
|
</button>
|
||||||
|
</clr-tree-node>
|
||||||
|
</ng-container>
|
||||||
|
</clr-tree>
|
||||||
|
</app-sidebar>
|
||||||
|
|
||||||
|
<div class="content-area">
|
||||||
|
<div *ngIf="!selectedXLMap" class="no-table-selected">
|
||||||
|
<clr-icon
|
||||||
|
shape="warning-standard"
|
||||||
|
size="60"
|
||||||
|
class="is-info icon-dc-fill"
|
||||||
|
></clr-icon>
|
||||||
|
<h3 *ngIf="xlmaps.length > 0" class="text-center color-gray">
|
||||||
|
Please select a map
|
||||||
|
</h3>
|
||||||
|
<h3 *ngIf="xlmaps.length < 1" class="text-center color-gray">
|
||||||
|
No excel map is found
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="loadingSpinner" *ngIf="isLoading">
|
||||||
|
<span class="spinner"> Loading... </span>
|
||||||
|
<div>
|
||||||
|
<h4>{{ isLoadingDesc }}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
appDragNdrop
|
||||||
|
(fileDraggedOver)="onShowUploadModal()"
|
||||||
|
class="card h-100 d-flex clr-flex-column"
|
||||||
|
*ngIf="!isLoading && selectedXLMap"
|
||||||
|
>
|
||||||
|
<clr-tabs>
|
||||||
|
<clr-tab>
|
||||||
|
<button clrTabLink (click)="selectedTab = TabsEnum.Rules">Rules</button>
|
||||||
|
<clr-tab-content *clrIfActive="selectedTab === TabsEnum.Rules">
|
||||||
|
</clr-tab-content>
|
||||||
|
</clr-tab>
|
||||||
|
<clr-tab>
|
||||||
|
<button clrTabLink (click)="selectedTab = TabsEnum.Data">Data</button>
|
||||||
|
<clr-tab-content *clrIfActive="selectedTab === TabsEnum.Data">
|
||||||
|
</clr-tab-content>
|
||||||
|
</clr-tab>
|
||||||
|
</clr-tabs>
|
||||||
|
|
||||||
|
<ng-container *ngTemplateOutlet="actionButtons"></ng-container>
|
||||||
|
|
||||||
|
<div class="clr-row m-0 mb-10-i viewerTitle">
|
||||||
|
<h3 class="d-flex clr-col-12 clr-justify-content-center mt-5-i">
|
||||||
|
{{ selectedXLMap.id }}
|
||||||
|
</h3>
|
||||||
|
<i class="d-flex clr-col-12 clr-justify-content-center mt-5-i">{{
|
||||||
|
selectedXLMap.description
|
||||||
|
}}</i>
|
||||||
|
<h5 class="d-flex clr-col-12 clr-justify-content-center mt-5-i">
|
||||||
|
Rules Source:
|
||||||
|
<a class="ml-10" [routerLink]="'/view/data/' + rulesSource">
|
||||||
|
{{ rulesSource }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
<h5 class="d-flex clr-col-12 clr-justify-content-center mt-5-i">
|
||||||
|
Target dataset:
|
||||||
|
<a class="ml-10" [routerLink]="'/view/data/' + selectedXLMap.targetDS">
|
||||||
|
{{ selectedXLMap.targetDS }}
|
||||||
|
</a>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clr-flex-1">
|
||||||
|
<hot-table
|
||||||
|
hotId="hotInstance"
|
||||||
|
id="hot-table"
|
||||||
|
[multiColumnSorting]="true"
|
||||||
|
[viewportRowRenderingOffset]="50"
|
||||||
|
[data]="selectedTab === TabsEnum.Rules ? xlmapRules : xlData"
|
||||||
|
[colHeaders]="
|
||||||
|
selectedTab === TabsEnum.Rules ? xlmapRulesHeaders : xlUploadHeader
|
||||||
|
"
|
||||||
|
[columns]="
|
||||||
|
selectedTab === TabsEnum.Rules ? xlmapRulesColumns : xlUploadColumns
|
||||||
|
"
|
||||||
|
[filters]="true"
|
||||||
|
[height]="'100%'"
|
||||||
|
stretchH="all"
|
||||||
|
[modifyColWidth]="maxWidthChecker"
|
||||||
|
[cells]="getCellConfiguration"
|
||||||
|
[maxRows]="hotTableMaxRows"
|
||||||
|
[manualColumnResize]="true"
|
||||||
|
[rowHeaders]="rowHeaders"
|
||||||
|
[rowHeaderWidth]="15"
|
||||||
|
[rowHeights]="20"
|
||||||
|
[licenseKey]="hotTableLicenseKey"
|
||||||
|
>
|
||||||
|
</hot-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<clr-modal
|
||||||
|
appFileDrop
|
||||||
|
(fileOver)="fileOverBase($event)"
|
||||||
|
(fileDrop)="getFileDesc($event, true)"
|
||||||
|
[uploader]="uploader"
|
||||||
|
[clrModalSize]="'xl'"
|
||||||
|
[clrModalStaticBackdrop]="false"
|
||||||
|
[clrModalClosable]="true"
|
||||||
|
[(clrModalOpen)]="showUploadModal"
|
||||||
|
class="relative"
|
||||||
|
>
|
||||||
|
<h3 class="modal-title">Upload File</h3>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="drop-area">
|
||||||
|
<span>Drop file anywhere to upload!</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clr-col-md-12">
|
||||||
|
<div class="clr-row card-block mt-15 d-flex justify-content-between">
|
||||||
|
<div class="clr-col-md-3 filterBtn">
|
||||||
|
<span class="filterBtn w-100">
|
||||||
|
<label
|
||||||
|
for="file-upload"
|
||||||
|
class="btn btn-sm btn-outline profile-buttons w-100"
|
||||||
|
>
|
||||||
|
Browse
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
hidden
|
||||||
|
#fileUploadInput
|
||||||
|
id="file-upload"
|
||||||
|
type="file"
|
||||||
|
appFileSelect
|
||||||
|
[uploader]="uploader"
|
||||||
|
(change)="getFileDesc($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</clr-modal>
|
||||||
|
|
||||||
|
<clr-modal [(clrModalOpen)]="submitLimitNotice">
|
||||||
|
<h3 class="modal-title">Notice</h3>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="m-0">
|
||||||
|
Due to current licence, only
|
||||||
|
{{ licenceState.value.submit_rows_limit }} rows in a file will be
|
||||||
|
submitted. To remove the restriction, contact
|
||||||
|
support@datacontroller.io
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
(click)="submitLimitNotice = false"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
(click)="submit(); submitLimitNotice = false"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</clr-modal>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #actionButtons>
|
||||||
|
<div class="clr-row m-0 clr-justify-content-center">
|
||||||
|
<div
|
||||||
|
*ngIf="status === StatusEnum.ReadyToUpload"
|
||||||
|
class="d-flex clr-justify-content-center clr-col-12 clr-col-lg-4"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-success btn-block mr-0"
|
||||||
|
(click)="onShowUploadModal()"
|
||||||
|
>
|
||||||
|
<clr-icon shape="upload"></clr-icon>
|
||||||
|
<span>Upload</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
*ngIf="status === StatusEnum.ReadyToSubmit"
|
||||||
|
class="d-flex clr-justify-content-center clr-col-12 clr-col-lg-4"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-success btn-block mr-0"
|
||||||
|
(click)="submitExcel()"
|
||||||
|
>
|
||||||
|
<clr-icon shape="upload"></clr-icon>
|
||||||
|
<span>Submit</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
*ngIf="status === StatusEnum.ReadyToSubmit"
|
||||||
|
class="d-flex clr-justify-content-center clr-col-12 clr-col-lg-4"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-danger btn-block mr-0"
|
||||||
|
(click)="discardExtractedData()"
|
||||||
|
>
|
||||||
|
<clr-icon shape="times"></clr-icon>
|
||||||
|
<span>Discard</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
77
client/src/app/xlmap/xlmap.component.scss
Normal file
77
client/src/app/xlmap/xlmap.component.scss
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
.card {
|
||||||
|
margin-top: 0;
|
||||||
|
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
clr-tree-node button {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-table-selected {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
.title-col {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.options-col {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw {
|
||||||
|
margin: 1rem 0rem 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewerTitle {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardFlex {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
padding: 0.5rem !important;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
hot-table {
|
||||||
|
::ng-deep {
|
||||||
|
.primaryKeyHeaderStyle {
|
||||||
|
background: #306b006e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-area {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
margin: 1px;
|
||||||
|
|
||||||
|
border: 2px dashed #fff;
|
||||||
|
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
490
client/src/app/xlmap/xlmap.component.ts
Normal file
490
client/src/app/xlmap/xlmap.component.ts
Normal file
@ -0,0 +1,490 @@
|
|||||||
|
import {
|
||||||
|
AfterContentInit,
|
||||||
|
AfterViewInit,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
HostBinding,
|
||||||
|
OnInit,
|
||||||
|
QueryList,
|
||||||
|
ViewChildren
|
||||||
|
} from '@angular/core'
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
|
import { UploadFile } from '@sasjs/adapter'
|
||||||
|
import * as XLSX from '@sheet/crypto'
|
||||||
|
import { XLMapListItem, globals } from '../_globals'
|
||||||
|
import { FileUploader } from '../models/FileUploader.class'
|
||||||
|
import {
|
||||||
|
EventService,
|
||||||
|
LicenceService,
|
||||||
|
LoggerService,
|
||||||
|
SasService,
|
||||||
|
SasStoreService
|
||||||
|
} from '../services'
|
||||||
|
import { getCellAddress, getFinishingCell } from './utils/xl.utils'
|
||||||
|
import { blobToFile, byteArrayToBinaryString } from './utils/file.utils'
|
||||||
|
|
||||||
|
interface XLMapRule {
|
||||||
|
XLMAP_ID: string
|
||||||
|
XLMAP_SHEET: string
|
||||||
|
XLMAP_RANGE_ID: string
|
||||||
|
XLMAP_START: string
|
||||||
|
XLMAP_FINISH: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface XLUploadEntry {
|
||||||
|
LOAD_REF: string
|
||||||
|
XLMAP_ID: string
|
||||||
|
XLMAP_RANGE_ID: string
|
||||||
|
ROW_NO: number
|
||||||
|
COL_NO: number
|
||||||
|
VALUE_TXT: string
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Status {
|
||||||
|
NoMapSelected,
|
||||||
|
FetchingRules,
|
||||||
|
ReadyToUpload,
|
||||||
|
ExtractingData,
|
||||||
|
ReadyToSubmit,
|
||||||
|
SubmittingExtractedData,
|
||||||
|
Submitting
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Tabs {
|
||||||
|
Rules,
|
||||||
|
Data
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-xlmap',
|
||||||
|
templateUrl: './xlmap.component.html',
|
||||||
|
styleUrls: ['./xlmap.component.scss']
|
||||||
|
})
|
||||||
|
export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
|
||||||
|
@HostBinding('class.content-container') contentContainerClass = true
|
||||||
|
@ViewChildren('fileUploadInput')
|
||||||
|
fileUploadInputCompList: QueryList<ElementRef> = new QueryList()
|
||||||
|
|
||||||
|
StatusEnum = Status
|
||||||
|
TabsEnum = Tabs
|
||||||
|
|
||||||
|
public selectedTab = Tabs.Rules
|
||||||
|
public rulesSource = globals.dcLib + '.MPE_XLMAP_RULES'
|
||||||
|
|
||||||
|
public xlmaps: XLMapListItem[] = []
|
||||||
|
public selectedXLMap: XLMapListItem | undefined = undefined
|
||||||
|
public searchString = ''
|
||||||
|
public xlmapsLoading = true
|
||||||
|
public isLoading = false
|
||||||
|
public isLoadingDesc = ''
|
||||||
|
public status = Status.NoMapSelected
|
||||||
|
|
||||||
|
public xlmapRulesHeaders = [
|
||||||
|
'XLMAP_SHEET',
|
||||||
|
'XLMAP_RANGE_ID',
|
||||||
|
'XLMAP_START',
|
||||||
|
'XLMAP_FINISH'
|
||||||
|
]
|
||||||
|
public xlmapRulesColumns = [
|
||||||
|
{
|
||||||
|
data: 'XLMAP_SHEET'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'XLMAP_RANGE_ID'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
data: 'XLMAP_START'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'XLMAP_FINISH'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
public xlmapRules: XLMapRule[] = []
|
||||||
|
|
||||||
|
public xlUploadHeader = ['XLMAP_RANGE_ID', 'ROW_NO', 'COL_NO', 'VALUE_TXT']
|
||||||
|
public xlUploadColumns = [
|
||||||
|
{
|
||||||
|
data: 'XLMAP_RANGE_ID'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'ROW_NO'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'COL_NO'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'VALUE_TXT'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
public xlData: XLUploadEntry[] = []
|
||||||
|
|
||||||
|
public showUploadModal = false
|
||||||
|
public hasBaseDropZoneOver = false
|
||||||
|
public filename = ''
|
||||||
|
public submitLimitNotice = false
|
||||||
|
|
||||||
|
public uploader: FileUploader = new FileUploader()
|
||||||
|
|
||||||
|
public licenceState = this.licenceService.licenceState
|
||||||
|
|
||||||
|
public hotTableLicenseKey: string | undefined = undefined
|
||||||
|
public hotTableMaxRows =
|
||||||
|
this.licenceState.value.viewer_rows_allowed || Infinity
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private eventService: EventService,
|
||||||
|
private licenceService: LicenceService,
|
||||||
|
private loggerService: LoggerService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private sasStoreService: SasStoreService,
|
||||||
|
private sasService: SasService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public xlmapOnClick(xlmap: XLMapListItem) {
|
||||||
|
if (xlmap.id !== this.selectedXLMap?.id) {
|
||||||
|
this.selectedXLMap = xlmap
|
||||||
|
this.xlData = []
|
||||||
|
this.filename = ''
|
||||||
|
this.uploader.queue = []
|
||||||
|
if (this.fileUploadInputCompList.first) {
|
||||||
|
this.fileUploadInputCompList.first.nativeElement.value = ''
|
||||||
|
}
|
||||||
|
this.selectedTab = Tabs.Rules
|
||||||
|
this.viewXLMapRules()
|
||||||
|
this.router.navigateByUrl('/home/files/' + xlmap.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public xlmapListOnFilter() {
|
||||||
|
if (this.searchString.length > 0) {
|
||||||
|
const array: XLMapListItem[] = globals.xlmaps
|
||||||
|
this.xlmaps = array.filter((item) =>
|
||||||
|
item.id.toLowerCase().includes(this.searchString.toLowerCase())
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.xlmaps = globals.xlmaps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isActiveXLMap(id: string) {
|
||||||
|
return this.selectedXLMap?.id === id
|
||||||
|
}
|
||||||
|
|
||||||
|
public maxWidthChecker(width: any, col: any) {
|
||||||
|
if (width > 200) return 200
|
||||||
|
else return width
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCellConfiguration() {
|
||||||
|
return { readOnly: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
public rowHeaders() {
|
||||||
|
return ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
public onShowUploadModal() {
|
||||||
|
this.showUploadModal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by FileDropDirective
|
||||||
|
* @param e true if file is dragged over the drop zone
|
||||||
|
*/
|
||||||
|
public fileOverBase(e: boolean): void {
|
||||||
|
this.hasBaseDropZoneOver = e
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFileDesc(event: any, dropped = false) {
|
||||||
|
const file = dropped ? event[0] : event.target.files[0]
|
||||||
|
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
const filename = file.name
|
||||||
|
this.filename = filename
|
||||||
|
|
||||||
|
const fileType = filename.slice(
|
||||||
|
filename.lastIndexOf('.') + 1,
|
||||||
|
filename.lastIndexOf('.') + 4
|
||||||
|
)
|
||||||
|
|
||||||
|
if (fileType.toLowerCase() === 'xls') {
|
||||||
|
this.showUploadModal = false
|
||||||
|
this.isLoading = true
|
||||||
|
this.isLoadingDesc = 'Extracting Data'
|
||||||
|
this.status = Status.ExtractingData
|
||||||
|
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = async (theFile: any) => {
|
||||||
|
/* read workbook */
|
||||||
|
const bstr = byteArrayToBinaryString(theFile.target.result)
|
||||||
|
let wb: XLSX.WorkBook | undefined = undefined
|
||||||
|
|
||||||
|
const xlsxOptions: XLSX.ParsingOptions = {
|
||||||
|
type: 'binary',
|
||||||
|
cellDates: false,
|
||||||
|
cellFormula: true,
|
||||||
|
cellStyles: true,
|
||||||
|
cellNF: false,
|
||||||
|
cellText: false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
wb = XLSX.read(bstr, {
|
||||||
|
...xlsxOptions
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
this.eventService.showAbortModal(
|
||||||
|
null,
|
||||||
|
err,
|
||||||
|
undefined,
|
||||||
|
'Error reading file'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wb) {
|
||||||
|
this.isLoading = false
|
||||||
|
this.isLoadingDesc = ''
|
||||||
|
this.status = Status.ReadyToUpload
|
||||||
|
this.uploader.queue.pop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.extractData(wb)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
} else {
|
||||||
|
this.isLoading = false
|
||||||
|
this.isLoadingDesc = ''
|
||||||
|
this.status = Status.ReadyToUpload
|
||||||
|
this.showUploadModal = true
|
||||||
|
this.uploader.queue.pop()
|
||||||
|
|
||||||
|
const abortMsg =
|
||||||
|
'Invalid file type "<b>' +
|
||||||
|
this.filename +
|
||||||
|
'</b>". Please upload excel file.'
|
||||||
|
this.eventService.showAbortModal(null, abortMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public discardExtractedData() {
|
||||||
|
this.isLoading = false
|
||||||
|
this.isLoadingDesc = ''
|
||||||
|
this.status = Status.ReadyToUpload
|
||||||
|
this.xlData = []
|
||||||
|
this.selectedTab = Tabs.Rules
|
||||||
|
this.filename = ''
|
||||||
|
this.uploader.queue = []
|
||||||
|
if (this.fileUploadInputCompList.first) {
|
||||||
|
this.fileUploadInputCompList.first.nativeElement.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits attached excel file that is in preview mode
|
||||||
|
*/
|
||||||
|
public submitExcel() {
|
||||||
|
if (this.licenceState.value.submit_rows_limit !== Infinity) {
|
||||||
|
this.submitLimitNotice = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
public submit() {
|
||||||
|
if (!this.selectedXLMap || !this.xlData.length) return
|
||||||
|
|
||||||
|
this.status = Status.Submitting
|
||||||
|
this.isLoading = true
|
||||||
|
this.isLoadingDesc = 'Submitting extracted data'
|
||||||
|
|
||||||
|
const filesToUpload: UploadFile[] = []
|
||||||
|
|
||||||
|
for (const file of this.uploader.queue) {
|
||||||
|
filesToUpload.push({
|
||||||
|
file: file,
|
||||||
|
fileName: file.name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const csvContent =
|
||||||
|
Object.keys(this.xlData[0]).join(',') +
|
||||||
|
'\n' +
|
||||||
|
this.xlData
|
||||||
|
.slice(0, this.licenceState.value.submit_rows_limit)
|
||||||
|
.map((row: any) => Object.values(row).join(','))
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
const blob = new Blob([csvContent], { type: 'application/csv' })
|
||||||
|
const file: File = blobToFile(blob, this.filename + '.csv')
|
||||||
|
|
||||||
|
filesToUpload.push({
|
||||||
|
file: file,
|
||||||
|
fileName: file.name
|
||||||
|
})
|
||||||
|
|
||||||
|
const uploadUrl = 'services/editors/loadfile'
|
||||||
|
this.sasService
|
||||||
|
.uploadFile(uploadUrl, filesToUpload, {
|
||||||
|
table: this.selectedXLMap.targetDS
|
||||||
|
})
|
||||||
|
.then((res: any) => {
|
||||||
|
if (res.sasjsAbort) {
|
||||||
|
const abortRes = res
|
||||||
|
const abortMsg = abortRes.sasjsAbort[0].MSG
|
||||||
|
const macMsg = abortRes.sasjsAbort[0].MAC
|
||||||
|
|
||||||
|
this.eventService.showAbortModal('', abortMsg, {
|
||||||
|
SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT,
|
||||||
|
SYSERRORTEXT: abortRes.SYSERRORTEXT,
|
||||||
|
MAC: macMsg
|
||||||
|
})
|
||||||
|
} else if (res.sasparams) {
|
||||||
|
const params = res.sasparams[0]
|
||||||
|
const tableId = params.DSID
|
||||||
|
this.router.navigateByUrl('/stage/' + tableId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
this.eventService.catchResponseError('file upload', err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.status = Status.ReadyToSubmit
|
||||||
|
this.isLoading = false
|
||||||
|
this.isLoadingDesc = ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public extractData(wb: XLSX.WorkBook) {
|
||||||
|
const extractedData: XLUploadEntry[] = []
|
||||||
|
|
||||||
|
this.xlmapRules.forEach((rule) => {
|
||||||
|
let sheetName = rule.XLMAP_SHEET
|
||||||
|
// if sheet name is not an absolute name rather an index string like /1, /2, etc
|
||||||
|
// we extract the index and find absolute sheet name for specified index
|
||||||
|
if (sheetName.startsWith('/')) {
|
||||||
|
const temp = sheetName.split('/')[1]
|
||||||
|
const sheetIndex = parseInt(temp) - 1
|
||||||
|
sheetName = wb.SheetNames[sheetIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
const sheet = wb.Sheets[sheetName]
|
||||||
|
|
||||||
|
const arrayOfObjects = <any[]>XLSX.utils.sheet_to_json(sheet, {
|
||||||
|
raw: true,
|
||||||
|
header: 'A',
|
||||||
|
blankrows: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const start = getCellAddress(rule.XLMAP_START, arrayOfObjects)
|
||||||
|
const finish = getFinishingCell(start, rule.XLMAP_FINISH, arrayOfObjects)
|
||||||
|
|
||||||
|
const a1Range = `${start}:${finish}`
|
||||||
|
|
||||||
|
const range = XLSX.utils.decode_range(a1Range)
|
||||||
|
|
||||||
|
const rangedData = <any[]>XLSX.utils.sheet_to_json(sheet, {
|
||||||
|
raw: true,
|
||||||
|
range: a1Range,
|
||||||
|
header: 'A',
|
||||||
|
blankrows: true
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 0; i < rangedData.length; i++) {
|
||||||
|
const row = rangedData[i]
|
||||||
|
|
||||||
|
// `range.s.c` is the index of first column in the range
|
||||||
|
// `range.e.c` is the index of last column in the range
|
||||||
|
// we'll iterate from first column to last column and
|
||||||
|
// extract value where defined and push to extracted data array
|
||||||
|
for (let j = range.s.c, x = 0; j <= range.e.c; j++, x++) {
|
||||||
|
const col = XLSX.utils.encode_col(j)
|
||||||
|
|
||||||
|
if (col in row) {
|
||||||
|
// in excel's R1C1 notation indexing starts from 1 but in JS it starts from 0
|
||||||
|
// therefore, we'll have to add 1 to rows and cols
|
||||||
|
extractedData.push({
|
||||||
|
LOAD_REF: '0',
|
||||||
|
XLMAP_ID: rule.XLMAP_ID,
|
||||||
|
XLMAP_RANGE_ID: rule.XLMAP_RANGE_ID,
|
||||||
|
ROW_NO: i + 1,
|
||||||
|
COL_NO: x + 1,
|
||||||
|
VALUE_TXT: row[col]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.status = Status.ReadyToSubmit
|
||||||
|
this.isLoading = false
|
||||||
|
this.isLoadingDesc = ''
|
||||||
|
|
||||||
|
this.xlData = extractedData
|
||||||
|
this.selectedTab = Tabs.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
async viewXLMapRules() {
|
||||||
|
if (!this.selectedXLMap) return
|
||||||
|
|
||||||
|
this.isLoading = true
|
||||||
|
this.isLoadingDesc = 'Loading excel rules'
|
||||||
|
this.status = Status.FetchingRules
|
||||||
|
|
||||||
|
await this.sasStoreService
|
||||||
|
.getXLMapRules(this.selectedXLMap.id)
|
||||||
|
.then((res) => {
|
||||||
|
this.xlmapRules = res.xlmaprules
|
||||||
|
this.status = Status.ReadyToUpload
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.loggerService.error(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.isLoading = false
|
||||||
|
this.isLoadingDesc = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
private load() {
|
||||||
|
this.xlmaps = globals.xlmaps
|
||||||
|
this.xlmapsLoading = false
|
||||||
|
|
||||||
|
const id = this.route.snapshot.params['id']
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
const xlmapListItem = this.xlmaps.find((item) => item.id === id)
|
||||||
|
if (xlmapListItem) {
|
||||||
|
this.selectedXLMap = xlmapListItem
|
||||||
|
this.viewXLMapRules()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.licenceService.hot_license_key.subscribe(
|
||||||
|
(hot_license_key: string | undefined) => {
|
||||||
|
this.hotTableLicenseKey = hot_license_key
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterContentInit(): void {
|
||||||
|
if (globals.editor.startupSet) {
|
||||||
|
this.load()
|
||||||
|
} else {
|
||||||
|
this.eventService.onStartupDataLoaded.subscribe(() => {
|
||||||
|
this.load()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
client/src/app/xlmap/xlmap.module.ts
Normal file
31
client/src/app/xlmap/xlmap.module.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import { ClarityModule } from '@clr/angular'
|
||||||
|
import { HotTableModule } from '@handsontable/angular'
|
||||||
|
import { registerAllModules } from 'handsontable/registry'
|
||||||
|
import { AppSharedModule } from '../app-shared.module'
|
||||||
|
import { DirectivesModule } from '../directives/directives.module'
|
||||||
|
import { XLMapRouteComponent } from '../routes/xlmap-route/xlmap-route.component'
|
||||||
|
import { DcTreeModule } from '../shared/dc-tree/dc-tree.module'
|
||||||
|
import { XLMapRoutingModule } from './xlmap-routing.module'
|
||||||
|
import { XLMapComponent } from './xlmap.component'
|
||||||
|
|
||||||
|
// register Handsontable's modules
|
||||||
|
registerAllModules()
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [XLMapRouteComponent, XLMapComponent],
|
||||||
|
imports: [
|
||||||
|
HotTableModule,
|
||||||
|
XLMapRoutingModule,
|
||||||
|
FormsModule,
|
||||||
|
ClarityModule,
|
||||||
|
AppSharedModule,
|
||||||
|
CommonModule,
|
||||||
|
DcTreeModule,
|
||||||
|
DirectivesModule
|
||||||
|
],
|
||||||
|
exports: [XLMapComponent]
|
||||||
|
})
|
||||||
|
export class XLMapModule {}
|
@ -18,4 +18,4 @@ In any case, you must not make any such use of this software as to develop softw
|
|||||||
UNLESS EXPRESSLY AGREED OTHERWISE, 4GL APPS PROVIDES THIS SOFTWARE ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, AND IN NO EVENT AND UNDER NO LEGAL THEORY, SHALL 4GL APPS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER ARISING FROM USE OR INABILITY TO USE THIS SOFTWARE.
|
UNLESS EXPRESSLY AGREED OTHERWISE, 4GL APPS PROVIDES THIS SOFTWARE ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, AND IN NO EVENT AND UNDER NO LEGAL THEORY, SHALL 4GL APPS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER ARISING FROM USE OR INABILITY TO USE THIS SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
`
|
`
|
||||||
|
@ -288,8 +288,8 @@ function resolveDateString(data, ca, component, width, key) {
|
|||||||
resolved = hop.call(obj, width)
|
resolved = hop.call(obj, width)
|
||||||
? obj[width]
|
? obj[width]
|
||||||
: hop.call(obj, alts[width][0])
|
: hop.call(obj, alts[width][0])
|
||||||
? obj[alts[width][0]]
|
? obj[alts[width][0]]
|
||||||
: obj[alts[width][1]]
|
: obj[alts[width][1]]
|
||||||
|
|
||||||
// `key` wouldn't be specified for components 'dayPeriods'
|
// `key` wouldn't be specified for components 'dayPeriods'
|
||||||
return key != null ? resolved[key] : resolved
|
return key != null ? resolved[key] : resolved
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
/* You can add global styles to this file, and also import other style files */
|
||||||
@import '~handsontable/dist/handsontable.full.css';
|
@import '~handsontable/dist/handsontable.full.css';
|
||||||
|
|
||||||
@import "~@clr/ui/clr-ui.min.css";
|
@import '~@clr/ui/clr-ui.min.css';
|
||||||
@import "~@clr/icons/clr-icons.min.css";
|
@import '~@clr/icons/clr-icons.min.css';
|
||||||
|
|
||||||
@font-face{
|
@font-face {
|
||||||
font-family: text-security-disc;
|
font-family: text-security-disc;
|
||||||
src: url("https://raw.githubusercontent.com/noppa/text-security/master/dist/text-security-disc.woff");
|
src: url('https://raw.githubusercontent.com/noppa/text-security/master/dist/text-security-disc.woff');
|
||||||
}
|
}
|
||||||
|
|
||||||
body, html {
|
body,
|
||||||
font-weight: 400!important;
|
html {
|
||||||
|
font-weight: 400 !important;
|
||||||
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -29,7 +30,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Custom loading spinner
|
// Custom loading spinner
|
||||||
.slider{
|
.slider {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
margin-left: 75px;
|
margin-left: 75px;
|
||||||
@ -38,33 +39,45 @@ button {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line{
|
.line {
|
||||||
position:absolute;
|
position: absolute;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
background:#73D544;
|
background: #73d544;
|
||||||
width:150%;
|
width: 150%;
|
||||||
height:5px;
|
height: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subline{
|
.subline {
|
||||||
position:absolute;
|
position: absolute;
|
||||||
background:#73D544;
|
background: #73d544;
|
||||||
height:5px;
|
height: 5px;
|
||||||
}
|
}
|
||||||
.inc{
|
.inc {
|
||||||
animation: increase 2s infinite;
|
animation: increase 2s infinite;
|
||||||
}
|
}
|
||||||
.dec{
|
.dec {
|
||||||
animation: decrease 2s 0.5s infinite;
|
animation: decrease 2s 0.5s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes increase {
|
@keyframes increase {
|
||||||
from { left: -5%; width: 5%; }
|
from {
|
||||||
to { left: 130%; width: 100%;}
|
left: -5%;
|
||||||
|
width: 5%;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
left: 130%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@keyframes decrease {
|
@keyframes decrease {
|
||||||
from { left: -80%; width: 80%; }
|
from {
|
||||||
to { left: 110%; width: 10%;}
|
left: -80%;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
left: 110%;
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Custo loading spinner end
|
// Custo loading spinner end
|
||||||
|
|
||||||
@ -276,6 +289,10 @@ button {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mb-10-i {
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.mb-20 {
|
.mb-20 {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
@ -321,11 +338,11 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.color-dark-gray {
|
.color-dark-gray {
|
||||||
color: #495967
|
color: #495967;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-darker-gray{
|
.color-darker-gray {
|
||||||
color: #314351
|
color: #314351;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-white {
|
.color-white {
|
||||||
@ -333,7 +350,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.color-white-i {
|
.color-white-i {
|
||||||
color: white !important
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-green {
|
.color-green {
|
||||||
@ -341,15 +358,15 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.color-dc-green {
|
.color-dc-green {
|
||||||
color: #81b440
|
color: #81b440;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-red {
|
.color-red {
|
||||||
color: #e45454
|
color: #e45454;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-orange {
|
.color-orange {
|
||||||
color: #E67E22;
|
color: #e67e22;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-blue {
|
.color-blue {
|
||||||
@ -357,7 +374,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.color-yellow {
|
.color-yellow {
|
||||||
color: #f1c40f
|
color: #f1c40f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
@ -501,7 +518,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.z-index-highest {
|
.z-index-highest {
|
||||||
z-index: 10000000
|
z-index: 10000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical-align-middle {
|
.vertical-align-middle {
|
||||||
@ -519,35 +536,36 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.progresStatic {
|
.progresStatic {
|
||||||
margin-top:-6px!important;
|
margin-top: -6px !important;
|
||||||
position: absolute!important;
|
position: absolute !important;
|
||||||
z-index: 10000!important;
|
z-index: 10000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress, .progress-static {
|
.progress,
|
||||||
|
.progress-static {
|
||||||
background-color: #f5f6fe;
|
background-color: #f5f6fe;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
max-height: .583333rem;
|
max-height: 0.583333rem;
|
||||||
min-height: .166667rem;
|
min-height: 0.166667rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: block;
|
display: block;
|
||||||
width: calc(100% - 63px);
|
width: calc(100% - 63px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress.loop:after {
|
.progress.loop:after {
|
||||||
-webkit-animation: clr-progress-looper 1.5s ease-in-out infinite;
|
-webkit-animation: clr-progress-looper 1.5s ease-in-out infinite;
|
||||||
animation: clr-progress-looper 1.5s ease-in-out infinite;
|
animation: clr-progress-looper 1.5s ease-in-out infinite;
|
||||||
content: " ";
|
content: ' ';
|
||||||
top: .166667rem;
|
top: 0.166667rem;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
background-color: #60b515;
|
background-color: #60b515;
|
||||||
width: 75%;
|
width: 75%;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix for clarity bug, should be addressed when clarity is updated
|
// Fix for clarity bug, should be addressed when clarity is updated
|
||||||
@ -570,9 +588,9 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-app-level.alert-danger {
|
.alert-app-level.alert-danger {
|
||||||
background: #D94B2E;
|
background: #d94b2e;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
@ -581,7 +599,7 @@ button {
|
|||||||
|
|
||||||
.select select:focus {
|
.select select:focus {
|
||||||
border-bottom: 1px solid #495967;
|
border-bottom: 1px solid #495967;
|
||||||
background: linear-gradient(180deg,transparent 95%,#495a67 0) no-repeat;
|
background: linear-gradient(180deg, transparent 95%, #495a67 0) no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clr-treenode-children {
|
.clr-treenode-children {
|
||||||
@ -597,7 +615,9 @@ button {
|
|||||||
background: #d8e3e9;
|
background: #d8e3e9;
|
||||||
}
|
}
|
||||||
|
|
||||||
clr-select-container .clr-control-container, clr-select-container .clr-control-container .clr-select-wrapper, clr-select-container select {
|
clr-select-container .clr-control-container,
|
||||||
|
clr-select-container .clr-control-container .clr-select-wrapper,
|
||||||
|
clr-select-container select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -605,42 +625,46 @@ tbody {
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3, h4 {
|
h3,
|
||||||
color: #585858;
|
h4 {
|
||||||
font-weight: 400;
|
color: #585858;
|
||||||
letter-spacing: normal;
|
font-weight: 400;
|
||||||
line-height: 1rem;
|
letter-spacing: normal;
|
||||||
margin-top: 1rem;
|
line-height: 1rem;
|
||||||
margin-bottom: 0;
|
margin-top: 1rem;
|
||||||
/* text-transform: uppercase; */
|
margin-bottom: 0;
|
||||||
|
/* text-transform: uppercase; */
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2 {
|
h1,
|
||||||
color: #585858;
|
h2 {
|
||||||
font-weight: 400;
|
color: #585858;
|
||||||
/* font-family: Metropolis,Avenir Next,Helvetica Neue,Arial,sans-serif; */
|
font-weight: 400;
|
||||||
letter-spacing: normal;
|
/* font-family: Metropolis,Avenir Next,Helvetica Neue,Arial,sans-serif; */
|
||||||
line-height: 2rem;
|
letter-spacing: normal;
|
||||||
margin-top: 1rem;
|
line-height: 2rem;
|
||||||
margin-bottom: 0;
|
margin-top: 1rem;
|
||||||
/* text-transform: uppercase; */
|
margin-bottom: 0;
|
||||||
|
/* text-transform: uppercase; */
|
||||||
}
|
}
|
||||||
|
|
||||||
clr-icon.is-info {
|
clr-icon.is-info {
|
||||||
fill: #80b441;
|
fill: #80b441;
|
||||||
}
|
}
|
||||||
|
|
||||||
.datagrid-host, .datagrid-overlay-wrapper {
|
.datagrid-host,
|
||||||
display: -webkit-box;
|
.datagrid-overlay-wrapper {
|
||||||
display: -ms-flexbox;
|
display: -webkit-box;
|
||||||
display: -webkit-box!important;
|
display: -ms-flexbox;
|
||||||
-webkit-box-direction: normal;
|
display: -webkit-box !important;
|
||||||
|
-webkit-box-direction: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.btn-danger, .btn.btn-warning {
|
.btn.btn-danger,
|
||||||
border-color: #ef4f2e;
|
.btn.btn-warning {
|
||||||
background-color: #D94B2E;
|
border-color: #ef4f2e;
|
||||||
color: #fff;
|
background-color: #d94b2e;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.d-none {
|
.d-none {
|
||||||
@ -685,11 +709,16 @@ clr-icon.is-info {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.handsontable td.htInvalid {
|
.handsontable td.htInvalid {
|
||||||
background: #e62700ad!important;
|
background: #e62700ad !important;
|
||||||
border: 1px solid red !important;
|
border: 1px solid red !important;
|
||||||
color: #ffffff!important;
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
.margin-top-20{
|
|
||||||
|
.handsontable .numericListbox {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-top-20 {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
.hidden {
|
.hidden {
|
||||||
@ -823,7 +852,7 @@ clr-icon.is-info {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.datagrid-body {
|
.datagrid-body {
|
||||||
padding-bottom: 2rem!important;
|
padding-bottom: 2rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.abortMsg {
|
.abortMsg {
|
||||||
@ -831,16 +860,15 @@ clr-icon.is-info {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#graph svg {
|
#graph svg {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-table-selected {
|
.no-table-selected {
|
||||||
display:flex;
|
display: flex;
|
||||||
justify-content:center;
|
justify-content: center;
|
||||||
flex-direction:column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: white;
|
background: white;
|
||||||
@ -851,16 +879,15 @@ clr-icon.is-info {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.copyRight {
|
.copyRight {
|
||||||
background:#495967!important;
|
background: #495967 !important;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
display:flex !important;
|
display: flex !important;
|
||||||
justify-content:center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 5px 0px 4px 0px;
|
padding: 5px 0px 4px 0px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.nav-tree > clr-tree-node.clr-expanded {
|
.nav-tree > clr-tree-node.clr-expanded {
|
||||||
display: inline-block !important;
|
display: inline-block !important;
|
||||||
}
|
}
|
||||||
@ -903,13 +930,13 @@ clr-tree-node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tree-search-wrapper {
|
.tree-search-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
clr-input-container {
|
clr-input-container {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
clr-icon {
|
clr-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -956,7 +983,8 @@ input::-ms-clear {
|
|||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clr-treenode-content .clr-icon, .clr-treenode-content clr-icon {
|
.clr-treenode-content .clr-icon,
|
||||||
|
.clr-treenode-content clr-icon {
|
||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
min-height: 16px;
|
min-height: 16px;
|
||||||
}
|
}
|
||||||
@ -985,12 +1013,12 @@ input::-ms-clear {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.loadingSpinner {
|
.loadingSpinner {
|
||||||
height:70vh;
|
height: 70vh;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display:flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction:column;
|
flex-direction: column;
|
||||||
align-items:center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disable-password-manager {
|
.disable-password-manager {
|
||||||
@ -1025,7 +1053,8 @@ hr.light {
|
|||||||
position: relative;
|
position: relative;
|
||||||
min-width: 170px;
|
min-width: 170px;
|
||||||
|
|
||||||
clr-icon, .spinner {
|
clr-icon,
|
||||||
|
.spinner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 19px;
|
right: 19px;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
@ -1063,7 +1092,7 @@ hr.light {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Firefox */
|
/* Firefox */
|
||||||
input[type=number] {
|
input[type='number'] {
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1076,4 +1105,4 @@ hr.light {
|
|||||||
.link-it {
|
.link-it {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
5643
package-lock.json
generated
5643
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "dcfrontend",
|
"name": "dcfrontend",
|
||||||
"version": "6.0.0",
|
"version": "6.7.0",
|
||||||
"description": "Data Controller",
|
"description": "Data Controller",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@saithodev/semantic-release-gitea": "^2.1.0",
|
"@saithodev/semantic-release-gitea": "^2.1.0",
|
||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/commit-analyzer": "^10.0.1",
|
"@semantic-release/commit-analyzer": "^10.0.1",
|
||||||
|
"@semantic-release/npm": "11.0.0",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/git": "^10.0.1",
|
||||||
"@semantic-release/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.0.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install": "cd client && npm i && cd ../sas && npm i",
|
"install": "cd client && npm i && cd ../sas && npm i",
|
||||||
@ -23,5 +23,10 @@
|
|||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.datacontroller.io/dc/dc.git"
|
"url": "https://git.datacontroller.io/dc/dc.git"
|
||||||
}
|
},
|
||||||
|
"private": true,
|
||||||
|
"//": [
|
||||||
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user