src/app/viewer/viewer.component.ts

Implements

AfterContentInit AfterViewInit AfterViewChecked OnDestroy

Metadata

Relationships

Index

Properties
Methods

Constructor

constructor(licenceService: LicenceService, sasStoreService: SasStoreService, sasService: SasService, router: Router, route: ActivatedRoute, eventService: EventService, loggerService: LoggerService, helperService: HelperService, location: Location, cdf: ChangeDetectorRef)
Parameters :
Name Type Optional
licenceService LicenceService No
sasStoreService SasStoreService No
sasService SasService No
router Router No
route ActivatedRoute No
eventService EventService No
loggerService LoggerService No
helperService HelperService No
location Location No
cdf ChangeDetectorRef No

Methods

Private applyDisplayColHeaders
applyDisplayColHeaders()

Recomputes hotTable.colHeaders from colNames/filterCols for the current useLabels state, and pushes it into a live grid via updateSettings if one is mounted (no refetch needed).

Returns : void
Public checkExpand
checkExpand(event: any)
Parameters :
Name Type Optional
event any No
Returns : void
Private cleanupHotInstance
cleanupHotInstance()

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:

  • 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
Returns : void
Private clearGlobalsFilter
clearGlobalsFilter()
Returns : void
Public collapseLibraryItems
collapseLibraryItems(libraries: any, libraryToSkip: any)
Parameters :
Name Type Optional
libraries any No
libraryToSkip any No
Returns : void
Private configureHotInstance
configureHotInstance()

Applies settings that can't go through Angular [settings] binding:

  • Primary key column header styling
  • Column width cap
  • ARIA accessibility hooks (attached once per instance)
Returns : void
Public copyToClip
copyToClip()
Returns : void
Public datasetInfoModalRowClicked
datasetInfoModalRowClicked(value: Version | DSMeta)
Parameters :
Name Type Optional
value Version | DSMeta No
Returns : void
Public downloadData
downloadData()

Downloads file from backend, against getrawdata service, link is created and open in new tab

Returns : void
Public downloadDDL
downloadDDL()

Downloads file from backend, against getddl service, link is created and open in new tab

Returns : void
Public editTable
editTable()

Navigate to the edit page of a viewing table

Returns : void
Public filterFn
filterFn(input: string)

FIXME: Should be removed, not used

Parameters :
Name Type Optional
input string No
Returns : void
Private fixAriaAccessibility
fixAriaAccessibility()

Fixes ARIA accessibility issues in the Handsontable component This addresses the accessibility report issues with treegrid and presentation roles

Returns : void
Public goToLineage
goToLineage()

Navigate to the lineage of a viewing table

Returns : void
Public goToViewer
goToViewer()
Returns : void
Public libraryExpandedChange
libraryExpandedChange(expanded: any, library: any)
Parameters :
Name Type Optional
expanded any No
library any No
Returns : void
Public Async libraryOnClick
libraryOnClick(lib: string, library: any, expandOnly?: boolean)
Parameters :
Name Type Optional
lib string No
library any No
expandOnly boolean Yes
Returns : any
Public libraryOnFilter
libraryOnFilter()
Returns : void
Public libraryResetFilter
libraryResetFilter()
Returns : void
Public libTabActive
libTabActive(libraryRef: string, table: string)
Parameters :
Name Type Optional
libraryRef string No
table string No
Returns : boolean
Public loadMoreLibraries
loadMoreLibraries()
Returns : void
Async loadWithoutParameters
loadWithoutParameters()
Returns : any
Async loadWithParameters
loadWithParameters()
Returns : any
Public maxWidthCheker
maxWidthCheker(width: any, col: any)
Parameters :
Name Type Optional
width any No
col any No
Returns : any
Public newViewbox
newViewbox()

Open viewboxes modal

Returns : void
Async ngAfterContentInit
ngAfterContentInit()
Returns : any
ngAfterViewChecked
ngAfterViewChecked()
Returns : void
ngAfterViewInit
ngAfterViewInit()
Returns : void
ngOnDestroy
ngOnDestroy()
Returns : void
ngOnInit
ngOnInit()
Returns : void
Public onCliCommandFocus
onCliCommandFocus(evt: any)

When clicked on textarea in the Web Query Modal, this function will select all text inside.

Parameters :
Name Type Optional Description
evt any No

textarea which contains the web query text

Returns : void
Public onTableClick
onTableClick(libTable: any, library: any)
Parameters :
Name Type Optional
libTable any No
library any No
Returns : void
Public openQb
openQb()
Returns : void
Public Async reloadLibInfo
reloadLibInfo()

By default, SAS will send cached library information. This function calls SAS to update the backend database. It will also show the latest info on the page.

Returns : any
Public reloadTableData
reloadTableData()

Re sending request to backend and re-setting data in the HOT

Returns : void
Public removeQuery
removeQuery()
Returns : void
Public resetFilter
resetFilter()

Resetting filter variables

Returns : void
Private scheduleFilterOverflowCheck
scheduleFilterOverflowCheck()

Measures whether the applied-filter clause fits the collapsed single line, and shows or hides the chevron accordingly. The measurement is deferred because filterOverflows and filterExpanded are bound in this view - setting them inside the change-detection cycle would raise ExpressionChangedAfterItHasBeenChecked.

Returns : void
Public Async searchTable
searchTable(inputElement: any)

Searching table against particular string, data is comming from backend. There is also a toggle that will search for a numeric values

Parameters :
Name Type Optional Description
inputElement any No

input from which search string is captured

Returns : any
Public selectLibTable
selectLibTable(ev: string, initial?: boolean)
Parameters :
Name Type Optional
ev string No
initial boolean Yes
Returns : void
Public Async selectTable
selectTable(lib: string, initial?: boolean, library?: any)
Parameters :
Name Type Optional
lib string No
initial boolean Yes
library any Yes
Returns : any
Public Async sendClause
sendClause()
Returns : any
Private setDSNote
setDSNote()
Returns : void
Private setupHot
setupHot()

PERFORMANCE: Configures Handsontable with enhanced error handling (workaround needed for HOT version 16 and above)

  1. Duplicate call prevention (500ms window)
  2. Multiple validation checks to prevent race conditions
  3. Forced render for immediate primary key styling

Instance lifecycle is managed by Angular's hot-table component via [data] and [settings] bindings. This method only applies additional config that can't go through bindings (hooks, PK styling).

Returns : void
Public showTableSelect
showTableSelect()
Returns : void
Public showWebQuery
showWebQuery()

Displays web query modal

Returns : void
Public tableEditExists
tableEditExists()

Used to show/hide the edit table button

Returns : any

Wheter currently viewed table is edtiable

Public toggleFilterPanel
toggleFilterPanel()
Returns : void
Public treeNodeClicked
treeNodeClicked(event: any, library: any)
Parameters :
Name Type Optional
event any No
library any No
Returns : void
Public treeOnFilter
treeOnFilter(array: any, arrToFilter: string)
Parameters :
Name Type Optional
array any No
arrToFilter string No
Returns : void
Private updateHotTableSettings
updateHotTableSettings()
Returns : void
Public Async viewData
viewData(filter_pk: number)
Parameters :
Name Type Optional
filter_pk number No
Returns : any

Properties

Private _query
Type : Subscription
Private _queryParams
Type : Subscription
Public $dataFormats
Type : $DataFormats | null
Default value : null
Public abortActive
Type : boolean
Default value : false
Public actionDisable
Type : boolean
Default value : false
Private ariaCheckInterval
Type : any | undefined
Private ariaObserver
Type : MutationObserver | undefined
Public clauses
Type : any
Public colNames
Type : string[]
Default value : []
Public datasetInfo
Type : boolean
Default value : false
Public downloadFormat
Type : string
Default value : 'CSV'
Public dsmeta
Type : DSMeta[]
Default value : []
Public dsNote
Type : string
Default value : ''
Public filter
Type : boolean
Default value : false
Public filterCols
Type : Col[]
Default value : []
Public filterExpanded
Type : unknown
Default value : false

The applied-filter panel is collapsed to a single line by default. The chevron that expands it is only useful when the clause does not fit that line, which depends on the rendered width - so it is measured from the DOM rather than guessed from the length of the text.

Private filterMeasuredKey
Type : string
Default value : ''
Public filterOverflows
Type : unknown
Default value : false
Public headerPks
Type : string[]
Default value : []
Public hiddenViewColumns
Type : number[]
Default value : []
Private hooksAttached
Type : unknown
Default value : false
Private hotInstance
Type : Handsontable | null
Default value : null
Public hotInstanceClickListener
Type : boolean
Default value : false
hotInstanceViewChild
Type : Handsontable
Decorators :
@ViewChild('hotInstance', {static: false})
Public hotTable
Type : HotTableInterface
Default value : { data: [], colHeaders: [], columns: [], height: 'calc(100vh - 182px)', maxRows: this.licenceState.value.viewer_rows_allowed || Infinity, settings: {}, licenseKey: undefined, rowHeaders: (index: number) => { return ' ' }, afterGetColHeader: (col: number, th: any, headerLevel: number) => { // 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) { // 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 const isPKCol = column && this.headerPks.indexOf(column) > -1 if (isPKCol) th.classList.add('primaryKeyHeaderStyle') // 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, contextMenu: { items: { copy_with_column_headers: {}, copy_column_headers_only: {}, export_file: buildExportMenuItem(() => this.tableTitle || 'export'), // Navigates rather than just flipping `useLabels` locally, so the URL // stays the single source of truth (shareable/refreshable state) — // the queryParams subscription in ngOnInit picks up the change and // re-renders colHeaders. toggle_labels: { name: () => (this.useLabels ? 'Show names' : 'Show labels'), callback: () => { this.router.navigate([], { relativeTo: this.route, queryParams: { labels: this.useLabels ? null : true }, queryParamsHandling: 'merge' }) } } } }, copyPaste: { copyColumnHeaders: true, copyColumnHeadersOnly: true }, dropdownMenu: { items: { make_read_only: { name: 'make_read_only' }, alignment: { name: 'alignment' }, sp1: { name: '---------' }, info: { name: 'test info', renderer: ( hot: Handsontable.Core, wrapper: HTMLElement, row: number, col: number, prop: string | number, itemValue: string ) => { const elem = document.createElement('span') let colName = '' let colInfo: DataFormat | undefined let textInfo = 'No info found' if ( this.hotInstance && !this.hotInstance.isDestroyed && !this.isTableSwitching ) { try { // getSelected() is typed number[][] in HOT 18 (was a 4-tuple // array before); loosen the annotation to match. const hotSelected: number[][] = this.hotInstance.getSelected() || [] const selectedCol: number = hotSelected ? hotSelected[0][1] : -1 colName = this.hotInstance.colToProp(selectedCol) as string colInfo = this.$dataFormats?.vars[colName] } catch (error) { // Ignore errors during table switching colInfo = undefined } textInfo = buildColInfoHtml(colName, colInfo) } elem.innerHTML = textInfo return elem } } } } }
hotTableComponent
Type : HotTableComponent
Decorators :
@ViewChild(HotTableComponent, {static: false})
Public hotTableSettings
Type : Handsontable.GridSettings
Default value : {}
Public Infinity
Type : unknown
Default value : Infinity
Optional infoBar
Type : ElementRef<HTMLElement>
Decorators :
@ViewChild('infoBar')
Private isTableSwitching
Type : boolean
Default value : false
Private lastSetupTime
Type : number
Default value : 0
Public lib
Type : any
Public libDataset
Type : any
Public libds
Type : string
Public libinfo
Type : Libinfo[] | null
Default value : null
Public libraries
Type : Array<any>
Public librariesLoading
Type : boolean
Default value : false
Public librariesPaging
Type : boolean
Default value : false
Public librariesSearch
Type : string
Default value : ''
Public librariesTreeExpanded
Type : boolean
Default value : false
Public libraryTablesRef
Type : string
Default value : ''
Public libTab
Type : string
Public licenceState
Type : unknown
Default value : this.licenceService.licenceState
Public loadingTableView
Type : boolean
Default value : false
Public noData
Type : boolean
Default value : false
Public noDataReqErr
Type : boolean
Default value : false
Public nullVariables
Type : boolean
Default value : false
Public numberOfRows
Type : number | null
Default value : null
Public openDownload
Type : boolean
Default value : false
Public queryErr
Type : boolean
Default value : false
Public queryErrMessage
Type : string
Public queryFilter
Type : any
queryFilterCompList
Type : QueryList<QueryComponent>
Default value : new QueryList()
Decorators :
@ViewChildren('queryFilter')
Public queryText
Type : string
Default value : ''
Public sasjsConfig
Type : SASjsConfig
Default value : new SASjsConfig()
Public searchLoading
Type : boolean
Default value : false
Public searchNumeric
Type : boolean
Default value : false
Public submitLoading
Type : boolean
Public switchFlag
Type : boolean
Default value : false
Private switchingTimeout
Type : any
Default value : null
Public table
Type : any
Public tableDisable
Type : boolean
Default value : false
Public tableFlag
Type : boolean
Default value : true
Public tables
Type : any
Public tableTitle
Type : any
Public tableuri
Type : string | null
Default value : null
Public urlFilterPk
Type : number | null
Default value : null
Public useLabels
Type : boolean
Default value : false
Public versions
Type : Version[]
Default value : []
Public viewboxOpen
Type : boolean
Default value : false
Public webQuery
Type : boolean
Default value : false
Public webQueryTab
Type : boolean
Default value : false
Public webQueryText
Type : string
Default value : ''
Public whereString
Type : string
import {
  Component,
  AfterContentInit,
  ChangeDetectorRef,
  AfterViewInit,
  AfterViewChecked,
  ElementRef,
  OnDestroy,
  ViewChildren,
  QueryList,
  ViewEncapsulation,
  ViewChild
} from '@angular/core'
import { SasStoreService } from '../services/sas-store.service'
import { Subscription } from 'rxjs'

import { Router } from '@angular/router'
import { ActivatedRoute } from '@angular/router'
import { globals } from '../_globals'

import { EventService } from '../services/event.service'
import { HelperService } from '../services/helper.service'
import { HotTableComponent } from '@handsontable/angular-wrapper'
import { SasService } from '../services/sas.service'
import { SASjsConfig } from '@sasjs/adapter'
import { QueryComponent } from '../query/query.component'

import { FilterGroup, FilterQuery } from '../models/FilterQuery'
import { HotTableInterface } from '../models/HotTable.interface'
import { buildExportMenuItem } from '../shared/hot-export/hot-export.util'
import { LoggerService } from '../services/logger.service'
import Handsontable from 'handsontable'
import {
  $DataFormats,
  DSMeta,
  Version
} from '../models/sas/editors-getdata.model'
import { mergeColsRules } from '../shared/dc-validator/utils/mergeColsRules'
import { makeNumberFormatRenderer } from '../editor/utils/renderers.utils'
import { PublicViewtablesServiceResponse } from '../models/sas/public-viewtables.model'
import { PublicViewlibsServiceResponse } from '../models/sas/public-viewlibs.model'
import { PublicRefreshlibinfoServiceResponse } from '../models/sas/public-refreshlibinfo.model'
import { DataFormat } from '../models/sas/common/DateFormat'
import { Libinfo } from '../models/sas/common/Libinfo'
import { LicenceService } from '../services/licence.service'
import { Location } from '@angular/common'
import { Col } from '../shared/dc-validator/models/col.model'
import { parseLabelsParam } from '../shared/utils/parse-labels-param'
import { getDisplayColHeaders } from '../shared/utils/display-col-headers'
import { buildColInfoHtml } from '../shared/utils/col-info-html'

@Component({
  selector: 'app-viewer',
  templateUrl: './viewer.component.html',
  styleUrls: ['./viewer.component.scss'],
  host: {
    class: 'content-container'
  },
  encapsulation: ViewEncapsulation.None,
  standalone: false
})
export class ViewerComponent
  implements AfterContentInit, AfterViewInit, AfterViewChecked, OnDestroy
{
  @ViewChildren('queryFilter')
  queryFilterCompList: QueryList<QueryComponent> = new QueryList()

  @ViewChild('hotInstance', { static: false })
  hotInstanceViewChild!: Handsontable

  public libraries!: Array<any>
  public librariesPaging: boolean = false
  public librariesSearch: string = ''
  public libraryTablesRef: string = ''
  public lib: any
  public librariesTreeExpanded: boolean = false
  public tables: any
  public tableTitle: any
  public libinfo: Libinfo[] | null = null
  public table: any
  public tableuri: string | null = null
  public filter: boolean = false
  public filterCols: Col[] = []
  // NAMEs of the currently loaded columns, in grid order — the canonical
  // source `getDisplayColHeaders` maps against; `hotTable.colHeaders` holds
  // whichever of NAME/LABEL is currently displayed.
  public colNames: string[] = []
  // `?labels=true` toggle (see parseLabelsParam) — URL is the source of
  // truth, kept in sync via the queryParams subscription below.
  public useLabels: boolean = false
  public nullVariables: boolean = false
  public abortActive: boolean = false
  public queryFilter: any
  public urlFilterPk: number | null = null
  public whereString!: string
  public clauses: any
  public libds!: string
  public libTab!: string
  public queryText: string = ''
  public webQueryText: string = ''

  /**
   * The applied-filter panel is collapsed to a single line by default. The
   * chevron that expands it is only useful when the clause does not fit that
   * line, which depends on the rendered width - so it is measured from the DOM
   * rather than guessed from the length of the text.
   */
  public filterExpanded = false
  public filterOverflows = false

  @ViewChild('infoBar') infoBar?: ElementRef<HTMLElement>

  private filterMeasuredKey = ''
  public submitLoading!: boolean
  public queryErr: boolean = false
  public queryErrMessage!: string
  public libDataset: any
  public tableFlag: boolean = true
  public librariesLoading: boolean = false
  public loadingTableView: boolean = false
  public switchFlag: boolean = false
  public noData: boolean = false
  public noDataReqErr: boolean = false
  public tableDisable: boolean = false
  public actionDisable: boolean = false
  public openDownload: boolean = false
  public webQuery: boolean = false
  public webQueryTab: boolean = false
  public downloadFormat: string = 'CSV'
  public sasjsConfig: SASjsConfig = new SASjsConfig()
  public searchLoading: boolean = false
  public searchNumeric: boolean = false
  @ViewChild(HotTableComponent, { static: false })
  hotTableComponent!: HotTableComponent

  public hotTableSettings: Handsontable.GridSettings = {}

  private updateHotTableSettings(): void {
    this.hotTableSettings = {
      multiColumnSorting: true,
      viewportRowRenderingOffset: 30,
      colHeaders: this.hotTable.colHeaders,
      columns: this.hotTable.columns,
      copyPaste: this.hotTable.copyPaste,
      contextMenu: this.hotTable.contextMenu,
      filters: true,
      dropdownMenu: this.hotTable.dropdownMenu,
      height: this.hotTable.height,
      stretchH: 'all',
      modifyColWidth: this.maxWidthCheker,
      cells: this.hotTable.cells,
      hiddenColumns: { columns: this.hiddenViewColumns, indicators: false },
      maxRows: this.hotTable.maxRows,
      manualColumnResize: true,
      afterGetColHeader: this.hotTable.afterGetColHeader,
      rowHeaders: this.hotTable.rowHeaders,
      rowHeaderWidth: this.hotTable.rowHeaderWidth,
      rowHeights: this.hotTable.rowHeights,
      licenseKey: this.hotTable.licenseKey,
      className: 'htDark',
      theme: 'ht-theme-classic'
    }
  }
  public numberOfRows: number | null = null
  public headerPks: string[] = []
  public $dataFormats: $DataFormats | null = null
  // Physical column indexes to hide in the viewer (from HIDDEN DQ rules)
  public hiddenViewColumns: number[] = []
  public datasetInfo: boolean = false
  public dsmeta: DSMeta[] = []
  public versions: Version[] = []
  public dsNote = ''

  public licenceState = this.licenceService.licenceState
  public Infinity = Infinity

  private ariaObserver: MutationObserver | undefined
  private ariaCheckInterval: any | undefined

  public hotTable: HotTableInterface = {
    data: [],
    colHeaders: [],
    columns: [],
    height: 'calc(100vh - 182px)',
    maxRows: this.licenceState.value.viewer_rows_allowed || Infinity,
    settings: {},
    licenseKey: undefined,
    rowHeaders: (index: number) => {
      return ' '
    },
    afterGetColHeader: (col: number, th: any, headerLevel: number) => {
      // 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) {
        // 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

        const isPKCol = column && this.headerPks.indexOf(column) > -1
        if (isPKCol) th.classList.add('primaryKeyHeaderStyle')

        // 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,
    contextMenu: {
      items: {
        copy_with_column_headers: {},
        copy_column_headers_only: {},
        export_file: buildExportMenuItem(() => this.tableTitle || 'export'),
        // Navigates rather than just flipping `useLabels` locally, so the URL
        // stays the single source of truth (shareable/refreshable state) —
        // the queryParams subscription in ngOnInit picks up the change and
        // re-renders colHeaders.
        toggle_labels: {
          name: () => (this.useLabels ? 'Show names' : 'Show labels'),
          callback: () => {
            this.router.navigate([], {
              relativeTo: this.route,
              queryParams: { labels: this.useLabels ? null : true },
              queryParamsHandling: 'merge'
            })
          }
        }
      }
    },
    copyPaste: {
      copyColumnHeaders: true,
      copyColumnHeadersOnly: true
    },
    dropdownMenu: {
      items: {
        make_read_only: {
          name: 'make_read_only'
        },
        alignment: {
          name: 'alignment'
        },
        sp1: {
          name: '---------'
        },
        info: {
          name: 'test info',
          renderer: (
            hot: Handsontable.Core,
            wrapper: HTMLElement,
            row: number,
            col: number,
            prop: string | number,
            itemValue: string
          ) => {
            const elem = document.createElement('span')
            let colName = ''
            let colInfo: DataFormat | undefined
            let textInfo = 'No info found'

            if (
              this.hotInstance &&
              !this.hotInstance.isDestroyed &&
              !this.isTableSwitching
            ) {
              try {
                // getSelected() is typed number[][] in HOT 18 (was a 4-tuple
                // array before); loosen the annotation to match.
                const hotSelected: number[][] =
                  this.hotInstance.getSelected() || []
                const selectedCol: number = hotSelected ? hotSelected[0][1] : -1
                colName = this.hotInstance.colToProp(selectedCol) as string
                colInfo = this.$dataFormats?.vars[colName]
              } catch (error) {
                // Ignore errors during table switching
                colInfo = undefined
              }

              textInfo = buildColInfoHtml(colName, colInfo)
            }

            elem.innerHTML = textInfo

            return elem
          }
        }
      }
    }
  }

  private _query!: Subscription
  private _queryParams!: Subscription

  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(
    private licenceService: LicenceService,
    private sasStoreService: SasStoreService,
    private sasService: SasService,
    private router: Router,
    private route: ActivatedRoute,
    private eventService: EventService,
    private loggerService: LoggerService,
    private helperService: HelperService,
    private location: Location,
    private cdf: ChangeDetectorRef
  ) {
    this.sasjsConfig = this.sasService.getSasjsConfig()
  }

  ngOnInit() {
    if (
      globals.viewer.currentSelection !== '' &&
      Object.keys(this.route.snapshot.params).length === 0
    ) {
      this.router.navigate([globals.viewer.currentSelection], {
        queryParamsHandling: 'preserve'
      })
    }
    if (this.route.snapshot.params['libMem'] !== undefined) {
      globals.viewer.currentSelection = 'view/data'
    }

    this.licenceService.hot_license_key.subscribe(
      (hot_license_key: string | undefined) => {
        this.hotTable.licenseKey = hot_license_key
        this.updateHotTableSettings() // Update settings when license key changes
      }
    )

    // URL is the source of truth for the labels toggle: react to `?labels=`
    // changes (including same-page navigation from the dropdown menu item)
    // rather than reading it once from the route snapshot.
    this._queryParams = this.route.queryParams.subscribe((params) => {
      this.useLabels = parseLabelsParam(new URLSearchParams(params).toString())
      this.applyDisplayColHeaders()
    })
  }

  /**
   * Recomputes `hotTable.colHeaders` from `colNames`/`filterCols` for the
   * current `useLabels` state, and pushes it into a live grid via
   * `updateSettings` if one is mounted (no refetch needed).
   */
  private applyDisplayColHeaders() {
    this.hotTable.colHeaders = getDisplayColHeaders(
      this.colNames,
      this.filterCols,
      this.useLabels
    )

    if (this.hotInstance && !this.hotInstance.isDestroyed) {
      this.hotInstance.updateSettings({ colHeaders: this.hotTable.colHeaders })
    }
  }

  /**
   * Open viewboxes modal
   */
  public newViewbox() {
    this.viewboxOpen = true
  }

  /**
   * Resetting filter variables
   */
  public resetFilter() {
    if (this.queryFilterCompList.first) {
      this.queryFilterCompList.first.resetFilter()
    }
  }

  /**
   * Searching table against particular string, data is comming from backend.
   * There is also a toggle that will search for a numeric values
   *
   * @param inputElement input from which search string is captured
   */
  public async searchTable(inputElement: any) {
    this.searchLoading = true

    let searchValue = inputElement.value

    let libDataset = this.lib + '.' + this.table
    let filter_pk = parseInt(this.route.snapshot.params['filterId']) || 0
    await this.sasStoreService
      .viewDataSearch(searchValue, this.searchNumeric, libDataset, filter_pk)
      .then((res: any) => {
        if (!res.sasparams && !res.viewData) {
          this.searchLoading = false
          return
        }

        this.hotTable.data = res.viewdata
        this.$dataFormats = res.$viewdata
        this.dsmeta = res.dsmeta
        this.versions = res.versions || []
        this.setDSNote()
        this.numberOfRows = res.sasparams[0].NOBS
        this.queryText = res.sasparams[0].FILTER_TEXT
        this.headerPks = res.sasparams[0].PK_FIELDS.split(' ')

        if (this.hotTable.data.length === 0) {
          this.noData = true
        } else {
          this.noData = false
          this.tableFlag = false

          this.setupHot()
        }
      })
      .catch((err: any) => {
        this.loggerService.error(err)
      })

    this.searchLoading = false
  }

  /**
   * Re sending request to backend and re-setting data in the HOT
   */
  public reloadTableData() {
    this.viewData(this.urlFilterPk || 0)
  }

  /**
   * By default, SAS will send cached library information.
   * This function calls SAS to update the backend database.
   * It will also show the latest info on the page.
   */
  public async reloadLibInfo() {
    this.libinfo = null

    this.sasStoreService.refreshLibInfo(this.lib).then(
      async (res: PublicRefreshlibinfoServiceResponse) => {
        this.libinfo = res.libinfo
        globals.viewer.libinfo = this.libinfo

        const library = this.libraries.find((x) => x.LIBRARYREF === this.lib)

        if (library) library.libinfo = this.libinfo
      },
      (err: any) => {
        this.loggerService.error(err)

        if (this.libinfo === null) this.libinfo = []
      }
    )
  }

  /**
   * FIXME: Should be removed, not used
   */
  public filterFn(input: string) {
    let libraries = this.libraries
    this.libraries = libraries.filter(
      (item) =>
        item.LIBRARYNAME.toLowerCase().indexOf(input.toLocaleLowerCase()) !== -1
    )
  }

  /**
   * Downloads file from backend, against `getrawdata` service, link is created and open in new tab
   */
  public downloadData() {
    let storage = this.sasjsConfig.serverUrl
    let metaData = this.sasjsConfig.appLoc
    const path = this.sasService.getExecutionPath()
    let type = '&type=' + this.downloadFormat
    let table = '&table=' + this.tableTitle
    let contextname =
      this.sasjsConfig.serverType === 'SASVIYA'
        ? `&_contextname=${this.sasjsConfig.contextName}`
        : ''
    let filter_pk: number

    if (typeof this.route.snapshot.params['filterId'] === 'undefined') {
      filter_pk = 0
    } else {
      filter_pk = parseInt(this.route.snapshot.params['filterId'])
    }

    let filter = '&filter=' + filter_pk
    let downUrl =
      storage +
      path +
      '/?_program=' +
      metaData +
      '/services/public/getrawdata' +
      type +
      table +
      contextname +
      filter

    window.open(downUrl)
    this.openDownload = false
  }

  /**
   * Downloads file from backend, against `getddl` service, link is created and open in new tab
   */
  public downloadDDL() {
    let libref = this.lib
    let ds = this.table
    let flavour = this.downloadFormat.replace('_DDL', '')
    let storage = this.sasjsConfig.serverUrl
    let metaData = this.sasjsConfig.appLoc
    const path = this.sasService.getExecutionPath()
    let contextname =
      this.sasjsConfig.serverType === 'SASVIYA'
        ? `&_contextname=${this.sasjsConfig.contextName}`
        : ''

    let params = `&ds=${ds}&libref=${libref}&flavour=${flavour}${contextname}`

    let downUrl =
      storage +
      path +
      '/?_program=' +
      metaData +
      '/services/public/getddl' +
      params

    window.open(downUrl)
    this.openDownload = false
  }

  /**
   * When clicked on textarea in the Web Query Modal, this function will
   * select all text inside.
   * @param evt textarea which contains the web query text
   */
  public onCliCommandFocus(evt: any): void {
    evt.preventDefault()
    evt.target.select()
  }

  /**
   * Navigate to the edit page of a viewing table
   */
  public editTable() {
    this.router.navigateByUrl('/editor/' + this.libTab)
  }

  /**
   * Used to show/hide the edit table button
   * @returns Wheter currently viewed table is edtiable
   */
  public tableEditExists() {
    let editTables: any = {}
    editTables = globals.editor.libsAndTables
    let currentTable = this.libTab.split('.')[1]
    let currentLibrary = this.libTab.split('.')[0]

    // If this line is undefined, that means startupservice failed or similar.
    if (!editTables[currentLibrary]) return false

    return editTables[currentLibrary].includes(currentTable)
  }

  /**
   * Navigate to the lineage of a viewing table
   */
  public goToLineage() {
    let routeUri = this.tableuri!.split('\\')[1]
    let lineageUrl = `/view/lineage/${routeUri}/REVERSE`
    this.router.navigateByUrl(lineageUrl)
  }

  /**
   * Displays web query modal
   */
  public showWebQuery() {
    this.webQuery = true
    let filter_pk: number
    if (typeof this.route.snapshot.params['filterId'] === 'undefined') {
      filter_pk = 0
    } else {
      filter_pk = parseInt(this.route.snapshot.params['filterId'])
    }

    let port = window.location.port.length > 0 ? ':' + window.location.port : ''

    const sasPath = this.sasService.getExecutionPath()

    let filter = '&filter=' + filter_pk

    let downUrl = `${window.location.protocol}//${
      window.location.hostname
    }${port}/${sasPath}/?_program=${
      this.sasjsConfig.appLoc
    }/services/public/getrawdata&type=WEB${
      this.webQueryTab ? 'TAB' : 'CSV'
    }&table=${this.tableTitle}${filter}`

    this.webQueryText = downUrl.replace(/ /gim, '%20')
  }

  public copyToClip() {
    navigator.clipboard.writeText(this.webQueryText)
  }

  public goToViewer() {
    this.router.navigateByUrl('/view/data')
  }

  public showTableSelect() {
    this.tableFlag = !this.tableFlag
  }

  public checkExpand(event: any) {}

  public collapseLibraryItems(libraries: any, libraryToSkip: any) {
    libraries.forEach((library: any) => {
      if (library.LIBRARYREF !== libraryToSkip.LIBRARYREF) {
        library['expanded'] = false
      }
    })
  }

  public loadMoreLibraries() {
    if (!this.librariesPaging) {
      this.librariesPaging = true

      this.helperService.displayLibraries(this.libraries, true)

      this.librariesPaging = false
    }
  }

  public treeNodeClicked(event: any, library: any) {
    if (event.target.title === 'Collapse') {
      this.collapseLibraryItems(this.libraries, library)
    }
  }

  public libraryExpandedChange(expanded: any, library: any) {
    if (expanded) {
      this.collapseLibraryItems(this.libraries, library)
    }
  }

  public async libraryOnClick(lib: string, library: any, expandOnly?: boolean) {
    if (!library['tables']) {
      await this.selectTable(lib, false, library)
    } else {
      if (!!expandOnly) {
        library['expanded'] = true
      } else {
        library['expanded'] = !library['expanded']
      }
    }

    if (library['expanded']) {
      this.cdf.detectChanges()
      let libTreeSearchInput: HTMLElement | null = document.querySelector(
        `#search_${library.LIBRARYREF}`
      )

      if (libTreeSearchInput) libTreeSearchInput.focus()

      if (library && library.libinfo) this.libinfo = library.libinfo

      if (this.lib && this.table && !expandOnly) {
        this.router
          .navigate(['/view/data'], {
            skipLocationChange: true,
            queryParamsHandling: 'preserve'
          })
          .then(() => {
            this.router.navigate(['/view/data/' + this.lib], {
              queryParamsHandling: 'preserve'
            })
          })
      } else if (this.lib && !this.table) {
        this.location.replaceState('/view/data/' + this.lib)
      }
    }

    this.collapseLibraryItems(this.libraries, library)
  }

  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.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) {
    library['loadingTables'] = true

    this.table = false
    this.tableDisable = true
    if (lib !== 'Please select library') {
      if (globals.viewer.tablesSet && initial) {
        this.abortActive = false
        this.tableDisable = false
        this.tables = globals.viewer.tables
        this.libinfo = globals.viewer.libinfo
      } else {
        this.libinfo = null

        await this.sasStoreService
          .viewTables(lib)
          .then((res: PublicViewtablesServiceResponse) => {
            this.abortActive = false
            this.tableDisable = false

            let tables = res.mptables.map(function (item: any) {
              return item.MEMNAME
            })

            this.libinfo = res.libinfo || []
            this.tables = tables

            if (library) {
              library['tables'] = tables
              library['libinfo'] = this.libinfo
            }

            globals.viewer.libraries = this.libraries
            globals.viewer.library = this.lib
            globals.viewer.tables = this.tables
            globals.viewer.libinfo = this.libinfo
            globals.viewer.tablesSet = true
          })
          .catch((err: any) => {
            this.loggerService.error(err)

            this.abortActive = true
            this.noData = true
            this.tableTitle = ''
            this.tableDisable = true
          })
      }
    } else {
      this.tableDisable = true
    }
    // this.getTableState();

    globals.viewer.library = this.lib
    if (!initial) {
      this.clearGlobalsFilter()
    }

    library['loadingTables'] = false
    library['expanded']
      ? (library['expanded'] = false)
      : (library['expanded'] = true)
  }

  public selectLibTable(ev: string, initial?: boolean) {
    if (ev !== 'Please select table') {
      this.actionDisable = false
      this.libTab = this.lib + '.' + this.table
    } else {
      this.actionDisable = true
    }
    globals.viewer.table = ev
    if (!initial) {
      this.clearGlobalsFilter()
    }

    this.loggerService.log(this.libTab)
  }

  private clearGlobalsFilter() {
    globals.viewer.filter.libds = ''
    globals.viewer.filter.whereClause = ''
    globals.viewer.filter.groupLogic = ''
    globals.viewer.filter.clauses = []
    globals.viewer.filter.cols = []
    globals.viewer.filter.vals = []
  }

  public libTabActive(libraryRef: string, table: string) {
    let currentLibTab = libraryRef + '.' + table

    if (!this.libTab) {
      return false
    }

    return currentLibTab === this.libTab
  }

  public treeOnFilter(array: any, arrToFilter: string) {
    this.helperService.treeOnFilter(array, arrToFilter)
  }

  public libraryOnFilter() {
    this.helperService.libraryOnFilter(
      this.libraries,
      this.librariesSearch,
      'LIBRARYNAME'
    )

    globals.viewer.librariesSearch = this.librariesSearch
  }

  public libraryResetFilter() {
    this.helperService.resetArrayFilter(this.libraries)
  }

  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

    this.loadingTableView = true

    let libDataset: any
    if (typeof this.libDataset === 'undefined') {
      libDataset = this.lib + '.' + this.table
    } else if (
      typeof this.lib === 'undefined' &&
      typeof this.table === 'undefined'
    ) {
      let ds = []
      ds = this.libDataset.split('.')

      await this.sasStoreService
        .viewLibs()
        .then((res: any) => {
          this.libraries = res.saslibs
        })
        .catch((err: any) => {
          this.loggerService.error(err)
        })

      this.lib = ds[0]

      if (globals.viewer.startupSet) {
        this.tables = globals.viewer.tables
      } else {
        let library = this.libraries.find(
          (lib) => lib.LIBRARYREF.toLowerCase() === this.lib.toLowerCase()
        )

        await this.sasStoreService
          .viewTables(this.lib)
          .then((response: any) => {
            this.tables = response.mptables.map(function (item: any) {
              return item.MEMNAME
            })

            library.tables = this.tables
            globals.viewer.libraries = this.libraries
            globals.viewer.tables = this.tables
            globals.viewer.startupSet = true
          })
          .catch((err: any) => {
            this.loggerService.error(err)
          })
      }

      this.table = ds[1]

      this.tableFlag = false

      libDataset = this.libDataset
      this.libTab = libDataset
    } else {
      await this.sasStoreService
        .viewLibs()
        .then((res: any) => {
          this.libraries = res.saslibs
        })
        .catch((err: any) => {
          this.loggerService.error(err)
        })

      if (typeof this.table !== 'undefined') {
        if (globals.viewer.startupSet) {
          this.tables = globals.viewer.tables
        } else {
          await this.sasStoreService
            .viewTables(this.lib)
            .then((response: PublicViewtablesServiceResponse) => {
              this.tables = response.mptables.map(function (item: any) {
                return item.MEMNAME
              })
            })
            .catch((err: any) => {
              this.loggerService.error(err)

              this.router.navigate(['/view/data'], {
                queryParamsHandling: 'preserve'
              })
            })
        }

        this.tableFlag = false

        libDataset = this.lib + '.' + this.table
        this.tableTitle = libDataset
        this.libTab = libDataset
      }
    }

    if (this.router.url.split('/').length > 3 && libDataset) {
      await this.sasStoreService
        .viewData(libDataset, filter_pk)
        .then((res: any) => {
          if (
            res.query.length > 0 &&
            globals.rootParam === 'view' &&
            globals.viewer.filter.clauses.length === 0
          ) {
            globals.viewer.filter.query = this.helperService.deepClone(
              res.query
            )
            globals.viewer.filter.libds = this.route.snapshot.params['libMem']
            this.sasStoreService.initializeGlobalFilterClause(
              'viewer',
              res.cols
            )
          }
          this.abortActive = false
          this.filterCols = res.cols

          // This will merge $formats types with `Cols` object.
          mergeColsRules(this.filterCols, [], res.$viewdata)

          this.numberOfRows = res.sasparams[0].NOBS
          this.headerPks = res.sasparams[0].PK_FIELDS.split(' ')

          if (this.sasjsConfig.serverType === 'SAS9') {
            let tableuri = res.sasparams[0].TABLEURI

            if (tableuri) {
              if (tableuri.length > 0) {
                this.tableuri = tableuri
              }
            }
          }

          this.hotTable.data = res.viewdata
          this.$dataFormats = res.$viewdata
          this.dsmeta = res.dsmeta
          this.versions = res.versions || []
          this.setDSNote()
          this.queryText = res.sasparams[0].FILTER_TEXT

          // Initialize columns only if we have data
          if (res.viewdata && res.viewdata.length > 0) {
            let columns: any[] = []
            let colArr = []

            for (let key in res.viewdata[0]) {
              if (key) {
                colArr.push(key)
              }
            }

            // NUMBER_FORMAT (display) and HIDDEN (visibility) are the DQ rules
            // that make sense in the read-only viewer; READONLY/ROUND are
            // editor-only. Build lookups for both from the response.
            const numberFormats: { [col: string]: string } = {}
            const hiddenCols = new Set<string>()
            if (Array.isArray(res.dqrules)) {
              for (const rule of res.dqrules) {
                if (rule.RULE_TYPE === 'NUMBER_FORMAT') {
                  numberFormats[rule.BASE_COL] = rule.RULE_VALUE
                }
                if (rule.RULE_TYPE === 'HIDDEN') {
                  hiddenCols.add(rule.BASE_COL)
                }
              }
            }

            const hiddenColumnIndexes: number[] = []
            for (let index = 0; index < colArr.length; index++) {
              const col = colArr[index]
              const colDef: any = { data: col }
              if (numberFormats[col]) {
                colDef.renderer = makeNumberFormatRenderer(numberFormats[col])
              }
              if (hiddenCols.has(col)) {
                hiddenColumnIndexes.push(index)
              }
              columns.push(colDef)
            }

            this.colNames = colArr
            this.hotTable.colHeaders = getDisplayColHeaders(
              colArr,
              res.cols,
              this.useLabels
            )
            this.hotTable.columns = columns
            this.hiddenViewColumns = hiddenColumnIndexes
          } else {
            // Set empty arrays if no data
            this.colNames = []
            this.hotTable.colHeaders = []
            this.hotTable.columns = []
            this.hiddenViewColumns = []
          }

          // Set cells function
          this.hotTable.cells = () => {
            return { readOnly: true }
          }

          // Update hot table settings after data is loaded
          this.updateHotTableSettings()

          this.tableFlag = false
          let ds = []
          ds = libDataset.split('.')
          this.lib = ds[0]
          this.table = ds[1]

          if (this.hotTable.data.length === 0) {
            this.noData = true
            this.tableFlag = true
          } else {
            this.noData = false
            this.tableFlag = false
          }

          this.noDataReqErr = false
        })
        .catch((err: any) => {
          this.loggerService.error(err)

          this.abortActive = true
          this.noDataReqErr = true
        })
    }

    // If we move away from the viewer page, we don't renavigate to table view
    if (this.router.url.includes('/data') && libDataset) {
      if (filter_pk !== 0) {
        this.router.navigate(['/view/data/' + libDataset + '/' + filter_pk], {
          queryParamsHandling: 'preserve'
        })
      } else {
        this.router.navigate(['/view/data/' + libDataset], {
          queryParamsHandling: 'preserve'
        })
      }
    }

    let currentTable = this.table
    let libraryObject = this.libraries.find(
      (lib) => lib.LIBRARYREF.toLowerCase() === this.lib.toLowerCase()
    )

    if (libraryObject) {
      if (globals.viewer.libraries.length > 0) {
        this.libraries = globals.viewer.libraries
        this.librariesSearch = globals.viewer.librariesSearch
        this.libraryOnClick(this.lib, libraryObject, true)
      } else {
        this.libraryOnClick(this.lib, libraryObject, true)
      }
    } else {
      this.libinfo = []
    }

    this.table = currentTable ? currentTable : this.table

    if (this.libraries) {
      this.helperService.displayLibraries(this.libraries)
    }

    if (this.router.url.includes('/data')) {
      this.eventService.closeSidebar()
    }

    this.loadingTableView = false

    // 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()
    }, 500)

    /**
     * This is hacky fix for closing the nav drodpwon, when clicking on the handsontable area.
     * Without it, hot table does not steal focus, so it doesn't close the nav.
     */
    if (!this.hotInstanceClickListener) {
      setTimeout(() => {
        let hotInstanceEl = document.getElementById('hotInstance')

        if (hotInstanceEl) {
          hotInstanceEl.addEventListener('mousedown', (event) => {
            setTimeout(() => {
              let menuDebugItem: any =
                document.querySelector('.debug-switch-item') || undefined
              if (menuDebugItem) menuDebugItem.click()
            }, 100)
          })
          this.hotInstanceClickListener = true
        }
      }, 1000)
    }
  }

  public maxWidthCheker(width: any, col: any) {
    if (width > 200) return 200
    else return width
  }

  public openQb() {
    this.filter = true
    this.cdf.detectChanges()

    let libDataset = this.lib + '.' + this.table
    this.sasStoreService.setQueryVariables(libDataset, this.filterCols)
  }

  public async sendClause() {
    this.submitLoading = true
    let nullVariableArr = []
    let emptyVariablesArr = []
    // to check number of empty clauses
    if (typeof this.clauses === 'undefined') {
      this.nullVariables = true
      this.submitLoading = false
      return
    } else {
      let query = this.clauses.queryObj

      if (query[0].elements.length < 1) {
        // Clear cached filtering data
        if (globals.rootParam === 'view') {
          globals.viewer.filter.clauses = []
          globals.viewer.filter.query = []
          globals.viewer.filter.groupLogic = ''
        }

        this.router.navigate(['/view/data/' + this.libds], {
          queryParamsHandling: 'preserve'
        })
        return
      }

      for (let index = 0; index < query.length; index++) {
        const el = query[index].elements
        nullVariableArr = el.filter(function (item: any) {
          return item.variable === null
        })
        if (nullVariableArr.length) {
          emptyVariablesArr.push(el)
        }
      }
    }

    if (emptyVariablesArr.length) {
      // not allow to submit if some of clauses are empty
      this.nullVariables = true
      this.submitLoading = false
      return
    } else {
      const filterQuery: FilterQuery = {
        groupLogic: this.clauses.groupLogic,
        filterGroups: []
      }
      this.clauses.queryObj.forEach((group: any) => {
        const filterGroup: FilterGroup = {
          filterClauses: []
        }
        group.elements.forEach((clause: any) => {
          filterGroup.filterClauses.push(this.helperService.deepClone(clause))
        })
        filterGroup.clauseLogic = group.clauseLogic
        filterQuery.filterGroups.push(this.helperService.deepClone(filterGroup))
      })

      const filterQueryClauseTable =
        this.sasStoreService.createFilterQueryTable(filterQuery)
      await this.sasStoreService
        .saveQuery(this.libds, filterQueryClauseTable)
        .then((res: any) => {
          this.queryText = res.result[0].FILTER_TEXT
          let id = res.result[0].FILTER_RK
          this.router.navigate(['/view/data/' + this.libds + '/' + id], {
            queryParamsHandling: 'preserve'
          })

          this.viewData(id)

          this.filter = false
        })
        .catch((err: any) => {
          this.loggerService.error(err)
        })

      this.submitLoading = false
    }
  }

  public removeQuery() {
    this.sasStoreService.removeClause()
  }

  public datasetInfoModalRowClicked(value: Version | DSMeta) {
    if ((<Version>value).LOAD_REF !== undefined) {
      // Type is Version
      const row = value as Version
      const url = `/stage/${row.LOAD_REF}`

      this.router.navigate([url])
    }
  }

  private setDSNote() {
    const notes = this.dsmeta.find((item) => item.NAME === 'NOTES')
    const longDesc = this.dsmeta.find((item) => item.NAME === 'DD_LONGDESC')
    const shortDesc = this.dsmeta.find((item) => item.NAME === 'DD_SHORTDESC')

    if (notes && notes.VALUE) {
      this.dsNote = notes.VALUE
    } else if (longDesc && longDesc.VALUE) {
      this.dsNote = longDesc.VALUE
    } else if (shortDesc && shortDesc.VALUE) {
      this.dsNote = shortDesc.VALUE
    } else {
      this.dsNote = ''
    }
  }

  /**
   * 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:
   * - 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
    this.hooksAttached = false
  }

  /**
   * PERFORMANCE: Configures Handsontable with enhanced error handling (workaround needed for HOT version 16 and above)
   *
   * 1. Duplicate call prevention (500ms window)
   * 2. Multiple validation checks to prevent race conditions
   * 3. Forced render for immediate primary key styling
   *
   * Instance lifecycle is managed by Angular's hot-table component via [data] and [settings] bindings.
   * This method only applies additional config that can't go through bindings (hooks, PK styling).
   */
  private setupHot() {
    const now = Date.now()
    if (now - this.lastSetupTime < 500) {
      return
    }
    this.lastSetupTime = now

    // VALIDATION: Don't setup if we're currently switching tables or still loading
    if (this.loadingTableView || !this.libDataset) {
      return
    }

    if (!this.hotInstance || this.hotInstance.isDestroyed) {
      this.hotInstance = this.hotTableComponent?.hotInstance
    }

    if (this.hotInstance && !this.hotInstance.isDestroyed) {
      this.configureHotInstance()
      return
    }

    // Instance not ready yet — Angular may still be creating the component
    setTimeout(() => {
      if (this.isTableSwitching || this.loadingTableView || !this.libDataset) {
        return
      }

      this.hotInstance = this.hotTableComponent?.hotInstance
      this.configureHotInstance()
    }, 250)
  }

  private hooksAttached = false

  /**
   * Applies settings that can't go through Angular [settings] binding:
   * - Primary key column header styling
   * - Column width cap
   * - ARIA accessibility hooks (attached once per instance)
   */
  private configureHotInstance() {
    if (!this.hotInstance || this.hotInstance.isDestroyed) return

    this.hotInstance.updateSettings({
      height: this.hotTable.height,
      modifyColWidth: (width: any, col: any) => {
        if (width > 500) return 500
        else return width
      },
      afterGetColHeader: (col: number, th: any) => {
        // CRITICAL: Same safety checks as initial callback to prevent destroyed instance errors
        if (!this.hotInstance || this.hotInstance.isDestroyed) {
          th.classList.add(globals.handsontable.darkTableHeaderClass)
          return
        }

        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
          if (isPKCol) th.classList.add('primaryKeyHeaderStyle')

          // 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)
        }
      }
    })

    // Add hooks for accessibility fixes
    // Hooks are attached once per instance to avoid accumulating duplicate listeners
    if (!this.hooksAttached) {
      this.hotInstance.addHook('afterRender', () => {
        // Fix ARIA accessibility issues after each render
        this.fixAriaAccessibility()
      })

      this.hotInstance.addHook('afterChange', () => {
        // Fix ARIA accessibility issues after any data change
        setTimeout(() => {
          this.fixAriaAccessibility()
        }, 50)
      })

      this.hooksAttached = true
    }

    // Force immediate render to apply primary key styling
    // Without this, styling would wait for ~2 seconds to be applied
    // (workaround needed for HOT version 16 and above)
    this.hotInstance.render()
  }

  async loadWithParameters() {
    this.switchFlag = true
    this.tableTitle = this.route.snapshot.params['libMem'] || 0
    let id = this.route.snapshot.params['filterId'] || '0'

    this.urlFilterPk = parseInt(id)

    let libds = this.route.snapshot.params['libMem']
    this.libDataset = libds

    if (!libds.includes('.')) this.lib = libds

    await this.viewData(this.urlFilterPk)

    if (this.noData) {
      setTimeout(() => {
        this.tableFlag = true
      }, 1200)
    } else {
      setTimeout(() => {
        this.tableFlag = false
      }, 1200)
    }
  }

  async loadWithoutParameters() {
    this.switchFlag = false
    this.librariesLoading = true

    if (globals.viewer.startupSet) {
      setTimeout(() => {
        this.libraries = globals.viewer.libraries
        this.librariesSearch = globals.viewer.librariesSearch
        this.lib = globals.viewer.library
        this.librariesTreeExpanded = true
        this.librariesLoading = false

        this.helperService.displayLibraries(this.libraries)
      }, 100)
    } else {
      this.noDataReqErr = false

      await this.sasStoreService
        .viewLibs()
        .then((res: PublicViewlibsServiceResponse) => {
          this.libraries = res.saslibs
          globals.viewer.libraries = this.libraries
          globals.viewer.startupSet = true

          this.librariesLoading = false

          this.helperService.displayLibraries(this.libraries)
        })
        .catch((err: any) => {
          this.loggerService.error(err)

          this.librariesLoading = false
          this.noDataReqErr = true
        })
    }
  }

  ngAfterViewChecked() {
    this.scheduleFilterOverflowCheck()
  }

  /**
   * Measures whether the applied-filter clause fits the collapsed single line,
   * and shows or hides the chevron accordingly. The measurement is deferred
   * because `filterOverflows` and `filterExpanded` are bound in this view -
   * setting them inside the change-detection cycle would raise
   * ExpressionChangedAfterItHasBeenChecked.
   */
  private scheduleFilterOverflowCheck() {
    const el = this.infoBar?.nativeElement
    if (!el) {
      return
    }

    const text = el.querySelector('.infoBar-text') as HTMLElement | null
    if (!text) {
      return
    }

    // re-measure only when the clause, the available width, or the state changes
    const key = `${this.queryText}|${text.clientWidth}|${this.filterExpanded}`
    if (key === this.filterMeasuredKey) {
      return
    }
    this.filterMeasuredKey = key

    setTimeout(() => {
      // while expanded the clause wraps, so there is nothing to measure
      if (this.filterExpanded) {
        return
      }
      this.filterOverflows = text.scrollWidth > text.clientWidth
    })
  }

  public toggleFilterPanel() {
    this.filterExpanded = !this.filterExpanded
  }

  ngAfterViewInit() {
    // Fix ARIA accessibility issues after table initialization
    setTimeout(() => {
      this.fixAriaAccessibility()
    }, 1000)
  }

  ngOnDestroy() {
    // Proper component destruction to prevent memory leaks and errors

    this._queryParams?.unsubscribe()

    // 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
    }

    // Clear ARIA check intervals
    if (this.ariaCheckInterval) {
      clearInterval(this.ariaCheckInterval)
      this.ariaCheckInterval = undefined
    }
  }

  /**
   * Fixes ARIA accessibility issues in the Handsontable component
   * This addresses the accessibility report issues with treegrid and presentation roles
   */
  private fixAriaAccessibility() {
    // Use a more aggressive approach to find and fix all ARIA issues
    const fixAriaIssues = () => {
      // Specifically target Handsontable wrapper elements that are causing issues
      const hotWrappers = document.querySelectorAll(
        '.ht-wrapper, .wtHolder, [id^="ht_"]'
      )

      hotWrappers.forEach((wrapper) => {
        // Remove problematic ARIA attributes from Handsontable wrappers
        wrapper.removeAttribute('role')
        wrapper.removeAttribute('aria-rowcount')
        wrapper.removeAttribute('aria-colcount')
        wrapper.removeAttribute('aria-multiselectable')
      })

      // Find all elements with problematic ARIA roles in the entire document
      const allTreegridElements = document.querySelectorAll('[role="treegrid"]')
      const allPresentationElements = document.querySelectorAll(
        '[role="presentation"]'
      )

      // Fix treegrid role issues - remove them completely as they're causing problems
      allTreegridElements.forEach((element) => {
        element.removeAttribute('role')
        element.removeAttribute('aria-rowcount')
        element.removeAttribute('aria-colcount')
        element.removeAttribute('aria-multiselectable')
      })

      // Fix presentation role issues - remove them if they contain interactive elements
      allPresentationElements.forEach((element) => {
        const hasInteractiveChildren =
          element.querySelectorAll(
            'button, input, select, textarea, [tabindex], [onclick], [contenteditable]'
          ).length > 0
        if (hasInteractiveChildren) {
          element.removeAttribute('role')
        }
      })

      // Also fix any elements with aria-rowcount="-1" which is problematic
      const negativeRowCountElements = document.querySelectorAll(
        '[aria-rowcount="-1"]'
      )
      negativeRowCountElements.forEach((element) => {
        element.removeAttribute('aria-rowcount')
      })

      // Ensure proper table structure
      const tableElements = document.querySelectorAll('table')
      tableElements.forEach((table) => {
        if (!table.getAttribute('role')) {
          table.setAttribute('role', 'table')
        }

        // Ensure table headers have proper scope
        const headerCells = table.querySelectorAll('th')
        headerCells.forEach((th) => {
          if (!th.getAttribute('scope')) {
            th.setAttribute('scope', 'col')
          }
        })
      })

      // Add proper ARIA labels to interactive elements
      const interactiveElements = document.querySelectorAll(
        'button, input, select, textarea, [contenteditable]'
      )
      interactiveElements.forEach((element) => {
        if (
          !element.getAttribute('aria-label') &&
          !element.getAttribute('aria-labelledby')
        ) {
          const textContent = element.textContent?.trim()
          if (textContent) {
            element.setAttribute('aria-label', textContent)
          }
        }
      })
    }

    // Run the fix immediately
    fixAriaIssues()

    // Run it again after a short delay to catch any dynamically created elements
    setTimeout(fixAriaIssues, 100)
    setTimeout(fixAriaIssues, 500)
    setTimeout(fixAriaIssues, 1000)
    setTimeout(fixAriaIssues, 2000)

    // Set up a periodic check to ensure accessibility fixes are maintained
    if (!this.ariaCheckInterval) {
      this.ariaCheckInterval = setInterval(fixAriaIssues, 3000)
    }

    // Set up a MutationObserver to continuously monitor for new problematic elements
    if (!this.ariaObserver) {
      this.ariaObserver = new MutationObserver((mutations) => {
        let shouldFix = false
        mutations.forEach((mutation) => {
          if (
            mutation.type === 'attributes' &&
            (mutation.attributeName === 'role' ||
              mutation.attributeName === 'aria-rowcount')
          ) {
            shouldFix = true
          }
          if (mutation.type === 'childList') {
            mutation.addedNodes.forEach((node) => {
              if (node.nodeType === Node.ELEMENT_NODE) {
                const element = node as Element
                if (
                  element.hasAttribute('role') ||
                  element.hasAttribute('aria-rowcount')
                ) {
                  shouldFix = true
                }
              }
            })
          }
        })

        if (shouldFix) {
          setTimeout(fixAriaIssues, 50)
        }
      })

      // Start observing the entire document for changes
      this.ariaObserver.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: [
          'role',
          'aria-rowcount',
          'aria-colcount',
          'aria-multiselectable'
        ]
      })
    }
  }

  async ngAfterContentInit() {
    if (this.hotTable.data.length > 0) {
      this.tableFlag = true
    }
    this._query = this.sasStoreService.query.subscribe((query: any) => {
      this.whereString = query.string
      this.clauses = query.obj
      this.libds = query.libds
    })

    if (typeof this.route.snapshot.params['libMem'] !== 'undefined') {
      this.loadWithParameters()
    } else {
      this.loadWithoutParameters()
    }
  }
}
<app-sidebar (scrolledToBottom)="loadMoreLibraries()">
  @if (librariesLoading) {
    <div class="my-10-mx-auto text-center">
      <clr-spinner clrMedium></clr-spinner>
    </div>
  }

  <clr-tree>
    @if (libraries) {
      <clr-tree-node class="search-node">
        <div class="tree-search-wrapper">
          <input
            appStealFocus
            clrInput
            #searchLibTreeInput
            [(ngModel)]="librariesSearch"
            placeholder="Libraries"
            name="input"
            (keyup)="libraryOnFilter()"
            autocomplete="off"
          />
          @if (searchLibTreeInput.value.length < 1) {
            <clr-icon aria-hidden="true" shape="search"></clr-icon>
          }
          @if (searchLibTreeInput.value.length > 0) {
            <clr-icon
              (click)="librariesSearch = ''; libraryOnFilter()"
              aria-label="Clear search"
              shape="times"
            ></clr-icon>
          }
        </div>
      </clr-tree-node>
    }

    @for (library of libraries; track library) {
      @if (!library['hidden'] && library['inForeground']) {
        <clr-tree-node
          (click)="treeNodeClicked($event, library)"
          [(clrExpanded)]="library['expanded']"
          [clrLoading]="library['loadingTables'] && !library.tables"
          [class.clr-expanded]="library['expanded']"
        >
          <p
            (click)="
              lib = library.LIBRARYREF;
              libraryOnClick(library.LIBRARYREF, library)
            "
            class="m-0 cursor-pointer"
          >
            <clr-icon aria-hidden="true" shape="rack-server"></clr-icon>
            {{ library.LIBRARYNAME }}
          </p>
          @if (library['tables']) {
            <clr-tree-node class="search-node">
              <div class="tree-search-wrapper">
                <input
                  appStealFocus
                  clrInput
                  #searchTreeInput
                  [id]="'search_' + library.LIBRARYREF"
                  placeholder="Tables"
                  name="input"
                  [(ngModel)]="library['searchString']"
                  (keyup)="treeOnFilter(library, 'tables')"
                  autocomplete="off"
                />
                @if (searchTreeInput.value.length < 1) {
                  <clr-icon aria-hidden="true" shape="search"></clr-icon>
                }
                @if (searchTreeInput.value.length > 0) {
                  <clr-icon
                    (click)="
                      searchTreeInput.value = '';
                      library['searchString'] = '';
                      treeOnFilter(library, 'tables')
                    "
                    aria-label="Clear search"
                    shape="times"
                  ></clr-icon>
                }
              </div>
            </clr-tree-node>
          }
          @for (
            libTable of library['tables'];
            track libTable;
            let index = $index
          ) {
            <clr-tree-node>
              <clr-tooltip
                *ngVar="
                  index + 1 >
                  licenceState.value.tables_in_library_limit as tableLocked
                "
              >
                @if (libTable.length > 0) {
                  <button
                    clrTooltipTrigger
                    (click)="
                      !tableLocked ? onTableClick(libTable, library) : ''
                    "
                    class="clr-treenode-link"
                    [class.dc-locked-control]="tableLocked"
                    [class.active]="libTabActive(library.LIBRARYREF, libTable)"
                  >
                    @switch (libTable.includes('-FC')) {
                      @case (true) {
                        <clr-icon aria-hidden="true" shape="bolt"></clr-icon>
                      }
                      @case (false) {
                        <clr-icon aria-hidden="true" shape="table"></clr-icon>
                      }
                    }
                    {{ libTable.replace('-FC', '') }}
                  </button>
                }
                @if (tableLocked) {
                  <clr-tooltip-content
                    clrPosition="bottom-right"
                    clrSize="lg"
                    *clrIfOpen
                  >
                    <span>
                      To unlock all tables, contact
                      support&#64;datacontroller.io
                    </span>
                  </clr-tooltip-content>
                }
              </clr-tooltip>
            </clr-tree-node>
          }
        </clr-tree-node>
      }
    }
  </clr-tree>

  @if (librariesPaging) {
    <div class="w-100 text-center">
      <span class="spinner spinner-sm"> Loading... </span>
    </div>
  }
</app-sidebar>

<div class="content-area">
  @if (nullVariables) {
    <div class="modal z-index-highest">
      <div class="modal-dialog" role="dialog" aria-hidden="true">
        <div class="modal-content">
          <div class="modal-header">
            <button aria-label="Close" class="close" type="button">
              <clr-icon aria-hidden="true" shape="close"></clr-icon>
            </button>
            <h3 class="modal-title">Error</h3>
          </div>
          <div class="modal-body">
            <p>You cannot submit empty clauses</p>
          </div>
          <div class="modal-footer">
            <button
              class="btn btn-sm btn-primary"
              (click)="nullVariables = false"
              type="button"
            >
              Ok
            </button>
          </div>
        </div>
      </div>
    </div>
  }

  @if (nullVariables) {
    <div class="modal-backdrop" aria-hidden="true"></div>
  }
  <clr-modal [(clrModalOpen)]="openDownload" [clrModalSize]="'md'">
    <h3 class="modal-title center text-center color-darker-gray">Download</h3>
    <div class="modal-body">
      <div class="clr-col-md-6">
        <clr-select-container class="download-select">
          <label>Please choose download format</label>
          <select [(ngModel)]="downloadFormat" clrSelect>
            <option value="CSV">CSV</option>
            <option value="SAS">Datalines (cards file)</option>
            <option value="PGSQL_DDL">DDL (PGSQL Flavour)</option>
            <option value="SAS_DDL">DDL (SAS Flavour)</option>
            <option value="TSQL_DDL">DDL (TSQL Flavour)</option>
            <option value="EXCEL">Excel (.xlsx)</option>
            <option value="MARKDOWN">Markdown (.md)</option>
          </select>
        </clr-select-container>
      </div>
    </div>
    <div class="modal-footer">
      <button
        type="button"
        class="btn btn-sm btn-outline"
        (click)="openDownload = false"
      >
        Cancel
      </button>
      <button
        [id]="tableTitle"
        type="submit"
        class="btn btn-sm btn-success-outline"
        (click)="
          downloadFormat.includes('DDL') ? downloadDDL() : downloadData()
        "
      >
        Ok
      </button>
    </div>
  </clr-modal>

  <clr-modal [(clrModalOpen)]="webQuery" [clrModalSize]="'lg'">
    <h3 class="modal-title center text-center color-darker-gray">
      Web Query URL
    </h3>
    <div class="modal-body web-query">
      <div class="row">
        <div class="clr-col-lg-12 clr-col-md-12 clr-col-sm-12 clr-col-xs-12">
          <div class="card no-borders">
            <div class="card-header d-flex justify-content-between">
              <span>Copy the below into your preferred client tool:</span>

              <div class="d-block mr-0" class="btn-group">
                <div
                  class="radio btn"
                  (click)="webQueryTab = true; showWebQuery()"
                >
                  <input
                    type="radio"
                    name="btn-group-demo-radios"
                    [checked]="webQueryTab"
                  />
                  <label>TAB</label>
                </div>

                <div
                  class="radio btn"
                  (click)="webQueryTab = false; showWebQuery()"
                >
                  <input
                    type="radio"
                    name="btn-group-demo-radios"
                    [checked]="!webQueryTab"
                  />
                  <label>CSV</label>
                </div>
              </div>
            </div>
            <div class="card-block web-query-wrapper word-break-all">
              <textarea
                clrTextarea
                class="web-query-text"
                rows="4"
                cols="50"
                #cliCommandInput
                (focus)="onCliCommandFocus($event)"
                type="text"
                [value]="webQueryText"
                readonly
              >
              </textarea>
            </div>
            <div class="card-footer">
              <button
                class="btn btn-sm btn-link"
                [ngxClipboard]="cliCommandInput"
                (click)="copyToClip()"
              >
                copy to clipboard
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="modal-footer">
      <button
        type="button"
        class="btn btn-sm btn-outline"
        (click)="webQuery = false"
      >
        close
      </button>
      <!-- <button type="submit" class="btn btn-sm btn-success-outline" (click)="downloadData()">Ok</button> -->
    </div>
  </clr-modal>

  <clr-modal
    [(clrModalOpen)]="filter"
    [clrModalSize]="'xl'"
    [clrModalClosable]="false"
    aria-modal="true"
    class="filter-modal"
  >
    <h3 class="modal-title center text-center color-darker-gray">
      Filter for table:<span> {{ libTab }} </span>
    </h3>
    <div class="modal-body">
      @if (filter) {
        <app-query #queryFilter></app-query>
      }
    </div>
    <div class="modal-footer">
      <button (click)="resetFilter()" type="button" class="btn btn-sm btn-link">
        reset filter
      </button>
      <button
        type="button"
        class="btn btn-outline btn-sm"
        (click)="filter = false; removeQuery()"
      >
        Cancel
      </button>
      <button
        [clrLoading]="submitLoading"
        type="submit"
        class="btn btn-sm btn-success-outline"
        (click)="sendClause()"
      >
        Ok
      </button>
    </div>
  </clr-modal>

  <clr-modal [(clrModalOpen)]="queryErr">
    <h3 class="modal-title">Error</h3>
    <div class="modal-body">
      <p>{{ queryErrMessage }}</p>
    </div>
    <div class="modal-footer">
      <button type="button" class="btn btn-outline" (click)="queryErr = false">
        Cancel
      </button>
      <button type="button" class="btn btn-primary" (click)="queryErr = false">
        Ok
      </button>
    </div>
  </clr-modal>

  @if (loadingTableView) {
    <div class="loadingSpinner">
      <span class="spinner"> Loading... </span>
      <div class="mt-10">
        <p cds-text="section">Loading Table Viewer</p>
      </div>
    </div>
  }

  @if (!loadingTableView) {
    <div class="card no-borders h-100 d-flex clr-flex-column">
      @if (table) {
        <div
          class="header-row clr-row justify-content-between clr-justify-content-center w-100 m-0"
        >
          <section class="form-block table-search-wrapper sw clr-col-md">
            <form class="d-flex align-items-center" clrForm>
              <div class="input-wrapper">
                <input
                  #searchEl
                  (keyup.enter)="searchTable(searchEl)"
                  clrInput
                  [type]="searchNumeric ? 'number' : 'text'"
                  placeholder="Search"
                  name="search-input"
                />
                @if (!searchLoading) {
                  <clr-icon
                    (click)="searchTable(searchEl)"
                    aria-label="Search"
                    shape="search"
                  ></clr-icon>
                }
                @if (searchLoading) {
                  <span class="spinner spinner-inline"> Loading... </span>
                }
              </div>
              <clr-checkbox-container>
                <clr-checkbox-wrapper>
                  <input
                    type="checkbox"
                    clrCheckbox
                    [(ngModel)]="searchNumeric"
                    name="numeric_check"
                  />
                  <label>Numeric</label>
                </clr-checkbox-wrapper>
              </clr-checkbox-container>
            </form>
          </section>
          <div class="title-col clr-col-auto clr-flex-column clr-flex-sm-row">
            <p
              cds-text="section"
              class="clr-flex-column d-flex clr-flex-sm-row clr-align-items-center clr-justify-content-center"
            >
              <clr-tooltip class="d-flex clr-align-items-center">
                <clr-icon
                  clrTooltipTrigger
                  (click)="datasetInfo = true"
                  aria-label="View dataset meta info"
                  shape="info-circle"
                  class="is-highlight cursor-pointer"
                  size="24"
                ></clr-icon>
                @if (tableTitle?.includes('-FC')) {
                  <clr-icon
                    aria-hidden="true"
                    shape="bolt"
                    class="color-yellow mr-5"
                  ></clr-icon>
                }
                @if (tableTitle && tableTitle.length > 0) {
                  <span clrTooltipTrigger>
                    {{ tableTitle?.replace('-FC', '') }}
                  </span>
                }
                @if (this.dsNote && this.dsNote.length > 0) {
                  <clr-tooltip-content
                    clrPosition="bottom-left"
                    clrSize="lg"
                    *clrIfOpen
                  >
                    {{ this.dsNote }}
                  </clr-tooltip-content>
                }
              </clr-tooltip>
              @if (tableTitle && tableTitle.length > 0) {
                @if (numberOfRows !== null) {
                  <span>
                    ({{ numberOfRows | thousandSeparator: ',' }}
                    {{ numberOfRows! === 1 ? 'row' : 'rows' }},
                    {{ filterCols.length
                    }}{{ filterCols.length === 1 ? ' col' : ' cols' }})
                  </span>
                }
                <clr-icon
                  (click)="reloadTableData()"
                  aria-label="Reload data"
                  class="refresh-table"
                  shape="refresh"
                ></clr-icon>
              }
            </p>
          </div>
          <div class="options-col clr-col-md">
            @if (tableTitle && !abortActive) {
              <clr-dropdown
                class="options-dropdown"
                [clrCloseMenuOnItemClick]="true"
              >
                <button
                  type="button"
                  class="btn btn-sm btn-outline filterSide"
                  clrDropdownTrigger
                >
                  <clr-icon aria-hidden="true" shape="cog" size="15"></clr-icon>
                  options
                </button>
                <clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
                  <div (click)="newViewbox()" clrDropdownItem>
                    <clr-icon aria-hidden="true" shape="view-cards"></clr-icon>
                    <span>Viewboxes</span>
                  </div>
                  @if (tableEditExists()) {
                    <div (click)="editTable()" clrDropdownItem>
                      <clr-icon aria-hidden="true" shape="pencil"></clr-icon>
                      <span>Edit</span>
                    </div>
                  }
                  @if (tableuri) {
                    <div (click)="goToLineage()" clrDropdownItem>
                      <clr-icon aria-hidden="true" shape="switch"></clr-icon>
                      <span>Lineage</span>
                    </div>
                  }
                  <div (click)="openQb()" clrDropdownItem>
                    <clr-icon aria-hidden="true" shape="filter"></clr-icon>
                    <span>Filter</span>
                  </div>
                  <div (click)="openDownload = true" clrDropdownItem>
                    <clr-icon aria-hidden="true" shape="download"></clr-icon>
                    <span>Download</span>
                  </div>
                  <div (click)="showWebQuery()" clrDropdownItem>
                    <clr-icon
                      aria-hidden="true"
                      shape="download-cloud"
                    ></clr-icon>
                    <span>Web Query URL</span>
                  </div>
                </clr-dropdown-menu>
              </clr-dropdown>
            }
          </div>
          @if (
            queryText !== '1=1' &&
            !['', ' '].includes(queryText) &&
            !abortActive
          ) {
            <div
              #infoBar
              class="clr-col-md-12 infoBar"
              [class.expanded]="filterExpanded"
            >
              <span class="infoBar-text"
                >FILTER : <b>{{ queryText }}</b></span
              >
              @if (filterOverflows) {
                <button
                  type="button"
                  class="infoBar-toggle"
                  [attr.aria-expanded]="filterExpanded"
                  [attr.aria-label]="
                    filterExpanded
                      ? 'Collapse the filter clause'
                      : 'Expand the filter clause'
                  "
                  (click)="toggleFilterPanel()"
                >
                  <clr-icon aria-hidden="true" shape="caret down"></clr-icon>
                </button>
              }
            </div>
          }
        </div>
      }
      @if (!lib && !table && !noDataReqErr && !noData) {
        <div class="no-table-selected">
          <img
            src="images/select-library.png"
            class="select-table-icon"
            alt="select a library icon"
          />
          <h3 class="text-center color-gray mt-10" cds-text="section">
            Please select a library
          </h3>
        </div>
      }
      @if (!noData && !noDataReqErr && !table && lib) {
        <div
          class="header-row clr-row border-bottom-divider justify-content-between w-100 m-0"
        >
          <section
            class="form-block table-search-wrapper sw clr-col-md"
          ></section>
          <div class="title-col clr-col-auto mt-15 mb-15">
            <p cds-text="section">
              {{ lib }}
            </p>
            <clr-icon
              (click)="reloadLibInfo()"
              aria-label="Reload library info"
              class="refresh-table"
              shape="refresh"
            ></clr-icon>
          </div>
          <div class="options-col clr-col-md"></div>
        </div>
        <div class="text-center mt-10">
          @if (libinfo === null) {
            <clr-spinner clrMedium></clr-spinner>
          }
        </div>
        @if (libinfo !== null) {
          <div class="no-table-selected-info pointer-events-none">
            <img
              src="images/select-table.png"
              class="select-table-icon"
              alt="select table icon"
            />
            <p class="text-center color-gray mt-10" cds-text="section">
              Please select a table
            </p>
          </div>
        }
        @if (libinfo !== null) {
          <div class="libinfo m-0 clr-row">
            @if (libinfo.length < 1) {
              <p class="text-center m-0 w-100">
                No library info found. Click
                <clr-icon
                  (click)="reloadLibInfo()"
                  aria-label="Reload library info"
                  class="refresh-table m-0"
                  shape="refresh"
                ></clr-icon>
                button to refresh.
              </p>
            }
            @if (libinfo.length > 0) {
              <table>
                @if (libinfo[0].ENGINE !== '') {
                  <tr>
                    <td class="m-0">ENGINE:</td>
                    <td class="m-0 font-bold">
                      {{ libinfo[0] ? libinfo[0].ENGINE : '' }}
                    </td>
                  </tr>
                }
                @if (libinfo[0].LIBID !== '') {
                  <tr>
                    <td class="m-0">LIBID:</td>
                    <td class="m-0 font-bold">
                      {{ libinfo[0] ? libinfo[0].LIBID : '' }}
                    </td>
                  </tr>
                }
                @if (libinfo[0].LIBNAME !== '') {
                  <tr>
                    <td class="m-0">LIBNAME:</td>
                    <td class="m-0 font-bold">
                      {{ libinfo[0] ? libinfo[0].LIBNAME : '' }}
                    </td>
                  </tr>
                }
                @if (libinfo[0].LIBSIZE !== null) {
                  <tr>
                    <td class="m-0">LIBSIZE:</td>
                    <td class="m-0 font-bold">
                      {{ libinfo[0] ? (libinfo[0].LIBSIZE | convertSize) : '' }}
                    </td>
                  </tr>
                }
                @if (libinfo[0].OWNERS !== '') {
                  <tr>
                    <td class="m-0">OWNERS:</td>
                    <td class="m-0 font-bold">
                      {{ libinfo[0] ? libinfo[0].OWNERS : '' }}
                    </td>
                  </tr>
                }
                @if (libinfo[0].PATHS !== '') {
                  <tr>
                    <td class="m-0">PATHS:</td>
                    <td class="m-0 font-bold">
                      {{ libinfo[0] ? libinfo[0].PATHS : '' }}
                    </td>
                  </tr>
                }
                @if (libinfo[0].PERMS !== '') {
                  <tr>
                    <td class="m-0">PERMS:</td>
                    <td class="m-0 font-bold">
                      {{ libinfo[0] ? libinfo[0].PERMS : '' }}
                    </td>
                  </tr>
                }
                @if (libinfo[0].SCHEMAS !== '') {
                  <tr>
                    <td class="m-0">SCHEMAS:</td>
                    <td class="m-0 font-bold">
                      {{ libinfo[0] ? libinfo[0].SCHEMAS : '' }}
                    </td>
                  </tr>
                }
                @if (libinfo[0].TABLE_CNT !== null) {
                  <tr>
                    <td class="m-0">TABLE_CNT:</td>
                    <td class="m-0 font-bold">
                      {{ libinfo[0] ? libinfo[0].TABLE_CNT : '' }}
                    </td>
                  </tr>
                }
                @if (libinfo[0].CATALOG_CNT !== null) {
                  <tr>
                    <td class="m-0">CATALOG_CNT:</td>
                    <td class="m-0 font-bold">
                      {{ libinfo[0] ? libinfo[0].CATALOG_CNT : '' }}
                    </td>
                  </tr>
                }
              </table>
            }
          </div>
        }
      }
      @if (noData || noDataReqErr) {
        <div class="card-block noData">
          <clr-icon
            aria-hidden="true"
            shape="warning-standard"
            size="60"
            class="is-info"
          ></clr-icon>
          @if (noData) {
            <h3 class="text-center color-gray">
              No data found with given conditions
            </h3>
          }
          @if (noDataReqErr) {
            <h3 class="text-center color-gray">
              No data found due to sas request error
            </h3>
          }
        </div>
      }
      @if (!noData && !noDataReqErr && table) {
        <div class="clr-flex-1">
          <div class="hot-wrapper clr-flex-1">
            <hot-table
              #hotInstance
              id="hotTable"
              class="view-hot"
              [data]="hotTable.data"
              [settings]="hotTableSettings"
            >
            </hot-table>
          </div>
        </div>
      }
      <div>
        @if (
          licenceState.value.viewer_rows_allowed !== Infinity &&
          hotTable.data &&
          hotTable.data.length > licenceState.value.viewer_rows_allowed
        ) {
          <p class="mt-2-i w-100 text-center">
            To display more than
            {{ licenceState.value.viewer_rows_allowed }} rows, contact
            <contact-link />
          </p>
        }
      </div>
    </div>
  }
</div>

<app-dataset-info
  [(open)]="datasetInfo"
  [dsmeta]="dsmeta"
  [versions]="versions"
  (rowClicked)="datasetInfoModalRowClicked($event)"
>
</app-dataset-info>

<app-viewboxes [(viewboxModal)]="viewboxOpen"></app-viewboxes>

./viewer.component.scss

Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""