init
This commit is contained in:
commit
f268de21a3
|
@ -0,0 +1,18 @@
|
|||
#!/bin/sh
|
||||
RED="\033[1;31m"
|
||||
GREEN="\033[1;32m"
|
||||
|
||||
# Get the commit message (the parameter we're given is just the path to the
|
||||
# temporary file which holds the message).
|
||||
commit_message=$(cat "$1")
|
||||
|
||||
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 \-]+\))?!?: .+$") then
|
||||
echo "${GREEN} ✔ Commit message meets Conventional Commit standards"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "${RED}❌ Commit message does not meet the Conventional Commit standard!"
|
||||
echo "An example of a valid message is:"
|
||||
echo " feat(login): add the 'remember me' button"
|
||||
echo "ℹ More details at: https://www.conventionalcommits.org/en/v1.0.0/#summary"
|
||||
exit 1
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Avoid commits to the master branch
|
||||
BRANCH=`git rev-parse --abbrev-ref HEAD`
|
||||
REGEX="^(master|development)$"
|
||||
|
||||
if [[ "$BRANCH" =~ $REGEX ]]; then
|
||||
echo "You are on branch $BRANCH. Are you sure you want to commit to this branch?"
|
||||
echo "If so, commit with -n to bypass the pre-commit hook."
|
||||
exit 1
|
||||
fi
|
|
@ -0,0 +1,38 @@
|
|||
name: Build
|
||||
run-name: Building and testing DC
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
Build-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: 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: npm run lint:check
|
||||
# Install dependencies~
|
||||
- run: npm ci --legacy-peer-deps
|
||||
# Audit should fail and stop the CI if critical vulnerability found
|
||||
- run: npm audit --audit-level=critical
|
||||
- run: |
|
||||
cd ./sas
|
||||
npm audit --audit-level=critical
|
||||
- run: |
|
||||
cd ./client
|
||||
npm audit --audit-level=critical
|
||||
npm test -- --no-watch --no-progress --browsers=ChromeHeadlessCI
|
||||
npm run postinstall
|
||||
npm run build
|
|
@ -0,0 +1,172 @@
|
|||
name: Test
|
||||
run-name: Building and testing development branch
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- development
|
||||
|
||||
jobs:
|
||||
Build-and-test-development:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: echo "$NPMRC" > client/.npmrc
|
||||
shell: bash
|
||||
env:
|
||||
NPMRC: ${{ secrets.NPMRC}}
|
||||
|
||||
- run: apt-get update
|
||||
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
- run: apt install -y ./google-chrome*.deb;
|
||||
- run: export CHROME_BIN=/usr/bin/google-chrome
|
||||
- run: apt-get update -y
|
||||
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
|
||||
- run: apt -y install jq
|
||||
# - run: 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
|
||||
# - run: eval $(ssh-agent -s)
|
||||
# - run: echo "$ssh_key" | tr -d '\r' | ssh-add -
|
||||
- name: Write cypress credentials
|
||||
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
||||
shell: bash
|
||||
env:
|
||||
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
||||
|
||||
# - run: mkdir -p ~/.ssh
|
||||
# - run: chmod 700 ~/.ssh
|
||||
- run: npm ci --legacy-peer-deps
|
||||
# Install pm2 and prepare SASJS server
|
||||
- run: npm i -g pm2
|
||||
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||
- run: unzip linux.zip
|
||||
- run: touch .env
|
||||
- run: echo RUN_TIMES=js >> .env
|
||||
- run: echo NODE_PATH=node >> .env
|
||||
- run: echo CORS=enable >> .env
|
||||
- run: echo WHITELIST=http://localhost:4200 >> .env
|
||||
# - run: echo "SERVER_URL=$server_url" >> .env
|
||||
# - run: echo "SERVER_TYPE=$server_type" >> .env
|
||||
# - run: echo "CLIENT=$client_sasjs" >> .env
|
||||
# - run: echo "ACCESS_TOKEN=$access_token_sasjs" >> .env
|
||||
# - run: echo "REFRESH_TOKEN=$refresh_token_sasjs" >> .env
|
||||
# - run: cat .env
|
||||
- run: pm2 start api-linux --wait-ready
|
||||
|
||||
- name: Deploy mocked services
|
||||
run: |
|
||||
cd ./sas/mocks/sasjs
|
||||
npm install --legacy-peer-deps -g @sasjs/cli
|
||||
npm install --legacy-peer-deps -g replace-in-files-cli
|
||||
sasjs cbd -t server-ci
|
||||
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
|
||||
|
||||
- name: Prepare frontend
|
||||
run: |
|
||||
cd ./client
|
||||
# mv ./cypress.env.example.json ./cypress.env.json
|
||||
# replace-in-files --regex='"username".*' --replacement='"username":"'$cypress_username_sasjs'",' ./cypress.env.json
|
||||
# replace-in-files --regex='"password".*' --replacement='"password":"'$cypress_pwd_sasjs'" ' ./cypress.env.json
|
||||
cat ./cypress.env.json
|
||||
npm run postinstall
|
||||
# Prepare index.html to SASJS local
|
||||
replace-in-files --regex='serverUrl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./src/index.html
|
||||
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/Public/app/devtest"' ./src/index.html
|
||||
replace-in-files --regex='serverType=".*?"' --replacement='serverType="SASJS"' ./src/index.html
|
||||
# Prepare and deploy SASJS version
|
||||
# replace-in-files --regex='serverurl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./dist/index.html
|
||||
# replace-in-files --regex='apploc=".*?"' --replacement='appLoc="/30.SASApps/app/devtest"' ./dist/index.html
|
||||
# replace-in-files --regex='servertype=".*?"' --replacement='serverType="SASJS"' ./dist/index.html
|
||||
# scp -o stricthostkeychecking=no -r ./dist/* dcgitlab@sas.4gl.io:/var/www/html/dcviya/development/newadapter
|
||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
||||
# replace-in-files --regex='"appLocation".*' --replacement='appLocation:"/dcviya/development/newadapter",' ./cypress.config.ts
|
||||
cat ./cypress.config.ts
|
||||
# Start frontend and run cypress
|
||||
npm start & npx wait-on http://localhost:4200 && ./run-cypress-tests.sh
|
||||
|
||||
Build-and-test-development-latest-adapter:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: echo "$NPMRC" > client/.npmrc
|
||||
shell: bash
|
||||
env:
|
||||
NPMRC: ${{ secrets.NPMRC}}
|
||||
|
||||
- run: apt-get update
|
||||
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
- run: apt install -y ./google-chrome*.deb;
|
||||
- run: export CHROME_BIN=/usr/bin/google-chrome
|
||||
- run: apt-get update -y
|
||||
- run: apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb
|
||||
- run: apt -y install jq
|
||||
# - run: 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
|
||||
# - run: eval $(ssh-agent -s)
|
||||
# - run: echo "$ssh_key" | tr -d '\r' | ssh-add -
|
||||
- name: Write cypress credentials
|
||||
run: echo "$CYPRESS_CREDS" > ./client/cypress.env.json
|
||||
shell: bash
|
||||
env:
|
||||
CYPRESS_CREDS: ${{ secrets.CYPRESS_CREDS }}
|
||||
|
||||
# - run: mkdir -p ~/.ssh
|
||||
# - run: chmod 700 ~/.ssh
|
||||
- run: npm ci --legacy-peer-deps
|
||||
# Install pm2 and prepare SASJS server
|
||||
- run: npm i -g pm2
|
||||
- run: curl -L https://github.com/sasjs/server/releases/latest/download/linux.zip > linux.zip
|
||||
- run: unzip linux.zip
|
||||
- run: touch .env
|
||||
- run: echo RUN_TIMES=js >> .env
|
||||
- run: echo NODE_PATH=node >> .env
|
||||
- run: echo CORS=enable >> .env
|
||||
- run: echo WHITELIST=http://localhost:4200 >> .env
|
||||
# - run: echo "SERVER_URL=$server_url" >> .env
|
||||
# - run: echo "SERVER_TYPE=$server_type" >> .env
|
||||
# - run: echo "CLIENT=$client_sasjs" >> .env
|
||||
# - run: echo "ACCESS_TOKEN=$access_token_sasjs" >> .env
|
||||
# - run: echo "REFRESH_TOKEN=$refresh_token_sasjs" >> .env
|
||||
# - run: cat .env
|
||||
- run: pm2 start api-linux --wait-ready
|
||||
|
||||
- name: Deploy mocked services
|
||||
run: |
|
||||
cd ./sas/mocks/sasjs
|
||||
npm install --legacy-peer-deps -g @sasjs/cli
|
||||
npm install --legacy-peer-deps -g replace-in-files-cli
|
||||
sasjs cbd -t server-ci
|
||||
# sasjs request services/admin/makedata -t server-ci -d ./deploy/makeData4GL.json -c ./deploy/requestConfig.json -o ./output.json
|
||||
|
||||
- name: Prepare frontend
|
||||
run: |
|
||||
cd ./client
|
||||
# mv ./cypress.env.example.json ./cypress.env.json
|
||||
# replace-in-files --regex='"username".*' --replacement='"username":"'$cypress_username_sasjs'",' ./cypress.env.json
|
||||
# replace-in-files --regex='"password".*' --replacement='"password":"'$cypress_pwd_sasjs'" ' ./cypress.env.json
|
||||
cat ./cypress.env.json
|
||||
npm run postinstall
|
||||
npm install --legacy-peer-deps @sasjs/adapter@latest
|
||||
# Prepare index.html to SASJS local
|
||||
replace-in-files --regex='serverUrl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./src/index.html
|
||||
replace-in-files --regex='appLoc=".*?"' --replacement='appLoc="/Public/app/devtest"' ./src/index.html
|
||||
replace-in-files --regex='serverType=".*?"' --replacement='serverType="SASJS"' ./src/index.html
|
||||
# Prepare and deploy SASJS version
|
||||
# replace-in-files --regex='serverurl=".*?"' --replacement='serverUrl="http://localhost:5000"' ./dist/index.html
|
||||
# replace-in-files --regex='apploc=".*?"' --replacement='appLoc="/30.SASApps/app/devtest"' ./dist/index.html
|
||||
# replace-in-files --regex='servertype=".*?"' --replacement='serverType="SASJS"' ./dist/index.html
|
||||
# scp -o stricthostkeychecking=no -r ./dist/* dcgitlab@sas.4gl.io:/var/www/html/dcviya/development/newadapter
|
||||
replace-in-files --regex='"hosturl".*' --replacement='hosturl:"http://localhost:4200",' ./cypress.config.ts
|
||||
# replace-in-files --regex='"appLocation".*' --replacement='appLocation:"/dcviya/development/newadapter",' ./cypress.config.ts
|
||||
cat ./cypress.config.ts
|
||||
# Start frontend and run cypress
|
||||
npm start & npx wait-on http://localhost:4200 && ./run-cypress-tests.sh
|
|
@ -0,0 +1,50 @@
|
|||
name: Release
|
||||
run-name: Releasing DC
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
release:
|
||||
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
|
||||
echo "legacy-peer-deps=true" >> client/.npmrc
|
||||
shell: bash
|
||||
env:
|
||||
NPMRC: ${{ secrets.NPMRC}}
|
||||
|
||||
- name: Install ZIP
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install zip
|
||||
|
||||
- name: release-build
|
||||
run: |
|
||||
cd client
|
||||
npm ci
|
||||
npm run build
|
||||
zip -r dist.zip ./dist
|
||||
|
||||
- name: Install Semantic Release and plugins
|
||||
run: |
|
||||
npm i
|
||||
npm i -g semantic-release
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
GITEA_TOKEN=${{ secrets.RELEASE_TOKEN }} GITEA_URL=https://git.datacontroller.io semantic-release
|
||||
|
||||
- name: Release Typedoc
|
||||
run: |
|
||||
cd client
|
||||
npm run typedoc
|
||||
# deploy docs
|
|
@ -0,0 +1,19 @@
|
|||
coverage/
|
||||
dist/
|
||||
node_modules/
|
||||
client/.angular/cache/
|
||||
**/*npm-debug.log.*
|
||||
**/*yarn-error.log.*
|
||||
.idea/
|
||||
.DS_Store
|
||||
client/src/environments/version.ts
|
||||
client/cypress/screenshots
|
||||
client/cypress/results
|
||||
client/cypress/videos
|
||||
cypress.env.json
|
||||
sasjsbuild
|
||||
sasjsresults
|
||||
.env*
|
||||
.sasjsrc
|
||||
client/.npmrc
|
||||
*~
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"endOfLine": "auto"
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"branches": ["main"],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
[
|
||||
"@semantic-release/git",
|
||||
{
|
||||
"assets": [
|
||||
"CHANGELOG.md"
|
||||
]
|
||||
}
|
||||
],
|
||||
["@saithodev/semantic-release-gitea", {
|
||||
"giteaUrl": "https://git.datacontroller.io",
|
||||
"assets": [
|
||||
{"path": "client/dist.zip"}
|
||||
]
|
||||
}]
|
||||
]
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"SYSERRORTEXT",
|
||||
"SYSWARNINGTEXT"
|
||||
],
|
||||
"editor.rulers": [
|
||||
80
|
||||
],
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"[markdown]": {
|
||||
"files.trimTrailingWhitespace": false
|
||||
},
|
||||
"workbench.colorCustomizations": {
|
||||
"titleBar.activeForeground": "#ebe8e8",
|
||||
"titleBar.activeBackground": "#95ff0053",
|
||||
},
|
||||
"terminal.integrated.wordSeparators": " ()[]{}',\"`─‘’"
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
pipeline:
|
||||
build:
|
||||
image: debian
|
||||
commands:
|
||||
- echo "This is the build step"
|
||||
a-test-step:
|
||||
image: debian
|
||||
commands:
|
||||
- echo "Testing.."
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,66 @@
|
|||
# Data Controller
|
||||
# Contributing
|
||||
|
||||
## Dependencies that requires licences
|
||||
|
||||
[SheetJS Pro Version](https://www.npmjs.com/package/sheetjs)
|
||||
|
||||
|
||||
To auth for SheetJS Pro version you need to use their private registry
|
||||
Add `client/.npmrc` file with following content:
|
||||
```
|
||||
@sheet:registry=https://pylon.sheetjs.com:54111/
|
||||
//pylon.sheetjs.com:54111/:_authToken="TOKEN-GOES-HERE"
|
||||
```
|
||||
|
||||
[Handsontable](https://www.npmjs.com/package/handsontable)
|
||||
|
||||
Licence should be inserted in the `client/src/index.html` file:
|
||||
|
||||
```
|
||||
<sasjs
|
||||
...
|
||||
hotLicenceKey="LICENCE-GOES-HERE"
|
||||
>
|
||||
</sasjs>
|
||||
```
|
||||
## Development
|
||||
Update `client/src/index.html` so that it points to your SAS9, SASVIYA or SASJS backend.
|
||||
Be aware that VIYA can be configured in such way that it would not work with cross origin frontend.
|
||||
Follow this guide to disable CORS: https://sasjs.io/cors/ (NOTICE: Sometimes even this approach would fail to work, in such case it is imposible to set it up without reconfiguring the VIYA server)
|
||||
|
||||
Start dev server:
|
||||
```
|
||||
cd client
|
||||
|
||||
npm start
|
||||
```
|
||||
|
||||
## GUI Elements
|
||||
|
||||
For documentation on the Clarity Design System, including a list of components and example usage, see [our website](https://vmware.github.io/clarity).
|
||||
|
||||
## Code style
|
||||
Run prettier fix:
|
||||
```bash
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
## Generate docs
|
||||
Typedoc is used for generating typescript documentation based on the code.
|
||||
That part is automated and beign done as a part of CI job.
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## Makedata service "could not create directory" error
|
||||
|
||||
The dcpath folder should have its permissions set so that the system account (SYSUSERID) can both read and write to it.
|
||||
|
||||
Example:
|
||||
|
||||
If dcpath is: '/tmp/dc'
|
||||
|
||||
Run:
|
||||
```
|
||||
chmod 777 /tmp/dc
|
||||
```
|
|
@ -0,0 +1,27 @@
|
|||
Licence Agreement for Data Controller for SAS®
|
||||
====================
|
||||
|
||||
Copyright (c) Bowe IO Ltd
|
||||
|
||||
Data Controller is a software distributed by 4GL Apps, a brand owned by Bowe IO Ltd, a UK Limited Company headquarted in 29 Oldfield Rd, Cumbria, registered by companies house under number 08777171, VAT number: 203914240
|
||||
|
||||
This software is protected by applicable copyright laws, including international treaties, and dual-
|
||||
licensed – depending on whether your use for commercial purposes, meaning intended for or
|
||||
resulting in commercial advantage or monetary compensation, or not.
|
||||
|
||||
If your use is strictly personal or solely for evaluation purposes, meaning for the purposes of testing
|
||||
the suitability, performance, and usefulness of this software outside the production environment,
|
||||
you agree to be bound by the terms included in the "licence-non-commercial-datacontroller.md" file.
|
||||
|
||||
Your use of this software for commercial purposes is subject to the terms included in an applicable
|
||||
license agreement.
|
||||
|
||||
In any case, you must not make any such use of this software as to develop software which may be
|
||||
considered competitive with this software.
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
.vscode/
|
||||
coverage/
|
||||
docs/
|
||||
html-report/
|
||||
node_modules/
|
||||
typings/
|
||||
**/*npm-debug.log.*
|
||||
**/*yarn-error.log.*
|
||||
.idea/
|
||||
.DS_Store
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
.env*
|
||||
.sasjsrc
|
|
@ -0,0 +1,16 @@
|
|||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"root": true,
|
||||
"ignorePatterns": [
|
||||
"projects/**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@angular-eslint/recommended",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "app",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "app",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/template/recommended"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": [
|
||||
"development"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"search.exclude": {
|
||||
"**/sasjsbuild/**": true,
|
||||
"**/dist/**":true
|
||||
},
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 2,
|
||||
"trim_trailing_whitespace": true
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"name": "Launch Chrome",
|
||||
"port": 9222,
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"editor.rulers": [
|
||||
107
|
||||
]
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
FROM node:14-alpine as builder
|
||||
WORKDIR '/app'
|
||||
COPY ./package.json ./
|
||||
COPY ./package-lock.json ./
|
||||
COPY ./.npmrc ./
|
||||
RUN npm i
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx
|
||||
EXPOSE 3000
|
||||
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
|
@ -0,0 +1,8 @@
|
|||
FROM node:14-alpine
|
||||
WORKDIR '/app'
|
||||
COPY ./package.json ./
|
||||
COPY ./package-lock.json ./
|
||||
COPY ./.npmrc ./
|
||||
RUN npm i
|
||||
COPY . .
|
||||
CMD ["npm", "run", "start"]
|
|
@ -0,0 +1,169 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"cli": {
|
||||
"analytics": false
|
||||
},
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"datacontroller": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
},
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"allowedCommonJsDependencies": [
|
||||
"handsontable",
|
||||
"core-js",
|
||||
"pikaday",
|
||||
"querystring",
|
||||
"punycode",
|
||||
"url",
|
||||
"rxjs",
|
||||
"rxjs-compat",
|
||||
"d3-graphviz",
|
||||
"save-svg-as-png",
|
||||
"@sheet/perf",
|
||||
"@sheet/crypto",
|
||||
"iconv-lite",
|
||||
"buffer/",
|
||||
"zone.js",
|
||||
"text-encoding",
|
||||
"crypto-js/md5",
|
||||
"buffer",
|
||||
"numbro",
|
||||
"@clr/icons",
|
||||
"@sasjs/adapter",
|
||||
"@sasjs/utils/input/validators",
|
||||
"@sasjs/utils/utils/bytesToSize",
|
||||
"base64-arraybuffer",
|
||||
"@handsontable/formulajs"
|
||||
],
|
||||
"polyfills": [
|
||||
"src/polyfills.ts",
|
||||
"zone.js"
|
||||
],
|
||||
"outputPath": "dist",
|
||||
"resourcesOutputPath": "images",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/images"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [
|
||||
"node_modules/@clr/icons/clr-icons.min.js",
|
||||
"node_modules/marked/marked.min.js"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "13mb",
|
||||
"maximumError": "15mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "7kb",
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all",
|
||||
"optimization": {
|
||||
"scripts": true,
|
||||
"styles": {
|
||||
"minify": true,
|
||||
"inlineCritical": false
|
||||
},
|
||||
"fonts": true
|
||||
}
|
||||
},
|
||||
"development": {
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"buildOptimizer": false,
|
||||
"sourceMap": true,
|
||||
"optimization": false,
|
||||
"namedChunks": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "datacontroller:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "datacontroller:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "datacontroller:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"codeCoverage": true,
|
||||
"polyfills": [
|
||||
"src/polyfills.ts",
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": [
|
||||
|
||||
],
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"karmaConfig": "karma.conf.js"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
reporter: 'mochawesome',
|
||||
reporterOptions: {
|
||||
reportDir: 'cypress/results',
|
||||
overwrite: false,
|
||||
html: true,
|
||||
json: false,
|
||||
},
|
||||
chromeWebSecurity: false,
|
||||
defaultCommandTimeout: 30000,
|
||||
env: {
|
||||
hosturl:"http://localhost:4200",
|
||||
appLocation: "",
|
||||
site_id_SAS9: "70221618",
|
||||
site_id_SASVIYA: "70253615",
|
||||
site_id_SASJS: "123",
|
||||
serverType: "SASJS",
|
||||
libraryToOpenIncludes_SASVIYA: "viya",
|
||||
libraryToOpenIncludes_SAS9: "dc",
|
||||
libraryToOpenIncludes_SASJS: "dc",
|
||||
debug: false,
|
||||
screenshotOnRunFailure: false,
|
||||
longerCommandTimeout: 50000,
|
||||
testLicenceUserLimits: false
|
||||
}
|
||||
})
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"username": "sas_username",
|
||||
"password": "sas_password"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,255 @@
|
|||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
const fixturePath = 'excels_general/'
|
||||
const downloadsFolder = Cypress.config('downloadsFolder')
|
||||
|
||||
import { deleteDownloadsFolder } from '../util/deleteDownloadFolder'
|
||||
|
||||
context('download files test: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
cy.loginAndUpdateValidKey()
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
cy.get('input.username').type(username)
|
||||
cy.get('input.password').type(password)
|
||||
cy.get('.login-group button').click()
|
||||
|
||||
visitPage('home')
|
||||
})
|
||||
|
||||
this.afterEach(() => {
|
||||
deleteDownloadsFolder()
|
||||
})
|
||||
|
||||
it('1 | downloads audit file', (done) => {
|
||||
visitPage('approve/toapprove')
|
||||
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.btn.btn-success')
|
||||
.should('be.visible')
|
||||
.then((buttons) => {
|
||||
buttons[0].click()
|
||||
const id = buttons[0].id
|
||||
|
||||
checkForFileDownloaded(id, 'zip', () => done())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('2 | downloads viewer csv', (done) => {
|
||||
visitPage('view/data')
|
||||
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openDownloadModal(() => {
|
||||
cy.get('select')
|
||||
.select('CSV')
|
||||
.then(() => {
|
||||
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
|
||||
button.trigger('click')
|
||||
|
||||
const id = button[0].id
|
||||
|
||||
checkForFileDownloaded(id, 'csv', () => done())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('3 | downloads viewer excel', (done) => {
|
||||
visitPage('view/data')
|
||||
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openDownloadModal(() => {
|
||||
cy.get('select')
|
||||
.select('Excel')
|
||||
.then(() => {
|
||||
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
|
||||
button.trigger('click')
|
||||
|
||||
const id = button[0].id
|
||||
|
||||
checkForFileDownloaded(id, 'xlsx', () => done())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('4 | downloads viewer SAS Datalines', (done) => {
|
||||
visitPage('view/data')
|
||||
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openDownloadModal(() => {
|
||||
cy.get('select')
|
||||
.select('SAS Datalines')
|
||||
.then(() => {
|
||||
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
|
||||
button.trigger('click')
|
||||
|
||||
const id = button[0].id
|
||||
|
||||
checkForFileDownloaded(id, 'sas', () => done())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('5 | downloads viewer SAS DDL', (done) => {
|
||||
visitPage('view/data')
|
||||
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openDownloadModal(() => {
|
||||
cy.get('select')
|
||||
.select('SAS DDL')
|
||||
.then(() => {
|
||||
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
|
||||
button.trigger('click')
|
||||
|
||||
const id = button[0].id
|
||||
|
||||
checkForFileDownloaded(id, 'ddl', () => done(), '_')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('6 | downloads viewer TSQL DDL', (done) => {
|
||||
visitPage('view/data')
|
||||
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openDownloadModal(() => {
|
||||
cy.get('select')
|
||||
.select('TSQL DDL')
|
||||
.then(() => {
|
||||
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
|
||||
button.trigger('click')
|
||||
|
||||
const id = button[0].id
|
||||
|
||||
checkForFileDownloaded(id, 'ddl', () => done(), '_')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('7 | downloads viewer PGSQL DDL', (done) => {
|
||||
visitPage('view/data')
|
||||
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openDownloadModal(() => {
|
||||
cy.get('select')
|
||||
.select('PGSQL DDL')
|
||||
.then(() => {
|
||||
cy.get('.btn.btn-sm.btn-success-outline').then((button) => {
|
||||
button.trigger('click')
|
||||
|
||||
const id = button[0].id
|
||||
|
||||
checkForFileDownloaded(id, 'ddl', () => done(), '_')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
this.afterEach(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
})
|
||||
|
||||
this.afterAll(() => {
|
||||
cy.visit(`https://sas.4gl.io/mihmed/cypress_finish`)
|
||||
})
|
||||
})
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
|
||||
const checkForFileDownloaded = (
|
||||
id: string,
|
||||
extension: string,
|
||||
callback?: any,
|
||||
libDivider: string = '.'
|
||||
) => {
|
||||
cy.on('url:changed', (newUrl) => {
|
||||
console.log('newUrl', newUrl)
|
||||
})
|
||||
|
||||
id = id.replace('.', libDivider)
|
||||
|
||||
const filename = downloadsFolder + '/' + id + '.' + extension
|
||||
// browser might take a while to download the file,
|
||||
// so use "cy.readFile" to retry until the file exists
|
||||
// and has length - and we assume that it has finished downloading then
|
||||
cy.readFile(filename, { timeout: longerCommandTimeout })
|
||||
.should('have.length.gt', 10)
|
||||
.then((file) => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const openDownloadModal = (callback?: any) => {
|
||||
cy.get('.btn.btn-sm.btn-outline.filterSide.dropdown-toggle')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('clr-dropdown-menu button').then((buttons) => {
|
||||
for (let button of buttons) {
|
||||
if (button.innerText.toLowerCase().includes('download')) {
|
||||
button.click()
|
||||
|
||||
if (callback) callback()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let viyaLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
||||
viyaLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
console.log('viyaLib', viyaLib)
|
||||
|
||||
cy.get(viyaLib).within(() => {
|
||||
cy.get(
|
||||
'.clr-tree-node-content-container .clr-treenode-content p'
|
||||
).click()
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,257 @@
|
|||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
const fixturePath = 'excels_general/'
|
||||
|
||||
context('editor tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
cy.loginAndUpdateValidKey()
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
// cy.get('input.username').type(username)
|
||||
// cy.get('input.password').type(password)
|
||||
// cy.get('.login-group button').click()
|
||||
|
||||
visitPage('home')
|
||||
})
|
||||
|
||||
it('1 | Submits duplicate primary keys', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
|
||||
|
||||
attachExcelFile('MPE_DATADICTIONARY_duplicate_keys.xlsx', () => {
|
||||
clickOnUploadPreview(() => {
|
||||
confirmEditPreviewFile(() => {
|
||||
submitTable(() => {
|
||||
cy.get('.modal-body').then((modalBody: any) => {
|
||||
if (modalBody[0].innerText.includes(`Duplicates found:`)) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('2 | Submits null cells which must not be null', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
clickOnEdit(() => {
|
||||
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then(() => {
|
||||
cy.get('.ht_master tbody tr').then((rows: any) => {
|
||||
cy.get(rows[1].childNodes[2])
|
||||
.dblclick({ force: true })
|
||||
.then(() => {
|
||||
cy.focused()
|
||||
.clear()
|
||||
.type('{enter}')
|
||||
.then(() => {
|
||||
submitTable(() => {
|
||||
cy.get('.modal-body').then((modalBody: any) => {
|
||||
if (
|
||||
modalBody[0].innerHTML
|
||||
.toLowerCase()
|
||||
.includes(`invalid values are present`)
|
||||
) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('3 | Gets basic dynamic cell validation', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
clickOnEdit(() => {
|
||||
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then(() => {
|
||||
cy.get('.ht_master tbody tr').then((rows: any) => {
|
||||
cy.get(rows[1].childNodes[5])
|
||||
.click({ force: true })
|
||||
.then(($td) => {
|
||||
cy.get('.htAutocompleteArrow', { withinSubject: $td }).should(
|
||||
'exist'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('4 | Gets advanced dynamic cell validation', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_tables')
|
||||
|
||||
clickOnEdit(() => {
|
||||
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then(() => {
|
||||
cy.get('.ht_master tbody tr').then((rows: any) => {
|
||||
cy.get(rows[1].childNodes[3])
|
||||
.click({ force: true })
|
||||
.then(($td) => {
|
||||
cy.get('.htAutocompleteArrow', { withinSubject: $td }).should(
|
||||
'exist'
|
||||
)
|
||||
cy.get('.htAutocompleteArrow', {
|
||||
withinSubject: rows[1].childNodes[7]
|
||||
}).should('exist')
|
||||
cy.get('.htAutocompleteArrow', {
|
||||
withinSubject: rows[1].childNodes[8]
|
||||
}).should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
this.afterEach(() => {
|
||||
// cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
})
|
||||
})
|
||||
|
||||
const clickOnEdit = (callback?: any) => {
|
||||
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let viyaLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
||||
viyaLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get(viyaLib).within(() => {
|
||||
cy.get('.clr-tree-node-content-container > button').click()
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const attachExcelFile = (excelFilename: string, callback?: any) => {
|
||||
cy.get('.buttonBar button:last-child')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('input[type="file"]#file-upload')
|
||||
.attachFile(`/${fixturePath}/${excelFilename}`)
|
||||
.then(() => {
|
||||
cy.get('.modal-footer .btn.btn-primary').then((modalBtn) => {
|
||||
modalBtn.click()
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const clickOnUploadPreview = (callback?: any) => {
|
||||
cy.get('.buttonBar button.btn-primary.btn-upload-preview')
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const confirmEditPreviewFile = (callback?: any) => {
|
||||
cy.get('.modal-footer button.btn-success-outline')
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const submitTable = (callback?: any) => {
|
||||
cy.get('.btnCtrl button.btn-primary')
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const submitTableMessage = (callback?: any) => {
|
||||
cy.get('.modal-footer .btn.btn-sm.btn-success-outline')
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const submitExcel = (callback?: any) => {
|
||||
cy.get('.buttonBar button.preview-submit')
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const rejectExcel = (callback?: any) => {
|
||||
cy.get('button', { timeout: longerCommandTimeout })
|
||||
.should('contain', 'Go to approvals screen')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('go to approvals screen')
|
||||
) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get('button.btn-danger')
|
||||
.should('exist')
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.modal-footer button.btn-success-outline')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('app-history')
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
|
@ -0,0 +1,539 @@
|
|||
import { Callbacks } from 'cypress/types/jquery/index'
|
||||
|
||||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
const fixturePath = 'excels/'
|
||||
|
||||
// TODO: 4 and 9 failing
|
||||
|
||||
context('excel tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
cy.loginAndUpdateValidKey()
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
// cy.get('input.username').type(username)
|
||||
// cy.get('input.password').type(password)
|
||||
// cy.get('.login-group button').click()
|
||||
|
||||
visitPage('home')
|
||||
|
||||
colorLog(
|
||||
`TEST START ---> ${
|
||||
Cypress.mocha.getRunner().suite.ctx.currentTest.title
|
||||
}`,
|
||||
'#3498DB'
|
||||
)
|
||||
})
|
||||
|
||||
it('1 | Uploads regular Excel file', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('regular_excel.xlsx', () => {
|
||||
submitExcel()
|
||||
rejectExcel(done)
|
||||
})
|
||||
})
|
||||
|
||||
it('2 | Uploads Excel with data on the 7th tab', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('7th_tab_excel.xlsx', () => {
|
||||
submitExcel()
|
||||
rejectExcel(done)
|
||||
})
|
||||
})
|
||||
|
||||
it('3 | Uploads Excel with missing columns (should fail)', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('missing_columns_excel.xlsx', () => {
|
||||
cy.get('.abortMsg', { timeout: longerCommandTimeout })
|
||||
.should('exist')
|
||||
.then((elements: any) => {
|
||||
if (elements[0]) {
|
||||
if (elements[0].innerText.toLowerCase().includes('missing')) done()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('4 | Uploads Excel with formulas', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
|
||||
|
||||
attachExcelFile('formulas_excel.xlsx', () => {
|
||||
checkResultOfFormulaUpload(done)
|
||||
})
|
||||
})
|
||||
|
||||
it('5 | Uploads Excel with no data rows', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('nodata_rows_excel.xlsx', () => {
|
||||
cy.get('.abortMsg', { timeout: longerCommandTimeout })
|
||||
.should('exist')
|
||||
.then((elements: any) => {
|
||||
if (elements[0]) {
|
||||
if (
|
||||
elements[0].innerText
|
||||
.toLowerCase()
|
||||
.includes('no relevant data found')
|
||||
)
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('6 | Uploads Excel with a table that is surrounded by other data', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('surrounded_data_excel.xlsx', () => {
|
||||
submitExcel()
|
||||
rejectExcel(done)
|
||||
})
|
||||
})
|
||||
|
||||
it('7 | Uploads Excel with a extra columns in the middle', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('extra_column_excel.xlsx', () => {
|
||||
submitExcel()
|
||||
rejectExcel(done)
|
||||
})
|
||||
})
|
||||
|
||||
it('8 | Uploads Excel with a duplicate column', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('duplicate_column_excel.xlsx', () => {
|
||||
cy.get('.abortMsg', { timeout: longerCommandTimeout })
|
||||
.should('exist')
|
||||
.then((elements: any) => {
|
||||
if (elements[0]) {
|
||||
if (elements[0].innerText.toLowerCase().includes('missing')) done()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// it('9 | Uploads Excel with a duplicate row', (done) => {
|
||||
// openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
// attachExcelFile('duplicate_row_excel.xlsx', () => {
|
||||
// submitExcel(() => {
|
||||
// cy.get('.abortMsg', { timeout: longerCommandTimeout })
|
||||
// .should('exist')
|
||||
// .then((elements: any) => {
|
||||
// if (elements[0]) {
|
||||
// if (elements[0].innerText.toLowerCase().includes('duplicates'))
|
||||
// done()
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
|
||||
it('10 | Uploads Excel with a mixed content', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('mixed_content_excel.xlsx', () => {
|
||||
submitExcel(() => {
|
||||
cy.get('.modal-body').then((modalBody: any) => {
|
||||
if (
|
||||
modalBody[0].innerHTML
|
||||
.toLowerCase()
|
||||
.includes(`invalid values are present`)
|
||||
) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('11 | Uploads Excel with a blank columns', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('blank_columns_excel.xlsx', () => {
|
||||
cy.get('.abortMsg', { timeout: longerCommandTimeout })
|
||||
.should('exist')
|
||||
.then((elements: any) => {
|
||||
if (elements[0]) {
|
||||
if (elements[0].innerText.toLowerCase().includes('missing')) done()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('12 | Uploads Excel xls extension', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('regular_excel_xls.xls', () => {
|
||||
submitExcel()
|
||||
rejectExcel(done)
|
||||
})
|
||||
})
|
||||
|
||||
// For some strange reason this file breaks cypress. When uploaded manually in DC it is working.
|
||||
// it('13 | Uploads Excel xlsm extension', (done) => {
|
||||
// openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
// attachExcelFile('regular_excel_macro.xlsm', () => {
|
||||
// submitExcel()
|
||||
// rejectExcel(done)
|
||||
// })
|
||||
// })
|
||||
|
||||
it('14 | Uploads Excel with composite primary key', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
|
||||
|
||||
attachExcelFile('MPE_DATADICTIONARY_composite_keys.xlsx', () => {
|
||||
submitExcel()
|
||||
rejectExcel(done)
|
||||
})
|
||||
})
|
||||
|
||||
it('15 | Uploads Excel with missing row (empty table)', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
|
||||
|
||||
attachExcelFile('MPE_DATADICTIONARY_missing_row.xlsx', () => {
|
||||
cy.get('.abortMsg', { timeout: longerCommandTimeout })
|
||||
.should('exist')
|
||||
.then((elements: any) => {
|
||||
if (elements[0]) {
|
||||
if (
|
||||
elements[0].innerText
|
||||
.toLowerCase()
|
||||
.includes('no relevant data found')
|
||||
)
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('16 | Uploads Excel with merged cells', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_datadictionary')
|
||||
|
||||
attachExcelFile('MPE_DATADICTIONARY_merged_cells.xlsx', () => {
|
||||
submitExcel()
|
||||
rejectExcel(done)
|
||||
})
|
||||
})
|
||||
|
||||
it('17 | Check uploaded values from excel with xls extension', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('regular_excel_xls.xls', () => {
|
||||
checkResultOfXLSUpload(done)
|
||||
})
|
||||
})
|
||||
|
||||
it('18 | Uploads Excel with missing row (empty table)', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('blank_column_with_header.xlsx', () => {
|
||||
cy.get('.btn-upload-preview', { timeout: 60000 })
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
cy.get('#hotInstance', { timeout: 30000 })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
.find('div.wtSpreader')
|
||||
.find('table.htCore')
|
||||
.find('tbody')
|
||||
.should((data) => {
|
||||
let allEmpty = true
|
||||
|
||||
for (let col = 0; col < data[0].children.length; col++) {
|
||||
const cell: any = data[0].children[col].children[5]
|
||||
|
||||
if (cell.innerText !== '') {
|
||||
allEmpty = false
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (allEmpty) done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('19 | Uploads Excel with data on random sheet surrounded with all empty cells', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('surrounded_data_all_cells_empty_excel.xlsx', () => {
|
||||
checkResultOfXLSUpload(done)
|
||||
})
|
||||
})
|
||||
|
||||
it('20 | Uploads Excel with data surrounded with empty cells ', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('surrounded_data_empty_cells_excel.xlsx', () => {
|
||||
checkResultOfXLSUpload(done)
|
||||
})
|
||||
})
|
||||
|
||||
it('21 | Uploads regular Excel file with first row marked for Delete (yes)', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
attachExcelFile('regular_excel_with_delete.xlsx', () => {
|
||||
cy.get('.btn-upload-preview', { timeout: 60000 })
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
cy.get('#hotInstance', { timeout: 30000 })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
.find('div.wtSpreader')
|
||||
.find('table.htCore')
|
||||
.find('tbody')
|
||||
.should((data: JQuery<HTMLTableSectionElement>) => {
|
||||
const firstRowFirstCol: Partial<HTMLElement> =
|
||||
data[0].children[0].children[1]
|
||||
|
||||
if (
|
||||
firstRowFirstCol.innerText &&
|
||||
!firstRowFirstCol.innerText.toLowerCase().includes('yes')
|
||||
) {
|
||||
done('Delete? column from file not applied')
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
submitExcel()
|
||||
rejectExcel(done)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Large files break Cypress
|
||||
|
||||
// it ('? | Uploads Excel with size of 5MB', (done) => {
|
||||
// attachExcelFile('5mb_excel.xlsx', () => {
|
||||
// submitExcel();
|
||||
// rejectExcel(done);
|
||||
// });
|
||||
// })
|
||||
|
||||
// it ('? | Uploads Excel with size of 15MB', (done) => {
|
||||
// attachExcelFile('15mb_excel.xlsx', () => {
|
||||
// submitExcel();
|
||||
// rejectExcel(done);
|
||||
// });
|
||||
// })
|
||||
|
||||
//Large files tests end
|
||||
|
||||
this.afterEach(() => {
|
||||
colorLog(`TEST END -------------`, '#3498DB')
|
||||
// cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
})
|
||||
})
|
||||
|
||||
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let viyaLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
||||
viyaLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get(viyaLib).within(() => {
|
||||
cy.get('.clr-tree-node-content-container > button').click()
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const attachExcelFile = (excelFilename: string, callback?: any) => {
|
||||
cy.get('.buttonBar button:last-child')
|
||||
.should('exist')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('input[type="file"]#file-upload')
|
||||
.attachFile(`/${fixturePath}/${excelFilename}`)
|
||||
.then(() => {
|
||||
cy.get('.clr-abort-modal .modal-title').then((modalTitle) => {
|
||||
if (!modalTitle[0].innerHTML.includes('Abort Message')) {
|
||||
cy.get('.modal-footer .btn.btn-primary').then((modalBtn) => {
|
||||
modalBtn.click()
|
||||
if (callback) callback()
|
||||
})
|
||||
} else {
|
||||
if (callback) callback()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const submitExcel = (callback?: any) => {
|
||||
cy.get('.buttonBar button.preview-submit', { timeout: longerCommandTimeout })
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const rejectExcel = (callback?: any) => {
|
||||
cy.get('button', { timeout: longerCommandTimeout })
|
||||
.should('contain', 'Go to approvals screen')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('go to approvals screen')
|
||||
) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get('button.btn-danger')
|
||||
.should('exist')
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.modal-footer button.btn-success-outline')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('app-history')
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const acceptExcel = (callback?: any) => {
|
||||
cy.get('button', { timeout: longerCommandTimeout })
|
||||
.should('contain', 'Go to approvals screen')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('go to approvals screen')
|
||||
) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get('#acceptBtn')
|
||||
.should('exist')
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const checkResultOfFormulaUpload = (callback?: any) => {
|
||||
cy.get('#hotInstance', { timeout: longerCommandTimeout })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
.find('div.wtSpreader')
|
||||
.find('table.htCore')
|
||||
.find('tbody')
|
||||
.should((data) => {
|
||||
const cell: any = data[0].children[0].children[5]
|
||||
expect(cell.innerText).to.equal('=1+1')
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const checkResultOfXLSUpload = (callback?: any) => {
|
||||
cy.viewport(1280, 720)
|
||||
cy.get('#hotInstance', { timeout: 30000 })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.find('div.wtHider')
|
||||
.find('div.wtSpreader')
|
||||
.find('table.htCore')
|
||||
.find('tbody')
|
||||
.should((data) => {
|
||||
let cell: any = data[0].children[0].children[2]
|
||||
|
||||
expect(cell.innerText).to.equal('0')
|
||||
cell = data[0].children[0].children[3]
|
||||
expect(cell.innerText).to.equal('this is dummy data changed in excel')
|
||||
cell = data[0].children[0].children[4]
|
||||
expect(cell.innerText).to.equal('▼\nOption 1')
|
||||
cell = data[0].children[0].children[5]
|
||||
expect(cell.innerText).to.equal('42')
|
||||
cell = data[0].children[0].children[6]
|
||||
expect(cell.innerText).to.equal('▼\n1960-02-12')
|
||||
cell = data[0].children[0].children[7]
|
||||
expect(cell.innerText).to.equal('▼\n1960-01-01 00:00:42')
|
||||
cell = data[0].children[0].children[8]
|
||||
expect(cell.innerText).to.equal('00:00:42')
|
||||
cell = data[0].children[0].children[9]
|
||||
expect(cell.innerText).to.equal('3')
|
||||
|
||||
if (callback) callback()
|
||||
})
|
||||
|
||||
cy.get('#hotInstance', { timeout: 30000 })
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.scrollTo('right')
|
||||
.find('div.wtHider')
|
||||
.find('div.wtSpreader')
|
||||
.find('table.htCore')
|
||||
.find('tbody')
|
||||
.should((data) => {
|
||||
let cell: any = data[0].children[0].children[1]
|
||||
|
||||
cell = data[0].children[0].children[9]
|
||||
|
||||
expect(cell.innerText).to.equal('44')
|
||||
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
|
||||
const colorLog = (msg: string, color: string) => {
|
||||
console.log('%c' + msg, 'color:' + color + ';font-weight:bold;')
|
||||
}
|
|
@ -0,0 +1,383 @@
|
|||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
const fixturePath = 'excels_general/'
|
||||
|
||||
context('filtering tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`, { timeout: longerCommandTimeout })
|
||||
cy.loginAndUpdateValidKey()
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation, { timeout: longerCommandTimeout })
|
||||
// cy.get('input.username').type(username)
|
||||
// cy.get('input.password').type(password)
|
||||
// cy.get('.login-group button').click()
|
||||
|
||||
visitPage('home')
|
||||
})
|
||||
|
||||
it('1 | filter char field', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openFilterPopup(() => {
|
||||
setFilterWithValue('SOME_CHAR', 'this is dummy data', 'value', () => {
|
||||
checkInfoBarIncludes(
|
||||
`AND,AND,0,SOME_CHAR,=,"'this is dummy data'"`,
|
||||
(includes: boolean) => {
|
||||
if (includes) done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('2 | filter number field', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openFilterPopup(() => {
|
||||
setFilterWithValue('SOME_NUM', '42', 'value', () => {
|
||||
checkInfoBarIncludes(`AND,AND,0,SOME_NUM,=,42`, (includes: boolean) => {
|
||||
if (includes) done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it.only('3 | filter time field', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openFilterPopup(() => {
|
||||
setFilterWithValue('SOME_TIME', '00:00:42', 'time', () => {
|
||||
checkInfoBarIncludes(
|
||||
`AND,AND,0,SOME_TIME,=,42`,
|
||||
(includes: boolean) => {
|
||||
if (includes) done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('3.1 | Non picker - filter time field', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openFilterPopup(() => {
|
||||
setFilterWithValue('SOME_TIME', '42', 'value', () => {
|
||||
checkInfoBarIncludes(
|
||||
`AND,AND,0,SOME_TIME,=,42`,
|
||||
(includes: boolean) => {
|
||||
if (includes) done()
|
||||
}
|
||||
)
|
||||
})
|
||||
}, false)
|
||||
})
|
||||
|
||||
it('4 | filter date field', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openFilterPopup(() => {
|
||||
setFilterWithValue('SOME_DATE', '12/02/1960', 'date', () => {
|
||||
checkInfoBarIncludes(
|
||||
`AND,AND,0,SOME_DATE,=,42`,
|
||||
(includes: boolean) => {
|
||||
if (includes) done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('4.1 | Non picker - filter date field', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openFilterPopup(() => {
|
||||
setFilterWithValue('SOME_DATE', '42', 'value', () => {
|
||||
checkInfoBarIncludes(
|
||||
`AND,AND,0,SOME_DATE,=,42`,
|
||||
(includes: boolean) => {
|
||||
if (includes) done()
|
||||
}
|
||||
)
|
||||
})
|
||||
}, false)
|
||||
})
|
||||
|
||||
it('5 | filter datetime field', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openFilterPopup(() => {
|
||||
setFilterWithValue(
|
||||
'SOME_DATETIME',
|
||||
'01/01/1960 00:00:42',
|
||||
'datetime',
|
||||
() => {
|
||||
checkInfoBarIncludes(
|
||||
`AND,AND,0,SOME_DATETIME,=,42`,
|
||||
(includes: boolean) => {
|
||||
if (includes) done()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('5.1 | Non picker - filter datetime field', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openFilterPopup(() => {
|
||||
setFilterWithValue('SOME_DATETIME', '42', 'value', () => {
|
||||
checkInfoBarIncludes(
|
||||
`AND,AND,0,SOME_DATETIME,=,42`,
|
||||
(includes: boolean) => {
|
||||
if (includes) done()
|
||||
}
|
||||
)
|
||||
})
|
||||
}, false)
|
||||
})
|
||||
|
||||
it('6 | filter date field IN', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openFilterPopup(() => {
|
||||
setFilterWithValue('SOME_DATE', '', 'in', () => {
|
||||
checkInfoBarIncludes(
|
||||
`AND,AND,0,SOME_DATE,IN,(0)`,
|
||||
(includes: boolean) => {
|
||||
if (includes) done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('7 | filter bestnum field BETWEEN', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
openFilterPopup(() => {
|
||||
setFilterWithValue('SOME_BESTNUM', '0-10', 'between', () => {
|
||||
checkInfoBarIncludes(
|
||||
`AND,AND,0,SOME_BESTNUM,BETWEEN,0 AND 10`,
|
||||
(includes: boolean) => {
|
||||
if (includes) done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
this.afterEach(() => {
|
||||
// cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
})
|
||||
})
|
||||
|
||||
const checkInfoBarIncludes = (text: string, callback: any) => {
|
||||
cy.get('.infoBar b', { timeout: longerCommandTimeout }).then((el: any) => {
|
||||
const includes = el[0].innerText.toLowerCase().includes(text.toLowerCase())
|
||||
|
||||
if (callback) callback(includes)
|
||||
})
|
||||
}
|
||||
|
||||
const openFilterPopup = (
|
||||
callback?: any,
|
||||
usePickers: boolean = true,
|
||||
isViewerFiltering: boolean = false
|
||||
) => {
|
||||
const filterButton = isViewerFiltering
|
||||
? '.btn-outline.filterSide'
|
||||
: '.btnCtrl .btnView'
|
||||
|
||||
cy.get(filterButton, { timeout: longerCommandTimeout }).then(
|
||||
(optionsButton: any) => {
|
||||
optionsButton.click()
|
||||
|
||||
if (isViewerFiltering) {
|
||||
cy.wait(300)
|
||||
|
||||
cy.get('.dropdown-menu button').then(async (dropdownButtons: any) => {
|
||||
let filterButton = null
|
||||
|
||||
for (let btn of dropdownButtons) {
|
||||
if (btn.innerText.toLowerCase().includes('filter')) {
|
||||
filterButton = btn
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (filterButton) {
|
||||
filterButton.click()
|
||||
|
||||
if (usePickers) turnOnPickers()
|
||||
|
||||
if (callback) callback()
|
||||
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (usePickers) turnOnPickers()
|
||||
if (callback) callback()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const turnOnPickers = () => {
|
||||
cy.get('#usePickers')
|
||||
.should('exist')
|
||||
.then((picker: any) => {
|
||||
picker[0].click()
|
||||
})
|
||||
}
|
||||
|
||||
const setFilterWithValue = (
|
||||
variableValue: string,
|
||||
valueString: string,
|
||||
valueField: 'value' | 'time' | 'date' | 'datetime' | 'in' | 'between',
|
||||
callback?: any
|
||||
) => {
|
||||
cy.wait(600)
|
||||
|
||||
cy.focused().type(variableValue)
|
||||
cy.wait(100)
|
||||
// cy.focused().trigger('input')
|
||||
cy.get('.variable-col .autocomplete-wrapper', { withinSubject: null })
|
||||
.first()
|
||||
.trigger('keydown', { key: 'ArrowDown' })
|
||||
cy.get('.variable-col .autocomplete-wrapper', {
|
||||
withinSubject: null
|
||||
}).trigger('keydown', { key: 'Enter' })
|
||||
cy.focused().tab()
|
||||
cy.wait(100)
|
||||
|
||||
if (valueField === 'in') {
|
||||
cy.focused().select(valueField.toUpperCase()).trigger('change')
|
||||
} else if (valueField === 'between') {
|
||||
cy.focused().select(valueField.toUpperCase()).trigger('change')
|
||||
} else {
|
||||
cy.focused().tab()
|
||||
cy.wait(100)
|
||||
}
|
||||
|
||||
switch (valueField) {
|
||||
case 'value': {
|
||||
cy.focused().type(valueString)
|
||||
|
||||
break
|
||||
}
|
||||
case 'time': {
|
||||
cy.focused().type(valueString)
|
||||
|
||||
break
|
||||
}
|
||||
case 'date': {
|
||||
cy.focused().type(valueString)
|
||||
cy.focused().tab()
|
||||
|
||||
break
|
||||
}
|
||||
case 'datetime': {
|
||||
const date = valueString.split(' ')[0]
|
||||
const time = valueString.split(' ')[1]
|
||||
|
||||
cy.focused().type(date)
|
||||
cy.focused().tab()
|
||||
cy.focused().tab()
|
||||
cy.focused().type(time)
|
||||
|
||||
break
|
||||
}
|
||||
case 'in': {
|
||||
cy.get('.checkbox-vals').then(() => {
|
||||
cy.focused().tab()
|
||||
cy.focused().click()
|
||||
cy.get('.no-values')
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('clr-checkbox-wrapper input').then((inputs: any) => {
|
||||
inputs[0].click()
|
||||
cy.get('.in-values-modal .modal-footer button').click()
|
||||
|
||||
cy.get('.modal-footer .btn-success-outline').click()
|
||||
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'between': {
|
||||
cy.focused().tab()
|
||||
|
||||
const start = valueString.split('-')[0]
|
||||
const end = valueString.split('-')[1]
|
||||
|
||||
cy.focused().type(start)
|
||||
cy.focused().tab()
|
||||
cy.focused().type(end)
|
||||
}
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.wait(100)
|
||||
cy.focused().tab()
|
||||
cy.wait(100)
|
||||
cy.focused().tab()
|
||||
cy.wait(100)
|
||||
cy.focused().tab()
|
||||
cy.wait(100)
|
||||
cy.focused().tab()
|
||||
cy.wait(100)
|
||||
cy.focused().click()
|
||||
|
||||
if (callback) callback()
|
||||
}
|
||||
|
||||
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let viyaLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (new RegExp(libNameIncludes).test(node.innerText.toLowerCase())) {
|
||||
viyaLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get(viyaLib).within(() => {
|
||||
cy.get('.clr-tree-node-content-container p').click()
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
|
@ -0,0 +1,731 @@
|
|||
import { arrayBufferToBase64 } from './../util/helper-functions'
|
||||
import * as moment from 'moment'
|
||||
|
||||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const fixturePath = 'excels_general/'
|
||||
const serverType = Cypress.env('serverType')
|
||||
const site_id = Cypress.env(`site_id_${serverType}`)
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
const testLicenceUserLimits = Cypress.env('testLicenceUserLimits')
|
||||
|
||||
/** IMPORTANT NOTICE
|
||||
* Before running tests, make sure that table `MPE_USERS` is present
|
||||
*/
|
||||
|
||||
interface EditConfigTableCells {
|
||||
varName: string
|
||||
varValue: string
|
||||
}
|
||||
|
||||
context('licensing tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
// cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
cy.loginAndUpdateValidKey()
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
// cy.get('input.username').type(username)
|
||||
// cy.get('input.password').type(password)
|
||||
// cy.get('.login-group button').click()
|
||||
|
||||
visitPage('home')
|
||||
})
|
||||
|
||||
it('1 | key valid, not expired', (done) => {
|
||||
let keyData = {
|
||||
valid_until: moment().add(1, 'year').format('YYYY-MM-DD'),
|
||||
users_allowed: 4,
|
||||
hot_license_key: '',
|
||||
demo: false,
|
||||
site_id: site_id
|
||||
}
|
||||
|
||||
let keys: { licenseKey: any; activationKey: any }
|
||||
generateKeys(keyData, (keysGen: any) => {
|
||||
keys = keysGen
|
||||
|
||||
cy.wait(2000)
|
||||
|
||||
isLicensingPage((result: boolean) => {
|
||||
if (result) {
|
||||
inputLicenseKeyPage(keys.licenseKey, keys.activationKey)
|
||||
|
||||
cy.wait(2000)
|
||||
}
|
||||
|
||||
acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(10000)
|
||||
}
|
||||
|
||||
visitPage('home')
|
||||
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('2 | Key will expire in less then 14 days, not free tier', (done) => {
|
||||
// make 2 separate for this one
|
||||
let keyData = {
|
||||
valid_until: moment().add(10, 'day').format('YYYY-MM-DD'),
|
||||
users_allowed: 4,
|
||||
hot_license_key: '',
|
||||
demo: false,
|
||||
site_id: site_id
|
||||
}
|
||||
|
||||
let keys: { licenseKey: any; activationKey: any }
|
||||
generateKeys(keyData, (keysGen: any) => {
|
||||
keys = keysGen
|
||||
console.log('keys', keys)
|
||||
|
||||
cy.wait(2000)
|
||||
|
||||
acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
updateLicenseKeyQuick(keysGen, () => {
|
||||
cy.wait(2000)
|
||||
acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
verifyLicensingWarning('This license key will expire in ', () => {
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('3 | key expired, free tier works', (done) => {
|
||||
let keyData = {
|
||||
valid_until: moment().subtract(1, 'day').format('YYYY-MM-DD'),
|
||||
users_allowed: 4,
|
||||
hot_license_key: '',
|
||||
demo: false,
|
||||
site_id: site_id
|
||||
}
|
||||
|
||||
let keys: { licenseKey: any; activationKey: any }
|
||||
generateKeys(keyData, (keysGen: any) => {
|
||||
keys = keysGen
|
||||
|
||||
cy.wait(2000)
|
||||
|
||||
acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
cy.wait(2000)
|
||||
updateLicenseKeyQuick(keysGen, () => {
|
||||
cy.wait(2000)
|
||||
acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
verifyLicensingPage(
|
||||
'Licence key is expired - please contact',
|
||||
(success: boolean) => {
|
||||
if (success) {
|
||||
verifyLicensingWarning(
|
||||
'(FREE Tier) - Problem with licence',
|
||||
() => {
|
||||
done()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('4 | key invalid, free tier works', (done) => {
|
||||
let keyData = {
|
||||
valid_until: moment().subtract(1, 'day').format('YYYY-MM-DD'),
|
||||
users_allowed: 4,
|
||||
hot_license_key: '',
|
||||
demo: false,
|
||||
site_id: site_id
|
||||
}
|
||||
|
||||
let keys: { licenseKey: any; activationKey: any }
|
||||
generateKeys(keyData, (keysGen: any) => {
|
||||
keys = keysGen
|
||||
keys.activationKey = 'invalid' + keys.activationKey
|
||||
|
||||
cy.wait(2000)
|
||||
|
||||
acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
cy.wait(2000)
|
||||
updateLicenseKeyQuick(keysGen, () => {
|
||||
cy.wait(2000)
|
||||
acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
verifyLicensingPage(
|
||||
'Licence key is invalid - please contact',
|
||||
(success: boolean) => {
|
||||
if (success) {
|
||||
verifyLicensingWarning(
|
||||
'(FREE Tier) - Problem with licence',
|
||||
() => {
|
||||
done()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('5 | key for wrong organisation, free tier works', (done) => {
|
||||
let keyData = {
|
||||
valid_until: moment().add(1, 'year').format('YYYY-MM-DD'),
|
||||
users_allowed: 4,
|
||||
hot_license_key: '',
|
||||
demo: false,
|
||||
site_id: 100
|
||||
}
|
||||
|
||||
let keys: { licenseKey: any; activationKey: any }
|
||||
generateKeys(keyData, (keysGen: any) => {
|
||||
keys = keysGen
|
||||
keys.activationKey = keys.activationKey
|
||||
|
||||
cy.wait(2000)
|
||||
|
||||
acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
cy.wait(2000)
|
||||
updateLicenseKeyQuick(keysGen, () => {
|
||||
cy.wait(2000)
|
||||
acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
verifyLicensingPage(
|
||||
'SYSSITE (below) is not found',
|
||||
(success: boolean) => {
|
||||
if (success) {
|
||||
verifyLicensingWarning(
|
||||
'(FREE Tier) - Problem with licence',
|
||||
() => {
|
||||
done()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if (testLicenceUserLimits) {
|
||||
it('4 | User try to register when limit is reached', (done) => {
|
||||
let keyData = {
|
||||
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
|
||||
users_allowed: 10,
|
||||
hot_license_key: '',
|
||||
demo: false,
|
||||
site_id: site_id
|
||||
}
|
||||
let keyData2 = {
|
||||
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
|
||||
users_allowed: 1,
|
||||
hot_license_key: '',
|
||||
demo: false,
|
||||
site_id: site_id
|
||||
}
|
||||
generateKeys(keyData, (keysGen: any) => {
|
||||
generateKeys(keyData2, (keysGen2: any) => {
|
||||
cy.wait(2000)
|
||||
|
||||
acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
updateLicenseKeyQuick(keysGen, () => {
|
||||
cy.wait(2000)
|
||||
acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
|
||||
updateLicenseKeyQuick(keysGen2, () => {
|
||||
cy.wait(2000)
|
||||
|
||||
const random = Cypress._.random(0, 1000)
|
||||
const newUser = {
|
||||
username: `randomusername${random}notregistered`,
|
||||
last_seen_at: moment().add(1, 'month').format('YYYY-MM-DD'),
|
||||
registered_at: moment().add(1, 'month').format('YYYY-MM-DD')
|
||||
}
|
||||
updateUsersTable(
|
||||
{ deleteAll: true, newUsers: [newUser] },
|
||||
() => {
|
||||
logout(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
// cy.get('input.username').type(username)
|
||||
// cy.get('input.password').type(password)
|
||||
// cy.get('.login-group button').click()
|
||||
|
||||
visitPage('home')
|
||||
|
||||
cy.wait(2000)
|
||||
|
||||
verifyLicensingPage(
|
||||
'The registered number of users reached the limit specified for your licence.',
|
||||
(success: boolean) => {
|
||||
if (success) done()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('5 | Show warning banner when limit is exceeded', (done) => {
|
||||
let keyData = {
|
||||
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
|
||||
users_allowed: 10,
|
||||
hot_license_key: '',
|
||||
demo: false,
|
||||
site_id: site_id
|
||||
}
|
||||
let keyData2 = {
|
||||
valid_until: moment().add(1, 'month').format('YYYY-MM-DD'),
|
||||
users_allowed: 1,
|
||||
hot_license_key: '',
|
||||
demo: false,
|
||||
site_id: site_id
|
||||
}
|
||||
|
||||
generateKeys(keyData, (keysGen: any) => {
|
||||
generateKeys(keyData2, (keysGen2: any) => {
|
||||
cy.wait(2000)
|
||||
acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
updateLicenseKeyQuick(keysGen, () => {
|
||||
cy.wait(2000)
|
||||
acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
const random = Cypress._.random(0, 1000)
|
||||
const newUser = {
|
||||
username: `randomusername${random}`,
|
||||
last_seen_at: moment().add(1, 'month').format('YYYY-MM-DD'),
|
||||
registered_at: moment().add(1, 'month').format('YYYY-MM-DD')
|
||||
}
|
||||
updateUsersTable(
|
||||
{ deleteAll: true, keep: username, newUsers: [newUser] },
|
||||
() => {
|
||||
updateLicenseKeyQuick(keysGen2, () => {
|
||||
cy.wait(2000)
|
||||
verifyLicensingWarning(
|
||||
'The registered number of users exceeds the limit specified for your license.',
|
||||
() => {
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.afterEach(() => {
|
||||
// cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
})
|
||||
})
|
||||
|
||||
const logout = (callback?: any) => {
|
||||
cy.get('.header-actions .dropdown-toggle')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.header-actions .dropdown-menu > .separator')
|
||||
.next()
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const acceptTermsIfPresented = (callback?: any) => {
|
||||
cy.url().then((url: string) => {
|
||||
if (url.includes('licensing/register')) {
|
||||
cy.get('.card-block')
|
||||
.scrollTo('bottom')
|
||||
.then(() => {
|
||||
cy.get('#checkbox1')
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback(true)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
if (callback) callback(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isLicensingPage = (callback: any) => {
|
||||
return cy.url().then((url: string) => {
|
||||
callback(
|
||||
url.includes('#/licensing/') && !url.includes('licensing/register')
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const verifyLicensingPage = (text: string, callback: any) => {
|
||||
// visitPage('home')
|
||||
|
||||
cy.wait(1000)
|
||||
isLicensingPage((result: boolean) => {
|
||||
if (result) {
|
||||
cy.get('p.key-error')
|
||||
.should('contain', text)
|
||||
.then((treeNodes: any) => {
|
||||
callback(true)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const verifyLicensingWarning = (text: string, callback: any) => {
|
||||
visitPage('home')
|
||||
|
||||
cy.wait(1000)
|
||||
cy.get("div[role='alert'] .alert-text")
|
||||
.invoke('text')
|
||||
.should('contain', text)
|
||||
.then(() => {
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
const inputLicenseKeyPage = (licenseKey: string, activationKey: string) => {
|
||||
cy.get('button').contains('Paste licence').click()
|
||||
|
||||
cy.get('.license-key-form textarea', { timeout: longerCommandTimeout })
|
||||
.invoke('val', licenseKey)
|
||||
.trigger('input')
|
||||
.should('not.be.undefined')
|
||||
cy.get('.activation-key-form textarea', { timeout: longerCommandTimeout })
|
||||
.invoke('val', activationKey)
|
||||
.trigger('input')
|
||||
.should('not.be.undefined')
|
||||
cy.get('button.apply-keys').click()
|
||||
}
|
||||
|
||||
const updateUsersTable = (options: any, callback?: any) => {
|
||||
visitPage('home')
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_users')
|
||||
|
||||
clickOnEdit(() => {
|
||||
cy.get('.ht_master tbody tr').then((rows: any) => {
|
||||
if (options.deleteAll) {
|
||||
for (let row of rows) {
|
||||
const user_id = row.childNodes[2]
|
||||
if (!options.keep || user_id.innerText !== options.keep) {
|
||||
cy.get(row.childNodes[1])
|
||||
.dblclick()
|
||||
.then(() => {
|
||||
cy.focused().type('{selectall}').type('Yes').type('{enter}')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.newUsers && options.newUsers.length) {
|
||||
for (let newUser of options.newUsers) {
|
||||
clickOnAddRow(() => {
|
||||
cy.get('#hotInstance tbody tr:last-child').then((rows: any) => {
|
||||
cy.get(rows[0].childNodes[2])
|
||||
.dblclick()
|
||||
.then(() => {
|
||||
cy.focused()
|
||||
.type('{selectall}')
|
||||
.type(newUser.username)
|
||||
.type('{enter}')
|
||||
})
|
||||
// cy.get(rows[0].childNodes[3])
|
||||
// .dblclick()
|
||||
// .then(() => {
|
||||
// cy.focused()
|
||||
// .type('{selectall}')
|
||||
// .type(newUser.last_seen_at)
|
||||
// .type('{enter}')
|
||||
// })
|
||||
// cy.get(rows[0].childNodes[4])
|
||||
// .dblclick()
|
||||
// .then(() => {
|
||||
// cy.focused()
|
||||
// .type('{selectall}')
|
||||
// .type(newUser.registered_at)
|
||||
// .type('{enter}')
|
||||
// })
|
||||
submitTable(() => {
|
||||
cy.wait(2000)
|
||||
approveTable(callback)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const changeLicenseKeyTable = (keys: any, callback?: any) => {
|
||||
visitPage('home')
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_config')
|
||||
|
||||
clickOnEdit(() => {
|
||||
editTableField(
|
||||
[
|
||||
{ varName: 'DC_ACTIVATION_KEY', varValue: keys.activationKey },
|
||||
{ varName: 'DC_LICENCE_KEY', varValue: keys.licenseKey }
|
||||
],
|
||||
() => {
|
||||
submitTable(() => {
|
||||
cy.wait(2000)
|
||||
approveTable(() => {
|
||||
cy.reload()
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const updateLicenseKeyQuick = (keys: any, callback: any) => {
|
||||
isLicensingPage((result: boolean) => {
|
||||
if (!result) {
|
||||
visitPage('licensing/update')
|
||||
cy.wait(2000)
|
||||
}
|
||||
inputLicenseKeyPage(keys.licenseKey, keys.activationKey)
|
||||
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
const generateKeys = async (licenseData: any, resultCallback?: any) => {
|
||||
let keyPair = await window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
modulusLength: 2024,
|
||||
publicExponent: new Uint8Array([1, 0, 1]),
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
true,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
|
||||
const encoded = new TextEncoder().encode(JSON.stringify(licenseData))
|
||||
|
||||
const cipher = await window.crypto.subtle
|
||||
.encrypt(
|
||||
{
|
||||
name: 'RSA-OAEP'
|
||||
},
|
||||
keyPair.publicKey,
|
||||
encoded
|
||||
)
|
||||
.then(
|
||||
(value) => {
|
||||
return value
|
||||
},
|
||||
(err) => {
|
||||
console.log('Encrpyt error', err)
|
||||
}
|
||||
)
|
||||
|
||||
if (!cipher) {
|
||||
alert('Encryptin keys failed')
|
||||
throw new Error('Encryptin keys failed')
|
||||
}
|
||||
|
||||
const privateKeyBytes = await window.crypto.subtle.exportKey(
|
||||
'pkcs8',
|
||||
keyPair.privateKey
|
||||
)
|
||||
|
||||
const activationKey = await arrayBufferToBase64(privateKeyBytes)
|
||||
const licenseKey = await arrayBufferToBase64(cipher)
|
||||
|
||||
if (resultCallback)
|
||||
resultCallback({
|
||||
activationKey,
|
||||
licenseKey
|
||||
})
|
||||
}
|
||||
|
||||
const editTableField = (edits: EditConfigTableCells[], callback?: any) => {
|
||||
cy.get('td').then((tdNodes: any) => {
|
||||
for (let edit of edits) {
|
||||
let correctRow = false
|
||||
|
||||
for (let node of tdNodes) {
|
||||
if (correctRow) {
|
||||
cy.get(node)
|
||||
.dblclick()
|
||||
.then(() => {
|
||||
// textarea update on long keys
|
||||
cy.focused().invoke('val', edit.varValue).type('{enter}')
|
||||
})
|
||||
|
||||
correctRow = false
|
||||
break
|
||||
}
|
||||
|
||||
if (node.innerText.includes(edit.varName)) {
|
||||
correctRow = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const openTableFromTree = (
|
||||
libNameIncludes: string,
|
||||
tablename: string,
|
||||
callback?: any
|
||||
) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let viyaLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
||||
viyaLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get(viyaLib).within(() => {
|
||||
cy.get('.clr-tree-node-content-container > button').click()
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
if (callback) callback()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const clickOnAddRow = (callback?: any) => {
|
||||
cy.get('.btnCtrl button.btn-success')
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const clickOnEdit = (callback?: any) => {
|
||||
cy.get('.btnCtrl button.btn-primary', { timeout: longerCommandTimeout })
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const submitTable = (callback?: any) => {
|
||||
cy.get('.btnCtrl button.btn-primary', { timout: longerCommandTimeout })
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get(".modal.ng-star-inserted button[type='submit']")
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const approveTable = (callback?: any) => {
|
||||
cy.get('button', { timeout: longerCommandTimeout })
|
||||
.should('contain', 'Go to approvals screen')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('go to approvals screen')
|
||||
) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get('button#acceptBtn', { timeout: longerCommandTimeout })
|
||||
.should('exist')
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('app-history', { timeout: longerCommandTimeout })
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
const fixturePath = 'excels/'
|
||||
|
||||
context('liveness tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
if (serverType !== 'SASJS') {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
}
|
||||
cy.loginAndUpdateValidKey(true)
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
|
||||
// cy.get('input.username').type(username)
|
||||
// cy.get('input.password').type(password)
|
||||
// cy.get('.login-group button').click()
|
||||
|
||||
visitPage('home')
|
||||
})
|
||||
|
||||
it('1 | Login and submit test', (done) => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
libraryExistsInTree('viya', treeNodes)
|
||||
? openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
: openTableFromTree('dc', 'mpe_x_test')
|
||||
|
||||
attachExcelFile('regular_excel.xlsx', () => {
|
||||
submitExcel()
|
||||
rejectExcel(done)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Thist part will be needed if we add more tests in future
|
||||
*/
|
||||
// this.afterEach(() => {
|
||||
// cy.visit('https://sas.4gl.io/SASLogon/logout');
|
||||
// })
|
||||
})
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
||||
|
||||
const libraryExistsInTree = (libName: string, nodes: any) => {
|
||||
for (let node of nodes) {
|
||||
if (node.innerText.toLowerCase().includes(libName.toLowerCase()))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const openTableFromTree = (
|
||||
libNameIncludes: string,
|
||||
tablename: string,
|
||||
finish: any
|
||||
) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let viyaLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
||||
viyaLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!viyaLib && finish) finish(false)
|
||||
|
||||
cy.get(viyaLib).within(() => {
|
||||
cy.get('.clr-tree-node-content-container > button').click()
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
if (finish) finish(true)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const attachExcelFile = (excelFilename: string, callback?: any) => {
|
||||
cy.get('.buttonBar button:last-child')
|
||||
.should('exist')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('input[type="file"]#file-upload').attachFile(
|
||||
`/${fixturePath}/${excelFilename}`
|
||||
)
|
||||
cy.get('.modal-footer .btn.btn-primary').then((modalBtn) => {
|
||||
modalBtn.click()
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const submitExcel = (callback?: any) => {
|
||||
cy.get('.buttonBar button.preview-submit', { timeout: longerCommandTimeout })
|
||||
.click()
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
}
|
||||
|
||||
const rejectExcel = (callback?: any) => {
|
||||
cy.get('button', { timeout: longerCommandTimeout })
|
||||
.should('contain', 'Go to approvals screen')
|
||||
.then((allButtons: any) => {
|
||||
for (let approvalButton of allButtons) {
|
||||
if (
|
||||
approvalButton.innerText
|
||||
.toLowerCase()
|
||||
.includes('go to approvals screen')
|
||||
) {
|
||||
approvalButton.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get('button.btn-danger')
|
||||
.should('exist')
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.modal-footer button.btn-success-outline')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('app-history')
|
||||
.should('exist')
|
||||
.then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
const fixturePath = 'excels_general/'
|
||||
|
||||
context('metanav tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
cy.loginAndUpdateValidKey()
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
cy.get('input.username').type(username)
|
||||
cy.get('input.password').type(password)
|
||||
cy.get('.login-group button').click()
|
||||
|
||||
visitPage('view/metadata')
|
||||
})
|
||||
|
||||
it('1 | Opens metadata object', (done) => {
|
||||
openFirstMetadataFromTree(() => {
|
||||
// BLOCKER
|
||||
// For unkown reasons, .clr-accordion-header-button always null although it is present on the page.
|
||||
cy.get('.clr-accordion-header-button').then((panelNodes: any) => {
|
||||
panelNodes[0].querySelector('button').click()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
this.afterEach(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
})
|
||||
})
|
||||
|
||||
const openFirstMetadataFromTree = (callback?: any) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let firstMetaNode
|
||||
|
||||
firstMetaNode = treeNodes[1]
|
||||
|
||||
cy.get(firstMetaNode).within(() => {
|
||||
cy.get('.clr-treenode-content').click()
|
||||
callback()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
|
@ -0,0 +1,629 @@
|
|||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const username = Cypress.env('username')
|
||||
const password = Cypress.env('password')
|
||||
const hostUrl = Cypress.env('hosturl')
|
||||
const appLocation = Cypress.env('appLocation')
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const serverType = Cypress.env('serverType')
|
||||
const libraryToOpenIncludes = Cypress.env(`libraryToOpenIncludes_${serverType}`)
|
||||
const fixturePath = 'excels_general/'
|
||||
|
||||
context('editor tests: ', function () {
|
||||
this.beforeAll(() => {
|
||||
cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
cy.loginAndUpdateValidKey()
|
||||
})
|
||||
|
||||
this.beforeEach(() => {
|
||||
cy.visit(hostUrl + appLocation)
|
||||
|
||||
cy.wait(2000)
|
||||
|
||||
cy.get('body').then(($body) => {
|
||||
const usernameInput = $body.find('input.username')[0]
|
||||
|
||||
if (usernameInput && !Cypress.dom.isHidden(usernameInput)) {
|
||||
cy.get('input.username').type(username)
|
||||
cy.get('input.password').type(password)
|
||||
|
||||
cy.get('.login-group button').click()
|
||||
}
|
||||
})
|
||||
|
||||
visitPage('home')
|
||||
})
|
||||
|
||||
it('1 | Add one viewbox', (done) => {
|
||||
const viewbox_table = 'mpe_audit'
|
||||
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
||||
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
cy.get('.viewbox-open').click()
|
||||
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
||||
|
||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
|
||||
return
|
||||
}
|
||||
|
||||
checkColumns(columns, () => {
|
||||
done()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('2 | Add two viewboxes', (done) => {
|
||||
const viewboxes = [
|
||||
{
|
||||
viewbox_table: 'mpe_audit',
|
||||
columns: ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
||||
},
|
||||
{
|
||||
viewbox_table: 'mpe_alerts',
|
||||
columns: [
|
||||
'TX_FROM',
|
||||
'ALERT_EVENT',
|
||||
'ALERT_LIB',
|
||||
'ALERT_DS',
|
||||
'ALERT_USER'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
cy.get('.viewbox-open').click()
|
||||
openTableFromViewboxTree(
|
||||
libraryToOpenIncludes,
|
||||
viewboxes.map((viewbox) => viewbox.viewbox_table)
|
||||
)
|
||||
|
||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
||||
let found = 0
|
||||
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
for (let viewbox of viewboxes) {
|
||||
if (
|
||||
viewboxNode.innerText.toLowerCase().includes(viewbox.viewbox_table)
|
||||
)
|
||||
found++
|
||||
}
|
||||
}
|
||||
|
||||
if (found < viewboxes.length) return
|
||||
|
||||
cy.get('.viewboxes-container .viewbox').then((viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
cy.get(viewboxNode).within(() => {
|
||||
cy.get('.table-title').then((tableTitle) => {
|
||||
const title = tableTitle[0].innerText
|
||||
const viewbox = viewboxes.find((vb) =>
|
||||
title.toLowerCase().includes(vb.viewbox_table)
|
||||
)
|
||||
|
||||
if (viewbox) {
|
||||
cy.get('.ht_master.handsontable .htCore thead tr').then(
|
||||
(viewboxColNodes: any) => {
|
||||
let allColsHtml = viewboxColNodes[0].innerHTML
|
||||
|
||||
for (let col of viewbox?.columns) {
|
||||
if (!allColsHtml.includes(col)) return
|
||||
}
|
||||
|
||||
done()
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('3 | Add viewbox, add columns', (done) => {
|
||||
const viewbox_table = 'mpe_audit'
|
||||
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
||||
const additionalColumns = ['IS_PK']
|
||||
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
cy.get('.viewbox-open').click()
|
||||
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
||||
|
||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
|
||||
return
|
||||
}
|
||||
|
||||
openViewboxConfig(viewbox_table)
|
||||
|
||||
addColumns(additionalColumns)
|
||||
|
||||
checkColumns([...columns, ...additionalColumns], () => {
|
||||
done()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('4 | Add viewbox, add columns and reorder', (done) => {
|
||||
const viewbox_table = 'mpe_audit'
|
||||
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
||||
const additionalColumns = ['IS_PK', 'MOVE_TYPE']
|
||||
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
cy.get('.viewbox-open').click()
|
||||
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
||||
|
||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
|
||||
return
|
||||
}
|
||||
|
||||
openViewboxConfig(viewbox_table)
|
||||
|
||||
addColumns(additionalColumns, () => {
|
||||
cy.wait(1000)
|
||||
//reorder
|
||||
cy.get('.col-box.column-MOVE_TYPE')
|
||||
.realMouseDown({ button: 'left', position: 'center' })
|
||||
.realMouseMove(0, 10, { position: 'center' })
|
||||
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
|
||||
cy.get('.col-box.column-IS_PK')
|
||||
.realMouseMove(0, 0, { position: 'center' })
|
||||
.realMouseUp()
|
||||
//reorder end
|
||||
|
||||
cy.wait(500)
|
||||
|
||||
checkColumns([...columns, ...additionalColumns.reverse()], () => {
|
||||
done()
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('5 | Add viewbox, add columns, reorder, remove column, add again', (done) => {
|
||||
const viewbox_table = 'mpe_audit'
|
||||
const columns = ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM']
|
||||
const additionalColumns = ['IS_PK', 'MOVE_TYPE']
|
||||
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
cy.get('.viewbox-open').click()
|
||||
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
||||
|
||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
|
||||
return
|
||||
}
|
||||
|
||||
viewboxNode.click()
|
||||
|
||||
addColumns(additionalColumns, () => {
|
||||
cy.wait(1000)
|
||||
//reorder
|
||||
cy.get('.col-box.column-MOVE_TYPE')
|
||||
.realMouseDown({ button: 'left', position: 'center' })
|
||||
.realMouseMove(0, 10, { position: 'center' })
|
||||
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
|
||||
cy.get('.col-box.column-IS_PK')
|
||||
.realMouseMove(0, 0, { position: 'center' })
|
||||
.realMouseUp()
|
||||
//reorder end
|
||||
|
||||
cy.wait(500)
|
||||
|
||||
checkColumns([...columns, ...additionalColumns.reverse()], () => {
|
||||
const colToRemove = 'MOVE_TYPE'
|
||||
|
||||
removeColumn(colToRemove)
|
||||
checkColumns(
|
||||
[
|
||||
...columns,
|
||||
...additionalColumns.filter((col) => col !== colToRemove)
|
||||
],
|
||||
() => {
|
||||
addColumns([colToRemove], () => {
|
||||
checkColumns(
|
||||
[...columns, ...additionalColumns.reverse()],
|
||||
() => {
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('6 | Add viewboxes, reload and check url restored configuration', (done) => {
|
||||
const viewboxes = [
|
||||
{
|
||||
viewbox_table: 'mpe_audit',
|
||||
columns: ['LOAD_REF', 'LIBREF', 'DSN', 'KEY_HASH', 'TGTVAR_NM'],
|
||||
additionalColumns: ['IS_PK', 'MOVE_TYPE']
|
||||
},
|
||||
{
|
||||
viewbox_table: 'mpe_alerts',
|
||||
columns: [
|
||||
'TX_FROM',
|
||||
'ALERT_EVENT',
|
||||
'ALERT_LIB',
|
||||
'ALERT_DS',
|
||||
'ALERT_USER'
|
||||
],
|
||||
additionalColumns: ['TX_TO']
|
||||
}
|
||||
]
|
||||
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
cy.get('.viewbox-open').click()
|
||||
openTableFromViewboxTree(libraryToOpenIncludes, [
|
||||
viewboxes[0].viewbox_table,
|
||||
viewboxes[1].viewbox_table
|
||||
])
|
||||
|
||||
openViewboxConfig(viewboxes[0].viewbox_table)
|
||||
|
||||
cy.wait(500)
|
||||
|
||||
addColumns(viewboxes[0].additionalColumns, () => {
|
||||
cy.wait(1000)
|
||||
|
||||
if (viewboxes[0].viewbox_table === 'mpe_audit') {
|
||||
cy.get('.col-box.column-MOVE_TYPE')
|
||||
.realMouseDown({ button: 'left', position: 'center' })
|
||||
.realMouseMove(0, 10, { position: 'center' })
|
||||
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
|
||||
cy.get('.col-box.column-IS_PK')
|
||||
.realMouseMove(0, 0, { position: 'center' })
|
||||
.realMouseUp()
|
||||
}
|
||||
|
||||
cy.wait(1000)
|
||||
|
||||
openViewboxConfig(viewboxes[1].viewbox_table)
|
||||
addColumns(viewboxes[1].additionalColumns, () => {
|
||||
cy.wait(1000).reload()
|
||||
|
||||
let result = 0
|
||||
|
||||
checkColumns(
|
||||
[
|
||||
...viewboxes[0].columns,
|
||||
...cloneDeep(viewboxes[0].additionalColumns.reverse())
|
||||
],
|
||||
() => {
|
||||
result++
|
||||
|
||||
if (result === 2) done()
|
||||
}
|
||||
)
|
||||
checkColumns(
|
||||
[...viewboxes[1].columns, ...viewboxes[1].additionalColumns],
|
||||
() => {
|
||||
result++
|
||||
|
||||
if (result === 2) done()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('7 | Add viewboxes and filter', () => {
|
||||
const viewboxes = ['mpe_x_test', 'mpe_validations']
|
||||
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
cy.get('.viewbox-open').click()
|
||||
openTableFromViewboxTree(libraryToOpenIncludes, viewboxes)
|
||||
|
||||
cy.wait(1000)
|
||||
|
||||
closeViewboxModal()
|
||||
|
||||
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
|
||||
(viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
cy.get(viewboxNode).within(() => {
|
||||
cy.get('.table-title').then((title: any) => {
|
||||
cy.get('.hot-spinner')
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('clr-icon[shape="filter"]').then((filterButton) => {
|
||||
filterButton[0].click()
|
||||
})
|
||||
|
||||
if (title[0].innerText.includes('MPE_X_TEST')) {
|
||||
setFilterWithValue(
|
||||
'SOME_CHAR',
|
||||
'this is dummy data',
|
||||
'value',
|
||||
() => {
|
||||
cy.get('app-query', { withinSubject: null })
|
||||
.should('not.exist')
|
||||
.get('.ht_master.handsontable tbody tr')
|
||||
.then((rowNodes) => {
|
||||
const tr = rowNodes[0]
|
||||
|
||||
expect(rowNodes).to.have.length(1)
|
||||
expect(tr.innerText).to.equal('0')
|
||||
})
|
||||
}
|
||||
)
|
||||
} else if (title[0].innerText.includes('MPE_VALIDATIONS')) {
|
||||
setFilterWithValue('BASE_COL', 'ALERT_LIB', 'value', () => {
|
||||
cy.get('app-query', { withinSubject: null })
|
||||
.should('not.exist')
|
||||
.get('.ht_master.handsontable tbody tr')
|
||||
.then((rowNodes) => {
|
||||
const tr = rowNodes[0]
|
||||
|
||||
expect(rowNodes).to.have.length(1)
|
||||
expect(tr.innerText).to.contain('ALERT_LIB')
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
this.afterEach(() => {
|
||||
// cy.visit(`${hostUrl}/SASLogon/logout`)
|
||||
})
|
||||
})
|
||||
|
||||
const checkColumns = (columns: string[], callback: () => void) => {
|
||||
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
|
||||
(viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
cy.get(viewboxNode).within(() => {
|
||||
cy.get('.ht_master.handsontable thead tr th').then(
|
||||
(viewboxColNodes: any) => {
|
||||
for (let i = 0; i < viewboxColNodes.length; i++) {
|
||||
const col = columns[i]
|
||||
const colNode = viewboxColNodes[i]
|
||||
|
||||
if (
|
||||
!colNode.innerHTML.toLowerCase().includes(col.toLowerCase())
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
callback()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const closeViewboxModal = () => {
|
||||
cy.get('app-viewboxes .close', { withinSubject: null }).click()
|
||||
}
|
||||
|
||||
const removeColumn = (column: string) => {
|
||||
cy.get(`.col-box.column-${column} clr-icon`, { withinSubject: null }).click()
|
||||
}
|
||||
|
||||
const addColumns = (columns: string[], callback?: () => void) => {
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
const column = columns[i]
|
||||
|
||||
cy.get('.cols-search input', { withinSubject: null }).type(column)
|
||||
cy.get('.cols-search .autocomplete-wrapper', { withinSubject: null })
|
||||
.first()
|
||||
.trigger('keydown', { key: 'ArrowDown' })
|
||||
cy.get('.cols-search .autocomplete-wrapper', { withinSubject: null })
|
||||
.first()
|
||||
.trigger('keydown', { key: 'Enter' })
|
||||
.then(() => {
|
||||
if (i === columns.length - 1 && callback) callback()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const openViewboxConfig = (viewbox_tablename: string) => {
|
||||
cy.get('.open-viewbox').then((viewboxes: any) => {
|
||||
for (let openViewbox of viewboxes) {
|
||||
if (openViewbox.innerText.toLowerCase().includes(viewbox_tablename))
|
||||
openViewbox.click()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let viyaLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (new RegExp(libNameIncludes).test(node.innerText.toLowerCase())) {
|
||||
viyaLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get(viyaLib).within(() => {
|
||||
cy.get('.clr-tree-node-content-container p').click()
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const setFilterWithValue = (
|
||||
variableValue: string,
|
||||
valueString: string,
|
||||
valueField: 'value' | 'time' | 'date' | 'datetime' | 'in' | 'between',
|
||||
callback?: any
|
||||
) => {
|
||||
cy.wait(600)
|
||||
|
||||
cy.focused().type(variableValue)
|
||||
cy.wait(100)
|
||||
// cy.focused().trigger('input')
|
||||
cy.get('.variable-col .autocomplete-wrapper', { withinSubject: null })
|
||||
.first()
|
||||
.trigger('keydown', { key: 'ArrowDown' })
|
||||
cy.get('.variable-col .autocomplete-wrapper', {
|
||||
withinSubject: null
|
||||
}).trigger('keydown', { key: 'Enter' })
|
||||
cy.focused().tab()
|
||||
cy.wait(100)
|
||||
|
||||
if (valueField === 'in') {
|
||||
cy.focused().select(valueField.toUpperCase()).trigger('change')
|
||||
} else if (valueField === 'between') {
|
||||
cy.focused().select(valueField.toUpperCase()).trigger('change')
|
||||
} else {
|
||||
cy.focused().tab()
|
||||
cy.wait(100)
|
||||
}
|
||||
|
||||
switch (valueField) {
|
||||
case 'value': {
|
||||
cy.focused().type(valueString)
|
||||
|
||||
break
|
||||
}
|
||||
case 'time': {
|
||||
cy.focused().type(valueString)
|
||||
|
||||
break
|
||||
}
|
||||
case 'date': {
|
||||
cy.focused().type(valueString)
|
||||
cy.focused().tab()
|
||||
|
||||
break
|
||||
}
|
||||
case 'datetime': {
|
||||
const date = valueString.split(' ')[0]
|
||||
const time = valueString.split(' ')[1]
|
||||
|
||||
cy.focused().type(date)
|
||||
cy.focused().tab()
|
||||
cy.focused().tab()
|
||||
cy.focused().type(time)
|
||||
|
||||
break
|
||||
}
|
||||
case 'in': {
|
||||
cy.get('.checkbox-vals').then(() => {
|
||||
cy.focused().tab()
|
||||
cy.focused().click()
|
||||
cy.get('.no-values')
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('clr-checkbox-wrapper input').then((inputs: any) => {
|
||||
inputs[0].click()
|
||||
cy.get('.in-values-modal .modal-footer button').click()
|
||||
|
||||
cy.get('.modal-footer .btn-success-outline').click()
|
||||
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'between': {
|
||||
cy.focused().tab()
|
||||
|
||||
const start = valueString.split('-')[0]
|
||||
const end = valueString.split('-')[1]
|
||||
|
||||
cy.focused().type(start)
|
||||
cy.focused().tab()
|
||||
cy.focused().type(end)
|
||||
}
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.wait(100)
|
||||
cy.focused().tab()
|
||||
cy.wait(100)
|
||||
cy.focused().tab()
|
||||
cy.wait(100)
|
||||
cy.focused().tab()
|
||||
cy.wait(100)
|
||||
cy.focused().tab()
|
||||
cy.wait(100)
|
||||
cy.focused().click()
|
||||
|
||||
if (callback) callback()
|
||||
}
|
||||
|
||||
const openTableFromViewboxTree = (
|
||||
libNameIncludes: string,
|
||||
tablenames: string[]
|
||||
) => {
|
||||
cy.get('.add-new clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let viyaLib
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (node.innerText.toLowerCase().includes(libNameIncludes)) {
|
||||
viyaLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cy.get(viyaLib).within(() => {
|
||||
cy.get('p')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
for (let tablename of tablenames) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
const wp = require('@cypress/webpack-preprocessor')
|
||||
|
||||
const webpackOptions = {
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loaders: ['ts-loader', 'angular2-template-loader'],
|
||||
exclude: [/node_modules/],
|
||||
},
|
||||
{
|
||||
test: /\.(html|css)$/,
|
||||
loader: 'raw-loader',
|
||||
exclude: /\.async\.(html|css)$/
|
||||
},
|
||||
{
|
||||
test: /\.async\.(html|css)$/,
|
||||
loaders: ['file?name=[name].[hash].[ext]', 'extract']
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
webpackOptions
|
||||
}
|
||||
|
||||
module.exports = wp(options)
|
|
@ -0,0 +1,58 @@
|
|||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
const wp = require("@cypress/webpack-preprocessor");
|
||||
const { rmdir } = require('fs')
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
|
||||
const options = {
|
||||
webpackOptions: require("../webpack.config.js")
|
||||
};
|
||||
on("file:preprocessor", wp(options));
|
||||
|
||||
on("before:browser:launch", (browser = {}, launchOptions) => {
|
||||
if (browser.name === "chrome") {
|
||||
launchOptions.args.push("--disable-site-isolation-trials");
|
||||
launchOptions.args.push("--auto-open-devtools-for-tabs");
|
||||
launchOptions.args.push("--aggressive-cache-discard")
|
||||
launchOptions.args.push("--disable-cache")
|
||||
launchOptions.args.push("--disable-application-cache")
|
||||
launchOptions.args.push("--disable-offline-load-stale-cache")
|
||||
launchOptions.args.push("--disk-cache-size=0")
|
||||
launchOptions.args.push("--no-sandbox")
|
||||
|
||||
return launchOptions;
|
||||
}
|
||||
});
|
||||
|
||||
on('task', {
|
||||
deleteFolder(folderName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
rmdir(folderName, { maxRetries: 10, recursive: true }, (err) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
return reject(err)
|
||||
}
|
||||
resolve(null)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
import 'cypress-file-upload';
|
||||
|
||||
import { arrayBufferToBase64 } from './../util/helper-functions'
|
||||
import * as moment from 'moment'
|
||||
|
||||
const username = Cypress.env('username');
|
||||
const password = Cypress.env('password');
|
||||
const hostUrl = Cypress.env('hosturl');
|
||||
const appLocation = Cypress.env('appLocation');
|
||||
const longerCommandTimeout = Cypress.env('longerCommandTimeout')
|
||||
const site_id_SASJS = Cypress.env('site_id_SASJS')
|
||||
|
||||
Cypress.Commands.add('isLoggedIn', (callback: (exist: boolean) => void) => {
|
||||
cy.get('body').then($body => {
|
||||
if ($body.find(".nav-tree").length > 0) {
|
||||
if (callback) callback(true)
|
||||
} else {
|
||||
if (callback) callback(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add('loginAndUpdateValidKey', (forceLicenceKey: boolean = false) => {
|
||||
cy.visit(hostUrl + appLocation);
|
||||
|
||||
cy.wait(2000)
|
||||
|
||||
cy.get('body').then($body =>{
|
||||
const usernameInput = $body.find("input.username")[0]
|
||||
|
||||
if (usernameInput && !Cypress.dom.isHidden(usernameInput)) {
|
||||
cy.get('input.username').type(username);
|
||||
cy.get('input.password').type(password);
|
||||
|
||||
cy.get('.login-group button').click()
|
||||
}
|
||||
|
||||
cy.get('.app-loading', {timeout: longerCommandTimeout}).should('not.exist').then(() => {
|
||||
cy.wait(2000)
|
||||
|
||||
if ($body.find(".nav-tree").length > 0) {
|
||||
/**
|
||||
* If licence key is already working, then skip rest of the function
|
||||
*/
|
||||
return logout(() => {
|
||||
return
|
||||
})
|
||||
} else{
|
||||
const keyData = {
|
||||
valid_until: moment().add(20, 'day').format('YYYY-MM-DD'),
|
||||
number_of_users: 10,
|
||||
hot_license_key: '',
|
||||
site_id: '',
|
||||
demo: false
|
||||
}
|
||||
|
||||
return generateKeys(keyData.valid_until, keyData.number_of_users, keyData.hot_license_key, keyData.demo, (keysGen: any) => {
|
||||
return acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
|
||||
if (!forceLicenceKey) {
|
||||
return logout(() => {
|
||||
return
|
||||
})
|
||||
} else {
|
||||
return updateLicenseKeyQuick(keysGen, () => {
|
||||
cy.wait(25000)
|
||||
return acceptTermsIfPresented((result: boolean) => {
|
||||
if (result) {
|
||||
cy.wait(20000)
|
||||
}
|
||||
return logout(() => {
|
||||
return
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
const logout = (callback?: any) => {
|
||||
cy.get('.header-actions .dropdown-toggle').click().then(() => {
|
||||
cy.get('.header-actions .dropdown-menu > .separator').next().click().then(() => {
|
||||
if (callback) callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const updateLicenseKeyQuick = (keys: any, callback: any) => {
|
||||
isLicensingPage((result: boolean) => {
|
||||
if (!result) {
|
||||
visitPage('licensing/update')
|
||||
cy.wait(2000)
|
||||
}
|
||||
inputLicenseKeyPage(keys.licenseKey, keys.activationKey)
|
||||
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
const acceptTermsIfPresented = (callback?: any) => {
|
||||
cy.url().then((url: string) => {
|
||||
if (url.includes('licensing/register')) {
|
||||
cy.get('.card-block').scrollTo('bottom').then(() => {
|
||||
cy.get('#checkbox1').click().then(() => {
|
||||
if (callback) callback(true)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
if (callback) callback(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isLicensingPage = (callback: any) => {
|
||||
cy.url().then((url: string) => {
|
||||
callback(url.includes('licensing') && !url.includes('licensing/register'))
|
||||
})
|
||||
}
|
||||
|
||||
const inputLicenseKeyPage = (licenseKey: string, activationKey: string) => {
|
||||
cy.get('button').contains('Paste licence').click()
|
||||
cy.get('.license-key-form textarea', {timeout: longerCommandTimeout}).invoke('val', licenseKey).trigger('input').should('not.be.undefined')
|
||||
cy.get('.activation-key-form textarea', {timeout: longerCommandTimeout}).invoke('val', activationKey).trigger('input').should('not.be.undefined')
|
||||
cy.get('button.apply-keys').click()
|
||||
}
|
||||
|
||||
const visitPage = (url: string) => {
|
||||
cy.visit(`${hostUrl}${appLocation}/#/${url}`);
|
||||
}
|
||||
const generateKeys = async (valid_until: string, users_allowed: number, hot_license_key: string, demo: boolean, resultCallback?: any) => {
|
||||
let keyPair = await window.crypto.subtle.generateKey({
|
||||
name: "RSA-OAEP",
|
||||
modulusLength: 2024,
|
||||
publicExponent: new Uint8Array([1, 0, 1]),
|
||||
hash: "SHA-256"
|
||||
},
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
)
|
||||
|
||||
let licenseData = {
|
||||
valid_until: valid_until,
|
||||
users_allowed: users_allowed,
|
||||
hot_license_key: hot_license_key,
|
||||
site_id: site_id_SASJS,
|
||||
demo: demo
|
||||
}
|
||||
|
||||
console.log('License data', licenseData)
|
||||
|
||||
let encoded = new TextEncoder().encode(JSON.stringify(licenseData))
|
||||
|
||||
console.log(encoded)
|
||||
|
||||
let cipher = await window.crypto.subtle.encrypt(
|
||||
{
|
||||
name: "RSA-OAEP"
|
||||
},
|
||||
keyPair.publicKey,
|
||||
encoded
|
||||
).then((value) => {
|
||||
return value
|
||||
}, (err) => {
|
||||
console.log('Encrpyt error', err)
|
||||
})
|
||||
|
||||
if (!cipher) {
|
||||
alert('Encryptin keys failed')
|
||||
throw new Error('Encryptin keys failed')
|
||||
}
|
||||
|
||||
let privateKeyBytes = await window.crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
|
||||
|
||||
let activationKey = await arrayBufferToBase64(privateKeyBytes)
|
||||
let licenseKey = await arrayBufferToBase64(cipher)
|
||||
|
||||
if (resultCallback) resultCallback({
|
||||
activationKey,
|
||||
licenseKey
|
||||
})
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands.ts'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
import 'cypress-plugin-tab'
|
||||
import "cypress-real-events";
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": "../node_modules",
|
||||
"target": "es6",
|
||||
"lib": ["es2019", "dom"],
|
||||
"types": ["cypress", "cypress-real-events"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export const deleteDownloadsFolder = () => {
|
||||
const downloadsFolder = Cypress.config('downloadsFolder')
|
||||
cy.task('deleteFolder', downloadsFolder)
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
export const base64ToArrayBuffer = (base64: string) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const dataUrl = "data:application/octet-binary;base64," + base64;
|
||||
|
||||
fetch(dataUrl)
|
||||
.then(res => res.arrayBuffer())
|
||||
.then(buffer => {
|
||||
resolve(new Uint8Array(buffer))
|
||||
}).catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const arrayBufferToBase64 = (arrayBuffer: any) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const blob = new Blob([arrayBuffer])
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = async function(event){
|
||||
if (event.target) {
|
||||
var base64: any = event.target.result
|
||||
base64 = base64.substring(37, base64.length)
|
||||
|
||||
resolve(base64)
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
})
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
module.exports = {
|
||||
mode: "development",
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"]
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
exclude: [/node_modules/],
|
||||
use: [
|
||||
{
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
// skip typechecking for speed
|
||||
transpileOnly: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
},
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/datacontroller'),
|
||||
subdir: '.',
|
||||
reporters: [
|
||||
{ type: 'html' },
|
||||
{ type: 'text-summary' }
|
||||
]
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
browsers: ['Chrome', 'ChromeHeadless', 'ChromeHeadlessCI'],
|
||||
restartOnFileChange: true,
|
||||
customLaunchers: {
|
||||
ChromeHeadlessCI: {
|
||||
base: 'ChromeHeadless',
|
||||
flags: ['--no-sandbox']
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
Binary file not shown.
|
@ -0,0 +1,28 @@
|
|||
const licenseChecker = require('license-checker')
|
||||
|
||||
const check = (cwd) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
licenseChecker.init(
|
||||
{
|
||||
production: true,
|
||||
start: cwd,
|
||||
excludePrivatePackages: true,
|
||||
onlyAllow:
|
||||
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
|
||||
excludePackages:
|
||||
'@cds/city@1.1.0;@handsontable/angular@13.0.0;handsontable@13.0.0;hyperformula@2.5.0;jackspeak@2.2.0;path-scurry@1.7.0'
|
||||
},
|
||||
(error, json) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(json)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
check(process.cwd(), true)
|
||||
.then((res) => console.log('All packages are licensed properly'))
|
||||
.catch((err) => console.log('license checker err', err))
|
|
@ -0,0 +1,8 @@
|
|||
server {
|
||||
listen 3000;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,128 @@
|
|||
{
|
||||
"name": "dc-client",
|
||||
"description": "dc-client",
|
||||
"angular-cli": {},
|
||||
"scripts": {
|
||||
"start": "node --max_old_space_size=4096 node_modules/@angular/cli/bin/ng serve",
|
||||
"prestart": "npm run generate-eula",
|
||||
"generate-eula": "node ./src/eula.ts",
|
||||
"license-checker": "node licenseChecker.js",
|
||||
"prebuild": "node ./src/version.ts && npm run generate-eula && npm run license-checker",
|
||||
"build": "node --max_old_space_size=4096 node_modules/@angular/cli/bin/ng build --configuration production",
|
||||
"postbuild": "rimraf dist/3rdpartylicenses.txt",
|
||||
"build-dev": "node --max_old_space_size=4096 node_modules/@angular/cli/bin/ng build --configuration development",
|
||||
"build-watch": "node --max_old_space_size=4096 node_modules/@angular/cli/bin/ng build --configuration development --watch",
|
||||
"sync": "./node_modules/.bin/watch --wait=3 \"echo Account: ${npm_config_account} && npm run deploy_${npm_config_server} --account=${npm_config_account} && notify-send 'Data Controller' 'App is synced!' ; echo 'App is synced!'\" dist",
|
||||
"deploy_sas9": "rsync -avhe ssh ./dist/* --delete ${npm_config_account}@sas.4gl.io:/opt/sas/sas9/config/Lev1/Web/WebServer/htdocs/${npm_config_account}/dc/dev",
|
||||
"deploy_viya": "rsync -avhe ssh ./dist/* --delete ${npm_config_account}@sas.4gl.io:/var/www/html/${npm_config_account}/dc/dev",
|
||||
"deploy_sasjs": "rsync -avhe ssh ./dist/* --delete root@${npm_config_account}.4gl.io:/var/www/html/dc/dev",
|
||||
"viyabuild": "cd build; ./viyabuild.sh",
|
||||
"lint": "cd .. && npm run lint",
|
||||
"test": "ng test",
|
||||
"test:headless": "ng test --browsers ChromeHeadless",
|
||||
"watch": "ng test watch=true",
|
||||
"pree2e": "webdriver-manager update",
|
||||
"e2e": "protractor protractor.config.js",
|
||||
"postinstall": "node ./src/version.ts && npm run add-githook",
|
||||
"add-githook": "[ -d ../.git ] && git config core.hooksPath ./.git-hooks || true",
|
||||
"cypress": "cypress open",
|
||||
"cy:run": "cypress run",
|
||||
"audit:prod": "npm audit --omit=dev",
|
||||
"sasdocs": "sasjs doc && ./sasjs/utils/deploydocs.sh",
|
||||
"typedoc": "typedoc --options typedoc.json && cd ../dc-devdocs"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^16.1.2",
|
||||
"@angular/cdk": "^15.2.0",
|
||||
"@angular/common": "^16.1.2",
|
||||
"@angular/compiler": "^16.1.2",
|
||||
"@angular/core": "^16.1.2",
|
||||
"@angular/forms": "^16.1.2",
|
||||
"@angular/platform-browser": "^16.1.2",
|
||||
"@angular/platform-browser-dynamic": "^16.1.2",
|
||||
"@angular/router": "^16.1.2",
|
||||
"@cds/core": "^6.4.2",
|
||||
"@clr/angular": "^13.17.0",
|
||||
"@clr/icons": "^13.0.2",
|
||||
"@clr/ui": "^13.17.0",
|
||||
"@handsontable/angular": "^13.0.0",
|
||||
"@sasjs/adapter": "4.3.6",
|
||||
"@sasjs/utils": "^3.3.0",
|
||||
"@sheet/crypto": "1.20211122.1",
|
||||
"@types/d3-graphviz": "^2.6.7",
|
||||
"@types/text-encoding": "0.0.35",
|
||||
"base64-arraybuffer": "^0.2.0",
|
||||
"buffer": "^5.4.3",
|
||||
"crypto-browserify": "3.12.0",
|
||||
"crypto-js": "^3.3.0",
|
||||
"d3-graphviz": "^5.0.2",
|
||||
"fs-extra": "^7.0.1",
|
||||
"handsontable": "^13.0.0",
|
||||
"https-browserify": "1.0.0",
|
||||
"hyperformula": "^2.5.0",
|
||||
"iconv-lite": "^0.5.0",
|
||||
"jquery-datetimepicker": "^2.5.21",
|
||||
"jsrsasign": "^10.2.0",
|
||||
"marked": "^5.0.0",
|
||||
"moment": "^2.26.0",
|
||||
"ngx-clipboard": "^16.0.0",
|
||||
"ngx-json-viewer": "file:libraries/ngx-json-viewer-3.2.1.tgz",
|
||||
"nodejs": "0.0.0",
|
||||
"numbro": "^2.1.1",
|
||||
"os-browserify": "0.3.0",
|
||||
"rxjs": "^7.8.0",
|
||||
"save-svg-as-png": "^1.4.17",
|
||||
"stream-browserify": "3.0.0",
|
||||
"stream-http": "3.2.0",
|
||||
"text-encoding": "^0.7.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^16.1.0",
|
||||
"@angular-eslint/builder": "16.0.3",
|
||||
"@angular-eslint/eslint-plugin": "16.0.3",
|
||||
"@angular-eslint/eslint-plugin-template": "16.0.3",
|
||||
"@angular-eslint/schematics": "16.0.3",
|
||||
"@angular-eslint/template-parser": "16.0.3",
|
||||
"@angular/cli": "^16.1.0",
|
||||
"@angular/compiler-cli": "^16.1.2",
|
||||
"@cypress/webpack-preprocessor": "^5.11.1",
|
||||
"@types/core-js": "^2.5.5",
|
||||
"@types/crypto-js": "^4.0.1",
|
||||
"@types/es6-shim": "^0.31.39",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/lodash-es": "^4.17.3",
|
||||
"@types/marked": "^4.3.0",
|
||||
"@types/node": "12.20.50",
|
||||
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||
"@typescript-eslint/parser": "^5.29.0",
|
||||
"core-js": "^2.5.4",
|
||||
"cypress": "^9.5.3",
|
||||
"cypress-file-upload": "^5.0.8",
|
||||
"cypress-plugin-tab": "^1.0.5",
|
||||
"cypress-real-events": "^1.7.6",
|
||||
"es6-shim": "^0.35.5",
|
||||
"eslint": "^8.33.0",
|
||||
"git-describe": "^4.0.4",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"karma": "~6.3.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.1.0",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "~1.7.0",
|
||||
"license-checker": "25.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mochawesome": "^7.1.3",
|
||||
"mutationobserver-shim": "^0.3.3",
|
||||
"replace-in-file": "^6.3.5",
|
||||
"rimraf": "3.0.2",
|
||||
"ts-loader": "^9.2.8",
|
||||
"ts-node": "^3.3.0",
|
||||
"typedoc": "^0.23.24",
|
||||
"typescript": "~4.9.4",
|
||||
"wait-on": "^6.0.1",
|
||||
"watch": "^1.0.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
npm run cy:run -- --browser chrome --spec "cypress/integration/liveness.tests.ts"
|
||||
npm run cy:run -- --browser chrome --spec "cypress/integration/editor.tests.ts"
|
||||
npm run cy:run -- --browser chrome --spec "cypress/integration/excel.tests.ts"
|
||||
npm run cy:run -- --browser chrome --spec "cypress/integration/filtering.tests.ts"
|
||||
npm run cy:run -- --browser chrome --spec "cypress/integration/licensing.tests.ts"
|
|
@ -0,0 +1,118 @@
|
|||
import { QueryClause } from './models/TableData'
|
||||
|
||||
interface FilterCache {
|
||||
cols: any[]
|
||||
vals: any[]
|
||||
groupLogic: string
|
||||
whereClause: string
|
||||
libds: string
|
||||
clauses: any[]
|
||||
query: QueryClause[]
|
||||
}
|
||||
|
||||
interface ViewboxCache {
|
||||
[key: number]: {
|
||||
filter: FilterCache
|
||||
}
|
||||
}
|
||||
|
||||
export const initFilter: { filter: FilterCache } = {
|
||||
filter: {
|
||||
cols: <any[]>[],
|
||||
vals: <any[]>[],
|
||||
groupLogic: <string>'',
|
||||
whereClause: <string>'',
|
||||
libds: <string>'',
|
||||
clauses: <any>[],
|
||||
query: <QueryClause[]>[]
|
||||
}
|
||||
}
|
||||
|
||||
export const globals: {
|
||||
rootParam: string
|
||||
editor: any
|
||||
viewer: any
|
||||
viewboxes: ViewboxCache
|
||||
lineage: any
|
||||
metadata: any
|
||||
viyaApi: any
|
||||
usernav: any
|
||||
operators: any
|
||||
[key: string]: any
|
||||
} = {
|
||||
rootParam: <string>'',
|
||||
editor: {
|
||||
startupSet: <boolean>false,
|
||||
treeNodeLibraries: <any[] | null>[],
|
||||
libsAndTables: <any[]>[],
|
||||
libraries: <String[] | undefined>[],
|
||||
library: <string>'',
|
||||
table: <string>'',
|
||||
filter: <FilterCache>{
|
||||
cols: <any[]>[],
|
||||
vals: <any[]>[],
|
||||
groupLogic: <string>'',
|
||||
whereClause: <string>'',
|
||||
libds: <string>'',
|
||||
clauses: <any>[],
|
||||
query: <QueryClause[]>[]
|
||||
}
|
||||
},
|
||||
viewer: {
|
||||
startupSet: <boolean>false,
|
||||
tablesSet: <boolean>false,
|
||||
libraries: <any[]>[],
|
||||
tables: <any>null,
|
||||
library: '',
|
||||
table: '',
|
||||
libinfo: [],
|
||||
librariesSearch: <string>'',
|
||||
filter: <FilterCache>{
|
||||
cols: <any[]>[],
|
||||
vals: <any[]>[],
|
||||
groupLogic: <string>'',
|
||||
whereClause: <string>'',
|
||||
libds: <string>'',
|
||||
clauses: <any>[],
|
||||
query: <QueryClause[]>[]
|
||||
},
|
||||
currentSelection: <string>''
|
||||
},
|
||||
viewboxes: {},
|
||||
lineage: {
|
||||
libraryList: <any[] | undefined>[],
|
||||
tablesList: <any[] | undefined>[],
|
||||
columnsList: <any[] | undefined>[],
|
||||
librariesSearch: <string>'',
|
||||
lib: <String | null>'',
|
||||
table: <String | undefined>'',
|
||||
column: <String | undefined>'',
|
||||
currentLineagePathLibTable: <String>'',
|
||||
currentLineagePathColumn: <String>''
|
||||
},
|
||||
metadata: {
|
||||
metaDataList: <any[] | undefined>undefined,
|
||||
metaDataSearch: <string>'',
|
||||
metaObjectList: <any[] | undefined>[],
|
||||
metaObjectSearch: <string>'',
|
||||
metaRepositories: <any[] | undefined>undefined,
|
||||
selectedRepository: <string>''
|
||||
},
|
||||
viyaApi: {
|
||||
collectionsList: <any[] | undefined>undefined,
|
||||
collectionsSearch: <string>'',
|
||||
selectedRepository: <string>''
|
||||
},
|
||||
usernav: {
|
||||
userList: <any[] | undefined>undefined,
|
||||
userSearch: <string>'',
|
||||
groupList: <any[] | undefined>undefined,
|
||||
groupSearch: <string>'',
|
||||
roleList: <any[] | undefined>undefined,
|
||||
roleSearch: <string>''
|
||||
},
|
||||
operators: {
|
||||
numOperators: ['=', '<', '>', '<=', '>=', 'BETWEEN', 'IN', 'NOT IN', 'NE'],
|
||||
charOperators: ['=', '<', '>', '<=', '>=', 'CONTAINS', 'IN', 'NOT IN', 'NE']
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<div class="content-area">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex flex-column justify-content-center">
|
||||
<h3 class="text-center">
|
||||
You succesfully edited table
|
||||
<span class="color-blue font-weight-700">{{ libds }}</span>
|
||||
</h3>
|
||||
<p class="text-center">
|
||||
<b>Please choose from the following actions</b>
|
||||
</p>
|
||||
<div class="row d-flex justify-content-center mt-20">
|
||||
<button
|
||||
class="btn btn-sm btn-outline text-center"
|
||||
(click)="submittedTableScreen()"
|
||||
>
|
||||
Go to submitted table screen
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-outline text-center"
|
||||
(click)="viewerTableScreen()"
|
||||
>
|
||||
Go to base table screen
|
||||
</button>
|
||||
<button
|
||||
id="approvalBtn"
|
||||
class="btn btn-sm btn-success-outline text-center"
|
||||
(click)="approveTableScreen()"
|
||||
>
|
||||
Go to approvals screen
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-info-outline text-center"
|
||||
(click)="goBack()"
|
||||
>
|
||||
Go back to editor
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,50 @@
|
|||
import { AfterViewInit, Component, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
|
||||
@Component({
|
||||
selector: 'app-actions',
|
||||
templateUrl: './actions.component.html',
|
||||
styleUrls: ['./actions.component.scss'],
|
||||
host: {
|
||||
class: 'content-container'
|
||||
}
|
||||
})
|
||||
export class ActionsComponent implements OnInit, AfterViewInit {
|
||||
public dsid: any
|
||||
public libds: string | undefined
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
public submittedTableScreen() {
|
||||
this.router.navigateByUrl('/stage/' + this.dsid)
|
||||
}
|
||||
|
||||
public approveTableScreen() {
|
||||
this.router.navigateByUrl('/approve/approveDet/' + this.dsid)
|
||||
}
|
||||
|
||||
public viewerTableScreen() {
|
||||
this.router.navigateByUrl('/view/data/' + this.libds)
|
||||
}
|
||||
|
||||
public goBack() {
|
||||
this.router.navigateByUrl('/editor/' + this.libds)
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.dsid = this.route.snapshot.params['dsid']
|
||||
this.libds = this.route.snapshot.params['libds']
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
let approvalBtn: any = window.document.getElementById('approvalBtn')
|
||||
if (!!approvalBtn) {
|
||||
approvalBtn.focus()
|
||||
}
|
||||
}, 700)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { SidebarComponent } from './shared/sidebar/sidebar.component'
|
||||
import { SoftSelectComponent } from './shared/soft-select/soft-select.component'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharedModule } from './shared/shared.module'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { PipesModule } from './pipes/pipes.module'
|
||||
import { DirectivesModule } from './directives/directives.module'
|
||||
import { AutocompleteModule } from './shared/autocomplete/autocomplete.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [SidebarComponent, SoftSelectComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ClarityModule,
|
||||
RouterModule,
|
||||
SharedModule,
|
||||
PipesModule,
|
||||
DirectivesModule,
|
||||
AutocompleteModule
|
||||
],
|
||||
exports: [SidebarComponent, SoftSelectComponent]
|
||||
})
|
||||
export class AppSharedModule {}
|
|
@ -0,0 +1,299 @@
|
|||
<div class="main-container">
|
||||
<ng-container *ngIf="!router.url.includes('licensing')">
|
||||
<div
|
||||
*ngIf="
|
||||
freeTierBanner && (!licenseExpiringDays || licenseExpiringDays < 0)
|
||||
"
|
||||
class="alert alert-app-level alert-warning"
|
||||
id="demo-banner"
|
||||
role="alert"
|
||||
>
|
||||
<ng-container *ngIf="licenceProblem.value === null">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<clr-icon class="mt-2" shape="warning-standard"></clr-icon>
|
||||
</div>
|
||||
<div class="alert-text">
|
||||
Data Controller (FREE Tier) - to upgrade contact
|
||||
<contact-link classes="color-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a routerLink="/licensing/update" class="update-key"
|
||||
>Update Licence Key</a
|
||||
>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="licenceProblem.value !== null">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<clr-icon class="mt-2" shape="warning-standard"></clr-icon>
|
||||
</div>
|
||||
<div class="alert-text">
|
||||
Data Controller (FREE Tier) - Problem with licence
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
(click)="licenceProblemDetails(licenceProblem.value)"
|
||||
class="update-key cursor-pointer"
|
||||
>More details</a
|
||||
>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="licenseExpiringDays && !freeTierBanner"
|
||||
class="alert alert-app-level alert-danger"
|
||||
id="demo-banner"
|
||||
role="alert"
|
||||
>
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<clr-icon class="mt-2" shape="warning-standard"></clr-icon>
|
||||
</div>
|
||||
|
||||
<div class="alert-text">
|
||||
This license key will expire in {{ licenseExpiringDays }}
|
||||
{{ licenseExpiringDays === 1 ? 'day' : 'days' }}. Please contact
|
||||
<contact-link classes="color-white" />
|
||||
or your reseller to arrange additional licence for site id
|
||||
{{ syssite.getValue() }}.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
*ngIf="!freeTierBanner"
|
||||
routerLink="/licensing/update"
|
||||
class="update-key"
|
||||
>Update Licence Key</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="appOverCapacity"
|
||||
class="alert alert-app-level alert-danger"
|
||||
id="demo-banner"
|
||||
role="alert"
|
||||
>
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<clr-icon class="mt-2" shape="warning-standard"></clr-icon>
|
||||
</div>
|
||||
|
||||
<div class="alert-text">
|
||||
The registered number of users exceeds the limit specified for your
|
||||
license. Please contact
|
||||
<contact-link classes="color-white" />
|
||||
or your reseller to arrange additional licence for site id
|
||||
{{ syssite.getValue() }}.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
*ngIf="!licenseExpiringDays && !freeTierBanner"
|
||||
routerLink="/licensing/update"
|
||||
class="update-key"
|
||||
>Update Licence Key</a
|
||||
>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<header class="app-header">
|
||||
<!-- <button
|
||||
*ngIf="
|
||||
isMainRoute('view') ||
|
||||
(isMainRoute('home') && !router.url.includes('licensing'))
|
||||
"
|
||||
class="header-hamburger-trigger"
|
||||
(click)="toggleSidebar()"
|
||||
type="button"
|
||||
>
|
||||
<span></span>
|
||||
</button> -->
|
||||
|
||||
<div
|
||||
*ngIf="
|
||||
isMainRoute('view') ||
|
||||
(isMainRoute('home') && !router.url.includes('licensing'))
|
||||
"
|
||||
(click)="toggleSidebar()"
|
||||
type="button"
|
||||
class="cursor-pointer select-none ml-10 d-flex clr-justify-content-center clr-align-items-center"
|
||||
>
|
||||
<clr-icon size="24" shape="tree-view"></clr-icon>
|
||||
</div>
|
||||
|
||||
<div class="logo d-flex clr-align-items-center">
|
||||
<a
|
||||
*ngIf="!router.url.includes('deploy')"
|
||||
href="#"
|
||||
[routerLink]="['/']"
|
||||
class="nav-link"
|
||||
>
|
||||
<img class="without-text d-block d-md-none" src="images/dc-logo.svg" />
|
||||
<img
|
||||
class="with-text d-none d-md-block"
|
||||
src="images/datacontroller.svg"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a *ngIf="router.url.includes('deploy')">
|
||||
<span class="clr-icon header-logo ml-10"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ng-container
|
||||
*ngIf="
|
||||
!router.url.includes('deploy') && !router.url.includes('licensing')
|
||||
"
|
||||
>
|
||||
<div class="header-nav d-flex d-sm-none">
|
||||
<clr-dropdown>
|
||||
<button
|
||||
class="nav-icon color-white-i"
|
||||
clrDropdownTrigger
|
||||
aria-label="toggle settings menu"
|
||||
>
|
||||
Menu
|
||||
<!-- <clr-icon size="20" shape="bars"></clr-icon> -->
|
||||
</button>
|
||||
<clr-dropdown-menu *clrIfOpen clrPosition="bottom-left">
|
||||
<a [routerLink]="['/view']" clrDropdownItem>VIEW</a>
|
||||
<a [routerLink]="['/home']" clrDropdownItem>EDIT</a>
|
||||
<a [routerLink]="['/submitted']" clrDropdownItem>REVIEW</a>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="header-nav d-none d-sm-flex">
|
||||
<a
|
||||
[routerLink]="['/view']"
|
||||
class="nav-link nav-text"
|
||||
routerLinkActive="active"
|
||||
>VIEW</a
|
||||
>
|
||||
<a
|
||||
[routerLink]="['/home']"
|
||||
class="nav-link nav-text"
|
||||
[class.active]="
|
||||
router.url.includes('editor') ||
|
||||
router.url.includes('edit-record') ||
|
||||
router.url.includes('home')
|
||||
"
|
||||
>EDIT</a
|
||||
>
|
||||
<a
|
||||
[routerLink]="['/submitted']"
|
||||
[class.active]="
|
||||
router.url.includes('submitted') ||
|
||||
router.url.includes('approve') ||
|
||||
router.url.includes('history')
|
||||
"
|
||||
class="nav-link nav-text cursor-pointer"
|
||||
>REVIEW</a
|
||||
>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="header-actions">
|
||||
<div class="nav-text">
|
||||
<app-loading-indicator></app-loading-indicator>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<app-user-nav-dropdown></app-user-nav-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<nav
|
||||
*ngIf="
|
||||
router.url.includes('submitted') ||
|
||||
router.url.includes('approve') ||
|
||||
router.url.includes('history')
|
||||
"
|
||||
class="subnav"
|
||||
>
|
||||
<ul class="nav">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
[routerLink]="['/submitted']"
|
||||
class="nav-link nav-text"
|
||||
routerLinkActive="active"
|
||||
>SUBMIT</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
[routerLink]="['/approve']"
|
||||
class="nav-link nav-text"
|
||||
routerLinkActive="active"
|
||||
>APPROVE</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
[routerLink]="['/history']"
|
||||
class="nav-link nav-text"
|
||||
routerLinkActive="active"
|
||||
>HISTORY</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<app-alerts *ngIf="!errTop"></app-alerts>
|
||||
<app-requests-modal [(opened)]="requestsModal"></app-requests-modal>
|
||||
|
||||
<!-- <app-terms *ngIf="showRegistration"></app-terms> -->
|
||||
|
||||
<router-outlet *ngIf="startupDataLoaded"></router-outlet>
|
||||
|
||||
<app-login></app-login>
|
||||
<app-alerts *ngIf="errTop"></app-alerts>
|
||||
<app-info-modal
|
||||
*ngFor="let abort of sasjsAborts"
|
||||
[data]="abort"
|
||||
[forceReload]="!startupDataLoaded && sasjsAborts.length === 1"
|
||||
(onConfirmModalClick)="closeAbortModal(abort.id!)"
|
||||
>
|
||||
</app-info-modal>
|
||||
|
||||
<clr-modal
|
||||
appDragNdrop
|
||||
[(clrModalOpen)]="demoLimitNotice.open"
|
||||
[clrModalClosable]="true"
|
||||
[clrModalSize]="'lg'"
|
||||
class="position-relative"
|
||||
>
|
||||
<h3 class="modal-title">
|
||||
Locked Feature ({{ demoLimitNotice.featureName }})
|
||||
<clr-icon size="20" shape="lock"></clr-icon>
|
||||
</h3>
|
||||
|
||||
<div class="modal-body">
|
||||
Contact
|
||||
<contact-link />
|
||||
with your site id ({{ syssite.value }}) to activate!
|
||||
</div>
|
||||
</clr-modal>
|
||||
</div>
|
||||
|
||||
<!-- App Loading Page -->
|
||||
<div *ngIf="!startupDataLoaded" class="app-loading">
|
||||
<img class="loading-logo" src="images/datacontroller.svg" />
|
||||
|
||||
<div *ngIf="appActive === null" class="slider">
|
||||
<div class="line"></div>
|
||||
<div class="subline inc"></div>
|
||||
<div class="subline dec"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /App Loading Page -->
|
|
@ -0,0 +1,461 @@
|
|||
// Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
||||
// This software is released under MIT license.
|
||||
// The full license information can be found in LICENSE in the root directory of this project.
|
||||
app-requests-modal {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
header.app-header {
|
||||
background: #314351 !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.logo img.without-text {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.logo img.with-text {
|
||||
width: 210px;
|
||||
}
|
||||
|
||||
.header-hamburger-trigger {
|
||||
display: block;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.demo-expired-notice {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
z-index: 105;
|
||||
background: rgba(33, 33, 33, .5);
|
||||
|
||||
.expired-details {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 30px;
|
||||
z-index: 110;
|
||||
background: #314351;
|
||||
|
||||
.expired-notice {
|
||||
color: #e0e0e0;
|
||||
font-size: 16px;
|
||||
|
||||
.mailto {
|
||||
color: #8dc53e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-container .update-key {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: white;
|
||||
padding: 0px 10px;
|
||||
background: #00000026;
|
||||
}
|
||||
|
||||
.alert-icon-wrapper {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
padding-left: 10px;
|
||||
|
||||
clr-icon {
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
.header-actions {
|
||||
.dropdown {
|
||||
position: unset; //without it, when opening user dropdown scrollbar was displaying without reason
|
||||
}
|
||||
}
|
||||
|
||||
.nav
|
||||
.nav-link {
|
||||
color: #fafafa;
|
||||
opacity: .9;
|
||||
line-height: 1.45rem;
|
||||
}
|
||||
|
||||
.nav .nav-link:hover {
|
||||
box-shadow: inset 0 -3px 0 transparent;
|
||||
transition: box-shadow .2s ease-in;
|
||||
}
|
||||
|
||||
.nav
|
||||
.nav-link:hover {
|
||||
color: #fafafa;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav .nav-link.active {
|
||||
background: #61717D;
|
||||
opacity: 1;
|
||||
box-shadow: inset 0 -3px transparent;
|
||||
// padding: 0 1rem 0 1rem;
|
||||
}
|
||||
|
||||
.nav .nav-item {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.notf {
|
||||
background: #16a57a;
|
||||
color: #fffcfc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn.btn-success {
|
||||
border-color: #62a420;
|
||||
background-color: #16a57a!important;
|
||||
color: #fff;
|
||||
}
|
||||
.btn.btn-success:hover {
|
||||
background-color: #2add39;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.toggle-switch input[type=checkbox]:checked+label:before {
|
||||
border-color: #61717D;
|
||||
background-color: #61717D;
|
||||
transition: .15s ease-in;
|
||||
transition-property: border-color,background-color;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
min-height: 100vh !important;
|
||||
}
|
||||
|
||||
.main-container .content-container .content-area {
|
||||
padding: 0rem 1rem 1rem 1rem;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
z-index: 0!important;
|
||||
}
|
||||
|
||||
.navBarResp {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: #495A67;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.navBarResp {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
background: #495A67;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.main-container .sub-nav.clr-nav-level-1 .nav .nav-link, .main-container .sub-nav.clr-nav-level-2 .nav .nav-link, .main-container .subnav.clr-nav-level-1 .nav .nav-link, .main-container .subnav.clr-nav-level-2 .nav .nav-link {
|
||||
padding: 0 .5rem 0 1rem;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border-radius: .125rem 0 0 .125rem;
|
||||
color: #95c84b;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.card-block, .card-footer {
|
||||
padding: 10px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.main-container[_ngcontent-c0] .content-container[_ngcontent-c0] .content-area[_ngcontent-c0] {
|
||||
padding: 0rem 0rem 0rem 0rem;
|
||||
}
|
||||
|
||||
}
|
||||
::ng-deep {
|
||||
.htInvalid {
|
||||
background: black!important;
|
||||
}
|
||||
|
||||
@media screen and (max-width:480px) {
|
||||
h2 {
|
||||
font-size: .7rem!important;
|
||||
|
||||
}
|
||||
h3 {
|
||||
font-size: .7rem;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
padding: 0rem 1rem 0rem 1rem;
|
||||
}
|
||||
|
||||
.btn-primary .btn, .btn.btn-primary {
|
||||
border-color: #314351;
|
||||
background-color: #314351;
|
||||
color: #fff;
|
||||
}
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
-webkit-appearance: none!important;
|
||||
border-radius: .125rem;
|
||||
border: 1px solid;
|
||||
min-width: 3rem;
|
||||
max-width: 15rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
vertical-align: middle;
|
||||
line-height: 1.5rem;
|
||||
letter-spacing: .12em;
|
||||
font-size: .5rem;
|
||||
font-weight: 500;
|
||||
height: 1.5rem;
|
||||
padding: 0 .5rem;
|
||||
border-color: #314351;
|
||||
background-color: transparent;
|
||||
color: #314351;
|
||||
}
|
||||
|
||||
.btn.btn-outline {
|
||||
border-color: #314351;
|
||||
background-color: transparent;
|
||||
color: #314351;
|
||||
}
|
||||
|
||||
.btn.btn-outline:hover {
|
||||
border-color: #314351;
|
||||
background-color: #495A67;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn.btn-success-outline:hover {
|
||||
background-color: #5ea71f;
|
||||
color: #fff7f7;
|
||||
border-color: #9a9696;
|
||||
}
|
||||
// .btn.btn-success-outline {
|
||||
// border-color: #266900;
|
||||
// background-color: transparent;
|
||||
// color: #318700;
|
||||
// }
|
||||
// .wtSpreader {
|
||||
|
||||
// }
|
||||
|
||||
.htMobileEditorContainer .inputs textarea {
|
||||
font-size: 13pt;
|
||||
border: 2px solid #485967;
|
||||
border-radius: 4px;
|
||||
-webkit-appearance: none;
|
||||
box-shadow: none;
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
right: 0px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
padding: 7pt;
|
||||
width: 290px;
|
||||
}
|
||||
|
||||
.htMobileEditorContainer .positionControls {
|
||||
width: 333px;
|
||||
position: absolute;
|
||||
right: 5pt;
|
||||
top: 50px;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.htMobileEditorContainer.active {
|
||||
display: block;
|
||||
height: 120px;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.handsontable {
|
||||
background-color: #ffffff;
|
||||
// border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.handsontable th {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* Left and right */
|
||||
.ht_clone_left th {
|
||||
border-right: 1px solid #ccc;
|
||||
border-left: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* Column headers */
|
||||
.ht_clone_top th {
|
||||
border-top: 1px solid #ccc;
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.ht_clone_top_left_corner th {
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.ht_master tr:nth-of-type(odd) > td {
|
||||
background-color: #f3f3f3;
|
||||
border: 1px solid rgb(197, 197, 197);
|
||||
border-bottom: 1px solid rgb(236, 235, 235);
|
||||
// padding: 1px 1px;
|
||||
}
|
||||
|
||||
.ht_master tr:nth-of-type(even) > td {
|
||||
background-color: white;
|
||||
border: 1px solid rgb(197, 197, 197);
|
||||
border-bottom: 1px solid rgb(236, 235, 235);
|
||||
// padding: 1px 1px;
|
||||
}
|
||||
|
||||
.wtBorder {
|
||||
background-color: #495A67!important;
|
||||
}
|
||||
|
||||
.handsontable .handsontable.ht_clone_top .wtHider {
|
||||
padding: 0 0 0px 0!important;
|
||||
margin: 0px;
|
||||
border-bottom: 3px solid #d6d3d3;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
background: #F5F6FF;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 0.125rem 0 0 #d7d7d7;
|
||||
border-radius: .0rem;
|
||||
border: 1px solid transparent;
|
||||
// min-height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
.datagrid-compact, .datagrid-history{
|
||||
.datagrid {
|
||||
border-collapse: separate;
|
||||
border: 1px solid transparent;
|
||||
border-radius: .125rem;
|
||||
background-color: #fff;
|
||||
color: #565656;
|
||||
margin: 0;
|
||||
margin-top: 1rem;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
padding: 15px 15px 50px 15px;
|
||||
}
|
||||
.datagrid-foot {
|
||||
-webkit-box-pack: end;
|
||||
-ms-flex-pack: end;
|
||||
justify-content: flex-end;
|
||||
height: 1.5rem;
|
||||
padding: 0 .5rem;
|
||||
line-height: calc(1.5rem - 3px);
|
||||
font-size: .45833rem;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #ccc;
|
||||
border-radius: 0px;
|
||||
// border-radius: 0 0 .125rem .125rem;
|
||||
}
|
||||
.datagrid-footer {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 2px;
|
||||
}
|
||||
.datagrid .datagrid-head {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: .083333rem;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
background: #f5f6ff;
|
||||
padding: .5rem 0;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 1px 0.125rem hsla(0,0%,45%,.25);
|
||||
min-width: 5rem;
|
||||
max-width: 15rem;
|
||||
border-radius: .125rem;
|
||||
visibility: hidden;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.table {
|
||||
border-collapse: separate;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0px;
|
||||
background-color: #fff;
|
||||
color: #565656;
|
||||
margin: 0;
|
||||
margin-top: 1rem;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-size: .45833rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: .03em;
|
||||
background-color: #fff;
|
||||
vertical-align: bottom;
|
||||
border-bottom: 1px solid #ccc;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 2px solid #e4e4e4;
|
||||
padding: 0 0 .5rem 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.main-container .content-container {
|
||||
min-height: 0px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.app-loading {
|
||||
.loading-logo {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
import { ChangeDetectorRef, Component, ElementRef } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { VERSION } from '../environments/version'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { Location } from '@angular/common'
|
||||
import '@clr/icons'
|
||||
import '@clr/icons/shapes/all-shapes'
|
||||
import { globals } from './_globals'
|
||||
import * as moment from 'moment'
|
||||
import { EventService } from './services/event.service'
|
||||
import { AppService } from './services/app.service'
|
||||
import { InfoModal } from './models/InfoModal'
|
||||
import { DcAdapterSettings } from './models/DcAdapterSettings'
|
||||
import { AppStoreService } from './services/app-store.service'
|
||||
import { LicenceService } from './services/licence.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
private dcAdapterSettings: DcAdapterSettings | undefined
|
||||
public commitVer: string
|
||||
public version: any
|
||||
public routeUrl: any
|
||||
public errTop: boolean | undefined
|
||||
public licenseExpiringDays: number | null = null
|
||||
public sasjsAborts: InfoModal[] = []
|
||||
|
||||
public editorActive: boolean = false
|
||||
public approveActive: boolean = false
|
||||
public freeTierBanner: boolean = this.licenceService.isAppFreeTier.value
|
||||
public licenceProblem = this.licenceService.licenceProblem
|
||||
public appOverCapacity: boolean = false
|
||||
public appActive: boolean | null = null
|
||||
public requestsModal: boolean = false
|
||||
public showRegistration: boolean = true
|
||||
public startupDataLoaded: boolean = false
|
||||
public demoLimitNotice: { open: boolean; featureName: string } = {
|
||||
open: false,
|
||||
featureName: ''
|
||||
}
|
||||
|
||||
public syssite = this.appService.syssite
|
||||
public licenceState = this.licenceService.licenceState
|
||||
|
||||
constructor(
|
||||
private appService: AppService,
|
||||
private licenceService: LicenceService,
|
||||
public router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private location: Location,
|
||||
private eventService: EventService,
|
||||
private appStoreService: AppStoreService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private elementRef: ElementRef
|
||||
) {
|
||||
this.parseDcAdapterSettings()
|
||||
;(window as any).appinfo = () => {
|
||||
const licenseKeyData = this.licenceService.getLicenseKeyData()
|
||||
|
||||
if (licenseKeyData) {
|
||||
const expiry_date = moment(
|
||||
licenseKeyData.valid_until,
|
||||
'YYYY-MM-DD'
|
||||
).startOf('day')
|
||||
const current_date = moment().startOf('day')
|
||||
const daysToExpiry = expiry_date.diff(current_date, 'days')
|
||||
|
||||
licenseKeyData.valid_until += ` (${daysToExpiry} ${
|
||||
daysToExpiry === 1 ? 'day' : 'days'
|
||||
} remaining)`
|
||||
|
||||
if (isNaN(daysToExpiry)) licenseKeyData.valid_until = 'Unlimited'
|
||||
}
|
||||
|
||||
console.table({
|
||||
'Adapter version': VERSION.adapterVersion || 'n/a',
|
||||
'App version': (VERSION.tag || '').replace('v', ''),
|
||||
'Build timestamp': moment(parseInt(VERSION.timestamp)).format(
|
||||
'DD-MMM-YYYY HH:MM'
|
||||
),
|
||||
'...': '...'
|
||||
})
|
||||
}
|
||||
|
||||
this.subscribeToLicenseEvents()
|
||||
|
||||
this.commitVer = (VERSION.tag || '').replace('v', '') + '.' + VERSION.hash
|
||||
router.events.subscribe((val) => {
|
||||
this.routeUrl = this.router.url
|
||||
|
||||
if (typeof this.routeUrl !== 'undefined' && this.routeUrl.length > 4) {
|
||||
let rootParam = this.routeUrl.split('/')[1]
|
||||
|
||||
if (rootParam === 'editor') {
|
||||
this.errTop = true
|
||||
this.editorActive = true
|
||||
this.approveActive = false
|
||||
} else if (rootParam === 'home') {
|
||||
this.errTop = false
|
||||
this.editorActive = true
|
||||
this.approveActive = false
|
||||
} else {
|
||||
this.errTop = true
|
||||
this.editorActive = false
|
||||
}
|
||||
|
||||
globals.rootParam = rootParam
|
||||
}
|
||||
|
||||
if (typeof this.routeUrl !== 'undefined' && this.routeUrl.length > 6) {
|
||||
if (this.routeUrl.includes('approveDet')) {
|
||||
this.approveActive = true
|
||||
} else if (this.routeUrl.includes('toapprove')) {
|
||||
this.approveActive = true
|
||||
} else {
|
||||
this.approveActive = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.subscribeToShowAbortModal()
|
||||
this.subscribeToRequestsModal()
|
||||
this.subscribeToStartupData()
|
||||
this.subscribeToAppActive()
|
||||
this.subscribeToDemoLimitModal()
|
||||
|
||||
/* In Viya streaming apps, content is served within an iframe. This code
|
||||
makes that iframe "full screen" so it looks like a regular window. */
|
||||
if (window.frameElement) {
|
||||
window.frameElement.setAttribute(
|
||||
'style',
|
||||
'height:100%;width:100%;position:absolute'
|
||||
)
|
||||
window.frameElement.setAttribute('allowfullscreen', '')
|
||||
window.frameElement.setAttribute('frameborder', '0')
|
||||
window.frameElement.setAttribute('marginheight', '0')
|
||||
window.frameElement.setAttribute('marginwidth', '0')
|
||||
window.frameElement.setAttribute('scrolling', 'auto')
|
||||
window.focus()
|
||||
}
|
||||
}
|
||||
|
||||
private parseDcAdapterSettings() {
|
||||
const sasjsElement = document.querySelector('sasjs')
|
||||
|
||||
if (!sasjsElement) {
|
||||
this.licenceService.deactivateApp()
|
||||
setTimeout(() => {
|
||||
this.eventService.showAbortModal(
|
||||
null,
|
||||
"Please make sure 'SASJS' tag with config attributes is added to index.html",
|
||||
null,
|
||||
'SASjs Config not found'
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const getAppAttribute = (attribute: string) =>
|
||||
sasjsElement.getAttribute(attribute) || undefined
|
||||
|
||||
const dcAdapterSettings = {
|
||||
serverUrl: getAppAttribute('serverUrl') || '',
|
||||
appLoc: getAppAttribute('appLoc') || '',
|
||||
serverType: getAppAttribute('serverType'),
|
||||
loginMechanism: getAppAttribute('loginMechanism') || '',
|
||||
adminGroup: getAppAttribute('adminGroup') || '',
|
||||
dcPath: getAppAttribute('dcPath') || '',
|
||||
debug: getAppAttribute('debug') === 'true' || false,
|
||||
useComputeApi: this.parseComputeApi(getAppAttribute('useComputeApi')),
|
||||
contextName: getAppAttribute('contextName') || '',
|
||||
hotLicenceKey: getAppAttribute('hotLicenceKey') || ''
|
||||
}
|
||||
|
||||
this.dcAdapterSettings = dcAdapterSettings as any
|
||||
this.appStoreService.setDcAdapterSettings(dcAdapterSettings as any)
|
||||
this.appService.sasServiceInit()
|
||||
}
|
||||
|
||||
public licenceProblemDetails(url: string) {
|
||||
this.router.navigateByUrl(url)
|
||||
}
|
||||
/**
|
||||
* Based on string provided we return true, false or null
|
||||
* True -> Compute API
|
||||
* False -> JES API
|
||||
* Null -> JES WEB
|
||||
* @param value provided in the html <sasjs> tag
|
||||
* @returns true, false or null
|
||||
*/
|
||||
private parseComputeApi(value: string | undefined): boolean | null {
|
||||
if (value === undefined) return null
|
||||
|
||||
if (value === 'undefined' || value === 'null') return null
|
||||
|
||||
return value === 'true' || false
|
||||
}
|
||||
|
||||
public subscribeToDemoLimitModal() {
|
||||
this.eventService.onDemoLimitModalShow.subscribe((featureName: string) => {
|
||||
this.demoLimitNotice = {
|
||||
open: true,
|
||||
featureName
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public subscribeToLicenseEvents() {
|
||||
this.licenceService.isAppFreeTier.subscribe((isAppFreeTier: boolean) => {
|
||||
this.freeTierBanner = isAppFreeTier
|
||||
})
|
||||
|
||||
this.licenceService.licenseExpiresInDays.subscribe(
|
||||
(licenseExpiringDays: number | null) => {
|
||||
if (licenseExpiringDays && licenseExpiringDays <= 14)
|
||||
this.licenseExpiringDays = licenseExpiringDays
|
||||
}
|
||||
)
|
||||
|
||||
this.licenceService.isAppOverCapacity.subscribe(
|
||||
(isAppOverAppCapacity: boolean) => {
|
||||
this.appOverCapacity = isAppOverAppCapacity
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
public subscribeToAppActive() {
|
||||
this.licenceService.isAppActivated.subscribe((value: any) => {
|
||||
this.appActive = value
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Listnes to an event that is fired when showing abort modal
|
||||
* Then pushes object to array and modal is displayed
|
||||
* `abortId` is calculated and assigned so that modal can be removed and closed
|
||||
* it's an incrementing number
|
||||
*/
|
||||
public subscribeToShowAbortModal() {
|
||||
this.eventService.onShowAbortModal.subscribe((sasjsAbort: InfoModal) => {
|
||||
let abortId = this.sasjsAborts.length + 1
|
||||
sasjsAbort.id = abortId
|
||||
this.sasjsAborts.push(sasjsAbort)
|
||||
this.cdr.detectChanges() //Changes were not triggered while hot is focused
|
||||
})
|
||||
}
|
||||
|
||||
public subscribeToStartupData() {
|
||||
this.eventService.onStartupDataLoaded.subscribe(() => {
|
||||
this.startupDataLoaded = true
|
||||
})
|
||||
}
|
||||
|
||||
public subscribeToRequestsModal() {
|
||||
this.eventService.onRequestsModalOpen.subscribe((value: boolean) => {
|
||||
this.requestsModal = true
|
||||
})
|
||||
}
|
||||
|
||||
public closeAbortModal(abortId: number) {
|
||||
let abortIndex = this.sasjsAborts.findIndex((abort) => abort.id === abortId)
|
||||
this.sasjsAborts.splice(abortIndex, 1)
|
||||
}
|
||||
|
||||
public toggleSidebar() {
|
||||
this.eventService.toggleSidebar()
|
||||
}
|
||||
|
||||
public isMainRoute(route: string): boolean {
|
||||
return this.router.url.includes(route)
|
||||
}
|
||||
|
||||
public openLicencingPage() {
|
||||
this.router.navigateByUrl('/licensing/update')
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
declare module 'save-svg-as-png'
|
||||
|
||||
declare interface Navigator {
|
||||
msSaveBlob: (blob: any, defaultName?: string) => boolean
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import { HttpClientModule } from '@angular/common/http'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { ClarityModule } from '@clr/angular'
|
||||
|
||||
import { AppComponent } from './app.component'
|
||||
import { ROUTING } from './app.routing'
|
||||
import { NotFoundComponent } from './not-found/not-found.component'
|
||||
|
||||
import { SasStoreService } from './services/sas-store.service'
|
||||
import { SharedModule } from './shared/shared.module'
|
||||
// import { EditorComponent } from './editor/editor.component'
|
||||
import { ActionsComponent } from './actions/actions.component'
|
||||
import { AppSharedModule } from './app-shared.module'
|
||||
import { ApproveDetailsComponent } from './approve-details/approve-details.component'
|
||||
import { ApproveComponent } from './approve/approve.component'
|
||||
import { DeployComponent } from './deploy/deploy.component'
|
||||
import { AutomaticComponent } from './deploy/sections/automatic/automatic.component'
|
||||
import { ManualComponent } from './deploy/sections/manual/manual.component'
|
||||
import { SasjsConfiguratorComponent } from './deploy/sections/sasjs-configurator/sasjs-configurator.component'
|
||||
import { GroupComponent } from './group/group.component'
|
||||
import { HistoryComponent } from './history/history.component'
|
||||
import { LicensingComponent } from './licensing/licensing.component'
|
||||
import { LineageComponent } from './lineage/lineage.component'
|
||||
import { MetadataComponent } from './metadata/metadata.component'
|
||||
import { PipesModule } from './pipes/pipes.module'
|
||||
import { RoleComponent } from './role/role.component'
|
||||
import { ApproveRouteComponent } from './routes/approve-route/approve-route.component'
|
||||
import { HistoryRouteComponent } from './routes/history-route/history-route.component'
|
||||
import { LicensingGuard } from './routes/licensing.guard'
|
||||
import { UsernavRouteComponent } from './routes/usernav-route/usernav-route.component'
|
||||
import { AppService } from './services/app.service'
|
||||
import { InfoModalComponent } from './shared/abort-modal/info-modal.component'
|
||||
import { RequestsModalComponent } from './shared/requests-modal/requests-modal.component'
|
||||
import { SubmitterComponent } from './submitter/submitter.component'
|
||||
import { UserComponent } from './user/user.component'
|
||||
import { HomeModule } from './home/home.module'
|
||||
import { SystemComponent } from './system/system.component'
|
||||
import { DirectivesModule } from './directives/directives.module'
|
||||
import { ViyaApiExplorerComponent } from './viya-api-explorer/viya-api-explorer.component'
|
||||
import { NgxJsonViewerModule } from 'ngx-json-viewer'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
NotFoundComponent,
|
||||
ApproveComponent,
|
||||
ApproveDetailsComponent,
|
||||
ActionsComponent,
|
||||
HistoryComponent,
|
||||
LineageComponent,
|
||||
SubmitterComponent,
|
||||
ApproveRouteComponent,
|
||||
HistoryRouteComponent,
|
||||
MetadataComponent,
|
||||
UsernavRouteComponent,
|
||||
UserComponent,
|
||||
GroupComponent,
|
||||
RoleComponent,
|
||||
RequestsModalComponent,
|
||||
DeployComponent,
|
||||
InfoModalComponent,
|
||||
LicensingComponent,
|
||||
ManualComponent,
|
||||
AutomaticComponent,
|
||||
SasjsConfiguratorComponent,
|
||||
SystemComponent,
|
||||
ViyaApiExplorerComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
HttpClientModule,
|
||||
ROUTING,
|
||||
SharedModule,
|
||||
ClarityModule,
|
||||
AppSharedModule,
|
||||
HomeModule,
|
||||
PipesModule,
|
||||
DirectivesModule,
|
||||
NgxJsonViewerModule
|
||||
],
|
||||
providers: [AppService, SasStoreService, ApproveComponent, LicensingGuard],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule {}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
||||
* This software is released under MIT license.
|
||||
* The full license information can be found in LICENSE in the root directory of this project.
|
||||
*/
|
||||
import { ModuleWithProviders } from '@angular/core'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
|
||||
import { HomeComponent } from './home/home.component'
|
||||
import { ApproveComponent } from './approve/approve.component'
|
||||
import { ApproveDetailsComponent } from './approve-details/approve-details.component'
|
||||
import { ActionsComponent } from './actions/actions.component'
|
||||
import { HistoryComponent } from './history/history.component'
|
||||
import { NotFoundComponent } from './not-found/not-found.component'
|
||||
import { SubmitterComponent } from './submitter/submitter.component'
|
||||
|
||||
import { ApproveRouteComponent } from './routes/approve-route/approve-route.component'
|
||||
import { DeployComponent } from './deploy/deploy.component'
|
||||
import { LicensingComponent } from './licensing/licensing.component'
|
||||
import { LicensingGuard } from './routes/licensing.guard'
|
||||
import { StageModule } from './stage/stage.module'
|
||||
import { EditorModule } from './editor/editor.module'
|
||||
import { ViewerModule } from './viewer/viewer.module'
|
||||
import { SystemComponent } from './system/system.component'
|
||||
|
||||
export const ROUTES: Routes = [
|
||||
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
||||
{
|
||||
path: 'view',
|
||||
loadChildren: () => ViewerModule
|
||||
},
|
||||
{
|
||||
path: 'approve',
|
||||
component: ApproveRouteComponent,
|
||||
children: [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'toapprove' },
|
||||
{ path: 'toapprove', component: ApproveComponent },
|
||||
{ path: 'approveDet/:tableId', component: ApproveDetailsComponent },
|
||||
{ path: 'submitted', component: SubmitterComponent }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'licensing/:action',
|
||||
component: LicensingComponent,
|
||||
canActivate: [LicensingGuard],
|
||||
canDeactivate: [LicensingGuard]
|
||||
},
|
||||
{ path: 'home', component: HomeComponent },
|
||||
{
|
||||
path: 'editor',
|
||||
loadChildren: () => EditorModule
|
||||
},
|
||||
{
|
||||
path: 'stage',
|
||||
loadChildren: () => StageModule
|
||||
},
|
||||
{ path: 'system', component: SystemComponent },
|
||||
{ path: 'actions/:libds/:dsid', component: ActionsComponent },
|
||||
{ path: 'history', component: HistoryComponent },
|
||||
{ path: 'submitted', component: SubmitterComponent },
|
||||
{ path: 'submitted/:tableId', component: SubmitterComponent },
|
||||
{ path: 'deploy', component: DeployComponent },
|
||||
{ path: 'deploy/manualdeploy', component: DeployComponent },
|
||||
{ path: '**', component: NotFoundComponent }
|
||||
]
|
||||
|
||||
export const ROUTING: ModuleWithProviders<RouterModule> = RouterModule.forRoot(
|
||||
ROUTES,
|
||||
{ useHash: true }
|
||||
)
|
|
@ -0,0 +1,588 @@
|
|||
<clr-modal [(clrModalOpen)]="detailsOpen">
|
||||
<h3 class="modal-title">Approval Details</h3>
|
||||
<div class="modal-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="left">Name</th>
|
||||
<th class="left">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let key of keysArray">
|
||||
<td class="left">{{ key }}</td>
|
||||
<td
|
||||
*ngIf="key.includes('TABLE_ID')"
|
||||
class="left link-it"
|
||||
[routerLink]="'/stage/' + jsParams[key]"
|
||||
>
|
||||
{{ jsParams[key] }}
|
||||
</td>
|
||||
<td *ngIf="!key.includes('TABLE_ID')" class="left">
|
||||
{{ jsParams[key] }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
(click)="detailsOpen = false"
|
||||
>
|
||||
Ok
|
||||
</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
<clr-modal [(clrModalOpen)]="tableFlag">
|
||||
<h3 class="modal-title">All Details</h3>
|
||||
<div class="modal-body">
|
||||
<clr-tabs *ngIf="tableFlag">
|
||||
<clr-tab>
|
||||
<button clrTabLink>Submitted Table Details</button>
|
||||
<clr-tab-content *clrIfActive="true">
|
||||
<div class="overflow-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="left">Name</th>
|
||||
<th class="left">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let detail of submitArr">
|
||||
<td class="left">{{ detail }}</td>
|
||||
<td class="left">{{ submitDetails[detail] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
<clr-tab>
|
||||
<button clrTabLink>Base Table Details</button>
|
||||
<clr-tab-content>
|
||||
<div class="overflow-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="left">Name</th>
|
||||
<th class="left">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let key of keysArray">
|
||||
<td class="left">{{ key }}</td>
|
||||
<td class="left">{{ jsParams[key] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
</clr-tabs>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
(click)="tableFlag = false"
|
||||
>
|
||||
Ok
|
||||
</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
<clr-modal [(clrModalOpen)]="rejectOpen">
|
||||
<h3 class="modal-title">Reason Message</h3>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label for="formFields_8">Reason for rejecting?</label>
|
||||
<textarea
|
||||
class="w-100"
|
||||
id="formFields_8"
|
||||
rows="5"
|
||||
[(ngModel)]="submitReason"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[innerHTML]="submitReason"
|
||||
></textarea>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="rejectOpen = false">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
[clrLoading]="rejectLoading"
|
||||
type="submit"
|
||||
class="btn btn-success-outline"
|
||||
(click)="rejecting()"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
<div class="content-area">
|
||||
<div class="card" *ngIf="!submitted">
|
||||
<div
|
||||
class="card-header d-flex flex-column justify-content-center"
|
||||
*ngIf="loaded"
|
||||
>
|
||||
<div class="card" *ngIf="loaded" class="mt-0">
|
||||
<div class="card-header p-0">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-md-4 approvalBack">
|
||||
<span
|
||||
class="btn btn-sm btn-outline m-0"
|
||||
(click)="goToApprovalsList()"
|
||||
>
|
||||
<clr-icon shape="caret" dir="left" size="20"></clr-icon>Back to
|
||||
approvals list
|
||||
</span>
|
||||
</div>
|
||||
<div class="clr-col-md-4 d-flex justify-content-center">
|
||||
<h3 class="mt-0 font-weight-300">
|
||||
{{ jsParams?.TABLE_NM }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="clr-col-md-4 approvalInfo">
|
||||
<a
|
||||
(click)="getDetails()"
|
||||
class="tooltip tooltip-sm tooltip-top-left"
|
||||
>
|
||||
<clr-icon shape="info-standard" size="28"></clr-icon>
|
||||
<!-- <span *ngIf="!detailsOpen" class="tooltip-content">Approval Details</span> -->
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-center mt-10">
|
||||
{{ jsParams?.TABLE_DESC }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-block p-0">
|
||||
<div class="card-text" *ngIf="loaded">
|
||||
<div class="clr-row font-size-15">
|
||||
<div class="clr-col-md-5">
|
||||
<p *ngIf="!tableDetails" class="text-center font-size-18">
|
||||
There are no details to show
|
||||
</p>
|
||||
|
||||
<ng-container *ngIf="tableDetails">
|
||||
<div class="mt-15">
|
||||
<span>Table Id:</span>
|
||||
<strong
|
||||
class="link-it"
|
||||
[routerLink]="'/stage/' + tableDetails?.TABLE_ID"
|
||||
>
|
||||
{{ tableDetails?.TABLE_ID }}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Submitter:</span>
|
||||
<span class="mt-10">
|
||||
{{ tableDetails?.SUBMITTED_BY_NM }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Submitted on:</span>
|
||||
<span class="mt-10">
|
||||
{{ tableDetails?.SUBMITTED_ON_DTTM }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Submitted Reason:</span>
|
||||
<span class="mt-10">
|
||||
{{ tableDetails?.SUBMITTED_REASON_TXT }}
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="clr-col-md-7">
|
||||
<div class="card-block d-flex justify-content-center">
|
||||
<div class="d-flex justify-content-center mt-0">
|
||||
<div class="clr-row clr-gap-5 clr-gap-sm-0">
|
||||
<button
|
||||
class="btn btn-sm btn-outline text-center mt-5"
|
||||
(click)="goToBase(jsParams?.TABLE_NM)"
|
||||
>
|
||||
Go to base table screen
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-success-outline text-center mt-5"
|
||||
(click)="getTable(tableId)"
|
||||
>
|
||||
Go to edited screen
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-info-outline text-center mt-5"
|
||||
(click)="goBack(jsParams?.TABLE_NM)"
|
||||
>
|
||||
Go back to editor
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="card-block d-flex justify-content-center clr-flex-column clr-gap-5 clr-flex-sm-row clr-gap-sm-0 clr-align-items-center"
|
||||
>
|
||||
<button
|
||||
id="acceptBtn"
|
||||
[clrLoading]="acceptLoading"
|
||||
type="submit"
|
||||
class="btn btn-sm btn-success"
|
||||
(click)="approveTable()"
|
||||
[disabled]="
|
||||
!loadingTable || params?.ISAPPROVER === 'NO' || noChanges
|
||||
"
|
||||
>
|
||||
ACCEPT
|
||||
</button>
|
||||
<button
|
||||
id="rejectBtn"
|
||||
class="btn btn-sm btn btn-danger mr-0"
|
||||
(click)="rejectOpen = true"
|
||||
[disabled]="
|
||||
!loadingTable || params?.ISAPPROVER === 'NO' || noChanges
|
||||
"
|
||||
>
|
||||
REJECT
|
||||
</button>
|
||||
|
||||
<clr-toggle-container class="m-0 ml-20i">
|
||||
<clr-toggle-wrapper>
|
||||
<input
|
||||
type="checkbox"
|
||||
clrToggle
|
||||
checked
|
||||
[(ngModel)]="formattedValues"
|
||||
(change)="formattingChanged()"
|
||||
/>
|
||||
<label class="formatted-values-toggle">{{
|
||||
formattedValues ? 'Formatted' : 'Unformatted'
|
||||
}}</label>
|
||||
</clr-toggle-wrapper>
|
||||
</clr-toggle-container>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="card-block d-flex clr-align-items-center clr-flex-column"
|
||||
>
|
||||
<span *ngIf="diffsLimit" class="rows-notice">
|
||||
<clr-icon
|
||||
class="mb-5 color-orange"
|
||||
shape="exclamation-triangle"
|
||||
></clr-icon>
|
||||
Only the first 100 inserts, updates or deletes are displayed
|
||||
</span>
|
||||
<div class="clr-row">
|
||||
<div
|
||||
class="d-flex clr-flex-column clr-gap-5 clr-flex-sm-row clr-gap-sm-0"
|
||||
>
|
||||
<span class="label label-warning">
|
||||
Changed Rows
|
||||
<span class="badge badge-warning">{{
|
||||
lens.updated
|
||||
}}</span>
|
||||
</span>
|
||||
<span class="label label-success">
|
||||
Added Rows
|
||||
<span class="badge badge-success">{{ lens.new }}</span>
|
||||
</span>
|
||||
<span class="label label-danger">
|
||||
Deleted Rows
|
||||
<span class="badge badge-danger">{{
|
||||
lens.deleted
|
||||
}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="!loadingTable"
|
||||
class="h-24vh d-flex flex-column justify-content-center align-items-center"
|
||||
>
|
||||
<span class="spinner"> Loading... </span>
|
||||
<div *ngIf="!loadingTable">
|
||||
<h3>Loading table</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tableCont mt-0">
|
||||
<p *ngIf="loadingTable && noChanges" class="text-center font-size-18">
|
||||
There are no changes to show
|
||||
</p>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr class="d-flex">
|
||||
<th class="left" *ngFor="let col of rowHeader">{{ col }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
class="d-flex"
|
||||
*ngFor="let key of rowKeys; let i = index"
|
||||
[ngClass]="{
|
||||
addedRow: chArr[i] == 'added',
|
||||
deletedRow: chArr[i] == 'deleted',
|
||||
updatedRow: chArr[i] == 'updated'
|
||||
}"
|
||||
>
|
||||
<ng-container *ngIf="chArr[i] === 'updated'">
|
||||
<td
|
||||
class="left"
|
||||
*ngFor="let col of rowKeys[i]; let chIndex = index"
|
||||
[ngClass]="{
|
||||
'ch tooltip tooltip-md tooltip-top-right':
|
||||
arrChanged[i][chIndex] == true && chArr[i] == 'updated'
|
||||
}"
|
||||
>
|
||||
{{ diffTable.data[i][col] }}
|
||||
<span
|
||||
*ngIf="
|
||||
arrChanged[i][chIndex] == true && chArr[i] == 'updated'
|
||||
"
|
||||
class="tooltip-content"
|
||||
>Original value is: {{ arrOfChanges[i][chIndex] }}</span
|
||||
>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="chArr[i] !== 'updated'">
|
||||
<td
|
||||
class="left"
|
||||
*ngFor="let col of rowKeys[i]; let chIndex = index"
|
||||
>
|
||||
{{ diffTable.data[i][col] }}
|
||||
</td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-auto mr-12">
|
||||
<div
|
||||
*ngIf="!loaded"
|
||||
class="h-70vh d-flex justify-content-center flex-column align-items-center"
|
||||
>
|
||||
<span class="spinner" *ngIf="!loaded"> Loading... </span>
|
||||
<div *ngIf="!loaded">
|
||||
<h3>Loading preview</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- submitted page layout -->
|
||||
|
||||
<div *ngIf="submitted">
|
||||
<div class="d-flex flex-column justify-content-center" *ngIf="loaded">
|
||||
<div class="card m-0" *ngIf="loaded">
|
||||
<div class="card-header">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-md-4 approvalBack">
|
||||
<span class="btn btn-sm btn-outline" (click)="goToSubmitList()">
|
||||
<clr-icon shape="caret" dir="left" size="20"></clr-icon>Back to
|
||||
submitted list
|
||||
</span>
|
||||
</div>
|
||||
<div class="clr-col-md-4">
|
||||
<h3 class="mt-0 font-weight-300 text-center">
|
||||
{{ subObj.base }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="clr-col-md-4 approvalInfo">
|
||||
<a
|
||||
(click)="tableFlag = true"
|
||||
class="tooltip tooltip-sm tooltip-top-left"
|
||||
>
|
||||
<clr-icon shape="info-standard" size="28"></clr-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0 text-center color-darker-gray">
|
||||
{{ tableDescription }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="clr-row font-size-15">
|
||||
<div class="clr-col-md-5">
|
||||
<div class="mt-15">
|
||||
<span>Table Id:</span>
|
||||
<strong class="link-it" [routerLink]="'/stage/' + subObj.tableId">
|
||||
<span> {{ subObj.tableId }}</span>
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Submitted on:</span>
|
||||
<span class="mt-10">
|
||||
{{ subObj.submitted }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Submit Message:</span>
|
||||
<span>
|
||||
{{ subObj.submitReason }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-col-md-7">
|
||||
<div class="card-block d-flex justify-content-center">
|
||||
<div class="d-flex justify-content-center mt-0">
|
||||
<div class="clr-row clr-gap-5 clr-gap-sm-0">
|
||||
<button
|
||||
class="btn btn-sm btn-outline text-center mt-5"
|
||||
(click)="goToBase(subObj.base)"
|
||||
>
|
||||
Go to base table screen
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-success-outline text-center mt-5"
|
||||
(click)="getTable(subObj.tableId)"
|
||||
>
|
||||
Go to edited screen
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-info-outline text-center mt-5"
|
||||
(click)="goBack(subObj.base)"
|
||||
>
|
||||
Go back to editor
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="card-block d-flex clr-align-items-center clr-flex-column"
|
||||
>
|
||||
<span *ngIf="diffsLimit" class="rows-notice">
|
||||
<clr-icon
|
||||
class="mb-5 color-orange"
|
||||
shape="exclamation-triangle"
|
||||
></clr-icon>
|
||||
Only the first 100 inserts, updates or deletes are displayed
|
||||
</span>
|
||||
<div class="clr-row">
|
||||
<div
|
||||
class="d-flex clr-flex-column clr-gap-5 clr-flex-sm-row clr-gap-sm-0"
|
||||
>
|
||||
<span class="label label-warning">
|
||||
Changed Rows
|
||||
<span class="badge badge-warning">{{ lens.updated }}</span>
|
||||
</span>
|
||||
<span class="label label-success">
|
||||
Added Rows
|
||||
<span class="badge badge-success">{{ lens.new }}</span>
|
||||
</span>
|
||||
<span class="label label-danger">
|
||||
Deleted Rows
|
||||
<span class="badge badge-danger">{{ lens.deleted }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-block d-flex justify-content-center">
|
||||
<clr-toggle-container class="m-0 ml-20-i">
|
||||
<clr-toggle-wrapper>
|
||||
<input
|
||||
type="checkbox"
|
||||
clrToggle
|
||||
checked
|
||||
[(ngModel)]="formattedValues"
|
||||
(change)="formattingChanged()"
|
||||
/>
|
||||
<label>{{
|
||||
formattedValues ? 'Formatted' : 'Unformatted'
|
||||
}}</label>
|
||||
</clr-toggle-wrapper>
|
||||
</clr-toggle-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-block p-0 overflow-auto mr-12">
|
||||
<div class="card-text" *ngIf="loaded"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="!loadingTable"
|
||||
class="h-25vh d-flex flex-column justify-content-center align-items-center"
|
||||
>
|
||||
<span class="spinner"> Loading... </span>
|
||||
<div *ngIf="!loadingTable">
|
||||
<h3>Loading table</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tableCont">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr class="d-flex">
|
||||
<th class="left" *ngFor="let col of rowHeader">{{ col }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
class="d-flex"
|
||||
*ngFor="let key of rowKeys; let i = index"
|
||||
[ngClass]="{
|
||||
addedRow: chArr[i] == 'added',
|
||||
deletedRow: chArr[i] == 'deleted',
|
||||
updatedRow: chArr[i] == 'updated'
|
||||
}"
|
||||
>
|
||||
<ng-container *ngIf="chArr[i] === 'updated'">
|
||||
<td
|
||||
class="left"
|
||||
*ngFor="let col of rowKeys[i]; let chIndex = index"
|
||||
[ngClass]="{
|
||||
'ch tooltip tooltip-md tooltip-top-right':
|
||||
arrChanged[i][chIndex] == true && chArr[i] == 'updated'
|
||||
}"
|
||||
>
|
||||
{{ diffTable.data[i][col] }}
|
||||
<span
|
||||
*ngIf="
|
||||
arrChanged[i][chIndex] == true && chArr[i] == 'updated'
|
||||
"
|
||||
class="tooltip-content"
|
||||
>Original value is: {{ arrOfChanges[i][chIndex] }}</span
|
||||
>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="chArr[i] !== 'updated'">
|
||||
<td
|
||||
class="left"
|
||||
*ngFor="let col of rowKeys[i]; let chIndex = index"
|
||||
>
|
||||
{{ diffTable.data[i][col] }}
|
||||
</td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-block" *ngIf="!loaded">
|
||||
<div class="loader">
|
||||
<span class="spinner"> Loading... </span>
|
||||
<div *ngIf="!loaded">
|
||||
<h3>Loading submitted table</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,179 @@
|
|||
.loader {
|
||||
display:flex;
|
||||
justify-content: center;
|
||||
height:75vh;
|
||||
align-items:center;
|
||||
flex-direction:column
|
||||
}
|
||||
.modalLarge {
|
||||
width: 50rem!important;
|
||||
}
|
||||
|
||||
.addedRow {
|
||||
background: rgb(146, 208, 154);
|
||||
border: 1px solid rgba(9, 77, 117, 0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.deletedRow {
|
||||
background: rgb(230, 179, 179);
|
||||
border: 1px solid rgba(70, 71, 70, 0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.updatedRow {
|
||||
background: #fafda8;
|
||||
border: 1px solid rgba(9, 117, 9, 0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.table {
|
||||
border: 0px solid;
|
||||
}
|
||||
|
||||
.ch {
|
||||
background: rgba(0,0,0,.1);
|
||||
border: 1px solid rgba(104, 100, 0, 0.4);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.ch:hover {
|
||||
background: rgba(252, 135, 120, 0.4);
|
||||
}
|
||||
|
||||
.tooltip .tooltip-content.tooltip-top-right, .tooltip.tooltip-top-right>.tooltip-content, .tooltip>.tooltip-content {
|
||||
font-size: .54167rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: normal;
|
||||
background: #314351;
|
||||
border-radius: .125rem;
|
||||
color: #f0f1ec;;
|
||||
line-height: .75rem;
|
||||
margin: 0;
|
||||
padding: .375rem .5rem;
|
||||
width: 235px;
|
||||
position: absolute;
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
left: 12px;
|
||||
right: auto;
|
||||
border-bottom-left-radius: 0;
|
||||
margin-bottom: .66667rem;
|
||||
}
|
||||
|
||||
.tooltip .tooltip-content.tooltip-top-right:before, .tooltip.tooltip-top-right>.tooltip-content:before, .tooltip>.tooltip-content:before {
|
||||
position: absolute;
|
||||
bottom: -.375rem;
|
||||
left: 0;
|
||||
top: auto;
|
||||
right: auto;
|
||||
content: "";
|
||||
border-left: .25rem solid #314351;
|
||||
border-top: .20833rem solid #314351;
|
||||
border-right: .25rem solid transparent;
|
||||
border-bottom: .20833rem solid transparent;
|
||||
}
|
||||
|
||||
.table {
|
||||
border: 0px solid;
|
||||
}
|
||||
|
||||
.toggle-switch input[type=checkbox]:checked+label:before {
|
||||
border-color: #314351;
|
||||
background-color: #314351!important;
|
||||
transition: .15s ease-in;
|
||||
transition-property: border-color,background-color;
|
||||
}
|
||||
|
||||
.tableCont {
|
||||
overflow:auto;
|
||||
margin: 15px 10px 10px 10px;
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.approvalInfo {
|
||||
display: flex;
|
||||
justify-content: flex-end
|
||||
}
|
||||
|
||||
.approvalBack {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@media screen and (max-width:768px) {
|
||||
.approvalInfo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
|
||||
}
|
||||
|
||||
.approvalBack {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-top:0rem!important;
|
||||
min-height: calc(100vh - 0px)!important;
|
||||
}
|
||||
|
||||
.table td.left, .table th.left {
|
||||
text-align: left;
|
||||
width: 150px!important;
|
||||
flex: 0
|
||||
}
|
||||
}
|
||||
|
||||
.table td.left, .table th.left {
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
width: 300px!important;
|
||||
}
|
||||
|
||||
|
||||
.tooll {
|
||||
position: absolute;
|
||||
background: #e6b3b3;
|
||||
color: #314351;
|
||||
top: 0px;
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
left: 0px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#acceptBtn, #rejectBtn {
|
||||
width: 175px
|
||||
}
|
||||
|
||||
.formatted-values-toggle {
|
||||
min-width: 75px
|
||||
}
|
||||
|
||||
clr-modal {
|
||||
::ng-deep {
|
||||
.modal-body-wrapper {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rows-notice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
color: #6a6a6a;
|
||||
font-size: 15px;
|
||||
|
||||
clr-icon {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,407 @@
|
|||
import { ActivatedRoute } from '@angular/router'
|
||||
import { SasStoreService } from '../services/sas-store.service'
|
||||
import { Component, AfterViewInit, OnDestroy } from '@angular/core'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { Router } from '@angular/router'
|
||||
import { EventService } from '../services/event.service'
|
||||
import {
|
||||
AuditorsPostdataSASResponse,
|
||||
Param
|
||||
} from '../models/sas/auditors-postdata.model'
|
||||
|
||||
interface ChangesObj {
|
||||
ind: any
|
||||
field: any
|
||||
prop: any
|
||||
original: any
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-approve-details',
|
||||
templateUrl: './approve-details.component.html',
|
||||
styleUrls: ['./approve-details.component.scss'],
|
||||
host: {
|
||||
class: 'content-container'
|
||||
}
|
||||
})
|
||||
export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
|
||||
private _detailsSub: Subscription | undefined
|
||||
public tableId: any
|
||||
public detailsOpen: any = false
|
||||
public rejectOpen: any = false
|
||||
public jsParams: any
|
||||
public keysArray!: Array<string>
|
||||
public submitArr!: Array<string>
|
||||
public hotSelection: any
|
||||
public lens: { new: number; updated: number; deleted: number } = {
|
||||
new: 0,
|
||||
updated: 0,
|
||||
deleted: 0
|
||||
}
|
||||
public loaded: boolean = false
|
||||
public loadingTable: boolean = false
|
||||
public submitReason: string = ''
|
||||
public instance: string = 'hotInstance'
|
||||
public params: Param | undefined
|
||||
public subObj: any
|
||||
public submitDetails: any
|
||||
public acceptLoading: boolean = false
|
||||
public rejectLoading: boolean = false
|
||||
public submitted: boolean = false
|
||||
public tableFlag: boolean = false
|
||||
public originals: any
|
||||
public rowKeys: any = []
|
||||
public rowHeader!: Array<any>
|
||||
public arrChanged!: Array<any>
|
||||
public arrOfChanges!: Array<any>
|
||||
public chArr: Array<any> = []
|
||||
public tableDetails: any
|
||||
public secondOpen: boolean = false
|
||||
public addCount: number | undefined
|
||||
public tableDescription: string | undefined
|
||||
public formattedValues: boolean = true
|
||||
private response: AuditorsPostdataSASResponse | undefined
|
||||
|
||||
public changesArr: Array<ChangesObj> = []
|
||||
|
||||
public diffTable: any = {
|
||||
data: []
|
||||
}
|
||||
|
||||
public diffsLimit: boolean = false
|
||||
public recordsLimit: number = 100
|
||||
|
||||
constructor(
|
||||
private sasStoreService: SasStoreService,
|
||||
private eventService: EventService,
|
||||
private router: ActivatedRoute,
|
||||
private route: Router
|
||||
) {}
|
||||
|
||||
get noChanges() {
|
||||
return (
|
||||
this.lens.new === 0 && this.lens.updated === 0 && this.lens.deleted === 0
|
||||
)
|
||||
}
|
||||
|
||||
public goToBase(base: any) {
|
||||
this.route.navigateByUrl('/view/data/' + base)
|
||||
}
|
||||
|
||||
public goToApprovalsList() {
|
||||
this.route.navigateByUrl('/approve')
|
||||
}
|
||||
|
||||
public getTable(tableId: any) {
|
||||
this.route.navigateByUrl('/stage/' + tableId)
|
||||
}
|
||||
|
||||
public goBack(base: any) {
|
||||
this.route.navigateByUrl('/editor/' + base)
|
||||
}
|
||||
|
||||
public goToViewer() {
|
||||
this.route.navigateByUrl('/view/data')
|
||||
}
|
||||
|
||||
public showDetailsSelect($event: Event) {
|
||||
$event.preventDefault()
|
||||
this.tableFlag = !this.tableFlag
|
||||
}
|
||||
|
||||
public getDetails() {
|
||||
this.detailsOpen = true
|
||||
}
|
||||
|
||||
public onHotSelection(evt: any) {
|
||||
this.hotSelection = evt.slice(0, 4)
|
||||
}
|
||||
|
||||
public onHotDeselect() {
|
||||
setTimeout(() => {
|
||||
this.hotSelection = null
|
||||
}, 100)
|
||||
}
|
||||
|
||||
public async rejecting() {
|
||||
this.rejectLoading = true
|
||||
this.submitReason = this.submitReason.replace(/\n/g, '. ')
|
||||
|
||||
let rejParams = {
|
||||
STP_ACTION: 'REJECT_TABLE',
|
||||
TABLE: this.tableId,
|
||||
STP_REASON: this.submitReason
|
||||
}
|
||||
|
||||
await this.sasStoreService
|
||||
.rejecting(rejParams, 'BrowserParams', 'approvers/rejection')
|
||||
.then((res: any) => {
|
||||
this.route.navigateByUrl('/history')
|
||||
})
|
||||
.catch((err: any) => {
|
||||
this.acceptLoading = false
|
||||
this.rejectLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
public async approveTable() {
|
||||
this.acceptLoading = true
|
||||
let approveParams = {
|
||||
ACTION: 'APPROVE_TABLE',
|
||||
TABLE: this.tableId,
|
||||
DIFFTIME: this.params?.DIFFTIME,
|
||||
LIBDS: this.params?.LIBDS
|
||||
}
|
||||
|
||||
await this.sasStoreService
|
||||
.approveTable(approveParams, 'SASControlTable', 'auditors/postdata')
|
||||
.then((res: any) => {
|
||||
this.route.navigateByUrl('/history')
|
||||
})
|
||||
.catch((err: any) => {
|
||||
this.acceptLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
public goToSubmitList() {
|
||||
this.route.navigateByUrl('/submitted')
|
||||
}
|
||||
|
||||
public async callChangesInfo(tableId: any) {
|
||||
await this.sasStoreService
|
||||
.getChangeInfo(tableId)
|
||||
.then((res: any) => {
|
||||
this.tableDetails = res.jsparams[0]
|
||||
this.jsParams = res.jsparams[0]
|
||||
|
||||
let keysArray: Array<string> = []
|
||||
|
||||
for (const key in this.jsParams) {
|
||||
if (this.jsParams.hasOwnProperty(key)) {
|
||||
keysArray.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
this.keysArray = keysArray
|
||||
})
|
||||
.catch((err: any) => {
|
||||
this.acceptLoading = false
|
||||
})
|
||||
.finally(() => {
|
||||
this.loaded = true
|
||||
})
|
||||
}
|
||||
|
||||
public formattingChanged() {
|
||||
this.calcDiff()
|
||||
}
|
||||
|
||||
public calcDiff() {
|
||||
if (!this.response) return
|
||||
|
||||
let news = this.response.new
|
||||
let updates = this.response.updates
|
||||
let deleted = this.response.deleted
|
||||
let originals = this.response.originals
|
||||
|
||||
if (this.formattedValues) {
|
||||
news = this.response.fmt_new
|
||||
updates = this.response.fmt_updates
|
||||
deleted = this.response.fmt_deleted
|
||||
originals = this.response.fmt_originals
|
||||
}
|
||||
|
||||
let delLen = deleted.length
|
||||
let upLen = updates.length
|
||||
let newLen = news.length
|
||||
this.originals = originals
|
||||
this.rowKeys = []
|
||||
|
||||
for (let index = 0; index < updates.length; index++) {
|
||||
let keys = Object.keys(updates[index])
|
||||
for (let ind = 0; ind < keys.length; ind++) {
|
||||
if (updates[index][keys[ind]] !== originals[index][keys[ind]]) {
|
||||
this.changesArr.push({
|
||||
ind: index,
|
||||
field: keys[ind],
|
||||
prop: updates[index][keys[ind]],
|
||||
original: originals[index][keys[ind]]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.lens = {
|
||||
new: this.params?.NUM_ADDED || 0,
|
||||
updated: this.params?.NUM_UPDATED || 0,
|
||||
deleted: this.params?.NUM_DELETED || 0
|
||||
}
|
||||
|
||||
let columns: Array<string> = []
|
||||
let all = updates.concat(news, deleted)
|
||||
|
||||
for (let index = 0; index < this.response.cols.length; index++) {
|
||||
const element = this.response.cols[index].NAME
|
||||
columns.push(element)
|
||||
}
|
||||
|
||||
// We need to limit lens in following calculation
|
||||
// since actual data returned is limited to 100 rows
|
||||
let added =
|
||||
this.lens.new > this.recordsLimit ? this.recordsLimit : this.lens.new
|
||||
let changed =
|
||||
this.lens.updated > this.recordsLimit
|
||||
? this.recordsLimit
|
||||
: this.lens.updated
|
||||
let del =
|
||||
this.lens.deleted > this.recordsLimit
|
||||
? this.recordsLimit
|
||||
: this.lens.deleted
|
||||
|
||||
if (
|
||||
this.lens.new > this.recordsLimit ||
|
||||
this.lens.updated > this.recordsLimit ||
|
||||
this.lens.deleted > this.recordsLimit
|
||||
) {
|
||||
this.diffsLimit = true
|
||||
} else {
|
||||
this.diffsLimit = false
|
||||
}
|
||||
|
||||
this.addCount = added
|
||||
let chArr: Array<any> = []
|
||||
|
||||
let cols: Array<any> = []
|
||||
for (let ind = 0; ind < columns.length; ind++) {
|
||||
const element = columns[ind]
|
||||
cols.push({
|
||||
data: element,
|
||||
readOnly: true
|
||||
// implement custom rendering
|
||||
})
|
||||
}
|
||||
this.diffTable.data = all
|
||||
for (let index = 0; index < all.length; index++) {
|
||||
const element = all[index]
|
||||
let rowKey = Object.keys(element)
|
||||
this.rowKeys.push(rowKey)
|
||||
}
|
||||
let arrChanged: Array<any> = []
|
||||
let arrOfChanges: Array<any> = []
|
||||
for (let index = 0; index < this.diffTable.data.length; index++) {
|
||||
if (index < changed && changed !== 0) {
|
||||
arrChanged.push([])
|
||||
arrOfChanges.push([])
|
||||
// if (index >= added && index < added + changed) {
|
||||
chArr.push('updated')
|
||||
let diffTableKeys = Object.keys(this.diffTable.data[index])
|
||||
for (let j = 0; j < diffTableKeys.length; j++) {
|
||||
let currColumn = diffTableKeys[j]
|
||||
if (originals[index][currColumn] !== updates[index][currColumn]) {
|
||||
arrChanged[index].push(true)
|
||||
arrOfChanges[index].push(originals[index][currColumn])
|
||||
} else {
|
||||
arrChanged[index].push(false)
|
||||
arrOfChanges[index].push(null)
|
||||
}
|
||||
}
|
||||
this.arrChanged = arrChanged
|
||||
this.arrOfChanges = arrOfChanges
|
||||
}
|
||||
if (index >= changed && index < changed + added) {
|
||||
chArr.push('added')
|
||||
}
|
||||
if (index > added + changed - 1) {
|
||||
chArr.push('deleted')
|
||||
}
|
||||
}
|
||||
this.chArr = chArr
|
||||
this.rowHeader = this.rowKeys[0]
|
||||
this.diffTable.data = all
|
||||
}
|
||||
|
||||
async ngAfterViewInit() {
|
||||
// submitted page
|
||||
this._detailsSub = this.sasStoreService.submittDetail.subscribe(
|
||||
async (allData: any) => {
|
||||
this.subObj = allData.viewData
|
||||
this.tableId = allData.viewData.tableId
|
||||
|
||||
this.submitted = allData.viewData.sub
|
||||
this.submitDetails = allData.data
|
||||
this.submitArr = []
|
||||
for (let item in this.submitDetails) {
|
||||
if (item !== 'sub') {
|
||||
this.submitArr.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
let diffs = {
|
||||
ACTION: 'SHOW_DIFFS',
|
||||
TABLE: this.tableId,
|
||||
DIFFTIME: new Date().toUTCString()
|
||||
}
|
||||
// show diffs and changes info in a same call
|
||||
this.sasStoreService
|
||||
.showDiffs(diffs, 'SASControlTable', 'auditors/postdata')
|
||||
.then((res: AuditorsPostdataSASResponse) => {
|
||||
let param = res.params[0]
|
||||
this.params = param
|
||||
this.response = res
|
||||
this.calcDiff()
|
||||
})
|
||||
.catch((err: any) => err)
|
||||
.finally(() => {
|
||||
this.loadingTable = true
|
||||
})
|
||||
|
||||
this.callChangesInfo(this.tableId)
|
||||
}
|
||||
)
|
||||
if (typeof this.router.snapshot.params['tableId'] === 'undefined') {
|
||||
return
|
||||
} else {
|
||||
this.tableId = this.router.snapshot.params['tableId']
|
||||
}
|
||||
|
||||
let params = {
|
||||
ACTION: 'SHOW_DIFFS',
|
||||
TABLE: this.tableId,
|
||||
DIFFTIME: new Date().toUTCString()
|
||||
}
|
||||
|
||||
// show diffs call and changes info both
|
||||
this.sasStoreService
|
||||
.showDiffs(params, 'SASControlTable', 'auditors/postdata')
|
||||
.then((res: AuditorsPostdataSASResponse) => {
|
||||
let param = res.params[0]
|
||||
this.params = param
|
||||
this.response = res
|
||||
this.calcDiff()
|
||||
})
|
||||
.catch((err: any) => {
|
||||
this.acceptLoading = false
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadingTable = true
|
||||
this.setFocus()
|
||||
})
|
||||
|
||||
this.callChangesInfo(this.tableId)
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this._detailsSub) {
|
||||
this._detailsSub.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private setFocus() {
|
||||
setTimeout(() => {
|
||||
let acceptBtn: any = window.document.getElementById('acceptBtn')
|
||||
if (!!acceptBtn) {
|
||||
acceptBtn.focus()
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
<div class="content-area">
|
||||
<div class="card">
|
||||
<div *ngIf="remained === 0" class="d-flex justify-content-center">
|
||||
<div class="card-block noapprovals-info-wrapper">
|
||||
<clr-icon
|
||||
shape="warning-standard"
|
||||
size="60"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
<h3 class="text-center color-gray">There are no approvals remaining</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-header" [ngClass]="{ noBorder: !loaded }">
|
||||
<h3
|
||||
class="center clr-col-md-12 text-center"
|
||||
*ngIf="loaded && remained !== 0"
|
||||
>
|
||||
REVIEW
|
||||
</h3>
|
||||
<p
|
||||
class="text-center font-weight-700 color-dark-gray"
|
||||
*ngIf="loaded && remained !== 0"
|
||||
>
|
||||
You have <span>{{ remained }} </span>approvals remaining
|
||||
</p>
|
||||
</div>
|
||||
<div *ngIf="!loaded" class="approvals-list-wrapper">
|
||||
<span class="spinner" *ngIf="!loaded"> Loading... </span>
|
||||
<div *ngIf="!loaded">
|
||||
<h3>Loading approvals list</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-col-md-12" ng-if="loaded">
|
||||
<div *ngIf="approveList && remained !== 0">
|
||||
<clr-datagrid class="datagrid-compact datagrid-custom-footer">
|
||||
<clr-dg-column [clrDgField]="'submitter'">SUBMITTER</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'baseTable'">BASE TABLE</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitted'">SUBMITTED</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitReason'"
|
||||
>SUBMIT REASON</clr-dg-column
|
||||
>
|
||||
<clr-dg-column>ACTION</clr-dg-column>
|
||||
<clr-dg-column>DOWNLOAD</clr-dg-column>
|
||||
|
||||
<clr-dg-row
|
||||
*clrDgItems="let approveItem of approveList; let i = index"
|
||||
>
|
||||
<clr-dg-cell>{{ approveItem.submitter }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ approveItem.baseTable }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ approveItem.submitted }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ approveItem.submitReason }}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<div
|
||||
class="clr-row"
|
||||
role="tooltip"
|
||||
class="d-flex justify-content-around"
|
||||
>
|
||||
<a
|
||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-green"
|
||||
(click)="getClicked(i)"
|
||||
>
|
||||
<clr-icon shape="check" size="24"></clr-icon>
|
||||
<span class="tooltip-content">Go to review page screen</span>
|
||||
</a>
|
||||
<a
|
||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-red"
|
||||
(click)="!approveItem.rejectLoading ? rejecting(i) : ''"
|
||||
>
|
||||
<clr-icon
|
||||
*ngIf="!approveItem.rejectLoading"
|
||||
shape="ban"
|
||||
size="22"
|
||||
></clr-icon>
|
||||
<clr-spinner
|
||||
*ngIf="approveItem.rejectLoading"
|
||||
[clrSmall]="true"
|
||||
></clr-spinner>
|
||||
<span class="tooltip-content">Reject</span>
|
||||
</a>
|
||||
<a
|
||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-blue"
|
||||
(click)="getTable(approveItem.tableId)"
|
||||
>
|
||||
<clr-icon shape="code" size="28"></clr-icon>
|
||||
<span class="tooltip-content">Go to staged data screen</span>
|
||||
</a>
|
||||
</div>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell class="p-0 d-flex justify-content-center">
|
||||
<button
|
||||
class="btn btn-success"
|
||||
[id]="approveItem.tableId"
|
||||
(click)="
|
||||
download(approveItem.tableId); $event.stopPropagation()
|
||||
"
|
||||
>
|
||||
<clr-icon shape="download"></clr-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer class="d-flex justify-content-start">
|
||||
<span>items per page</span>
|
||||
<select [(ngModel)]="itemsNum">
|
||||
<option [ngValue]="3">3</option>
|
||||
<option [ngValue]="5">5</option>
|
||||
<option [ngValue]="10">10</option>
|
||||
<option [ngValue]="15">15</option>
|
||||
</select>
|
||||
<clr-dg-pagination
|
||||
#pagination
|
||||
[clrDgPageSize]="itemsNum"
|
||||
class="center"
|
||||
>
|
||||
{{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }} of
|
||||
{{ pagination.totalItems }} approvals
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,41 @@
|
|||
.column-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.datagrid .datagrid-column .datagrid-column-title{
|
||||
outline: none!important;
|
||||
}
|
||||
|
||||
.links {
|
||||
font-weight: 700;cursor: pointer;
|
||||
}
|
||||
|
||||
.tooltip.tooltip-bottom-left>.tooltip-content, .tooltip .tooltip-content.tooltip-bottom-left {
|
||||
background: #314351!important;
|
||||
}
|
||||
.tooltip.tooltip-bottom-left>.tooltip-content:before, .tooltip .tooltip-content.tooltip-bottom-left:before {
|
||||
border-right: .25rem solid #314351;
|
||||
border-bottom: .20833rem solid #314351;
|
||||
}
|
||||
|
||||
.noBorder {
|
||||
border-bottom: 1px solid transparent!important;
|
||||
}
|
||||
|
||||
.approvals-list-wrapper {
|
||||
height: 70vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.noapprovals-info-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: calc(100vh - 200px);
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
import { Component, OnInit, ChangeDetectorRef } from '@angular/core'
|
||||
import { SasStoreService } from '../services/sas-store.service'
|
||||
import { Router } from '@angular/router'
|
||||
import { SasService } from '../services/sas.service'
|
||||
import { EventService } from '../services/event.service'
|
||||
|
||||
interface ApproveData {
|
||||
tableId: string
|
||||
submitter: string
|
||||
baseTable: string
|
||||
submitted: string
|
||||
submitReason: string
|
||||
approver: string
|
||||
rejectLoading?: boolean
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-approve',
|
||||
templateUrl: './approve.component.html',
|
||||
styleUrls: ['./approve.component.scss'],
|
||||
host: {
|
||||
class: 'content-container'
|
||||
}
|
||||
})
|
||||
export class ApproveComponent implements OnInit {
|
||||
public approveList: Array<ApproveData> | undefined
|
||||
public remained: any
|
||||
public tableId: any
|
||||
public loaded: boolean = false
|
||||
public itemsNum: number = 10
|
||||
|
||||
constructor(
|
||||
private sasStoreService: SasStoreService,
|
||||
private eventService: EventService,
|
||||
private route: Router,
|
||||
private sasService: SasService,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {}
|
||||
|
||||
public getTable(table_id: any) {
|
||||
this.route.navigateByUrl('/stage/' + table_id)
|
||||
}
|
||||
|
||||
public getClicked(ind: any) {
|
||||
if (this.approveList !== undefined) {
|
||||
this.tableId = this.approveList[ind].tableId
|
||||
this.route.navigateByUrl(
|
||||
'approve/approveDet/' + this.approveList[ind].tableId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public async rejecting(ind: any) {
|
||||
if (this.approveList !== undefined) {
|
||||
this.tableId = this.approveList[ind].tableId
|
||||
}
|
||||
let rejParams = {
|
||||
STP_ACTION: 'REJECT_TABLE',
|
||||
TABLE: this.tableId,
|
||||
STP_REASON: 'quick rejection'
|
||||
}
|
||||
try {
|
||||
;(this.approveList || [])[ind].rejectLoading = true
|
||||
|
||||
let res = await this.sasStoreService.rejecting(
|
||||
rejParams,
|
||||
'BrowserParams',
|
||||
'approvers/rejection'
|
||||
)
|
||||
|
||||
if (res.fromsas[0].RESPONSE.includes('SUCCESS')) {
|
||||
;(this.approveList || [])[ind].rejectLoading = false
|
||||
this.approveList?.splice(ind, 1)
|
||||
this.remained--
|
||||
this.cdr.detectChanges()
|
||||
}
|
||||
} catch (error) {
|
||||
this.eventService.catchResponseError('approvers/rejection', error)
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.fetchApprovals()
|
||||
}
|
||||
|
||||
private async fetchApprovals() {
|
||||
this.itemsNum = 10
|
||||
let myJsParams: any = {}
|
||||
myJsParams.STP_ACTION = 'OPEN_APPROVALS'
|
||||
|
||||
try {
|
||||
let res = await this.sasStoreService.getApprovals(
|
||||
myJsParams,
|
||||
'BrowserParams',
|
||||
'approvers/getapprovals'
|
||||
)
|
||||
|
||||
this.remained = res.fromsas.length
|
||||
let approveList: ApproveData[] = res.fromsas.map(function (item: any) {
|
||||
return {
|
||||
tableId: item.TABLE_ID,
|
||||
submitter: item.SUBMITTED_BY_NM,
|
||||
submitted: item.SUBMITTED_ON_DTTM,
|
||||
baseTable: item.BASE_TABLE,
|
||||
submitReason: item.SUBMITTED_REASON_TXT
|
||||
}
|
||||
})
|
||||
this.approveList = approveList
|
||||
this.loaded = true
|
||||
} catch (error) {
|
||||
this.eventService.catchResponseError('approvers/getapprovals', error)
|
||||
}
|
||||
}
|
||||
|
||||
public download(id: any) {
|
||||
let sasjsConfig = this.sasService.getSasjsConfig()
|
||||
let storage = sasjsConfig.serverUrl
|
||||
let metaData = sasjsConfig.appLoc
|
||||
let path = this.sasService.getExecutionPath()
|
||||
let downUrl =
|
||||
storage +
|
||||
path +
|
||||
'/?_program=' +
|
||||
metaData +
|
||||
'/services/auditors/getauditfile&table=' +
|
||||
id
|
||||
window.open(downUrl)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
<div class="content-area position-relative">
|
||||
<div class="clr-row">
|
||||
<!-- T&C section -->
|
||||
<div *ngIf="step === 0" id="TCS" class="card">
|
||||
<div class="card-header">Terms and Conditions</div>
|
||||
<div class="card-block">
|
||||
<div class="card-text">
|
||||
<p>
|
||||
The Demo version of Data Controller is free for EVALUATION purposes
|
||||
only. Before proceeding with configuration, please confirm that you
|
||||
have read, understood, and agreed to the
|
||||
<a
|
||||
href="https://docs.datacontroller.io/evaluation-licence-agreement"
|
||||
target="_blank"
|
||||
>Data Controller for SAS© Evaluation Agreement</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr class="light" />
|
||||
|
||||
<clr-checkbox-wrapper>
|
||||
<input clrCheckbox type="checkbox" (change)="termsAgreeChange()" />
|
||||
<label
|
||||
>I have read and agree to the terms of the
|
||||
<a
|
||||
href="https://docs.datacontroller.io/evaluation-licence-agreement"
|
||||
target="_blank"
|
||||
>Data Controller for SAS© Evaluation Agreement</a
|
||||
></label
|
||||
>
|
||||
</clr-checkbox-wrapper>
|
||||
|
||||
<!-- <hr />
|
||||
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input
|
||||
[(ngModel)]="autodeploy"
|
||||
type="checkbox"
|
||||
id="checkbox2"
|
||||
class="clr-checkbox"
|
||||
checked
|
||||
/>
|
||||
<label for="checkbox2"
|
||||
>Autodeploy
|
||||
{{ !jsonFile ? '(json file is not available)' : '' }}</label
|
||||
>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- T&C section end -->
|
||||
|
||||
<ng-container *ngIf="step > 0" [ngSwitch]="true">
|
||||
<ng-container *ngSwitchCase="sasJsConfig.serverType === ServerType.SasViya">
|
||||
<div *ngIf="autodeploy" class="autodeploy-section card">
|
||||
<app-automatic-deploy
|
||||
[sasJs]="sasJs"
|
||||
[sasJsConfig]="sasJsConfig"
|
||||
[dcAdapterSettings]="dcAdapterSettings"
|
||||
[appLoc]="appLoc"
|
||||
[dcPath]="dcPath"
|
||||
[selectedAdminGroup]="selectedAdminGroup"
|
||||
(onNavigateToHome)="onNavigateToHome()"
|
||||
></app-automatic-deploy>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!autodeploy" id="mainbody" class="card">
|
||||
<app-manual-deploy
|
||||
[sasJs]="sasJs"
|
||||
[sasJsConfig]="sasJsConfig"
|
||||
[dcAdapterSettings]="dcAdapterSettings"
|
||||
(onNavigateToHome)="onNavigateToHome()"
|
||||
></app-manual-deploy>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="sasJsConfig.serverType === ServerType.Sasjs">
|
||||
<div class="autodeploy-section card">
|
||||
<app-sasjs-configurator
|
||||
[sasJs]="sasJs"
|
||||
[sasJsConfig]="sasJsConfig"
|
||||
[dcAdapterSettings]="dcAdapterSettings"
|
||||
(onNavigateToHome)="onNavigateToHome()"
|
||||
></app-sasjs-configurator>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="sasJsConfig.serverType === ServerType.Sas9">
|
||||
<div class="autodeploy-section card">
|
||||
<app-sasjs-configurator
|
||||
[sasJs]="sasJs"
|
||||
[sasJsConfig]="sasJsConfig"
|
||||
[dcAdapterSettings]="dcAdapterSettings"
|
||||
(onNavigateToHome)="onNavigateToHome()"
|
||||
></app-sasjs-configurator>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
|
@ -0,0 +1,50 @@
|
|||
.card {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.log-wrapper {
|
||||
width: 100%;
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #c9c9c9;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#contexts-btn {
|
||||
padding: 0;
|
||||
min-width: 30px;
|
||||
margin-left: 10px;
|
||||
height: 30px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.validation-bar {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
|
||||
clr-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.autodeploy-section {
|
||||
padding: 0px 15px;
|
||||
|
||||
.clr-checkbox-wrapper {
|
||||
margin: 20px 0 20px 0;
|
||||
}
|
||||
|
||||
.btn-autodeploy {
|
||||
display: block;
|
||||
margin: 15px 0 15px 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { SasService } from '../services/sas.service'
|
||||
import { SASjsConfig } from '@sasjs/adapter'
|
||||
import { Router } from '@angular/router'
|
||||
import { LoggerService } from '../services/logger.service'
|
||||
import { ServerType } from '@sasjs/utils/types/serverType'
|
||||
import { AppStoreService } from '../services/app-store.service'
|
||||
import { DcAdapterSettings } from '../models/DcAdapterSettings'
|
||||
|
||||
@Component({
|
||||
selector: 'app-deploy',
|
||||
templateUrl: './deploy.component.html',
|
||||
styleUrls: ['./deploy.component.scss'],
|
||||
host: {
|
||||
class: 'content-container'
|
||||
}
|
||||
})
|
||||
export class DeployComponent implements OnInit {
|
||||
public step: number = 0
|
||||
public adminGroups: any = []
|
||||
|
||||
public client_id: string = ''
|
||||
public client_secret: string = ''
|
||||
public appLoc: string = ''
|
||||
public dcPath: string = ''
|
||||
public selectedAdminGroup: string = ''
|
||||
|
||||
public autodeploy: boolean = true
|
||||
public jsonFile: any = null
|
||||
|
||||
public sasJs: any
|
||||
public sasJsConfig: SASjsConfig = new SASjsConfig()
|
||||
|
||||
public dcAdapterSettings: DcAdapterSettings | undefined
|
||||
|
||||
ServerType = ServerType
|
||||
|
||||
constructor(
|
||||
private appStoreService: AppStoreService,
|
||||
private sasService: SasService,
|
||||
private loggerService: LoggerService,
|
||||
private router: Router
|
||||
) {
|
||||
this.dcAdapterSettings = this.appStoreService.getDcAdapterSettings()
|
||||
|
||||
if (this.router.url.includes('manualdeploy')) {
|
||||
this.autodeploy = false
|
||||
}
|
||||
|
||||
this.sasJs = this.sasService.getSasjsInstance()
|
||||
this.sasJsConfig = this.sasService.getSasjsConfig()
|
||||
this.appLoc = this.dcAdapterSettings?.appLoc || ''
|
||||
this.client_id = localStorage.getItem('deploy_client_id') || ''
|
||||
this.client_secret = localStorage.getItem('deploy_secret_key') || ''
|
||||
this.dcPath = localStorage.getItem('deploy_dc_loc') || ''
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.sasJsConfig.serverType === ServerType.SasViya) {
|
||||
fetch('sasbuild/viya.json')
|
||||
.then((res) => res.text())
|
||||
.then((res) => {
|
||||
let initJsonFile: any = null
|
||||
|
||||
try {
|
||||
initJsonFile = JSON.parse(res)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
if (initJsonFile) {
|
||||
this.jsonFile = initJsonFile
|
||||
this.loggerService.log(this.jsonFile)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.setDeployDefaults()
|
||||
}
|
||||
|
||||
public setDeployDefaults() {
|
||||
this.dcPath = this.dcAdapterSettings?.dcPath || ''
|
||||
this.selectedAdminGroup = this.dcAdapterSettings?.adminGroup || ''
|
||||
if (!this.selectedAdminGroup) {
|
||||
this.selectedAdminGroup = 'SASAdministrators'
|
||||
}
|
||||
}
|
||||
|
||||
public termsAgreeChange() {
|
||||
if (!this.autodeploy) {
|
||||
this.getAdminGroups()
|
||||
}
|
||||
|
||||
this.step++
|
||||
}
|
||||
|
||||
public getAdminGroups() {
|
||||
fetch(
|
||||
this.sasJsConfig.serverUrl + '/identities/groups?sortBy=name&limit=5000',
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
.then((res: any) => {
|
||||
return res.text()
|
||||
})
|
||||
.then((res: any) => {
|
||||
let jsonRes
|
||||
|
||||
try {
|
||||
jsonRes = JSON.parse(res)
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
if (jsonRes) {
|
||||
this.adminGroups = jsonRes.items
|
||||
this.selectedAdminGroup = this.adminGroups[0].id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public onNavigateToHome() {
|
||||
window.open(location.href.split('#')[0], '_blank')
|
||||
}
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
<div *ngIf="autodeploying" class="auto-deploy">
|
||||
<div class="spinner-box">
|
||||
<ng-container *ngIf="!autodeployDone">
|
||||
<span class="spinner spinner-md"> Loading... </span>
|
||||
<p>Deploying...</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="autodeployDone">
|
||||
<p class="m-0 align-self-start">Done</p>
|
||||
<hr class="w-100" />
|
||||
|
||||
<div class="deploy-status-row">
|
||||
<clr-icon
|
||||
*ngIf="autoDeployStatus.deployServicePack"
|
||||
class="deploy-success"
|
||||
shape="success-standard"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="!autoDeployStatus.deployServicePack"
|
||||
class="deploy-error"
|
||||
shape="times-circle"
|
||||
></clr-icon>
|
||||
<p>Deploy SAS Jobs</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="autoDeployStatus.runMakeData !== null"
|
||||
class="deploy-status-row"
|
||||
>
|
||||
<clr-icon
|
||||
*ngIf="autoDeployStatus.runMakeData"
|
||||
class="deploy-success"
|
||||
shape="success-standard"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="autoDeployStatus.runMakeData === false"
|
||||
class="deploy-error"
|
||||
shape="times-circle"
|
||||
></clr-icon>
|
||||
<p>Create database</p>
|
||||
</div>
|
||||
|
||||
<hr class="w-100" />
|
||||
|
||||
<div class="buttons">
|
||||
<button (click)="navigateToHome()" class="btn btn-primary mt-15 mr-0">
|
||||
<clr-icon
|
||||
*ngIf="
|
||||
autoDeployStatus.deployServicePack === false ||
|
||||
autoDeployStatus.runMakeData === false
|
||||
"
|
||||
class="deploy-error"
|
||||
shape="times-circle"
|
||||
></clr-icon>
|
||||
LAUNCH / CONFIGURE
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="downloadFile(makeDataResponse, 'create-database-log', 'txt')"
|
||||
class="btn btn-primary-outline mt-15 mr-0"
|
||||
>
|
||||
Download log
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr class="w-100" />
|
||||
|
||||
<div class="buttons">
|
||||
<button
|
||||
(click)="autodeploying = false; autodeployDone = false"
|
||||
class="btn btn-primary-outline mt-15 mr-0 align-self-end"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="openSasRequestsModal()"
|
||||
class="btn btn-primary-outline mt-15 mr-0 align-self-end"
|
||||
>
|
||||
SAS Requests
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="text-center my-15">Viya Deploy</h4>
|
||||
<hr />
|
||||
<label for="dcloc" class="mt-20 clr-control-label">App Loc</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<p class="mt-0">{{ appLoc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="dcloc" class="mt-20 clr-control-label">DC Loc</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<p class="mt-0">{{ dcPath }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="dcloc" class="mt-20 clr-control-label">SAS Admin group</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<p class="mt-0">{{ selectedAdminGroup }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<clr-checkbox-wrapper>
|
||||
<input
|
||||
clrCheckbox
|
||||
[(ngModel)]="recreateDatabase"
|
||||
(click)="recreateDatabaseClicked($event)"
|
||||
type="checkbox"
|
||||
checked
|
||||
/>
|
||||
<label>Recreate database</label>
|
||||
</clr-checkbox-wrapper>
|
||||
|
||||
<hr />
|
||||
|
||||
<button
|
||||
(click)="executeJson()"
|
||||
class="btn-autodeploy btn btn-primary d-inline-block mr-10"
|
||||
[disabled]="!jsonFile"
|
||||
>
|
||||
Deploy {{ !jsonFile ? '(json file is not available)' : '' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="uploadJsonAuto.click()"
|
||||
class="btn-autodeploy btn btn-primary d-inline-block mr-10"
|
||||
>
|
||||
Upload different file to deploy
|
||||
</button>
|
||||
<input
|
||||
#uploadJsonAuto
|
||||
type="file"
|
||||
hidden
|
||||
(click)="clearUploadInput($event)"
|
||||
(change)="onJsonFileChange($event)"
|
||||
/>
|
||||
|
||||
<clr-modal [(clrModalOpen)]="recreateDatabaseModal" [clrModalClosable]="false">
|
||||
<h3 class="modal-title">Warning</h3>
|
||||
<div class="modal-body">
|
||||
This action will recreate the database (if it exists). For an initial
|
||||
deployment, this is expected. If this is a re-deployment, you will lose any
|
||||
existing tables in <strong>{{ dcPath }}</strong
|
||||
>.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
(click)="recreateDatabaseModal = false; recreateDatabase = false"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
(click)="recreateDatabaseModal = false; recreateDatabase = true"
|
||||
>
|
||||
Ok
|
||||
</button>
|
||||
</div>
|
||||
</clr-modal>
|
|
@ -0,0 +1,61 @@
|
|||
.auto-deploy {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.spinner-box {
|
||||
width: 400px;
|
||||
padding: 20px;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
box-shadow: 1px 1px 8px 0px #00000082;
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.deploy-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
|
||||
p {
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.deploy-success {
|
||||
color: #6ECF44;
|
||||
}
|
||||
|
||||
.deploy-error {
|
||||
color: #E74C3C;
|
||||
// width: 20px;
|
||||
// height: 20px;
|
||||
}
|
||||
|
||||
.deploy-undeterminated {
|
||||
color: #cacaca;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #00000045;
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||
import SASjs, { SASjsConfig } from '@sasjs/adapter'
|
||||
import { DcAdapterSettings } from 'src/app/models/DcAdapterSettings'
|
||||
import { DeployService } from 'src/app/services/deploy.service'
|
||||
import { EventService } from 'src/app/services/event.service'
|
||||
import { LoggerService } from 'src/app/services/logger.service'
|
||||
import { SasService } from 'src/app/services/sas.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-automatic-deploy',
|
||||
templateUrl: './automatic.component.html',
|
||||
styleUrls: ['./automatic.component.scss']
|
||||
})
|
||||
export class AutomaticComponent implements OnInit {
|
||||
@Input() sasJs!: SASjs
|
||||
@Input() sasJsConfig: SASjsConfig = new SASjsConfig()
|
||||
@Input() dcAdapterSettings: DcAdapterSettings | undefined
|
||||
@Input() appLoc: string = ''
|
||||
@Input() dcPath: string = ''
|
||||
@Input() selectedAdminGroup: string = ''
|
||||
|
||||
@Output() onNavigateToHome: EventEmitter<any> = new EventEmitter<any>()
|
||||
|
||||
public makeDataResponse: string = ''
|
||||
public jsonFile: any = null
|
||||
public autodeploying: boolean = false
|
||||
public autodeployDone: boolean = false
|
||||
public recreateDatabaseModal: boolean = false
|
||||
public isSubmittingJson: boolean = false
|
||||
public isJsonSubmitted: boolean = false
|
||||
public recreateDatabase: boolean = false
|
||||
public createDatabaseLoading: boolean = false
|
||||
|
||||
/** autoDeployStatus
|
||||
* This object presents the status for two steps that we have for deploy.
|
||||
* `deployServicePack` - Creating services based on `viya.json`
|
||||
* `runMakeData` - Running `makedata` service
|
||||
* If any of them is `null` or `false` that means step failed
|
||||
* and will be shown to user on deploy done modal.
|
||||
*/
|
||||
public autoDeployStatus: {
|
||||
deployServicePack: any
|
||||
runMakeData: any
|
||||
} = {
|
||||
deployServicePack: null,
|
||||
runMakeData: null
|
||||
}
|
||||
|
||||
constructor(
|
||||
private eventService: EventService,
|
||||
private deployService: DeployService,
|
||||
private sasService: SasService,
|
||||
private loggerService: LoggerService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
public async executeJson() {
|
||||
this.autodeploying = true
|
||||
this.isSubmittingJson = true
|
||||
|
||||
try {
|
||||
let uploadJsonFile = await this.sasJs.deployServicePack(
|
||||
this.jsonFile,
|
||||
this.dcAdapterSettings?.appLoc,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
|
||||
this.autoDeployStatus.deployServicePack = true
|
||||
this.isJsonSubmitted = true
|
||||
} catch (ex: any) {
|
||||
let textEx = ''
|
||||
|
||||
if (typeof ex.message !== 'string') {
|
||||
textEx = JSON.stringify(ex).replace(/\\/gm, '')
|
||||
} else {
|
||||
textEx = ex.message
|
||||
}
|
||||
|
||||
this.autoDeployStatus.deployServicePack = false
|
||||
this.eventService.showInfoModal(
|
||||
'Deploy error',
|
||||
`Exception: \n ${textEx !== '' ? textEx : ex}`
|
||||
)
|
||||
this.autodeploying = false
|
||||
this.autodeployDone = false
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.isSubmittingJson = false
|
||||
|
||||
if (this.recreateDatabase) {
|
||||
this.createDatabase()
|
||||
} else {
|
||||
this.autodeployDone = true
|
||||
}
|
||||
}
|
||||
|
||||
public createDatabase() {
|
||||
let data = {
|
||||
fromjs: [
|
||||
{
|
||||
ADMIN: this.selectedAdminGroup,
|
||||
DCPATH: this.dcPath
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* We are overriding default `sasjsConfig` object fields with this object fields.
|
||||
* Here we want to run this request using original WEB method.
|
||||
* contextName: null is the MUST field for it.
|
||||
*/
|
||||
let overrideConfig = {
|
||||
useComputeApi: false,
|
||||
contextName: this.sasJsConfig.contextName,
|
||||
debug: true
|
||||
}
|
||||
|
||||
this.sasJs
|
||||
.request(`services/admin/makedata`, data, overrideConfig, () => {
|
||||
this.sasService.shouldLogin.next(true)
|
||||
})
|
||||
.then((res: any) => {
|
||||
this.autodeployDone = true
|
||||
|
||||
try {
|
||||
this.makeDataResponse = JSON.stringify(res)
|
||||
} catch {
|
||||
this.makeDataResponse = res
|
||||
}
|
||||
|
||||
if (res.result && res.result.length > 0) {
|
||||
this.autoDeployStatus.runMakeData = true
|
||||
} else {
|
||||
this.autoDeployStatus.runMakeData = false
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
this.autoDeployStatus.runMakeData = false
|
||||
this.autodeployDone = true
|
||||
|
||||
try {
|
||||
this.makeDataResponse = JSON.stringify(err)
|
||||
} catch {
|
||||
this.makeDataResponse = err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public downloadFile(
|
||||
content: any,
|
||||
filename: string,
|
||||
extension: string = 'txt'
|
||||
) {
|
||||
this.deployService.downloadFile(content, filename, extension)
|
||||
}
|
||||
|
||||
public async onJsonFileChange(event: any) {
|
||||
let file = event.target.files[0]
|
||||
|
||||
this.jsonFile = await this.deployService.readFile(file)
|
||||
}
|
||||
|
||||
public recreateDatabaseClicked(event: Event) {
|
||||
;(<HTMLInputElement>event.target).checked === true
|
||||
? (this.recreateDatabaseModal = true)
|
||||
: ''
|
||||
}
|
||||
|
||||
public clearUploadInput(event: Event) {
|
||||
this.deployService.clearUploadInput(event)
|
||||
}
|
||||
|
||||
public openSasRequestsModal() {
|
||||
this.eventService.openRequestsModal()
|
||||
}
|
||||
|
||||
public navigateToHome() {
|
||||
this.onNavigateToHome.emit()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
<div class="card-header position-relative">
|
||||
Configurator
|
||||
<p class="d-inline-block ml-10 mb-10 mt-0">App Location: {{ appLoc }}</p>
|
||||
<button
|
||||
(click)="deleteKeys()"
|
||||
class="btn btn-primary clear-memory-button position-absolute"
|
||||
>
|
||||
Clear memory
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<!-- <div class="card-title">
|
||||
Client Details
|
||||
</div> -->
|
||||
|
||||
<div *ngIf="needsLogin" id="loginForm" class="d-none">
|
||||
<p class="mb-10">Please log in first</p>
|
||||
<label for="username" class="clr-control-label">Username</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input type="text" id="username" class="clr-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="password" class="clr-control-label">Password</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input type="password" id="password" class="clr-input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary d-none" id="loginBtn">Log in</button>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!needsLogin">
|
||||
<form>
|
||||
<div class="clr-form-control">
|
||||
<label for="select-full" class="clr-control-label">Admin group</label>
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-select-wrapper">
|
||||
<select
|
||||
[(ngModel)]="selectedAdminGroup"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
name="selectedAdminGroup"
|
||||
id="adminGroupsSelect"
|
||||
class="clr-select"
|
||||
>
|
||||
<option
|
||||
*ngFor="let adminGroup of adminGroups"
|
||||
[value]="adminGroup.id"
|
||||
>
|
||||
{{ adminGroup.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-form-control">
|
||||
<div [class.hidden]="contextsLoading">
|
||||
<label for="select-full" class="clr-control-label">Context</label>
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-select-wrapper">
|
||||
<select
|
||||
[(ngModel)]="selectedContext"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
name="selectedContext"
|
||||
class="clr-select"
|
||||
>
|
||||
<option
|
||||
*ngFor="let context of allContexts"
|
||||
[value]="context.name"
|
||||
>
|
||||
{{ context.name }}
|
||||
<span *ngIf="(context.attributes | json) != '{}'"
|
||||
>( {{ context.attributes.sysUserId }} )</span
|
||||
>
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
(click)="executableContext()"
|
||||
type="button"
|
||||
class="btn btn-icon"
|
||||
id="contexts-btn"
|
||||
>
|
||||
<clr-icon shape="play"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div [class.hidden]="!contextsLoading" class="d-flex">
|
||||
<span class="spinner spinner-inline mr-10">
|
||||
Loading contexts...
|
||||
</span>
|
||||
<span> Loading contexts... </span>
|
||||
</div>
|
||||
|
||||
<label for="dcloc" class="mt-20 clr-control-label">DC Loc</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input
|
||||
[(ngModel)]="dcPath"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
(focusout)="saveDcPath()"
|
||||
name="dcPath"
|
||||
type="text"
|
||||
id="dcloc"
|
||||
class="clr-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-10">
|
||||
Select JSON file to upload (json build file preloaded):
|
||||
</p>
|
||||
<div class="d-flex flex-column">
|
||||
<input
|
||||
type="file"
|
||||
(click)="clearUploadInput($event)"
|
||||
(change)="onJsonFileChange($event)"
|
||||
/>
|
||||
<!-- <button *ngIf="downloadFileBtn" (click)="downloadSasPrecodeFile()" style="width: 40px; min-width: 0;" class="btn btn-sm btn-icon">
|
||||
<clr-icon shape="download"></clr-icon>
|
||||
</button> -->
|
||||
</div>
|
||||
|
||||
<div class="mt-20 d-flex align-items-center">
|
||||
<button
|
||||
(click)="executeJson()"
|
||||
class="btn btn-primary"
|
||||
[clrLoading]="isSubmittingJson"
|
||||
id=""
|
||||
class="mt-0"
|
||||
>
|
||||
SUBMIT JSON
|
||||
</button>
|
||||
<span *ngIf="isJsonSubmitted">JSON Submitted Successfully</span>
|
||||
|
||||
<!-- <span *ngIf="executingScript" class="spinner spinner-inline ml-3">
|
||||
Loading...
|
||||
</span> -->
|
||||
</div>
|
||||
|
||||
<p class="mt-10">Select SAS file to upload:</p>
|
||||
<div class="d-flex flex-column">
|
||||
<input
|
||||
type="file"
|
||||
(click)="clearUploadInput($event)"
|
||||
(change)="onSasFileChange($event); downloadFileBtn = true"
|
||||
/>
|
||||
<button
|
||||
*ngIf="downloadFileBtn"
|
||||
(click)="downloadSasPrecodeFile()"
|
||||
class="btn btn-sm btn-icon min-w-0 w-40"
|
||||
>
|
||||
<clr-icon shape="download"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-20 d-flex align-items-center">
|
||||
<button
|
||||
(click)="executeSAS()"
|
||||
class="btn btn-primary"
|
||||
[clrLoading]="executingScript"
|
||||
id="deploy"
|
||||
class="mt-0"
|
||||
>
|
||||
SUBMIT
|
||||
</button>
|
||||
|
||||
<!-- <span *ngIf="executingScript" class="spinner spinner-inline ml-3">
|
||||
Loading...
|
||||
</span> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="jobLog.length > 0">
|
||||
<p class="mb-0 mt-10">File execute completed</p>
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<button
|
||||
(click)="downloadFile(jobLog, 'execute-script-log', 'txt')"
|
||||
class="btn btn-primary mt-0 mr-20"
|
||||
>
|
||||
Download log
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<button
|
||||
[clrLoading]="createDatabaseLoading"
|
||||
(click)="createDatabase()"
|
||||
class="btn btn-primary mt-10"
|
||||
>
|
||||
Create Database
|
||||
</button>
|
||||
|
||||
<ng-container *ngIf="makeDataResponse.length > 0">
|
||||
<p class="mb-0 mt-10">Create Database Completed</p>
|
||||
<hr />
|
||||
|
||||
<div *ngIf="makeDataResponse.length > 0" class="log-wrapper">
|
||||
{{ makeDataResponse }}
|
||||
</div>
|
||||
|
||||
<button (click)="navigateToHome()" class="btn btn-primary mt-15">
|
||||
Let's get started
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="downloadFile(makeDataResponse, 'create-database-log', 'txt')"
|
||||
class="btn btn-primary mt-15"
|
||||
>
|
||||
Download log
|
||||
</button>
|
||||
|
||||
<button (click)="validateDeploy()" class="btn btn-primary mt-15">
|
||||
Validate
|
||||
</button>
|
||||
|
||||
<div
|
||||
*ngIf="validationState !== 'none' || isValidating"
|
||||
class="validation-bar"
|
||||
>
|
||||
<ng-container *ngIf="isValidating">
|
||||
<span class="spinner spinner-inline mr-10">
|
||||
Validating deploy...
|
||||
</span>
|
||||
<span> Validating deploy... </span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!isValidating && validationState === 'error'">
|
||||
<clr-icon shape="exclamation-circle" class="is-error"></clr-icon>
|
||||
<span> Validation failed </span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!isValidating && validationState === 'success'">
|
||||
<clr-icon shape="check-circle" class="is-success"></clr-icon>
|
||||
<span> Validation succeeded </span>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</form>
|
||||
</ng-container>
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue