From 468e3c4e4e8b797443e452d88cf64ff8acde0dd4 Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Tue, 19 Aug 2025 17:04:28 -0400 Subject: [PATCH 1/5] feat(LibraryNavigator) wip for adding properties and column information Signed-off-by: Kishan Patel --- .../LibraryNavigator/LibraryDataProvider.ts | 9 + .../LibraryNavigator/LibraryModel.ts | 8 + .../src/components/LibraryNavigator/index.ts | 49 +++ .../src/components/LibraryNavigator/types.ts | 3 +- .../src/connection/itc/ItcLibraryAdapter.ts | 24 +- client/src/connection/itc/script.ts | 38 +++ .../src/connection/rest/RestLibraryAdapter.ts | 24 +- client/src/panels/TablePropertiesViewer.ts | 310 ++++++++++++++++++ package.json | 28 ++ package.nls.json | 2 + 10 files changed, 492 insertions(+), 3 deletions(-) create mode 100644 client/src/panels/TablePropertiesViewer.ts diff --git a/client/src/components/LibraryNavigator/LibraryDataProvider.ts b/client/src/components/LibraryNavigator/LibraryDataProvider.ts index 999d32d68..0a2eb0c9b 100644 --- a/client/src/components/LibraryNavigator/LibraryDataProvider.ts +++ b/client/src/components/LibraryNavigator/LibraryDataProvider.ts @@ -66,6 +66,7 @@ class LibraryDataProvider registerAPI("getColumns", model.fetchColumns.bind(model)); registerAPI("getTables", model.getTables.bind(model)); registerAPI("getLibraries", model.getLibraries.bind(model)); + registerAPI("getTableInfo", model.getTableInfo.bind(model)); } public getSubscriptions(): Disposable[] { @@ -178,6 +179,14 @@ class LibraryDataProvider this.model.useAdapter(libraryAdapter); this._onDidChangeTreeData.fire(undefined); } + + public async getTableInfo(item: LibraryItem) { + return await this.model.getTableInfo(item); + } + + public async fetchColumns(item: LibraryItem) { + return await this.model.fetchColumns(item); + } } export default LibraryDataProvider; diff --git a/client/src/components/LibraryNavigator/LibraryModel.ts b/client/src/components/LibraryNavigator/LibraryModel.ts index 05a690389..e0b1f6d6d 100644 --- a/client/src/components/LibraryNavigator/LibraryModel.ts +++ b/client/src/components/LibraryNavigator/LibraryModel.ts @@ -126,6 +126,14 @@ class LibraryModel { } } + public async getTableInfo(item: LibraryItem) { + await this.libraryAdapter.setup(); + if (this.libraryAdapter.getTableInfo) { + return await this.libraryAdapter.getTableInfo(item); + } + throw new Error("Table properties not supported for this connection type"); + } + public async getChildren(item?: LibraryItem): Promise { if (!this.libraryAdapter) { return []; diff --git a/client/src/components/LibraryNavigator/index.ts b/client/src/components/LibraryNavigator/index.ts index 221483059..1e2ecdfa9 100644 --- a/client/src/components/LibraryNavigator/index.ts +++ b/client/src/components/LibraryNavigator/index.ts @@ -17,6 +17,7 @@ import * as path from "path"; import { profileConfig } from "../../commands/profile"; import { Column } from "../../connection/rest/api/compute"; import DataViewer from "../../panels/DataViewer"; +import TablePropertiesViewer from "../../panels/TablePropertiesViewer"; import { WebViewManager } from "../../panels/WebviewManager"; import { SubscriptionProvider } from "../SubscriptionProvider"; import LibraryAdapterFactory from "./LibraryAdapterFactory"; @@ -101,6 +102,54 @@ class LibraryNavigator implements SubscriptionProvider { ); }, ), + commands.registerCommand( + "SAS.showTableProperties", + async (item: LibraryItem) => { + try { + const tableInfo = await this.libraryDataProvider.getTableInfo(item); + const columns = await this.libraryDataProvider.fetchColumns(item); + + this.webviewManager.render( + new TablePropertiesViewer( + this.extensionUri, + item.uid, + tableInfo, + columns, + false, // Show properties tab + ), + `properties-${item.uid}`, + ); + } catch (error) { + window.showErrorMessage( + `Failed to load table properties: ${error.message}`, + ); + } + }, + ), + commands.registerCommand( + "SAS.showTableColumns", + async (item: LibraryItem) => { + try { + const tableInfo = await this.libraryDataProvider.getTableInfo(item); + const columns = await this.libraryDataProvider.fetchColumns(item); + + this.webviewManager.render( + new TablePropertiesViewer( + this.extensionUri, + item.uid, + tableInfo, + columns, + true, // Show columns tab + ), + `columns-${item.uid}`, + ); + } catch (error) { + window.showErrorMessage( + `Failed to load table columns: ${error.message}`, + ); + } + }, + ), commands.registerCommand("SAS.collapseAllLibraries", () => { commands.executeCommand( "workbench.actions.treeView.librarydataprovider.collapseAll", diff --git a/client/src/components/LibraryNavigator/types.ts b/client/src/components/LibraryNavigator/types.ts index b67d567e1..591d33787 100644 --- a/client/src/components/LibraryNavigator/types.ts +++ b/client/src/components/LibraryNavigator/types.ts @@ -1,6 +1,6 @@ // Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ColumnCollection } from "../../connection/rest/api/compute"; +import { ColumnCollection, TableInfo } from "../../connection/rest/api/compute"; export const LibraryType = "library"; export const TableType = "table"; @@ -56,5 +56,6 @@ export interface LibraryAdapter { items: LibraryItem[]; count: number; }>; + getTableInfo?(item: LibraryItem): Promise; setup(): Promise; } diff --git a/client/src/connection/itc/ItcLibraryAdapter.ts b/client/src/connection/itc/ItcLibraryAdapter.ts index 6739672fa..96ae048ef 100644 --- a/client/src/connection/itc/ItcLibraryAdapter.ts +++ b/client/src/connection/itc/ItcLibraryAdapter.ts @@ -11,7 +11,7 @@ import { TableData, TableRow, } from "../../components/LibraryNavigator/types"; -import { ColumnCollection } from "../rest/api/compute"; +import { ColumnCollection, TableInfo } from "../rest/api/compute"; import { getColumnIconType } from "../util"; import { executeRawCode, runCode } from "./CodeRunner"; import { Config } from "./types"; @@ -196,6 +196,28 @@ class ItcLibraryAdapter implements LibraryAdapter { } } + public async getTableInfo(item: LibraryItem): Promise { + const code = ` + $runner.GetTableInfo("${item.library}", "${item.name}") + `; + const output = await executeRawCode(code); + const tableInfo = JSON.parse(output); + + return { + name: tableInfo.name, + type: tableInfo.type, + creationTimeStamp: tableInfo.creationTimeStamp, + modifiedTimeStamp: tableInfo.modifiedTimeStamp, + rowCount: tableInfo.rowCount, + columnCount: tableInfo.columnCount, + compressionRoutine: tableInfo.compressionRoutine, + label: tableInfo.label, + engine: tableInfo.engine, + extendedType: tableInfo.extendedType, + libref: tableInfo.libref, + }; + } + protected async executionHandler( callback: () => Promise, ): Promise { diff --git a/client/src/connection/itc/script.ts b/client/src/connection/itc/script.ts index 1df0e64d7..e98124a43 100644 --- a/client/src/connection/itc/script.ts +++ b/client/src/connection/itc/script.ts @@ -580,6 +580,44 @@ class SASRunner{ Write-Host $(ConvertTo-Json -Depth 10 -InputObject $result -Compress) } + [void]GetTableInfo([string]$libname, [string]$memname) { + $objRecordSet = New-Object -comobject ADODB.Recordset + $objRecordSet.ActiveConnection = $this.dataConnection + $query = @" + select memname, memtype, crdate, modate, nobs, nvar, compress, + memlabel, engine, typemem, filesize, delobs + from sashelp.vtable + where libname='$libname' and memname='$memname'; +"@ + $objRecordSet.Open( + $query, + [System.Reflection.Missing]::Value, # Use the active connection + 2, # adOpenDynamic + 1, # adLockReadOnly + 1 # adCmdText + ) + + $result = New-Object psobject + if (-not $objRecordSet.EOF) { + $result | Add-Member -MemberType NoteProperty -Name "name" -Value $objRecordSet.Fields.Item(0).Value + $result | Add-Member -MemberType NoteProperty -Name "type" -Value $objRecordSet.Fields.Item(1).Value + $result | Add-Member -MemberType NoteProperty -Name "creationTimeStamp" -Value $objRecordSet.Fields.Item(2).Value + $result | Add-Member -MemberType NoteProperty -Name "modifiedTimeStamp" -Value $objRecordSet.Fields.Item(3).Value + $result | Add-Member -MemberType NoteProperty -Name "rowCount" -Value $objRecordSet.Fields.Item(4).Value + $result | Add-Member -MemberType NoteProperty -Name "columnCount" -Value $objRecordSet.Fields.Item(5).Value + $result | Add-Member -MemberType NoteProperty -Name "compressionRoutine" -Value $objRecordSet.Fields.Item(6).Value + $result | Add-Member -MemberType NoteProperty -Name "label" -Value $objRecordSet.Fields.Item(7).Value + $result | Add-Member -MemberType NoteProperty -Name "engine" -Value $objRecordSet.Fields.Item(8).Value + $result | Add-Member -MemberType NoteProperty -Name "extendedType" -Value $objRecordSet.Fields.Item(9).Value + $result | Add-Member -MemberType NoteProperty -Name "fileSize" -Value $objRecordSet.Fields.Item(10).Value + $result | Add-Member -MemberType NoteProperty -Name "deletedObs" -Value $objRecordSet.Fields.Item(11).Value + $result | Add-Member -MemberType NoteProperty -Name "libref" -Value $libname + } + $objRecordSet.Close() + + Write-Host $(ConvertTo-Json -Depth 10 -InputObject $result -Compress) + } + [void]GetTables([string]$libname) { $objRecordSet = New-Object -comobject ADODB.Recordset $objRecordSet.ActiveConnection = $this.dataConnection diff --git a/client/src/connection/rest/RestLibraryAdapter.ts b/client/src/connection/rest/RestLibraryAdapter.ts index b29b065a9..26f24dbb3 100644 --- a/client/src/connection/rest/RestLibraryAdapter.ts +++ b/client/src/connection/rest/RestLibraryAdapter.ts @@ -9,7 +9,12 @@ import { TableData, } from "../../components/LibraryNavigator/types"; import { appendSessionLogFn } from "../../components/logViewer"; -import { ColumnCollection, DataAccessApi, RowCollection } from "./api/compute"; +import { + ColumnCollection, + DataAccessApi, + RowCollection, + TableInfo, +} from "./api/compute"; import { getApiConfig } from "./common"; const requestOptions = { @@ -137,6 +142,23 @@ class RestLibraryAdapter implements LibraryAdapter { return { rowCount: response.data.rowCount, maxNumberOfRowsToRead: 1000 }; } + public async getTableInfo(item: LibraryItem): Promise { + await this.setup(); + const response = await this.retryOnFail( + async () => + await this.dataAccessApi.getTable( + { + sessionId: this.sessionId, + libref: item.library || "", + tableName: item.name, + }, + { headers: { Accept: "application/json" } }, + ), + ); + + return response.data; + } + private async retryOnFail( callbackFn: () => Promise>, ): Promise> { diff --git a/client/src/panels/TablePropertiesViewer.ts b/client/src/panels/TablePropertiesViewer.ts new file mode 100644 index 000000000..c0a4ed2bd --- /dev/null +++ b/client/src/panels/TablePropertiesViewer.ts @@ -0,0 +1,310 @@ +// Copyright © 2024, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { Uri } from "vscode"; + +import { Column } from "../connection/rest/api/compute"; +import { TableInfo } from "../connection/rest/api/compute"; +import { WebView } from "./WebviewManager"; + +class TablePropertiesViewer extends WebView { + constructor( + private readonly extensionUri: Uri, + private readonly tableName: string, + private readonly tableInfo: TableInfo, + private readonly columns: Column[], + private readonly showColumns: boolean = false, + ) { + super(); + } + + public render(): WebView { + this.panel.webview.html = this.getContent(); + return this; + } + + public processMessage(): void { + // No messages to process for this static viewer + } + + public getContent(): string { + return ` + + + + + + Table ${this.showColumns ? "Columns" : "Properties"} + + + +
+

Table: ${this.tableName}

+ +
+
+ General +
+
+ Columns +
+
+ +
+ ${this.generatePropertiesContent()} +
+ +
+ ${this.generateColumnsContent()} +
+
+ + + + + `; + } + + private generatePropertiesContent(): string { + const formatValue = (value: unknown): string => { + if (value === null || value === undefined) { + return ""; + } + if (typeof value === "number") { + return value.toLocaleString(); + } + return String(value); + }; + + const formatDate = (value: unknown): string => { + if (!value) { + return ""; + } + try { + return new Date(String(value)).toLocaleString(); + } catch { + return String(value); + } + }; + + return ` +
General Information
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Name${formatValue(this.tableInfo.name)}
Library${formatValue(this.tableInfo.libref)}
Type${formatValue(this.tableInfo.type)}
Label${formatValue(this.tableInfo.label)}
Engine${formatValue(this.tableInfo.engine)}
Extended Type${formatValue(this.tableInfo.extendedType)}
+ +
Size Information
+ + + + + + + + + + + + + + + + + + + + + +
Number of Rows${formatValue(this.tableInfo.rowCount)}
Number of Columns${formatValue(this.tableInfo.columnCount)}
Logical Record Count${formatValue(this.tableInfo.logicalRecordCount)}
Physical Record Count${formatValue(this.tableInfo.physicalRecordCount)}
Record Length${formatValue(this.tableInfo.recordLength)}
+ +
Technical Information
+ + + + + + + + + + + + + + + + + + + + + +
Created${formatDate(this.tableInfo.creationTimeStamp)}
Modified${formatDate(this.tableInfo.modifiedTimeStamp)}
Compression${formatValue(this.tableInfo.compressionRoutine)}
Character Encoding${formatValue(this.tableInfo.encoding)}
Bookmark Length${formatValue(this.tableInfo.bookmarkLength)}
+ `; + } + + private generateColumnsContent(): string { + const formatValue = (value: unknown): string => { + if (value === null || value === undefined) { + return ""; + } + return String(value); + }; + + const columnsRows = this.columns + .map( + (column, index) => ` + + ${index + 1} + ${formatValue(column.name)} + ${formatValue(column.type)} + ${formatValue(column.length)} + ${formatValue(column.format?.name)} + ${formatValue(column.informat?.name)} + ${formatValue(column.label)} + + `, + ) + .join(""); + + return ` +
Columns (${this.columns.length})
+ + + + + + + + + + + + + + ${columnsRows} + +
#NameTypeLengthFormatInformatLabel
+ `; + } +} + +export default TablePropertiesViewer; diff --git a/package.json b/package.json index e2c547eec..dc9e9878f 100644 --- a/package.json +++ b/package.json @@ -814,6 +814,16 @@ "title": "%commands.SAS.download%", "category": "SAS" }, + { + "command": "SAS.showTableProperties", + "title": "%commands.SAS.showTableProperties%", + "category": "SAS" + }, + { + "command": "SAS.showTableColumns", + "title": "%commands.SAS.showTableColumns%", + "category": "SAS" + }, { "command": "SAS.notebook.new", "shortTitle": "%commands.SAS.notebook.new.short%", @@ -932,6 +942,16 @@ "when": "viewItem =~ /table-/ && view == librarydataprovider", "group": "download@0" }, + { + "command": "SAS.showTableProperties", + "when": "viewItem =~ /table-/ && view == librarydataprovider", + "group": "properties@0" + }, + { + "command": "SAS.showTableColumns", + "when": "viewItem =~ /table-/ && view == librarydataprovider", + "group": "properties@1" + }, { "command": "SAS.content.addFolderResource", "when": "viewItem =~ /createChild/ && view == contentdataprovider", @@ -1181,6 +1201,14 @@ "when": "false", "command": "SAS.downloadTable" }, + { + "when": "false", + "command": "SAS.showTableProperties" + }, + { + "when": "false", + "command": "SAS.showTableColumns" + }, { "when": "false", "command": "SAS.content.downloadResource" diff --git a/package.nls.json b/package.nls.json index 5c199acc7..b3d1b36d3 100644 --- a/package.nls.json +++ b/package.nls.json @@ -12,6 +12,8 @@ "commands.SAS.deleteResource": "Delete", "commands.SAS.deleteTable": "Delete", "commands.SAS.download": "Download", + "commands.SAS.showTableProperties": "Properties", + "commands.SAS.showTableColumns": "Columns", "commands.SAS.emptyRecycleBin": "Empty Recycle Bin", "commands.SAS.file.new": "New SAS File", "commands.SAS.file.new.short": "SAS File", From 84130a2a80669714192b61b4db9cd7fc6cd3a814 Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Fri, 12 Sep 2025 16:24:39 -0400 Subject: [PATCH 2/5] feat(LibraryNavigator): added ITC Support Signed-off-by: Kishan Patel --- .../src/components/LibraryNavigator/index.ts | 24 ------ .../src/connection/itc/ItcLibraryAdapter.ts | 80 ++++++++++++------- client/src/connection/itc/script.ts | 17 ++-- client/src/panels/TablePropertiesViewer.ts | 50 ++++++------ package.json | 14 ---- package.nls.json | 1 - 6 files changed, 84 insertions(+), 102 deletions(-) diff --git a/client/src/components/LibraryNavigator/index.ts b/client/src/components/LibraryNavigator/index.ts index 1e2ecdfa9..c7ad4dac4 100644 --- a/client/src/components/LibraryNavigator/index.ts +++ b/client/src/components/LibraryNavigator/index.ts @@ -126,30 +126,6 @@ class LibraryNavigator implements SubscriptionProvider { } }, ), - commands.registerCommand( - "SAS.showTableColumns", - async (item: LibraryItem) => { - try { - const tableInfo = await this.libraryDataProvider.getTableInfo(item); - const columns = await this.libraryDataProvider.fetchColumns(item); - - this.webviewManager.render( - new TablePropertiesViewer( - this.extensionUri, - item.uid, - tableInfo, - columns, - true, // Show columns tab - ), - `columns-${item.uid}`, - ); - } catch (error) { - window.showErrorMessage( - `Failed to load table columns: ${error.message}`, - ); - } - }, - ), commands.registerCommand("SAS.collapseAllLibraries", () => { commands.executeCommand( "workbench.actions.treeView.librarydataprovider.collapseAll", diff --git a/client/src/connection/itc/ItcLibraryAdapter.ts b/client/src/connection/itc/ItcLibraryAdapter.ts index 96ae048ef..429fce995 100644 --- a/client/src/connection/itc/ItcLibraryAdapter.ts +++ b/client/src/connection/itc/ItcLibraryAdapter.ts @@ -50,7 +50,8 @@ class ItcLibraryAdapter implements LibraryAdapter { $runner.GetColumns("${item.library}", "${item.name}") `; const output = await executeRawCode(code); - const columns = JSON.parse(output).map((column) => ({ + const rawColumns = JSON.parse(output); + const columns = rawColumns.map((column) => ({ ...column, type: getColumnIconType(column), })); @@ -114,20 +115,8 @@ class ItcLibraryAdapter implements LibraryAdapter { start: number, limit: number, ): Promise { - // We only need the columns for the first page of results - const columns = - start === 0 - ? { - columns: ["INDEX"].concat( - (await this.getColumns(item)).items.map((column) => column.name), - ), - } - : {}; - const { rows } = await this.getRows(item, start, limit); - rows.unshift(columns); - // Fetching csv doesn't rely on count. Instead, we get the count // upfront via getTableRowCount return { rows, count: -1 }; @@ -197,25 +186,54 @@ class ItcLibraryAdapter implements LibraryAdapter { } public async getTableInfo(item: LibraryItem): Promise { - const code = ` - $runner.GetTableInfo("${item.library}", "${item.name}") - `; - const output = await executeRawCode(code); - const tableInfo = JSON.parse(output); + try { + // Use the PowerShell GetTableInfo function which queries sashelp.vtable + const code = ` + $runner.GetTableInfo("${item.library}", "${item.name}") + `; + const output = await executeRawCode(code); + const tableInfo = JSON.parse(output); - return { - name: tableInfo.name, - type: tableInfo.type, - creationTimeStamp: tableInfo.creationTimeStamp, - modifiedTimeStamp: tableInfo.modifiedTimeStamp, - rowCount: tableInfo.rowCount, - columnCount: tableInfo.columnCount, - compressionRoutine: tableInfo.compressionRoutine, - label: tableInfo.label, - engine: tableInfo.engine, - extendedType: tableInfo.extendedType, - libref: tableInfo.libref, - }; + return { + name: tableInfo.name || item.name, + libref: tableInfo.libref || item.library, + type: tableInfo.type || "DATA", + label: tableInfo.label || "", + engine: "", // Not available in sashelp.vtable for SAS 9.4 + extendedType: tableInfo.extendedType || "", + rowCount: tableInfo.rowCount || 0, + columnCount: tableInfo.columnCount || 0, + logicalRecordCount: tableInfo.rowCount || 0, + physicalRecordCount: tableInfo.rowCount || 0, + recordLength: 0, // Not available in vtable + bookmarkLength: 0, // Not available in vtable + compressionRoutine: tableInfo.compressionRoutine || "", + encoding: "", // Not available in vtable + creationTimeStamp: tableInfo.creationTimeStamp || "", + modifiedTimeStamp: tableInfo.modifiedTimeStamp || "", + }; + } catch (error) { + console.warn("Failed to get table info:", error); + // If anything fails, return basic info + return { + name: item.name, + libref: item.library, + type: "DATA", + label: "", + engine: "", + extendedType: "", + rowCount: 0, + columnCount: 0, + logicalRecordCount: 0, + physicalRecordCount: 0, + recordLength: 0, + bookmarkLength: 0, + compressionRoutine: "", + encoding: "", + creationTimeStamp: "", + modifiedTimeStamp: "", + }; + } } protected async executionHandler( diff --git a/client/src/connection/itc/script.ts b/client/src/connection/itc/script.ts index e98124a43..420941f44 100644 --- a/client/src/connection/itc/script.ts +++ b/client/src/connection/itc/script.ts @@ -295,9 +295,10 @@ class SASRunner{ $objRecordSet = New-Object -comobject ADODB.Recordset $objRecordSet.ActiveConnection = $this.dataConnection $query = @" - select name, type, format + select name, type, format, label, length, varnum from sashelp.vcolumn - where libname='$libname' and memname='$memname'; + where libname='$libname' and memname='$memname' + order by varnum; "@ $objRecordSet.Open( $query, @@ -318,6 +319,9 @@ class SASRunner{ name = $rows[0, $i] type = $rows[1, $i] format = $rows[2, $i] + label = $rows[3, $i] + length = $rows[4, $i] + varnum = $rows[5, $i] } $parsedRows += $parsedRow } @@ -585,7 +589,7 @@ class SASRunner{ $objRecordSet.ActiveConnection = $this.dataConnection $query = @" select memname, memtype, crdate, modate, nobs, nvar, compress, - memlabel, engine, typemem, filesize, delobs + memlabel, typemem, filesize, delobs from sashelp.vtable where libname='$libname' and memname='$memname'; "@ @@ -607,10 +611,9 @@ class SASRunner{ $result | Add-Member -MemberType NoteProperty -Name "columnCount" -Value $objRecordSet.Fields.Item(5).Value $result | Add-Member -MemberType NoteProperty -Name "compressionRoutine" -Value $objRecordSet.Fields.Item(6).Value $result | Add-Member -MemberType NoteProperty -Name "label" -Value $objRecordSet.Fields.Item(7).Value - $result | Add-Member -MemberType NoteProperty -Name "engine" -Value $objRecordSet.Fields.Item(8).Value - $result | Add-Member -MemberType NoteProperty -Name "extendedType" -Value $objRecordSet.Fields.Item(9).Value - $result | Add-Member -MemberType NoteProperty -Name "fileSize" -Value $objRecordSet.Fields.Item(10).Value - $result | Add-Member -MemberType NoteProperty -Name "deletedObs" -Value $objRecordSet.Fields.Item(11).Value + $result | Add-Member -MemberType NoteProperty -Name "extendedType" -Value $objRecordSet.Fields.Item(8).Value + $result | Add-Member -MemberType NoteProperty -Name "fileSize" -Value $objRecordSet.Fields.Item(9).Value + $result | Add-Member -MemberType NoteProperty -Name "deletedObs" -Value $objRecordSet.Fields.Item(10).Value $result | Add-Member -MemberType NoteProperty -Name "libref" -Value $libname } $objRecordSet.Close() diff --git a/client/src/panels/TablePropertiesViewer.ts b/client/src/panels/TablePropertiesViewer.ts index c0a4ed2bd..ec3e7737a 100644 --- a/client/src/panels/TablePropertiesViewer.ts +++ b/client/src/panels/TablePropertiesViewer.ts @@ -33,7 +33,7 @@ class TablePropertiesViewer extends WebView { - Table ${this.showColumns ? "Columns" : "Properties"} + Table Properties + + + ${l10n.t("Table Properties")}
-

Table: ${this.tableName}

+

${l10n.t("Table: {tableName}", { tableName: this.tableName })}

- - + +
@@ -138,23 +67,10 @@ class TablePropertiesViewer extends WebView {
- + `; @@ -183,78 +99,78 @@ class TablePropertiesViewer extends WebView { }; return ` -
General Information
+
${l10n.t("General Information")}
- + - + - + - + - + - +
Name${l10n.t("Name")} ${formatValue(this.tableInfo.name)}
Library${l10n.t("Library")} ${formatValue(this.tableInfo.libref)}
Type${l10n.t("Type")} ${formatValue(this.tableInfo.type)}
Label${l10n.t("Label")} ${formatValue(this.tableInfo.label)}
Engine${l10n.t("Engine")} ${formatValue(this.tableInfo.engine)}
Extended Type${l10n.t("Extended Type")} ${formatValue(this.tableInfo.extendedType)}
-
Size Information
+
${l10n.t("Size Information")}
- + - + - + - + - +
Number of Rows${l10n.t("Number of Rows")} ${formatValue(this.tableInfo.rowCount)}
Number of Columns${l10n.t("Number of Columns")} ${formatValue(this.tableInfo.columnCount)}
Logical Record Count${l10n.t("Logical Record Count")} ${formatValue(this.tableInfo.logicalRecordCount)}
Physical Record Count${l10n.t("Physical Record Count")} ${formatValue(this.tableInfo.physicalRecordCount)}
Record Length${l10n.t("Record Length")} ${formatValue(this.tableInfo.recordLength)}
-
Technical Information
+
${l10n.t("Technical Information")}
- + - + - + - + - +
Created${l10n.t("Created")} ${formatDate(this.tableInfo.creationTimeStamp)}
Modified${l10n.t("Modified")} ${formatDate(this.tableInfo.modifiedTimeStamp)}
Compression${l10n.t("Compression")} ${formatValue(this.tableInfo.compressionRoutine)}
Character Encoding${l10n.t("Character Encoding")} ${formatValue(this.tableInfo.encoding)}
Bookmark Length${l10n.t("Bookmark Length")} ${formatValue(this.tableInfo.bookmarkLength)}
@@ -286,17 +202,17 @@ class TablePropertiesViewer extends WebView { .join(""); return ` -
Columns (${this.columns.length})
+
${l10n.t("Columns ({count})", { count: this.columns.length })}
- - - - - - - + + + + + + + diff --git a/client/src/webview/TablePropertiesViewer.css b/client/src/webview/TablePropertiesViewer.css new file mode 100644 index 000000000..611a0511e --- /dev/null +++ b/client/src/webview/TablePropertiesViewer.css @@ -0,0 +1,83 @@ +body { + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); + font-weight: var(--vscode-font-weight); + color: var(--vscode-foreground); + background-color: var(--vscode-editor-background); + margin: 0; + padding: 16px; +} + +.container { + max-width: 1200px; + margin: 0 auto; +} + +.tabs { + display: flex; + border-bottom: 1px solid var(--vscode-panel-border); + margin-bottom: 16px; +} + +.tab { + padding: 10px 20px; + cursor: pointer; + border: none; + background: none; + color: var(--vscode-foreground); + border-bottom: 2px solid transparent; + font-family: inherit; + font-size: inherit; +} + +.tab:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.tab.active { + border-bottom-color: var(--vscode-focusBorder); + color: var(--vscode-tab-activeForeground); +} + +.tab-content { + display: none; + padding: 20px 0; +} + +.tab-content.active { + display: block; +} + +.properties-table { + width: 100%; + border-collapse: collapse; + margin-bottom: 16px; +} + +.properties-table th, +.properties-table td { + border: 1px solid var(--vscode-panel-border); + padding: 8px 12px; + text-align: left; +} + +.properties-table th { + background-color: var(--vscode-list-hoverBackground); + font-weight: bold; +} + +.properties-table tr:nth-child(even) { + background-color: var(--vscode-list-hoverBackground); +} + +.property-label { + font-weight: bold; + min-width: 200px; +} + +.section-title { + font-size: 1.2em; + font-weight: bold; + margin: 20px 0 10px 0; + color: var(--vscode-foreground); +} diff --git a/client/src/webview/TablePropertiesViewer.ts b/client/src/webview/TablePropertiesViewer.ts new file mode 100644 index 000000000..dc7acc759 --- /dev/null +++ b/client/src/webview/TablePropertiesViewer.ts @@ -0,0 +1,37 @@ +// Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import "./TablePropertiesViewer.css"; + +function showTab(tabName: string, clickedTab?: HTMLElement): void { + const contents = document.querySelectorAll(".tab-content"); + contents.forEach((content) => content.classList.remove("active")); + + const tabs = document.querySelectorAll(".tab"); + tabs.forEach((tab) => tab.classList.remove("active")); + + const selectedContent = document.getElementById(tabName); + if (selectedContent) { + selectedContent.classList.add("active"); + } + + if (clickedTab) { + clickedTab.classList.add("active"); + } +} + +document.addEventListener("DOMContentLoaded", () => { + const tabButtons = document.querySelectorAll(".tab"); + tabButtons.forEach((button) => { + button.addEventListener("click", (event) => { + const target = event.currentTarget; + if (target instanceof HTMLElement) { + const tabName = target.getAttribute("data-tab"); + if (tabName) { + showTab(tabName, target); + } + } + }); + }); +}); + +export {}; diff --git a/tools/build.mjs b/tools/build.mjs index 65b35f30d..025784f64 100644 --- a/tools/build.mjs +++ b/tools/build.mjs @@ -49,6 +49,8 @@ const browserBuildOptions = { format: "esm", entryPoints: { "./client/dist/webview/DataViewer": "./client/src/webview/DataViewer.tsx", + "./client/dist/webview/TablePropertiesViewer": + "./client/src/webview/TablePropertiesViewer.ts", "./client/dist/notebook/LogRenderer": "./client/src/components/notebook/renderers/LogRenderer.ts", "./client/dist/notebook/HTMLRenderer":
#NameTypeLengthFormatInformatLabel${l10n.t("#")}${l10n.t("Name")}${l10n.t("Type")}${l10n.t("Length")}${l10n.t("Format")}${l10n.t("Informat")}${l10n.t("Label")}