Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
113e0bbc3c | ||
| 2af97e40bf | |||
|
|
83cbe3aece | ||
| ceac1ba614 | |||
|
|
765fdbdf9d | ||
|
|
43ae73c5f3 | ||
|
|
e57a0de8a9 | ||
|
|
3de491105b | ||
|
|
2fe690e962 | ||
|
|
b826d37086 | ||
|
|
bfbfd55fe7 | ||
|
|
15f38efd52 | ||
|
|
5d25681485 | ||
|
|
d26df376f8 | ||
| cff3fb3bad | |||
|
|
fb3c49aa8b | ||
|
|
af1657e226 | ||
|
|
d7c7302c12 | ||
| 26ce95f7c1 | |||
|
|
4924df2ef3 | ||
| 2e141a5d52 | |||
|
|
cb1978bcaf | ||
|
|
387f5122f1 | ||
|
|
db5887de21 | ||
| fe24d9bcbd | |||
|
|
6c6b1cbf46 | ||
|
|
4d65c9c999 | ||
| 4417279275 | |||
|
|
365f12996d |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.15.1
|
node-version: 24.5.0
|
||||||
|
|
||||||
- name: Install Google Chrome
|
- name: Install Google Chrome
|
||||||
run: |
|
run: |
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.15.1
|
node-version: 24.5.0
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [20.15.1]
|
node-version: [24.5.0]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.14.0
|
node-version: 24.5.0
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.14.0
|
node-version: 24.5.0
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
@@ -158,7 +158,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.14.0
|
node-version: 24.5.0
|
||||||
|
|
||||||
- name: Write .npmrc file
|
- name: Write .npmrc file
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -1,3 +1,37 @@
|
|||||||
|
## [7.2.5](https://git.datacontroller.io/dc/dc/compare/v7.2.4...v7.2.5) (2025-12-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* (build) rebuilt package-lock files ([bfbfd55](https://git.datacontroller.io/dc/dc/commit/bfbfd55fe7e2dff3ce707763a2c7939ff365318b))
|
||||||
|
* (deps) bump @sasjs/cli and @sasjs/core ([d7c7302](https://git.datacontroller.io/dc/dc/commit/d7c7302c12ac60f355ab9b3b1b461fcf7d0719b8))
|
||||||
|
* (deps) bumped @sasjs/core, @sasjs/cli, @sasjs/utils and @sasjs/adapter ([af1657e](https://git.datacontroller.io/dc/dc/commit/af1657e226a4efd22cc87401a3850c4a665c2680))
|
||||||
|
* configurable audit table on restore check ([26ce95f](https://git.datacontroller.io/dc/dc/commit/26ce95f7c1d2260f81c240cd6b058db154d997e4)), closes [#193](https://git.datacontroller.io/dc/dc/issues/193)
|
||||||
|
* improved testing ([fb3c49a](https://git.datacontroller.io/dc/dc/commit/fb3c49aa8bfdc6acf2ae3034b885010dcdce32a6))
|
||||||
|
* output values to intended macro variables ([43ae73c](https://git.datacontroller.io/dc/dc/commit/43ae73c5f3ad919394201f54984b61bb2a52fcfe))
|
||||||
|
|
||||||
|
## [7.2.4](https://git.datacontroller.io/dc/dc/compare/v7.2.3...v7.2.4) (2025-10-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* ensure reload after applying licence key ([cb1978b](https://git.datacontroller.io/dc/dc/commit/cb1978bcaf23b0bf45b5d3b78b9707fd4e48a5f4))
|
||||||
|
* snyk report security patches ([387f512](https://git.datacontroller.io/dc/dc/commit/387f5122f1ea6dff55d23c9223f17737283a94d3))
|
||||||
|
|
||||||
|
## [7.2.3](https://git.datacontroller.io/dc/dc/compare/v7.2.2...v7.2.3) (2025-10-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* opening second table in viewer throws an error ([6c6b1cb](https://git.datacontroller.io/dc/dc/commit/6c6b1cbf460e5291ec746af017e764b894fff8d5))
|
||||||
|
|
||||||
|
## [7.2.2](https://git.datacontroller.io/dc/dc/compare/v7.2.1...v7.2.2) (2025-09-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* jsrsasign, @sasjs/cli bump ([365f129](https://git.datacontroller.io/dc/dc/commit/365f12996db3ef50a4f4f099d5af15696c43bb42))
|
||||||
|
|
||||||
## [7.2.1](https://git.datacontroller.io/dc/dc/compare/v7.2.0...v7.2.1) (2025-08-08)
|
## [7.2.1](https://git.datacontroller.io/dc/dc/compare/v7.2.0...v7.2.1) (2025-08-08)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const check = (cwd) => {
|
|||||||
onlyAllow:
|
onlyAllow:
|
||||||
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
|
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
|
||||||
excludePackages:
|
excludePackages:
|
||||||
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;handsontable@16.0.1;hyperformula@2.7.1;hyperformula@3.0.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1'
|
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;handsontable@^16.0.1;handsontable@16.2.0;hyperformula@2.7.1;hyperformula@3.0.0;hyperformula@3.1.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1'
|
||||||
},
|
},
|
||||||
(error, json) => {
|
(error, json) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
3578
client/package-lock.json
generated
3578
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -50,8 +50,8 @@
|
|||||||
"@clr/icons": "^13.0.2",
|
"@clr/icons": "^13.0.2",
|
||||||
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
|
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
|
||||||
"@handsontable/angular-wrapper": "16.0.1",
|
"@handsontable/angular-wrapper": "16.0.1",
|
||||||
"@sasjs/adapter": "^4.12.2",
|
"@sasjs/adapter": "^4.16.0",
|
||||||
"@sasjs/utils": "^3.4.0",
|
"@sasjs/utils": "^3.5.3",
|
||||||
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
||||||
"@types/d3-graphviz": "^2.6.7",
|
"@types/d3-graphviz": "^2.6.7",
|
||||||
"@types/text-encoding": "0.0.35",
|
"@types/text-encoding": "0.0.35",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"hyperformula": "^2.5.0",
|
"hyperformula": "^2.5.0",
|
||||||
"iconv-lite": "^0.5.0",
|
"iconv-lite": "^0.5.0",
|
||||||
"jquery-datetimepicker": "^2.5.21",
|
"jquery-datetimepicker": "^2.5.21",
|
||||||
"jsrsasign": "^10.2.0",
|
"jsrsasign": "^11.1.0",
|
||||||
"marked": "^5.0.0",
|
"marked": "^5.0.0",
|
||||||
"moment": "^2.26.0",
|
"moment": "^2.26.0",
|
||||||
"ngx-clipboard": "^16.0.0",
|
"ngx-clipboard": "^16.0.0",
|
||||||
|
|||||||
@@ -171,24 +171,9 @@ export class EditRecordComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public copyToClip(text: string) {
|
public copyToClip(text: string) {
|
||||||
const modalElement = document.querySelector('#recordModalRef .modal-title')
|
navigator.clipboard.writeText(text)
|
||||||
|
|
||||||
if (modalElement) {
|
|
||||||
const selBox = document.createElement('textarea')
|
|
||||||
selBox.style.position = 'fixed'
|
|
||||||
selBox.style.left = '0'
|
|
||||||
selBox.style.top = '0'
|
|
||||||
selBox.style.opacity = '0'
|
|
||||||
selBox.style.zIndex = '5000'
|
|
||||||
selBox.value = text
|
|
||||||
modalElement.appendChild(selBox)
|
|
||||||
selBox.focus()
|
|
||||||
selBox.select()
|
|
||||||
document.execCommand('copy')
|
|
||||||
modalElement.removeChild(selBox)
|
|
||||||
this.generatedRecordUrl = text
|
this.generatedRecordUrl = text
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async generateEditRecordUrl() {
|
async generateEditRecordUrl() {
|
||||||
if (this.generatedRecordUrl) {
|
if (this.generatedRecordUrl) {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export class LicensingComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
private licenceService: LicenceService,
|
private licenceService: LicenceService,
|
||||||
private sasService: SasService,
|
private sasService: SasService,
|
||||||
private appService: AppService
|
private appService: AppService
|
||||||
@@ -124,7 +125,9 @@ export class LicensingComponent implements OnInit {
|
|||||||
res.adapterResponse.return[0] &&
|
res.adapterResponse.return[0] &&
|
||||||
res.adapterResponse.return[0].MSG === 'SUCCESS'
|
res.adapterResponse.return[0].MSG === 'SUCCESS'
|
||||||
) {
|
) {
|
||||||
location.replace(location.href.split('#')[0])
|
this.router.navigateByUrl('/').then(() => {
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|||||||
@@ -746,28 +746,13 @@ export class LineageComponent {
|
|||||||
return URL.createObjectURL(svg_blob)
|
return URL.createObjectURL(svg_blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSVGBlob() {
|
|
||||||
let svg: any = document.getElementById('graph')
|
|
||||||
let serializer = new XMLSerializer()
|
|
||||||
let svg_blob = new Blob([serializer.serializeToString(svg)], {
|
|
||||||
type: 'image/svg+xml'
|
|
||||||
})
|
|
||||||
return svg_blob
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadSVG() {
|
downloadSVG() {
|
||||||
d3Viz.graphviz('#graph').resetZoom()
|
d3Viz.graphviz('#graph').resetZoom()
|
||||||
|
|
||||||
if (navigator.appVersion.toString().indexOf('.NET') > 0) {
|
|
||||||
window.navigator.msSaveBlob(this.getSVGBlob(), this.constructName('svg'))
|
|
||||||
} else {
|
|
||||||
let downloadLink = document.createElement('a')
|
let downloadLink = document.createElement('a')
|
||||||
downloadLink.href = this.getSVGURL()
|
downloadLink.href = this.getSVGURL()
|
||||||
downloadLink.download = this.constructName('svg')
|
downloadLink.download = this.constructName('svg')
|
||||||
document.body.appendChild(downloadLink)
|
|
||||||
downloadLink.click()
|
downloadLink.click()
|
||||||
document.body.removeChild(downloadLink)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadPNG() {
|
async downloadPNG() {
|
||||||
@@ -795,16 +780,11 @@ export class LineageComponent {
|
|||||||
var a = document.createElement('a')
|
var a = document.createElement('a')
|
||||||
var blob = new Blob([csvArray], { type: 'text/csv' })
|
var blob = new Blob([csvArray], { type: 'text/csv' })
|
||||||
|
|
||||||
if (navigator.appVersion.toString().indexOf('.NET') > 0) {
|
|
||||||
window.navigator.msSaveBlob(blob, this.constructName('csv'))
|
|
||||||
} else {
|
|
||||||
var url = window.URL.createObjectURL(blob)
|
var url = window.URL.createObjectURL(blob)
|
||||||
a.href = url
|
a.href = url
|
||||||
a.download = this.constructName('csv')
|
a.download = this.constructName('csv')
|
||||||
a.click()
|
a.click()
|
||||||
window.URL.revokeObjectURL(url)
|
window.URL.revokeObjectURL(url)
|
||||||
a.remove()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDotUrl() {
|
private getDotUrl() {
|
||||||
@@ -813,23 +793,11 @@ export class LineageComponent {
|
|||||||
return window.URL.createObjectURL(dot_blob)
|
return window.URL.createObjectURL(dot_blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDotBlob() {
|
|
||||||
let data = this.vizInput
|
|
||||||
let dot_blob = new Blob([data], { type: 'text/plain' })
|
|
||||||
return dot_blob
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadDot() {
|
downloadDot() {
|
||||||
if (navigator.appVersion.toString().indexOf('.NET') > 0) {
|
|
||||||
window.navigator.msSaveBlob(this.getDotBlob(), this.constructName('txt'))
|
|
||||||
} else {
|
|
||||||
let downloadLink = document.createElement('a')
|
let downloadLink = document.createElement('a')
|
||||||
downloadLink.href = this.getDotUrl()
|
downloadLink.href = this.getDotUrl()
|
||||||
downloadLink.download = this.constructName('txt')
|
downloadLink.download = this.constructName('txt')
|
||||||
document.body.appendChild(downloadLink)
|
|
||||||
downloadLink.click()
|
downloadLink.click()
|
||||||
document.body.removeChild(downloadLink)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public showSvg() {
|
public showSvg() {
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ class SubmittedFilter implements ClrDatagridStringFilterInterface<ApproveData> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubmitReasonFilter
|
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<ApproveData> {
|
||||||
implements ClrDatagridStringFilterInterface<ApproveData>
|
|
||||||
{
|
|
||||||
accepts(data: ApproveData, search: string): boolean {
|
accepts(data: ApproveData, search: string): boolean {
|
||||||
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ class SubmitterFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubmitReasonFilter
|
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||||
implements ClrDatagridStringFilterInterface<HistoryData>
|
|
||||||
{
|
|
||||||
accepts(data: HistoryData, search: string): boolean {
|
accepts(data: HistoryData, search: string): boolean {
|
||||||
return data.submittedReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
return data.submittedReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,17 +17,13 @@ interface SubmitterData {
|
|||||||
approver: string
|
approver: string
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubmittedFilter
|
class SubmittedFilter implements ClrDatagridStringFilterInterface<SubmitterData> {
|
||||||
implements ClrDatagridStringFilterInterface<SubmitterData>
|
|
||||||
{
|
|
||||||
accepts(data: SubmitterData, search: string): boolean {
|
accepts(data: SubmitterData, search: string): boolean {
|
||||||
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubmitReasonFilter
|
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<SubmitterData> {
|
||||||
implements ClrDatagridStringFilterInterface<SubmitterData>
|
|
||||||
{
|
|
||||||
accepts(data: SubmitterData, search: string): boolean {
|
accepts(data: SubmitterData, search: string): boolean {
|
||||||
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import Handsontable from 'handsontable'
|
import Handsontable from 'handsontable'
|
||||||
import Core from 'handsontable/core'
|
import Core from 'handsontable/core'
|
||||||
|
|
||||||
export class CustomAutocompleteEditor extends Handsontable.editors
|
export class CustomAutocompleteEditor
|
||||||
.AutocompleteEditor {
|
extends Handsontable.editors.AutocompleteEditor
|
||||||
|
{
|
||||||
constructor(instance: Core) {
|
constructor(instance: Core) {
|
||||||
super(instance)
|
super(instance)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export interface DcColumnSettings {
|
|||||||
export interface DcValidation extends HotColumnSettings, DcColumnSettings {}
|
export interface DcValidation extends HotColumnSettings, DcColumnSettings {}
|
||||||
|
|
||||||
export interface DcValidationRuleUpdate
|
export interface DcValidationRuleUpdate
|
||||||
extends Handsontable.ColumnSettings,
|
extends Handsontable.ColumnSettings, DcColumnSettings {
|
||||||
DcColumnSettings {
|
|
||||||
data?: string
|
data?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,15 +157,30 @@ export class ViewerComponent
|
|||||||
return ' '
|
return ' '
|
||||||
},
|
},
|
||||||
afterGetColHeader: (col: number, th: any, headerLevel: number) => {
|
afterGetColHeader: (col: number, th: any, headerLevel: number) => {
|
||||||
const column = this.hotInstance?.colToProp(col) as string
|
// CRITICAL: Prevent "colToProp method cannot be called because this Handsontable instance has been destroyed" error
|
||||||
|
// This callback can be triggered even after the instance is destroyed during rapid table switching
|
||||||
|
if (
|
||||||
|
!this.hotInstance ||
|
||||||
|
this.hotInstance.isDestroyed ||
|
||||||
|
this.isTableSwitching
|
||||||
|
) {
|
||||||
|
// Graceful fallback: apply only dark mode styling when instance is unavailable
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const column = this.hotInstance.colToProp(col) as string
|
||||||
|
|
||||||
// header columns styling - primary keys
|
|
||||||
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
||||||
|
|
||||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||||
|
|
||||||
// Dark mode
|
// Apply dark mode styling to all headers
|
||||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
} catch (error) {
|
||||||
|
// Safety net: if colToProp() fails, still apply basic styling
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
rowHeaderWidth: 15,
|
rowHeaderWidth: 15,
|
||||||
rowHeights: 20,
|
rowHeights: 20,
|
||||||
@@ -199,12 +214,21 @@ export class ViewerComponent
|
|||||||
let colInfo: DataFormat | undefined
|
let colInfo: DataFormat | undefined
|
||||||
let textInfo = 'No info found'
|
let textInfo = 'No info found'
|
||||||
|
|
||||||
if (this.hotInstance) {
|
if (
|
||||||
|
this.hotInstance &&
|
||||||
|
!this.hotInstance.isDestroyed &&
|
||||||
|
!this.isTableSwitching
|
||||||
|
) {
|
||||||
|
try {
|
||||||
const hotSelected: [number, number, number, number][] =
|
const hotSelected: [number, number, number, number][] =
|
||||||
this.hotInstance.getSelected() || []
|
this.hotInstance.getSelected() || []
|
||||||
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
|
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
|
||||||
const colName = this.hotInstance?.colToProp(selectedCol)
|
const colName = this.hotInstance.colToProp(selectedCol)
|
||||||
colInfo = this.$dataFormats?.vars[colName]
|
colInfo = this.$dataFormats?.vars[colName]
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors during table switching
|
||||||
|
colInfo = undefined
|
||||||
|
}
|
||||||
|
|
||||||
if (colInfo)
|
if (colInfo)
|
||||||
textInfo = `LABEL: ${colInfo?.label}<br>TYPE: ${colInfo?.type}<br>LENGTH: ${colInfo?.length}<br>FORMAT: ${colInfo?.format}`
|
textInfo = `LABEL: ${colInfo?.label}<br>TYPE: ${colInfo?.type}<br>LENGTH: ${colInfo?.length}<br>FORMAT: ${colInfo?.format}`
|
||||||
@@ -224,6 +248,13 @@ export class ViewerComponent
|
|||||||
private hotInstance: Handsontable | null = null
|
private hotInstance: Handsontable | null = null
|
||||||
public hotInstanceClickListener: boolean = false
|
public hotInstanceClickListener: boolean = false
|
||||||
|
|
||||||
|
// Race condition prevention for rapid table switching
|
||||||
|
private isTableSwitching: boolean = false
|
||||||
|
private switchingTimeout: any = null
|
||||||
|
|
||||||
|
// Prevents duplicate setupHot() calls within short time windows
|
||||||
|
private lastSetupTime: number = 0
|
||||||
|
|
||||||
public viewboxOpen: boolean = false
|
public viewboxOpen: boolean = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -505,17 +536,7 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public copyToClip() {
|
public copyToClip() {
|
||||||
let selBox = document.createElement('textarea')
|
navigator.clipboard.writeText(this.webQueryText)
|
||||||
selBox.style.position = 'fixed'
|
|
||||||
selBox.style.left = '0'
|
|
||||||
selBox.style.top = '0'
|
|
||||||
selBox.style.opacity = '0'
|
|
||||||
selBox.value = this.webQueryText
|
|
||||||
document.body.appendChild(selBox)
|
|
||||||
selBox.focus()
|
|
||||||
selBox.select()
|
|
||||||
document.execCommand('copy')
|
|
||||||
document.body.removeChild(selBox)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public goToViewer() {
|
public goToViewer() {
|
||||||
@@ -599,10 +620,24 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onTableClick(libTable: any, library: any) {
|
public onTableClick(libTable: any, library: any) {
|
||||||
|
// OPTIMIZATION: Prevent race conditions and destroyed instance errors during rapid table switching
|
||||||
|
if (this.isTableSwitching) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing timeout to prevent stale operations
|
||||||
|
if (this.switchingTimeout) {
|
||||||
|
clearTimeout(this.switchingTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PERFORMANCE: Debounce table switches to prevent rapid successive calls
|
||||||
|
// This ensures only the final table selection is processed
|
||||||
|
this.switchingTimeout = setTimeout(() => {
|
||||||
this.lib = library.LIBRARYREF
|
this.lib = library.LIBRARYREF
|
||||||
this.table = libTable
|
this.table = libTable
|
||||||
this.selectLibTable(libTable)
|
this.selectLibTable(libTable)
|
||||||
this.viewData(0)
|
this.viewData(0)
|
||||||
|
}, 50) // 50ms debounce - fast enough for good UX, slow enough to prevent issues
|
||||||
}
|
}
|
||||||
|
|
||||||
public async selectTable(lib: string, initial?: boolean, library?: any) {
|
public async selectTable(lib: string, initial?: boolean, library?: any) {
|
||||||
@@ -721,6 +756,14 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async viewData(filter_pk: number) {
|
public async viewData(filter_pk: number) {
|
||||||
|
// CRITICAL: Set switching flag to prevent concurrent operations and race conditions
|
||||||
|
// This prevents callbacks from accessing destroyed instances during table switching
|
||||||
|
this.isTableSwitching = true
|
||||||
|
|
||||||
|
// CLEANUP: Ensure any existing Handsontable instance is properly destroyed
|
||||||
|
// This prevents "instance destroyed" errors
|
||||||
|
this.cleanupHotInstance()
|
||||||
|
|
||||||
this.loadingTableView = true
|
this.loadingTableView = true
|
||||||
|
|
||||||
let libDataset: any
|
let libDataset: any
|
||||||
@@ -961,14 +1004,25 @@ export class ViewerComponent
|
|||||||
|
|
||||||
this.loadingTableView = false
|
this.loadingTableView = false
|
||||||
|
|
||||||
//If we try to setup hot when no data is returned it errors `isDestoryed`.
|
// Setup Handsontable after async operations complete
|
||||||
//That is intorduced by HOT update
|
// Original issue: setupHot() called before API responses populated headerPks array
|
||||||
if (!this.noData && !this.noDataReqErr && libDataset) this.setupHot()
|
// Solution: Delay ensures both API paths (lines 328 & 886) have chance to set headerPks
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.noData && !this.noDataReqErr && libDataset) {
|
||||||
|
this.setupHot()
|
||||||
|
}
|
||||||
|
}, 50) // Optimized from 100ms - fast enough for API completion, slow enough to prevent race conditions
|
||||||
|
|
||||||
|
// RACE CONDITION PREVENTION: Reset switching flag after setup completion
|
||||||
|
// This allows new table switches after current operation finishes
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isTableSwitching = false
|
||||||
|
}, 300) // Optimized from 700ms to match reduced setup times
|
||||||
|
|
||||||
// Fix ARIA accessibility issues after data loading
|
// Fix ARIA accessibility issues after data loading
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.fixAriaAccessibility()
|
this.fixAriaAccessibility()
|
||||||
}, 1500)
|
}, 500)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area.
|
* This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area.
|
||||||
@@ -988,7 +1042,7 @@ export class ViewerComponent
|
|||||||
})
|
})
|
||||||
this.hotInstanceClickListener = true
|
this.hotInstanceClickListener = true
|
||||||
}
|
}
|
||||||
}, 2000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1116,12 +1170,74 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CRITICAL CLEANUP (workaround needed for HOT version 16 and above): Safely destroys Handsontable instances
|
||||||
|
*
|
||||||
|
* Purpose: Prevents "instance destroyed" errors and memory leaks during table switching
|
||||||
|
*
|
||||||
|
* Called from:
|
||||||
|
* - viewData() - before loading new table data
|
||||||
|
* - setupHot() - before creating new instance
|
||||||
|
* - ngOnDestroy() - component cleanup
|
||||||
|
*
|
||||||
|
* Safety features:
|
||||||
|
* - Checks if instance exists and is not already destroyed
|
||||||
|
* - Try-catch prevents errors during destruction
|
||||||
|
* - Sets instance to null to prevent stale references
|
||||||
|
*/
|
||||||
|
private cleanupHotInstance() {
|
||||||
|
if (this.hotInstance && !this.hotInstance.isDestroyed) {
|
||||||
|
try {
|
||||||
|
this.hotInstance.destroy()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error destroying Handsontable instance:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.hotInstance = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PERFORMANCE: Configures Handsontable with enhanced error handling (workaround needed for HOT version 16 and above)
|
||||||
|
*
|
||||||
|
* 1. Duplicate call prevention (500ms window)
|
||||||
|
* 2. Reduced timeout delays (200ms + 50ms vs original 1000ms + 200ms)
|
||||||
|
* 3. Multiple validation checks to prevent race conditions
|
||||||
|
* 4. Forced render for immediate primary key styling
|
||||||
|
*
|
||||||
|
* Timeline: 50ms (viewData) + 200ms (main) + 50ms (component ready) = ~300ms total
|
||||||
|
* Previous: 100ms + 600ms + 100ms = 800ms (plus render delays = ~2 seconds)
|
||||||
|
*/
|
||||||
private setupHot() {
|
private setupHot() {
|
||||||
|
// DUPLICATE PREVENTION: Avoid multiple setup calls during rapid table switching
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - this.lastSetupTime < 500) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.lastSetupTime = now
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.loadingTableView && this.libDataset) {
|
// VALIDATION: Don't setup if we're currently switching tables or still loading
|
||||||
|
if (this.loadingTableView || !this.libDataset) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLEANUP: Ensure clean slate before new setup
|
||||||
|
this.cleanupHotInstance()
|
||||||
|
|
||||||
|
// TIMING: Wait for Angular component to be ready (optimized from 100ms to 50ms)
|
||||||
|
setTimeout(() => {
|
||||||
|
// DOUBLE-CHECK: Ensure we're still in valid state after delays
|
||||||
|
if (
|
||||||
|
this.isTableSwitching ||
|
||||||
|
this.loadingTableView ||
|
||||||
|
!this.libDataset
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.hotInstance = this.hotTableComponent?.hotInstance
|
this.hotInstance = this.hotTableComponent?.hotInstance
|
||||||
|
|
||||||
if (this.hotInstance) {
|
if (this.hotInstance && !this.hotInstance.isDestroyed) {
|
||||||
this.hotInstance.updateSettings({
|
this.hotInstance.updateSettings({
|
||||||
height: this.hotTable.height,
|
height: this.hotTable.height,
|
||||||
modifyColWidth: (width: any, col: any) => {
|
modifyColWidth: (width: any, col: any) => {
|
||||||
@@ -1129,15 +1245,29 @@ export class ViewerComponent
|
|||||||
else return width
|
else return width
|
||||||
},
|
},
|
||||||
afterGetColHeader: (col: number, th: any) => {
|
afterGetColHeader: (col: number, th: any) => {
|
||||||
const column = this.hotInstance?.colToProp(col) as string
|
// CRITICAL: Same safety checks as initial callback to prevent destroyed instance errors
|
||||||
|
if (
|
||||||
|
!this.hotInstance ||
|
||||||
|
this.hotInstance.isDestroyed ||
|
||||||
|
this.isTableSwitching
|
||||||
|
) {
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// header columns styling - primary keys
|
try {
|
||||||
|
const column = this.hotInstance.colToProp(col) as string
|
||||||
|
|
||||||
|
// PRIMARY KEY STYLING: Apply special styling to PK columns (populated from API response)
|
||||||
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
||||||
|
|
||||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||||
|
|
||||||
// Dark mode
|
// DARK MODE: Apply to all headers
|
||||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
} catch (error) {
|
||||||
|
// SAFETY NET: Ensure basic styling is always applied
|
||||||
|
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1153,14 +1283,18 @@ export class ViewerComponent
|
|||||||
this.fixAriaAccessibility()
|
this.fixAriaAccessibility()
|
||||||
}, 50)
|
}, 50)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix ARIA accessibility issues after table setup
|
// Force immediate render to apply primary key styling
|
||||||
|
// Without this, styling would wait for ~2 seconds to be applied
|
||||||
|
// With this, styling appears in ~300ms total (workaround needed for HOT version 16 and above)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.fixAriaAccessibility()
|
if (this.hotInstance && !this.hotInstance.isDestroyed) {
|
||||||
}, 500)
|
this.hotInstance.render()
|
||||||
}, 1000)
|
}
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
}, 50) // Optimized Angular component readiness delay
|
||||||
|
}, 200) // Optimized main setup delay (was 600ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadWithParameters() {
|
async loadWithParameters() {
|
||||||
@@ -1233,13 +1367,27 @@ export class ViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
// Clean up the MutationObserver
|
// Proper component destruction to prevent memory leaks and errors
|
||||||
|
|
||||||
|
// Prevent any new operations during cleanup
|
||||||
|
this.isTableSwitching = true
|
||||||
|
|
||||||
|
// Clear any pending debounced table switches
|
||||||
|
if (this.switchingTimeout) {
|
||||||
|
clearTimeout(this.switchingTimeout)
|
||||||
|
this.switchingTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safely destroy Handsontable instance
|
||||||
|
this.cleanupHotInstance()
|
||||||
|
|
||||||
|
// Clean up ARIA accessibility observers
|
||||||
if (this.ariaObserver) {
|
if (this.ariaObserver) {
|
||||||
this.ariaObserver.disconnect()
|
this.ariaObserver.disconnect()
|
||||||
this.ariaObserver = undefined
|
this.ariaObserver = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up the interval
|
// Clear ARIA check intervals
|
||||||
if (this.ariaCheckInterval) {
|
if (this.ariaCheckInterval) {
|
||||||
clearInterval(this.ariaCheckInterval)
|
clearInterval(this.ariaCheckInterval)
|
||||||
this.ariaCheckInterval = undefined
|
this.ariaCheckInterval = undefined
|
||||||
|
|||||||
559
package-lock.json
generated
559
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dcfrontend",
|
"name": "dcfrontend",
|
||||||
"version": "7.2.1",
|
"version": "7.2.5",
|
||||||
"description": "Data Controller",
|
"description": "Data Controller",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@saithodev/semantic-release-gitea": "^2.1.0",
|
"@saithodev/semantic-release-gitea": "^2.1.0",
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"@semantic-release/npm": "11.0.0",
|
"@semantic-release/npm": "11.0.0",
|
||||||
"@semantic-release/release-notes-generator": "^11.0.4",
|
"@semantic-release/release-notes-generator": "^11.0.4",
|
||||||
"commit-and-tag-version": "^11.2.2",
|
"commit-and-tag-version": "^11.2.2",
|
||||||
"prettier": "3.6.2"
|
"prettier": "^3.7.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install": "cd client && npm i && cd ../sas && npm i",
|
"install": "cd client && npm i && cd ../sas && npm i",
|
||||||
@@ -32,6 +32,5 @@
|
|||||||
"//": [
|
"//": [
|
||||||
"Readme",
|
"Readme",
|
||||||
"We must set private: true so that semantic-release/npm plugin will update the package.json version but not try to release it as NPM package"
|
"We must set private: true so that semantic-release/npm plugin will update the package.json version but not try to release it as NPM package"
|
||||||
],
|
]
|
||||||
"dependencies": {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
377
sas/package-lock.json
generated
377
sas/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sasjs/cli": "^4.12.10",
|
"@sasjs/cli": "^4.12.15",
|
||||||
"@sasjs/core": "^4.59.5"
|
"@sasjs/core": "^4.59.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,15 +39,35 @@
|
|||||||
%let &outresult=NO;
|
%let &outresult=NO;
|
||||||
%let &outreason=NOTFOUND;
|
%let &outreason=NOTFOUND;
|
||||||
|
|
||||||
/* check if there is actually a version to restore */
|
%local libds;
|
||||||
|
proc sql noprint;
|
||||||
|
select upcase(cats(base_lib,'.',base_ds)) into: libds
|
||||||
|
from &dc_libref..mpe_submit
|
||||||
|
where TABLE_ID="&load_ref";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if there is actually a version to restore
|
||||||
|
*/
|
||||||
|
%local audtab;
|
||||||
|
proc sql noprint;
|
||||||
|
select coalescec(audit_libds,"&dc_libref..MPE_AUDIT") into: audtab
|
||||||
|
from &dclib..MPE_TABLES
|
||||||
|
where &dc_dttmtfmt. lt tx_to
|
||||||
|
and libref="%scan(&libds,1,.)" and dsn="%scan(&libds,2,.)";
|
||||||
|
%if "&audtab"="0" %then %do;
|
||||||
|
%let &outresult=NO;
|
||||||
|
%let &outreason= &libds has no audit table configured;
|
||||||
|
%return;
|
||||||
|
%end;
|
||||||
|
|
||||||
%local chk;
|
%local chk;
|
||||||
%let chk=0;
|
%let chk=0;
|
||||||
proc sql noprint;
|
proc sql noprint;
|
||||||
select count(*) into: chk from &dc_libref..mpe_audit
|
select count(*) into: chk from &audtab
|
||||||
where load_ref="&load_ref";
|
where load_ref="&load_ref";
|
||||||
%if &chk=0 %then %do;
|
%if &chk=0 %then %do;
|
||||||
%let allow_restore=NO;
|
%let &outresult=NO;
|
||||||
%let reason=No entry for &load_ref in MPE_AUDIT;
|
%let &outreason=No entry for &load_ref in &audtab;
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
@@ -64,24 +84,19 @@
|
|||||||
where groupname="&dc_admin_group";
|
where groupname="&dc_admin_group";
|
||||||
|
|
||||||
%if &is_admin>0 %then %do;
|
%if &is_admin>0 %then %do;
|
||||||
%let allow_restore=YES;
|
%let &outresult=YES;
|
||||||
%let reason=IS ADMIN;
|
%let &outreason=IS ADMIN;
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
/* check if user has basic access */
|
/* check if user has basic access */
|
||||||
%local libds;
|
|
||||||
proc sql noprint;
|
|
||||||
select cats(base_lib,'.',base_ds) into: libds
|
|
||||||
from &mpelib..mpe_submit
|
|
||||||
where TABLE_ID="&load_ref";
|
|
||||||
%mpe_accesscheck(&libds,outds=work.access_check
|
%mpe_accesscheck(&libds,outds=work.access_check
|
||||||
,user=&user
|
,user=&user
|
||||||
,access_level=EDIT
|
,access_level=EDIT
|
||||||
)
|
)
|
||||||
%if %mf_nobs(access_check)=0 %then %do;
|
%if %mf_nobs(access_check)=0 %then %do;
|
||||||
%let allow_restore=NO;
|
%let &outresult=NO;
|
||||||
%let reason=No access in MPE_TABLES;
|
%let &outreason=No access in MPE_TABLES;
|
||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
@@ -97,8 +112,8 @@
|
|||||||
and CLS_LIBREF="%upcase(&base_lib)"
|
and CLS_LIBREF="%upcase(&base_lib)"
|
||||||
and CLS_TABLE="%upcase(&base_ds)";
|
and CLS_TABLE="%upcase(&base_ds)";
|
||||||
%if %mf_nobs(work.cls_rules)>0 %then %do;
|
%if %mf_nobs(work.cls_rules)>0 %then %do;
|
||||||
%let allow_restore=NO;
|
%let &outresult=NO;
|
||||||
%let reason=User has restrictions in MPE_COLUMN_LEVEL_SECURITY;
|
%let &outreason=User has restrictions in MPE_COLUMN_LEVEL_SECURITY;
|
||||||
data _null_;
|
data _null_;
|
||||||
set work.cls_rules;
|
set work.cls_rules;
|
||||||
putlog (_all_)(=);
|
putlog (_all_)(=);
|
||||||
@@ -119,8 +134,8 @@
|
|||||||
and rls_table="&base_ds"
|
and rls_table="&base_ds"
|
||||||
and rls_active=1;
|
and rls_active=1;
|
||||||
%if %mf_nobs(work.rls_rules)>0 %then %do;
|
%if %mf_nobs(work.rls_rules)>0 %then %do;
|
||||||
%let allow_restore=NO;
|
%let &outresult=NO;
|
||||||
%let reason=User has restrictions in MPE_ROW_LEVEL_SECURITY;
|
%let &outreason=User has restrictions in MPE_ROW_LEVEL_SECURITY;
|
||||||
data _null_;
|
data _null_;
|
||||||
set work.rls_rules;
|
set work.rls_rules;
|
||||||
putlog (_all_)(=);
|
putlog (_all_)(=);
|
||||||
@@ -129,7 +144,7 @@
|
|||||||
%return;
|
%return;
|
||||||
%end;
|
%end;
|
||||||
%else %do;
|
%else %do;
|
||||||
%let allow_restore=YES;
|
%let &outresult=YES;
|
||||||
%let reason=CHECKS PASSED;
|
%let &outreason=CHECKS PASSED;
|
||||||
%end;
|
%end;
|
||||||
%mend mpe_checkrestore;
|
%mend mpe_checkrestore;
|
||||||
|
|||||||
@@ -35,14 +35,12 @@ run;
|
|||||||
%dc_getsettings()
|
%dc_getsettings()
|
||||||
|
|
||||||
%put checking it is restorable;
|
%put checking it is restorable;
|
||||||
%global allow_restore reason;
|
|
||||||
%mp_assertscope(SNAPSHOT)
|
%mp_assertscope(SNAPSHOT)
|
||||||
%mpe_checkrestore(&loadref,outresult=ALLOW_RESTORE,outreason=REASON)
|
%mpe_checkrestore(&loadref,outresult=ALLOW_RESTORE,outreason=REASON)
|
||||||
%mp_assertscope(COMPARE,
|
%mp_assertscope(COMPARE,
|
||||||
desc=Checking macro variables against previous snapshot,
|
desc=Checking macro variables against previous snapshot,
|
||||||
ignorelist=ALLOW_RESTORE REASON
|
ignorelist=ALLOW_RESTORE REASON
|
||||||
MCLIB0_JADP1LEN MCLIB0_JADP2LEN MCLIB0_JADPNUM MCLIB0_JADVLEN
|
MC0_JADP1LEN MC0_JADP2LEN MC0_JADP3LEN MC0_JADPNUM MC0_JADVLEN
|
||||||
MCLIB2_JADP1LEN MCLIB2_JADP2LEN MCLIB2_JADPNUM MCLIB2_JADVLEN
|
|
||||||
)
|
)
|
||||||
|
|
||||||
%mp_assert(
|
%mp_assert(
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "viya",
|
"name": "viya",
|
||||||
"serverUrl": "https://viya-f0g8ht62vq.engage.sas.com",
|
"serverUrl": "",
|
||||||
"serverType": "SASVIYA",
|
"serverType": "SASVIYA",
|
||||||
"httpsAgentOptions": {
|
"httpsAgentOptions": {
|
||||||
"allowInsecureRequests": false
|
"allowInsecureRequests": false
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@brief testing getchangeinfo service
|
@brief testing getchangeinfo service
|
||||||
|
|
||||||
<h4> SAS Macros </h4>
|
<h4> SAS Macros </h4>
|
||||||
|
@li mp_assert.sas
|
||||||
@li mp_assertcolvals.sas
|
@li mp_assertcolvals.sas
|
||||||
@li mf_getuniquefileref.sas
|
@li mf_getuniquefileref.sas
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ data _null_;
|
|||||||
cnt=find(_pgm,'/tests/');
|
cnt=find(_pgm,'/tests/');
|
||||||
if cnt=0 then cnt=find(_pgm,'/services/');
|
if cnt=0 then cnt=find(_pgm,'/services/');
|
||||||
if cnt=0 then cnt=find(_pgm,'/jobs/');
|
if cnt=0 then cnt=find(_pgm,'/jobs/');
|
||||||
put cnt= apploc= _pgm=;
|
|
||||||
apploc=substr(_pgm,1,cnt-1);
|
apploc=substr(_pgm,1,cnt-1);
|
||||||
|
put cnt= apploc= _pgm=;
|
||||||
call symputx('apploc',apploc);
|
call symputx('apploc',apploc);
|
||||||
end;
|
end;
|
||||||
run;
|
run;
|
||||||
|
|||||||
Reference in New Issue
Block a user