init
Test / Build-and-test-development (push) Failing after 6m14s
Test / Build-and-test-development-latest-adapter (push) Failing after 6m13s

This commit is contained in:
Mihajlo Medjedovic
2023-07-13 13:44:05 +02:00
commit f268de21a3
682 changed files with 135708 additions and 0 deletions
+14
View File
@@ -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
+16
View File
@@ -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
+46
View File
@@ -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": {}
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": [
"development"
]
}
+9
View File
@@ -0,0 +1,9 @@
{
"search.exclude": {
"**/sasjsbuild/**": true,
"**/dist/**":true
},
"editor.insertSpaces": true,
"editor.tabSize": 2,
"trim_trailing_whitespace": true
}
+15
View File
@@ -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}"
}
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"files.trimTrailingWhitespace": true,
"editor.rulers": [
107
]
}
+13
View File
@@ -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
+8
View File
@@ -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"]
+169
View File
@@ -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"
]
}
}
}
}
}
}
+28
View File
@@ -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
}
})
+4
View File
@@ -0,0 +1,4 @@
{
"username": "sas_username",
"password": "sas_password"
}
+5
View File
@@ -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.
@@ -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
}
}
})
})
})
})
}
+257
View File
@@ -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}`)
}
+539
View File
@@ -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}`)
}
+629
View File
@@ -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)
+58
View File
@@ -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)
})
})
}
})
}
+213
View File
@@ -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
})
}
+23
View File
@@ -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";
+10
View File
@@ -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)
}
+32
View File
@@ -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);
})
}
+23
View File
@@ -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
}
}
]
}
]
}
};
+45
View File
@@ -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.
+28
View File
@@ -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))
+8
View File
@@ -0,0 +1,8 @@
server {
listen 3000;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
+31118
View File
File diff suppressed because it is too large Load Diff
+128
View File
@@ -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"
}
}
+7
View File
@@ -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"
+118
View File
@@ -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)
}
}
+27
View File
@@ -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 {}
+299
View File
@@ -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 -->
+461
View File
@@ -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%;
}
}
+279
View File
@@ -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')
}
}
+5
View File
@@ -0,0 +1,5 @@
declare module 'save-svg-as-png'
declare interface Navigator {
msSaveBlob: (blob: any, defaultName?: string) => boolean
}
+90
View File
@@ -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 {}
+70
View File
@@ -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);
}
+129
View File
@@ -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)
}
}
+100
View File
@@ -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&#169; 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&#169; 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