Compare commits
32 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 | ||
|
|
ef1015f33b | ||
| b43dfb5cf4 | |||
|
|
225e693d1f |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.15.1
|
||||
node-version: 24.5.0
|
||||
|
||||
- name: Install Google Chrome
|
||||
run: |
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.15.1
|
||||
node-version: 24.5.0
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: |
|
||||
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.15.1]
|
||||
node-version: [24.5.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -13,12 +13,12 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.14.0
|
||||
node-version: 24.5.0
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: |
|
||||
touch client/.npmrc
|
||||
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
||||
touch client/.npmrc
|
||||
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
||||
|
||||
- name: Install Chrome for Angular tests
|
||||
run: |
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
npm ci
|
||||
|
||||
- name: Check audit
|
||||
# Audit should fail and stop the CI if critical vulnerability found
|
||||
# Audit should fail and stop the CI if critical vulnerability found
|
||||
run: |
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
cd ./sas
|
||||
@@ -68,12 +68,12 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.14.0
|
||||
node-version: 24.5.0
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: |
|
||||
touch client/.npmrc
|
||||
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
||||
touch client/.npmrc
|
||||
echo '${{ secrets.NPMRC}}' > client/.npmrc
|
||||
|
||||
- run: apt-get update
|
||||
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.14.0
|
||||
node-version: 24.5.0
|
||||
|
||||
- name: Write .npmrc file
|
||||
run: |
|
||||
|
||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,3 +1,44 @@
|
||||
## [7.2.5](https://git.datacontroller.io/dc/dc/compare/v7.2.4...v7.2.5) (2025-12-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* (build) rebuilt package-lock files ([bfbfd55](https://git.datacontroller.io/dc/dc/commit/bfbfd55fe7e2dff3ce707763a2c7939ff365318b))
|
||||
* (deps) bump @sasjs/cli and @sasjs/core ([d7c7302](https://git.datacontroller.io/dc/dc/commit/d7c7302c12ac60f355ab9b3b1b461fcf7d0719b8))
|
||||
* (deps) bumped @sasjs/core, @sasjs/cli, @sasjs/utils and @sasjs/adapter ([af1657e](https://git.datacontroller.io/dc/dc/commit/af1657e226a4efd22cc87401a3850c4a665c2680))
|
||||
* configurable audit table on restore check ([26ce95f](https://git.datacontroller.io/dc/dc/commit/26ce95f7c1d2260f81c240cd6b058db154d997e4)), closes [#193](https://git.datacontroller.io/dc/dc/issues/193)
|
||||
* improved testing ([fb3c49a](https://git.datacontroller.io/dc/dc/commit/fb3c49aa8bfdc6acf2ae3034b885010dcdce32a6))
|
||||
* output values to intended macro variables ([43ae73c](https://git.datacontroller.io/dc/dc/commit/43ae73c5f3ad919394201f54984b61bb2a52fcfe))
|
||||
|
||||
## [7.2.4](https://git.datacontroller.io/dc/dc/compare/v7.2.3...v7.2.4) (2025-10-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ensure reload after applying licence key ([cb1978b](https://git.datacontroller.io/dc/dc/commit/cb1978bcaf23b0bf45b5d3b78b9707fd4e48a5f4))
|
||||
* snyk report security patches ([387f512](https://git.datacontroller.io/dc/dc/commit/387f5122f1ea6dff55d23c9223f17737283a94d3))
|
||||
|
||||
## [7.2.3](https://git.datacontroller.io/dc/dc/compare/v7.2.2...v7.2.3) (2025-10-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* opening second table in viewer throws an error ([6c6b1cb](https://git.datacontroller.io/dc/dc/commit/6c6b1cbf460e5291ec746af017e764b894fff8d5))
|
||||
|
||||
## [7.2.2](https://git.datacontroller.io/dc/dc/compare/v7.2.1...v7.2.2) (2025-09-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* jsrsasign, @sasjs/cli bump ([365f129](https://git.datacontroller.io/dc/dc/commit/365f12996db3ef50a4f4f099d5af15696c43bb42))
|
||||
|
||||
## [7.2.1](https://git.datacontroller.io/dc/dc/compare/v7.2.0...v7.2.1) (2025-08-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* removing localhost from index.html ([225e693](https://git.datacontroller.io/dc/dc/commit/225e693d1fd4381f2b8ce42fecb508f0a9e9dad8))
|
||||
|
||||
# [7.2.0](https://git.datacontroller.io/dc/dc/compare/v7.1.1...v7.2.0) (2025-08-08)
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ const check = (cwd) => {
|
||||
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-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) => {
|
||||
if (error) {
|
||||
|
||||
3582
client/package-lock.json
generated
3582
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/ui": "file:libraries/clr-ui-17.9.0.tgz",
|
||||
"@handsontable/angular-wrapper": "16.0.1",
|
||||
"@sasjs/adapter": "^4.12.2",
|
||||
"@sasjs/utils": "^3.4.0",
|
||||
"@sasjs/adapter": "^4.16.0",
|
||||
"@sasjs/utils": "^3.5.3",
|
||||
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
||||
"@types/d3-graphviz": "^2.6.7",
|
||||
"@types/text-encoding": "0.0.35",
|
||||
@@ -66,7 +66,7 @@
|
||||
"hyperformula": "^2.5.0",
|
||||
"iconv-lite": "^0.5.0",
|
||||
"jquery-datetimepicker": "^2.5.21",
|
||||
"jsrsasign": "^10.2.0",
|
||||
"jsrsasign": "^11.1.0",
|
||||
"marked": "^5.0.0",
|
||||
"moment": "^2.26.0",
|
||||
"ngx-clipboard": "^16.0.0",
|
||||
|
||||
@@ -171,23 +171,8 @@ export class EditRecordComponent implements OnInit {
|
||||
}
|
||||
|
||||
public copyToClip(text: string) {
|
||||
const modalElement = document.querySelector('#recordModalRef .modal-title')
|
||||
|
||||
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
|
||||
}
|
||||
navigator.clipboard.writeText(text)
|
||||
this.generatedRecordUrl = text
|
||||
}
|
||||
|
||||
async generateEditRecordUrl() {
|
||||
|
||||
@@ -52,6 +52,7 @@ export class LicensingComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private licenceService: LicenceService,
|
||||
private sasService: SasService,
|
||||
private appService: AppService
|
||||
@@ -124,7 +125,9 @@ export class LicensingComponent implements OnInit {
|
||||
res.adapterResponse.return[0] &&
|
||||
res.adapterResponse.return[0].MSG === 'SUCCESS'
|
||||
) {
|
||||
location.replace(location.href.split('#')[0])
|
||||
this.router.navigateByUrl('/').then(() => {
|
||||
window.location.reload()
|
||||
})
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -746,28 +746,13 @@ export class LineageComponent {
|
||||
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() {
|
||||
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')
|
||||
downloadLink.href = this.getSVGURL()
|
||||
downloadLink.download = this.constructName('svg')
|
||||
document.body.appendChild(downloadLink)
|
||||
downloadLink.click()
|
||||
document.body.removeChild(downloadLink)
|
||||
}
|
||||
let downloadLink = document.createElement('a')
|
||||
downloadLink.href = this.getSVGURL()
|
||||
downloadLink.download = this.constructName('svg')
|
||||
downloadLink.click()
|
||||
}
|
||||
|
||||
async downloadPNG() {
|
||||
@@ -795,16 +780,11 @@ export class LineageComponent {
|
||||
var a = document.createElement('a')
|
||||
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)
|
||||
a.href = url
|
||||
a.download = this.constructName('csv')
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
a.remove()
|
||||
}
|
||||
var url = window.URL.createObjectURL(blob)
|
||||
a.href = url
|
||||
a.download = this.constructName('csv')
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
private getDotUrl() {
|
||||
@@ -813,23 +793,11 @@ export class LineageComponent {
|
||||
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() {
|
||||
if (navigator.appVersion.toString().indexOf('.NET') > 0) {
|
||||
window.navigator.msSaveBlob(this.getDotBlob(), this.constructName('txt'))
|
||||
} else {
|
||||
let downloadLink = document.createElement('a')
|
||||
downloadLink.href = this.getDotUrl()
|
||||
downloadLink.download = this.constructName('txt')
|
||||
document.body.appendChild(downloadLink)
|
||||
downloadLink.click()
|
||||
document.body.removeChild(downloadLink)
|
||||
}
|
||||
let downloadLink = document.createElement('a')
|
||||
downloadLink.href = this.getDotUrl()
|
||||
downloadLink.download = this.constructName('txt')
|
||||
downloadLink.click()
|
||||
}
|
||||
|
||||
public showSvg() {
|
||||
|
||||
@@ -38,9 +38,7 @@ class SubmittedFilter implements ClrDatagridStringFilterInterface<ApproveData> {
|
||||
}
|
||||
}
|
||||
|
||||
class SubmitReasonFilter
|
||||
implements ClrDatagridStringFilterInterface<ApproveData>
|
||||
{
|
||||
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<ApproveData> {
|
||||
accepts(data: ApproveData, search: string): boolean {
|
||||
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||
}
|
||||
|
||||
@@ -38,9 +38,7 @@ class SubmitterFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||
}
|
||||
}
|
||||
|
||||
class SubmitReasonFilter
|
||||
implements ClrDatagridStringFilterInterface<HistoryData>
|
||||
{
|
||||
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<HistoryData> {
|
||||
accepts(data: HistoryData, search: string): boolean {
|
||||
return data.submittedReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||
}
|
||||
|
||||
@@ -17,17 +17,13 @@ interface SubmitterData {
|
||||
approver: string
|
||||
}
|
||||
|
||||
class SubmittedFilter
|
||||
implements ClrDatagridStringFilterInterface<SubmitterData>
|
||||
{
|
||||
class SubmittedFilter implements ClrDatagridStringFilterInterface<SubmitterData> {
|
||||
accepts(data: SubmitterData, search: string): boolean {
|
||||
return data.submitted.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||
}
|
||||
}
|
||||
|
||||
class SubmitReasonFilter
|
||||
implements ClrDatagridStringFilterInterface<SubmitterData>
|
||||
{
|
||||
class SubmitReasonFilter implements ClrDatagridStringFilterInterface<SubmitterData> {
|
||||
accepts(data: SubmitterData, search: string): boolean {
|
||||
return data.submitReason.toLowerCase().indexOf(search.toLowerCase()) >= 0
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Handsontable from 'handsontable'
|
||||
import Core from 'handsontable/core'
|
||||
|
||||
export class CustomAutocompleteEditor extends Handsontable.editors
|
||||
.AutocompleteEditor {
|
||||
export class CustomAutocompleteEditor
|
||||
extends Handsontable.editors.AutocompleteEditor
|
||||
{
|
||||
constructor(instance: Core) {
|
||||
super(instance)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ export interface DcColumnSettings {
|
||||
export interface DcValidation extends HotColumnSettings, DcColumnSettings {}
|
||||
|
||||
export interface DcValidationRuleUpdate
|
||||
extends Handsontable.ColumnSettings,
|
||||
DcColumnSettings {
|
||||
extends Handsontable.ColumnSettings, DcColumnSettings {
|
||||
data?: string
|
||||
}
|
||||
|
||||
@@ -157,15 +157,30 @@ export class ViewerComponent
|
||||
return ' '
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
// header columns styling - primary keys
|
||||
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
||||
try {
|
||||
const column = this.hotInstance.colToProp(col) as string
|
||||
|
||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||
|
||||
// Dark mode
|
||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||
// Apply dark mode styling to all headers
|
||||
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,
|
||||
rowHeights: 20,
|
||||
@@ -199,12 +214,21 @@ export class ViewerComponent
|
||||
let colInfo: DataFormat | undefined
|
||||
let textInfo = 'No info found'
|
||||
|
||||
if (this.hotInstance) {
|
||||
const hotSelected: [number, number, number, number][] =
|
||||
this.hotInstance.getSelected() || []
|
||||
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
|
||||
const colName = this.hotInstance?.colToProp(selectedCol)
|
||||
colInfo = this.$dataFormats?.vars[colName]
|
||||
if (
|
||||
this.hotInstance &&
|
||||
!this.hotInstance.isDestroyed &&
|
||||
!this.isTableSwitching
|
||||
) {
|
||||
try {
|
||||
const hotSelected: [number, number, number, number][] =
|
||||
this.hotInstance.getSelected() || []
|
||||
const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
|
||||
const colName = this.hotInstance.colToProp(selectedCol)
|
||||
colInfo = this.$dataFormats?.vars[colName]
|
||||
} catch (error) {
|
||||
// Ignore errors during table switching
|
||||
colInfo = undefined
|
||||
}
|
||||
|
||||
if (colInfo)
|
||||
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
|
||||
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
|
||||
|
||||
constructor(
|
||||
@@ -505,17 +536,7 @@ export class ViewerComponent
|
||||
}
|
||||
|
||||
public copyToClip() {
|
||||
let selBox = document.createElement('textarea')
|
||||
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)
|
||||
navigator.clipboard.writeText(this.webQueryText)
|
||||
}
|
||||
|
||||
public goToViewer() {
|
||||
@@ -599,10 +620,24 @@ export class ViewerComponent
|
||||
}
|
||||
|
||||
public onTableClick(libTable: any, library: any) {
|
||||
this.lib = library.LIBRARYREF
|
||||
this.table = libTable
|
||||
this.selectLibTable(libTable)
|
||||
this.viewData(0)
|
||||
// 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.table = libTable
|
||||
this.selectLibTable(libTable)
|
||||
this.viewData(0)
|
||||
}, 50) // 50ms debounce - fast enough for good UX, slow enough to prevent issues
|
||||
}
|
||||
|
||||
public async selectTable(lib: string, initial?: boolean, library?: any) {
|
||||
@@ -721,6 +756,14 @@ export class ViewerComponent
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
let libDataset: any
|
||||
@@ -961,14 +1004,25 @@ export class ViewerComponent
|
||||
|
||||
this.loadingTableView = false
|
||||
|
||||
//If we try to setup hot when no data is returned it errors `isDestoryed`.
|
||||
//That is intorduced by HOT update
|
||||
if (!this.noData && !this.noDataReqErr && libDataset) this.setupHot()
|
||||
// Setup Handsontable after async operations complete
|
||||
// Original issue: setupHot() called before API responses populated headerPks array
|
||||
// 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
|
||||
setTimeout(() => {
|
||||
this.fixAriaAccessibility()
|
||||
}, 1500)
|
||||
}, 500)
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}, 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() {
|
||||
// DUPLICATE PREVENTION: Avoid multiple setup calls during rapid table switching
|
||||
const now = Date.now()
|
||||
if (now - this.lastSetupTime < 500) {
|
||||
return
|
||||
}
|
||||
this.lastSetupTime = now
|
||||
|
||||
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
|
||||
|
||||
if (this.hotInstance) {
|
||||
if (this.hotInstance && !this.hotInstance.isDestroyed) {
|
||||
this.hotInstance.updateSettings({
|
||||
height: this.hotTable.height,
|
||||
modifyColWidth: (width: any, col: any) => {
|
||||
@@ -1129,15 +1245,29 @@ export class ViewerComponent
|
||||
else return width
|
||||
},
|
||||
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
|
||||
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
||||
try {
|
||||
const column = this.hotInstance.colToProp(col) as string
|
||||
|
||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||
// PRIMARY KEY STYLING: Apply special styling to PK columns (populated from API response)
|
||||
const isPKCol = column && this.headerPks.indexOf(column) > -1
|
||||
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
|
||||
|
||||
// Dark mode
|
||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||
// DARK MODE: Apply to all headers
|
||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||
} catch (error) {
|
||||
// SAFETY NET: Ensure basic styling is always applied
|
||||
th.classList.add(globals.handsontable.darkTableHeaderClass)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1153,14 +1283,18 @@ export class ViewerComponent
|
||||
this.fixAriaAccessibility()
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fix ARIA accessibility issues after table setup
|
||||
setTimeout(() => {
|
||||
this.fixAriaAccessibility()
|
||||
}, 500)
|
||||
}, 1000)
|
||||
// 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(() => {
|
||||
if (this.hotInstance && !this.hotInstance.isDestroyed) {
|
||||
this.hotInstance.render()
|
||||
}
|
||||
}, 10)
|
||||
}
|
||||
}, 50) // Optimized Angular component readiness delay
|
||||
}, 200) // Optimized main setup delay (was 600ms)
|
||||
}
|
||||
|
||||
async loadWithParameters() {
|
||||
@@ -1233,13 +1367,27 @@ export class ViewerComponent
|
||||
}
|
||||
|
||||
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) {
|
||||
this.ariaObserver.disconnect()
|
||||
this.ariaObserver = undefined
|
||||
}
|
||||
|
||||
// Clean up the interval
|
||||
// Clear ARIA check intervals
|
||||
if (this.ariaCheckInterval) {
|
||||
clearInterval(this.ariaCheckInterval)
|
||||
this.ariaCheckInterval = undefined
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
-->
|
||||
|
||||
<sasjs
|
||||
serverUrl="http://localhost:5000"
|
||||
serverUrl=""
|
||||
appLoc="/Public/app/devtest"
|
||||
serverType="SASJS"
|
||||
loginMechanism="Redirected"
|
||||
|
||||
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",
|
||||
"version": "7.2.0",
|
||||
"version": "7.2.5",
|
||||
"description": "Data Controller",
|
||||
"devDependencies": {
|
||||
"@saithodev/semantic-release-gitea": "^2.1.0",
|
||||
@@ -10,7 +10,7 @@
|
||||
"@semantic-release/npm": "11.0.0",
|
||||
"@semantic-release/release-notes-generator": "^11.0.4",
|
||||
"commit-and-tag-version": "^11.2.2",
|
||||
"prettier": "3.6.2"
|
||||
"prettier": "^3.7.4"
|
||||
},
|
||||
"scripts": {
|
||||
"install": "cd client && npm i && cd ../sas && npm i",
|
||||
@@ -32,6 +32,5 @@
|
||||
"//": [
|
||||
"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"
|
||||
],
|
||||
"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,
|
||||
"dependencies": {
|
||||
"@sasjs/cli": "^4.12.10",
|
||||
"@sasjs/core": "^4.59.5"
|
||||
"@sasjs/cli": "^4.12.15",
|
||||
"@sasjs/core": "^4.59.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,15 +39,35 @@
|
||||
%let &outresult=NO;
|
||||
%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;
|
||||
%let chk=0;
|
||||
proc sql noprint;
|
||||
select count(*) into: chk from &dc_libref..mpe_audit
|
||||
select count(*) into: chk from &audtab
|
||||
where load_ref="&load_ref";
|
||||
%if &chk=0 %then %do;
|
||||
%let allow_restore=NO;
|
||||
%let reason=No entry for &load_ref in MPE_AUDIT;
|
||||
%let &outresult=NO;
|
||||
%let &outreason=No entry for &load_ref in &audtab;
|
||||
%return;
|
||||
%end;
|
||||
|
||||
@@ -64,24 +84,19 @@
|
||||
where groupname="&dc_admin_group";
|
||||
|
||||
%if &is_admin>0 %then %do;
|
||||
%let allow_restore=YES;
|
||||
%let reason=IS ADMIN;
|
||||
%let &outresult=YES;
|
||||
%let &outreason=IS ADMIN;
|
||||
%return;
|
||||
%end;
|
||||
|
||||
/* 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
|
||||
,user=&user
|
||||
,access_level=EDIT
|
||||
)
|
||||
%if %mf_nobs(access_check)=0 %then %do;
|
||||
%let allow_restore=NO;
|
||||
%let reason=No access in MPE_TABLES;
|
||||
%let &outresult=NO;
|
||||
%let &outreason=No access in MPE_TABLES;
|
||||
%return;
|
||||
%end;
|
||||
|
||||
@@ -97,8 +112,8 @@
|
||||
and CLS_LIBREF="%upcase(&base_lib)"
|
||||
and CLS_TABLE="%upcase(&base_ds)";
|
||||
%if %mf_nobs(work.cls_rules)>0 %then %do;
|
||||
%let allow_restore=NO;
|
||||
%let reason=User has restrictions in MPE_COLUMN_LEVEL_SECURITY;
|
||||
%let &outresult=NO;
|
||||
%let &outreason=User has restrictions in MPE_COLUMN_LEVEL_SECURITY;
|
||||
data _null_;
|
||||
set work.cls_rules;
|
||||
putlog (_all_)(=);
|
||||
@@ -119,8 +134,8 @@
|
||||
and rls_table="&base_ds"
|
||||
and rls_active=1;
|
||||
%if %mf_nobs(work.rls_rules)>0 %then %do;
|
||||
%let allow_restore=NO;
|
||||
%let reason=User has restrictions in MPE_ROW_LEVEL_SECURITY;
|
||||
%let &outresult=NO;
|
||||
%let &outreason=User has restrictions in MPE_ROW_LEVEL_SECURITY;
|
||||
data _null_;
|
||||
set work.rls_rules;
|
||||
putlog (_all_)(=);
|
||||
@@ -129,7 +144,7 @@
|
||||
%return;
|
||||
%end;
|
||||
%else %do;
|
||||
%let allow_restore=YES;
|
||||
%let reason=CHECKS PASSED;
|
||||
%let &outresult=YES;
|
||||
%let &outreason=CHECKS PASSED;
|
||||
%end;
|
||||
%mend mpe_checkrestore;
|
||||
|
||||
@@ -35,14 +35,12 @@ run;
|
||||
%dc_getsettings()
|
||||
|
||||
%put checking it is restorable;
|
||||
%global allow_restore reason;
|
||||
%mp_assertscope(SNAPSHOT)
|
||||
%mpe_checkrestore(&loadref,outresult=ALLOW_RESTORE,outreason=REASON)
|
||||
%mp_assertscope(COMPARE,
|
||||
desc=Checking macro variables against previous snapshot,
|
||||
ignorelist=ALLOW_RESTORE REASON
|
||||
MCLIB0_JADP1LEN MCLIB0_JADP2LEN MCLIB0_JADPNUM MCLIB0_JADVLEN
|
||||
MCLIB2_JADP1LEN MCLIB2_JADP2LEN MCLIB2_JADPNUM MCLIB2_JADVLEN
|
||||
MC0_JADP1LEN MC0_JADP2LEN MC0_JADP3LEN MC0_JADPNUM MC0_JADVLEN
|
||||
)
|
||||
|
||||
%mp_assert(
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
},
|
||||
{
|
||||
"name": "viya",
|
||||
"serverUrl": "https://viya-f0g8ht62vq.engage.sas.com",
|
||||
"serverUrl": "",
|
||||
"serverType": "SASVIYA",
|
||||
"httpsAgentOptions": {
|
||||
"allowInsecureRequests": false
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@brief testing getchangeinfo service
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_assert.sas
|
||||
@li mp_assertcolvals.sas
|
||||
@li mf_getuniquefileref.sas
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ data _null_;
|
||||
cnt=find(_pgm,'/tests/');
|
||||
if cnt=0 then cnt=find(_pgm,'/services/');
|
||||
if cnt=0 then cnt=find(_pgm,'/jobs/');
|
||||
put cnt= apploc= _pgm=;
|
||||
apploc=substr(_pgm,1,cnt-1);
|
||||
put cnt= apploc= _pgm=;
|
||||
call symputx('apploc',apploc);
|
||||
end;
|
||||
run;
|
||||
|
||||
Reference in New Issue
Block a user