Skip to content

Table Viewer optimization in ITC #1466

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
176ab4b
Add changes from file explorer feature.
DanilaGrobovSEB Apr 13, 2025
c12016f
Use data connection instead of code execution.
DanilaGrobovSEB May 1, 2025
9d1aff6
Improve columns loading time.
DanilaGrobovSEB May 3, 2025
328b22d
Add column type mapping to the format.
DanilaGrobovSEB May 3, 2025
70069a6
Merge branch 'main' into table_viewer_optimization
danila-grobov May 3, 2025
179fcb7
DCO Remediation Commit for Danila Grobov (s4642g) <danila.grobov@seb.se>
DanilaGrobovSEB May 3, 2025
16eedec
Support empty arrays in json
DanilaGrobovSEB May 3, 2025
2ab7999
Fix incorrect label.
DanilaGrobovSEB May 6, 2025
e78ff54
Remove unnecessary parameters in data connection
DanilaGrobovSEB May 6, 2025
5383d38
Merge branch 'main' into table_viewer_optimization
danila-grobov May 6, 2025
ed6a9ed
Merge branch 'main' into table_viewer_optimization
danila-grobov May 8, 2025
cd0ef99
Fix tests
DanilaGrobovSEB May 11, 2025
019bd3d
Use proxyquire for mocking uuid.
danila-grobov May 12, 2025
d39ce5d
Merge remote-tracking branch 'upstream/main' into table_viewer_optimi…
danila-grobov May 12, 2025
bbcc56d
Merge branch 'main' into table_viewer_optimization
danila-grobov May 13, 2025
192c22f
Fix linting problems.
danila-grobov May 13, 2025
7ec2b55
Merge remote-tracking branch 'upstream/main' into table_viewer_optimi…
DanilaGrobovSEB May 25, 2025
7e4d8a2
Fix package-lock
danila-grobov May 25, 2025
b5785aa
Bring the file explorer changes back.
DanilaGrobovSEB May 25, 2025
207d325
DCO Remediation Commit for Danila Grobov <danila.grob@gmail.com>
danila-grobov May 25, 2025
674c87e
DCO Remediation Commit for Danila Grobov (s4642g) <danila.grobov@seb.se>
DanilaGrobovSEB May 25, 2025
a23005e
Merge branch 'main' into table_viewer_optimization
danila-grobov May 28, 2025
9cfb1f6
Fix formatting issues.
danila-grobov May 28, 2025
ce94b23
Refactor getColumnIconType.
danila-grobov May 30, 2025
3e92c0d
Merge branch 'main' into table_viewer_optimization
danila-grobov May 30, 2025
7bca4b8
chore(deps): bump the docusaurus group in /website with 4 updates (#1…
dependabot[bot] Jun 3, 2025
bdf1181
chore(deps-dev): bump the dev group across 1 directory with 4 updates…
dependabot[bot] Jun 3, 2025
ea8e546
Merge remote-tracking branch 'upstream/main' into table_viewer_optimi…
DanilaGrobovSEB Jun 9, 2025
6260bdc
Fix formatting.
danila-grobov Jun 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 36 additions & 16 deletions client/src/connection/itc/CodeRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,55 @@
// SPDX-License-Identifier: Apache-2.0
import { commands } from "vscode";

import { v4 } from "uuid";

import { ITCSession } from ".";
import { LogLine, getSession } from "..";
import { useRunStore } from "../../store";
import { Session } from "../session";
import { extractTextBetweenTags } from "../util";

let wait: Promise<string> | undefined;

export async function executeRawCode(code: string): Promise<string> {
const randomId = v4();
const startTag = `<${randomId}>`;
const endTag = `</${randomId}>`;
const task = () =>
_runCode(
async (session) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
await (session as ITCSession).execute(
`Write-Host "${startTag}"\n${code}\nWrite-Host "${endTag}"\n`,
);
},
startTag,
endTag,
);

wait = wait ? wait.then(task) : task();
return wait;
}

export async function runCode(
code: string,
startTag: string = "",
endTag: string = "",
): Promise<string> {
const task = () => _runCode(code, startTag, endTag);
const task = () =>
_runCode(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
async (session) => await (session as ITCSession).run(code, true),
startTag,
endTag,
);

wait = wait ? wait.then(task) : task();
return wait;
}

async function _runCode(
code: string,
runCallback: (session: Session) => void,
startTag: string = "",
endTag: string = "",
): Promise<string> {
Expand All @@ -45,30 +75,20 @@ async function _runCode(
const onExecutionLogFn = session.onExecutionLogFn;
const outputLines = [];

const addLine = (logLines: LogLine[]) =>
const addLine = (logLines: LogLine[]) => {
outputLines.push(...logLines.map(({ line }) => line));
};

try {
await session.setup(true);

// Lets capture output to use it on
session.onExecutionLogFn = addLine;

// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
await (session as ITCSession).run(code, true);
await runCallback(session);

const logOutput = outputLines.filter((line) => line.trim()).join("");

logText =
startTag && endTag
? logOutput
.slice(
logOutput.lastIndexOf(startTag),
logOutput.lastIndexOf(endTag),
)
.replace(startTag, "")
.replace(endTag, "")
: logOutput;
logText = extractTextBetweenTags(logOutput, startTag, endTag);
} finally {
unsubscribe && unsubscribe();
// Lets update our session to write to the log
Expand Down
103 changes: 30 additions & 73 deletions client/src/connection/itc/ItcLibraryAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import {
TableData,
TableRow,
} from "../../components/LibraryNavigator/types";
import { Column, ColumnCollection } from "../rest/api/compute";
import { runCode } from "./CodeRunner";
import { ColumnCollection } from "../rest/api/compute";
import { getColumnIconType } from "../util";
import { executeRawCode, runCode } from "./CodeRunner";
import { Config } from "./types";

class ItcLibraryAdapter implements LibraryAdapter {
Expand Down Expand Up @@ -45,31 +46,14 @@ class ItcLibraryAdapter implements LibraryAdapter {
}

public async getColumns(item: LibraryItem): Promise<ColumnCollection> {
const sql = `
%let OUTPUT;
proc sql;
select catx(',', name, type, varnum) as column into: OUTPUT separated by '~'
from sashelp.vcolumn
where libname='${item.library}' and memname='${item.name}'
order by varnum;
quit;
%put <COLOUTPUT> &OUTPUT; %put </COLOUTPUT>;
const code = `
$runner.GetColumns("${item.library}", "${item.name}")
`;

const columnLines = processQueryRows(
await this.runCode(sql, "<COLOUTPUT>", "</COLOUTPUT>"),
);

const columns = columnLines.map((lineText): Column => {
const [name, type, index] = lineText.split(",");

return {
name,
type,
index: parseInt(index, 10),
};
});

const output = await executeRawCode(code);
const columns = JSON.parse(output).map((column) => ({
...column,
type: getColumnIconType(column),
}));
return {
items: columns,
count: -1,
Expand Down Expand Up @@ -213,48 +197,16 @@ class ItcLibraryAdapter implements LibraryAdapter {
start: number,
limit: number,
): Promise<{ rows: Array<string[]>; count: number }> {
const maxTableNameLength = 32;
const tempTable = `${item.name}${hms()}${start}`.substring(
0,
maxTableNameLength,
);
const fullTableName = `${item.library}.${item.name}`;
const code = `
options nonotes nosource nodate nonumber;
%let COUNT;
proc sql;
SELECT COUNT(1) into: COUNT FROM ${item.library}.${item.name};
quit;
data work.${tempTable};
set ${item.library}.${item.name};
if ${start + 1} <= _N_ <= ${start + limit} then output;
run;

filename out temp;
proc json nokeys out=out pretty; export work.${tempTable}; run;

%put <TABLEDATA>;
%put <Count>&COUNT</Count>;
data _null_; infile out; input; put _infile_; run;
%put </TABLEDATA>;
proc datasets library=work nolist nodetails; delete ${tempTable}; run;
options notes source date number;
$runner.GetDatasetRecords("${fullTableName}", ${start}, ${limit})
`;

let output = await this.runCode(code, "<TABLEDATA>", "</TABLEDATA>");

// Extract result count
const countRegex = /<Count>(.*)<\/Count>/;
const countMatches = output.match(countRegex);
const count = parseInt(countMatches[1].replace(/\s|\n/gm, ""), 10);
output = output.replace(countRegex, "");

const rows = output.replace(/\n|\t/gm, "").slice(output.indexOf("{"));
const output = await executeRawCode(code);
try {
const tableData = JSON.parse(rows);
return { rows: tableData[`SASTableData+${tempTable}`], count };
return JSON.parse(output);
} catch (e) {
console.warn("Failed to load table data with error", e);
console.warn("Raw output", rows);
console.warn("Raw output", output);
throw new Error(
l10n.t(
"An error was encountered when loading table data. This usually happens when a table is too large or the data couldn't be processed. See console for more details.",
Expand All @@ -263,19 +215,29 @@ class ItcLibraryAdapter implements LibraryAdapter {
}
}

protected async runCode(
code: string,
startTag: string = "",
endTag: string = "",
protected async executionHandler(
callback: () => Promise<string>,
): Promise<string> {
try {
return await runCode(code, startTag, endTag);
return await callback();
} catch (e) {
onRunError(e);
commands.executeCommand("setContext", "SAS.librariesDisplayed", false);
return "";
}
}

protected async runCode(
code: string,
startTag: string = "",
endTag: string = "",
): Promise<string> {
return this.executionHandler(() => runCode(code, startTag, endTag));
}

protected async executeRawCode(code: string): Promise<string> {
return this.executionHandler(() => executeRawCode(code));
}
}

const processQueryRows = (response: string): string[] => {
Expand All @@ -289,9 +251,4 @@ const processQueryRows = (response: string): string[] => {
.filter((value, index, array) => array.indexOf(value) === index);
};

const hms = () => {
const date = new Date();
return `${date.getHours()}${date.getMinutes()}${date.getSeconds()}`;
};

export default ItcLibraryAdapter;
31 changes: 28 additions & 3 deletions client/src/connection/itc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
WORK_DIR_END_TAG,
WORK_DIR_START_TAG,
} from "./const";
import { scriptContent } from "./script";
import { getScript } from "./script";
import { Config, ITCProtocol, LineCodes } from "./types";
import { decodeEntities, escapePowershellString } from "./util";

Expand Down Expand Up @@ -110,9 +110,16 @@ export class ITCSession extends Session {
);
this._shellProcess.stdout.on("data", this.onShellStdOut);
this._shellProcess.stderr.on("data", this.onShellStdErr);
this._shellProcess.stdin.write(scriptContent + "\n", this.onWriteComplete);
this._shellProcess.stdin.write(
`$runner = New-Object -TypeName SASRunner -ArgumentList "${escapePowershellString(interopLibraryFolderPath || "")}"\n`,
getScript({
interopLibraryFolderPath: escapePowershellString(
interopLibraryFolderPath || "",
),
}) + "\n",
this.onWriteComplete,
);
this._shellProcess.stdin.write(
`$runner = New-Object -TypeName SASRunner\n`,
this.onWriteComplete,
);

Expand Down Expand Up @@ -246,6 +253,24 @@ export class ITCSession extends Session {
return runPromise;
};

public execute = async (code: string): Promise<RunResult> => {
const runPromise = new Promise<RunResult>((resolve, reject) => {
this._runResolve = resolve;
this._runReject = reject;
});

this._html5FileName = "";
this._pollingForLogResults = true;
const codeToExecute = `${code}\nWrite-Host "${LineCodes.RunEndCode}"\n`;
this._shellProcess.stdin.write(codeToExecute, async (error) => {
if (error) {
this._runReject(error);
}
});

return runPromise;
};

/**
* Cleans up resources for the given SAS session.
* @returns void promise.
Expand Down
Loading
Loading