Skip to content

Commit 2e53f16

Browse files
authored
feat: implement sas file system for IOM/COM connections (#1388)
**Summary** This adds sas file system support for sas9 file system for IOM/COM connection types. Additionally, it includes a small refactor to the way interop libraries are loaded. Loading them at SASRunner construction occurs too late, and doesn't provide the type-checking necessary for file service. **Testing** - [x] Test entering incorrect password **(note: an issue remains here that is present on main. You can't attempt to re-log in if you've entered a wrong password unless you reload window)** - [x] File/folder creation - [x] Create file/folder w/ context menu - [x] Create file/folder by upload - [x] Create file/folder by drag & drop (create multiple files) - [x] File/folder deletion - [x] Test file deletion with context menu - [x] Test multi-file deletion with context menu - [x] File/folder updates - [x] Test updating file/folder name - [x] Test updating file contents - [x] Test moving file/folder (multiple files/folders) - [x] Test downloading files/folders - [x] Make sure refresh works as expected - [x] Make sure connections are automatically refreshed after they become stale - [x] Make sure we're displaying all files/folders for sas server and that items are sorted by type (directories before files), then alphabetically - [x] Make sure we can collapse all folders - [x] Cases to test from previous rest implementation - [x] Create folder with special character, like `/` - [x] Create folder in same spot as folder with same name - [x] Create a .txt file named as `NewFile+!@$%^&*.txt` **(NOTE: File is not created in this instance. sas9's file service rejects this file name)** - [x] Check multi-selection behavior to make sure things work as expected - [x] Rename file (and folder) as same name as existing file - [x] Create file in folder, keep file open, delete folder (make sure file goes away) - [x] Create a file named `!@$%^&*(.txt`, attempt to download it **(NOTE: File is not created in this instance. sas9's file service rejects this file name)** - [x] Try dropping a file into a folder where there's already a file sharing the same name - [x] Try renaming an open file
1 parent bc3b38d commit 2e53f16

File tree

20 files changed

+825
-80
lines changed

20 files changed

+825
-80
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). If you introduce breaking changes, please group them together in the "Changed" section using the **BREAKING:** prefix.
66

7+
## [Unreleased]
8+
9+
### Added
10+
11+
- Add sas file system support for ITC-based (IOM/COM) connections ([#1388](https://github.com/sassoftware/vscode-sas-extension/pull/1388))
12+
713
## [v1.14.0] - 2025-04-28
814

915
### Added

client/src/commands/authorize.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const checkProfileAndAuthorize =
4141
case ConnectionType.IOM:
4242
case ConnectionType.COM:
4343
commands.executeCommand("setContext", "SAS.librariesDisplayed", true);
44+
commands.executeCommand("setContext", "SAS.serverDisplayed", true);
4445
libraryNavigator.refresh();
4546
return finishAuthorization(profileConfig);
4647
default:

client/src/commands/run.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ const isErrorRep = (err: unknown): err is ErrorRepresentation => {
273273

274274
export const onRunError = (err) => {
275275
console.dir(err);
276+
commands.executeCommand("setContext", "SAS.librariesDisplayed", false);
277+
commands.executeCommand("setContext", "SAS.serverDisplayed", false);
276278

277279
if (err.response) {
278280
// The request was made and we got a status code that falls out side of the 2xx range

client/src/components/ContentNavigator/ContentAdapterFactory.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright © 2024, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
3+
import ITCSASServerAdapter from "../../connection/itc/ITCSASServerAdapter";
34
import RestSASServerAdapter from "../../connection/rest/RestSASServerAdapter";
45
import SASContentAdapter from "../../connection/rest/SASContentAdapter";
56
import { ConnectionType } from "../profile";
@@ -10,7 +11,6 @@ import {
1011
} from "./types";
1112

1213
class ContentAdapterFactory {
13-
// TODO #889 Update this to return ITCSASServerAdapter
1414
public create(
1515
connectionType: ConnectionType,
1616
sourceType: ContentNavigatorConfig["sourceType"],
@@ -19,6 +19,9 @@ class ContentAdapterFactory {
1919
switch (key) {
2020
case `${ConnectionType.Rest}.${ContentSourceType.SASServer}`:
2121
return new RestSASServerAdapter();
22+
case `${ConnectionType.IOM}.${ContentSourceType.SASServer}`:
23+
case `${ConnectionType.COM}.${ContentSourceType.SASServer}`:
24+
return new ITCSASServerAdapter();
2225
case `${ConnectionType.Rest}.${ContentSourceType.SASContent}`:
2326
default:
2427
return new SASContentAdapter();

client/src/components/ContentNavigator/ContentDataProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ class ContentDataProvider
585585
} else if (target.type === FAVORITES_FOLDER_TYPE) {
586586
success = await this.addToMyFavorites(item);
587587
} else {
588-
const targetUri = target.resourceId;
588+
const targetUri = target.resourceId ?? target.uri;
589589
success = await this.moveItem(item, targetUri);
590590
if (success) {
591591
this.refresh();

client/src/components/ContentNavigator/utils.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import {
99
window,
1010
} from "vscode";
1111

12+
import { resourceType } from "../../connection/rest/util";
1213
import { DEFAULT_FILE_CONTENT_TYPE } from "./const";
1314
import mimeTypes from "./mime-types";
14-
import { ContentItem } from "./types";
15+
import { ContentItem, Permission } from "./types";
1516

1617
export const isContainer = (item: ContentItem): boolean =>
1718
item.fileStat.type === FileType.Directory;
@@ -84,6 +85,28 @@ export const createStaticFolder = (
8485
],
8586
});
8687

88+
export const convertStaticFolderToContentItem = (
89+
staticFolder: ReturnType<typeof createStaticFolder>,
90+
permission: Permission,
91+
): ContentItem => {
92+
const item: ContentItem = {
93+
...staticFolder,
94+
uid: staticFolder.id,
95+
creationTimeStamp: 0,
96+
modifiedTimeStamp: 0,
97+
permission,
98+
fileStat: {
99+
ctime: 0,
100+
mtime: 0,
101+
size: 0,
102+
type: FileType.Directory,
103+
},
104+
};
105+
item.contextValue = resourceType(item);
106+
item.typeName = staticFolder.type;
107+
return item;
108+
};
109+
87110
export const getEditorTabsForItem = (item: ContentItem) => {
88111
const fileUri = item.vscUri;
89112
const tabs: Tab[] = window.tabGroups.all.map((tg) => tg.tabs).flat();
@@ -94,3 +117,16 @@ export const getEditorTabsForItem = (item: ContentItem) => {
94117
tab.input.uri.query.includes(fileUri.query), // compare the file id
95118
);
96119
};
120+
121+
export const sortedContentItems = (items: ContentItem[]) =>
122+
items.sort((a, b) => {
123+
const aIsDirectory = a.fileStat?.type === FileType.Directory;
124+
const bIsDirectory = b.fileStat?.type === FileType.Directory;
125+
if (aIsDirectory && !bIsDirectory) {
126+
return -1;
127+
} else if (!aIsDirectory && bIsDirectory) {
128+
return 1;
129+
} else {
130+
return a.name.localeCompare(b.name);
131+
}
132+
});

client/src/connection/itc/CodeRunner.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,55 @@
22
// SPDX-License-Identifier: Apache-2.0
33
import { commands } from "vscode";
44

5+
import { v4 } from "uuid";
6+
57
import { ITCSession } from ".";
68
import { LogLine, getSession } from "..";
79
import { useRunStore } from "../../store";
10+
import { Session } from "../session";
11+
import { extractTextBetweenTags } from "../util";
812

913
let wait: Promise<string> | undefined;
1014

15+
export async function executeRawCode(code: string): Promise<string> {
16+
const randomId = v4();
17+
const startTag = `<${randomId}>`;
18+
const endTag = `</${randomId}>`;
19+
const task = () =>
20+
_runCode(
21+
async (session) => {
22+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
23+
await (session as ITCSession).execute(
24+
`Write-Host "${startTag}"\n${code}\nWrite-Host "${endTag}"\n`,
25+
);
26+
},
27+
startTag,
28+
endTag,
29+
);
30+
31+
wait = wait ? wait.then(task) : task();
32+
return wait;
33+
}
34+
1135
export async function runCode(
1236
code: string,
1337
startTag: string = "",
1438
endTag: string = "",
1539
): Promise<string> {
16-
const task = () => _runCode(code, startTag, endTag);
40+
const task = () =>
41+
_runCode(
42+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
43+
async (session) => await (session as ITCSession).run(code, true),
44+
startTag,
45+
endTag,
46+
);
1747

1848
wait = wait ? wait.then(task) : task();
1949
return wait;
2050
}
2151

2252
async function _runCode(
23-
code: string,
53+
runCallback: (session: Session) => void,
2454
startTag: string = "",
2555
endTag: string = "",
2656
): Promise<string> {
@@ -45,30 +75,20 @@ async function _runCode(
4575
const onExecutionLogFn = session.onExecutionLogFn;
4676
const outputLines = [];
4777

48-
const addLine = (logLines: LogLine[]) =>
78+
const addLine = (logLines: LogLine[]) => {
4979
outputLines.push(...logLines.map(({ line }) => line));
80+
};
5081

5182
try {
5283
await session.setup(true);
5384

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

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

6090
const logOutput = outputLines.filter((line) => line.trim()).join("");
61-
62-
logText =
63-
startTag && endTag
64-
? logOutput
65-
.slice(
66-
logOutput.lastIndexOf(startTag),
67-
logOutput.lastIndexOf(endTag),
68-
)
69-
.replace(startTag, "")
70-
.replace(endTag, "")
71-
: logOutput;
91+
logText = extractTextBetweenTags(logOutput, startTag, endTag);
7292
} finally {
7393
unsubscribe && unsubscribe();
7494
// Lets update our session to write to the log

0 commit comments

Comments
 (0)