Skip to content

feat: implement sas file system for viya connections #1203

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 30 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3e08a4b
chore: stub RestSASServerAdapter
Aug 23, 2024
7ae63a0
chore: add ability to view/open files and folders
Aug 30, 2024
4d47e7d
feat: add ability to create files and folders
Sep 6, 2024
ae71405
feat: allow update/rename of files and folders
Sep 6, 2024
266ba45
chore: implement delete/change explorer -> sas content
Sep 13, 2024
b963716
chore: implement move/paginate results/add error checking
Sep 20, 2024
0247512
chore: implement upload/download/drag drop
Sep 20, 2024
321801e
chore: add comments for unimplemented code
Sep 23, 2024
a59d2fb
chore: update notebook to flow functionality
Sep 27, 2024
d6c9933
chore: fix copyright,tests
Sep 27, 2024
8d1a681
chore: disable recycle bin support for sas server
Sep 27, 2024
9e3dac8
chore: add icons
Sep 27, 2024
0d3da38
chore: adjust todos/comments
Oct 4, 2024
2287ef3
fix: fix move issue
Oct 4, 2024
bdcc4d6
fix: type errors
Oct 4, 2024
7809320
chore: fix mixed download files/folders
Oct 4, 2024
ddef030
chore: fix session issues
Oct 11, 2024
b9bb3a9
fix: fix collapse all
Oct 25, 2024
d406462
fix: fix sas content showing up in server
Oct 25, 2024
341be17
chore: add documentation
Oct 25, 2024
bafebe3
fix: fix file name parsing/folder errors
Oct 25, 2024
039f860
chore: update sidebar position
Oct 28, 2024
5aa6f97
chore: update folder validation
Oct 28, 2024
8b3e24c
fix: fix file creation error/notebook naming
Oct 28, 2024
d040a77
fix: folder validation error
Oct 29, 2024
5f8b69a
chore: address code review comments
Nov 8, 2024
7115d20
fix: fix remaining issues
Nov 11, 2024
afad7aa
chore: update test coverage
Nov 14, 2024
f93ced3
fix: fix rename issues
Nov 15, 2024
d7e2e1b
chore: account for semicolons in move paths
Nov 19, 2024
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

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.

## [Unreleased]

### Added

- Added support for SAS server for viya connections ([#1203](https://github.com/sassoftware/vscode-sas-extension/pull/1203))

## [v1.11.0] - 2024-10-09

### Added
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright © 2024, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import RestSASServerAdapter from "../../connection/rest/RestSASServerAdapter";
import SASContentAdapter from "../../connection/rest/SASContentAdapter";
import { ConnectionType } from "../profile";
import {
Expand All @@ -9,13 +10,15 @@ import {
} from "./types";

class ContentAdapterFactory {
// TODO #889 Update this to return RestSASServerAdapter & ITCSASServerAdapter
// TODO #889 Update this to return ITCSASServerAdapter
public create(
connectionType: ConnectionType,
sourceType: ContentNavigatorConfig["sourceType"],
): ContentAdapter {
const key = `${connectionType}.${sourceType}`;
switch (key) {
case `${ConnectionType.Rest}.${ContentSourceType.SASServer}`:
return new RestSASServerAdapter();
case `${ConnectionType.Rest}.${ContentSourceType.SASContent}`:
default:
return new SASContentAdapter();
Expand Down
148 changes: 120 additions & 28 deletions client/src/components/ContentNavigator/ContentDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
FileType,
Position,
ProviderResult,
Tab,
TabInputNotebook,
TabInputText,
TextDocument,
Expand Down Expand Up @@ -45,14 +44,20 @@ import {
FAVORITES_FOLDER_TYPE,
Messages,
ROOT_FOLDER_TYPE,
SERVER_HOME_FOLDER_TYPE,
SERVER_ROOT_FOLDER_TYPE,
TRASH_FOLDER_TYPE,
} from "./const";
import {
ContentItem,
ContentNavigatorConfig,
FileManipulationEvent,
} from "./types";
import { getFileStatement, isContainer as getIsContainer } from "./utils";
import {
getEditorTabsForItem,
getFileStatement,
isContainer as getIsContainer,
} from "./utils";

class ContentDataProvider
implements
Expand All @@ -68,7 +73,7 @@ class ContentDataProvider
private _onDidChange: EventEmitter<Uri>;
private _treeView: TreeView<ContentItem>;
private _dropEditProvider: Disposable;
private readonly model: ContentModel;
private model: ContentModel;
private extensionUri: Uri;
private mimeType: string;

Expand Down Expand Up @@ -114,6 +119,10 @@ class ContentDataProvider
});
}

public useModel(contentModel: ContentModel) {
this.model = contentModel;
}

public async handleDrop(
target: ContentItem,
sources: DataTransfer,
Expand Down Expand Up @@ -278,23 +287,68 @@ class ContentDataProvider
name: string,
): Promise<Uri | undefined> {
const closing = closeFileIfOpen(item);
if (!(await closing)) {
const removedTabUris = await closing;
if (!removedTabUris) {
return;
}

const newItem = await this.model.renameResource(item, name);
if (newItem) {
const newUri = newItem.vscUri;
if (closing !== true) {
// File was open before rename, so re-open it
commands.executeCommand("vscode.open", newUri);
if (!newItem) {
return;
}

const newUri = newItem.vscUri;
const oldUriToNewUriMap = [[item.vscUri, newUri]];
const newItemIsContainer = getIsContainer(newItem);
if (closing !== true && !newItemIsContainer) {
await commands.executeCommand("vscode.openWith", newUri, "default", {
preview: false,
});
}
if (closing !== true && newItemIsContainer) {
const urisToOpen = getPreviouslyOpenedChildItems(
await this.getChildren(newItem),
);
for (const [, newUri] of urisToOpen) {
await commands.executeCommand("vscode.openWith", newUri, "default", {
preview: false,
});
}
oldUriToNewUriMap.push(...urisToOpen);
}
oldUriToNewUriMap.forEach(([uri, newUri]) =>
this._onDidManipulateFile.fire({
type: "rename",
uri: item.vscUri,
uri,
newUri,
});
return newUri;
}),
);
return newUri;

function getPreviouslyOpenedChildItems(childItems: ContentItem[]) {
const loadChildItems = closing !== true && newItemIsContainer;
if (!Array.isArray(removedTabUris) || !loadChildItems) {
return [];
}
// Here's where things get a little weird. When we rename folders in
// sas content, we _don't_ close those files. It doesn't matter since
// their path isn't hierarchical. In sas file system, the path is hierarchical,
// thus we need to re-open all the closed files. This does that by getting
// children and comparing the removedTabUris
const filteredChildItems = childItems
.map((childItem) => {
const matchingUri = removedTabUris.find((uri) =>
uri.path.endsWith(childItem.name),
);
if (!matchingUri) {
return;
}

return [matchingUri, childItem.vscUri];
})
.filter((exists) => exists);

return filteredChildItems;
}
}

Expand All @@ -314,6 +368,10 @@ class ContentDataProvider
return success;
}

public canRecycleResource(item: ContentItem): boolean {
return this.model.canRecycleResource(item);
}

public async recycleResource(item: ContentItem): Promise<boolean> {
if (!(await closeFileIfOpen(item))) {
return false;
Expand Down Expand Up @@ -495,13 +553,34 @@ class ContentDataProvider
return this.getChildren(selection);
}

private async moveItem(
item: ContentItem,
targetUri: string,
): Promise<boolean> {
if (!targetUri) {
return false;
}

const closing = closeFileIfOpen(item);
if (!(await closing)) {
return false;
}

const newUri = await this.model.moveTo(item, targetUri);
if (closing !== true) {
commands.executeCommand("vscode.open", newUri);
}

return !!newUri;
}

private async handleContentItemDrop(
target: ContentItem,
item: ContentItem,
): Promise<void> {
let success = false;
let message = Messages.FileDropError;
if (item.flags.isInRecycleBin) {
if (item.flags?.isInRecycleBin) {
message = Messages.FileDragFromTrashError;
} else if (item.isReference) {
message = Messages.FileDragFromFavorites;
Expand All @@ -511,10 +590,7 @@ class ContentDataProvider
success = await this.addToMyFavorites(item);
} else {
const targetUri = target.resourceId;
if (targetUri) {
success = await this.model.moveTo(item, targetUri);
}

success = await this.moveItem(item, targetUri);
if (success) {
this.refresh();
}
Expand Down Expand Up @@ -637,6 +713,12 @@ class ContentDataProvider
case FAVORITES_FOLDER_TYPE:
icon = "favoritesFolder";
break;
case SERVER_HOME_FOLDER_TYPE:
icon = "userWorkspace";
break;
case SERVER_ROOT_FOLDER_TYPE:
icon = "server";
break;
default:
icon = "folder";
break;
Expand All @@ -647,6 +729,7 @@ class ContentDataProvider
icon = "sasProgramFile";
}
}

return icon !== ""
? {
dark: Uri.joinPath(this.extensionUri, `icons/dark/${icon}Dark.svg`),
Expand All @@ -661,17 +744,26 @@ class ContentDataProvider

export default ContentDataProvider;

const closeFileIfOpen = (item: ContentItem) => {
const fileUri = item.vscUri;
const tabs: Tab[] = window.tabGroups.all.map((tg) => tg.tabs).flat();
const tab = tabs.find(
(tab) =>
(tab.input instanceof TabInputText ||
tab.input instanceof TabInputNotebook) &&
tab.input.uri.query === fileUri.query, // compare the file id
);
if (tab) {
return window.tabGroups.close(tab);
const closeFileIfOpen = (item: ContentItem): Promise<Uri[]> | boolean => {
const tabs = getEditorTabsForItem(item);
if (tabs.length > 0) {
return new Promise((resolve, reject) => {
Promise.all(tabs.map((tab) => window.tabGroups.close(tab)))
.then(() =>
resolve(
tabs
.map(
(tab) =>
(tab.input instanceof TabInputText ||
tab.input instanceof TabInputNotebook) &&
tab.input.uri,
)
.filter((exists) => exists),
),
)
.catch(reject);
});
}

return true;
};
63 changes: 57 additions & 6 deletions client/src/components/ContentNavigator/ContentModel.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { Uri } from "vscode";
import { Uri, l10n } from "vscode";

import { Messages, ROOT_FOLDERS } from "./const";
import { extname } from "path";

import { ALL_ROOT_FOLDERS, Messages } from "./const";
import { ContentAdapter, ContentItem } from "./types";
import { isItemInRecycleBin } from "./utils";

export class ContentModel {
private contentAdapter: ContentAdapter;
Expand Down Expand Up @@ -33,7 +36,8 @@ export class ContentModel {
return Object.entries(await this.contentAdapter.getRootItems())
.sort(
// sort the delegate folders as the order in the supportedDelegateFolders
(a, b) => ROOT_FOLDERS.indexOf(a[0]) - ROOT_FOLDERS.indexOf(b[0]),
(a, b) =>
ALL_ROOT_FOLDERS.indexOf(a[0]) - ALL_ROOT_FOLDERS.indexOf(b[0]),
)
.map((entry) => entry[1]);
}
Expand Down Expand Up @@ -90,6 +94,44 @@ export class ContentModel {
);
}

public async createUniqueFileOfPrefix(
parentItem: ContentItem,
fileName: string,
buffer?: ArrayBufferLike,
) {
const itemsInFolder = await this.getChildren(parentItem);
const uniqueFileName = getUniqueFileName();

return await this.createFile(parentItem, uniqueFileName, buffer);

function getUniqueFileName(): string {
const ext = extname(fileName);
const basename = fileName.replace(ext, "");
const usedFlowNames = itemsInFolder.reduce((carry, item) => {
if (item.name.endsWith(ext)) {
return { ...carry, [item.name]: true };
}
return carry;
}, {});

if (!usedFlowNames[fileName]) {
return fileName;
}

let number = 1;
let newFileName;
do {
newFileName = l10n.t("{basename}_Copy{number}{ext}", {
basename,
number: number++,
ext,
});
} while (usedFlowNames[newFileName]);

return newFileName || fileName;
}
}

public async createFolder(
item: ContentItem,
name: string,
Expand Down Expand Up @@ -127,7 +169,7 @@ export class ContentModel {
public async moveTo(
item: ContentItem,
targetParentFolderUri: string,
): Promise<boolean> {
): Promise<boolean | Uri> {
return await this.contentAdapter.moveItem(item, targetParentFolderUri);
}

Expand All @@ -139,11 +181,20 @@ export class ContentModel {
return await this.contentAdapter.getFolderPathForItem(contentItem);
}

public canRecycleResource(item: ContentItem): boolean {
return (
this.contentAdapter.recycleItem &&
this.contentAdapter.restoreItem &&
!isItemInRecycleBin(item) &&
item.permission.write
);
}

public async recycleResource(item: ContentItem) {
return await this.contentAdapter.recycleItem(item);
return await this.contentAdapter?.recycleItem(item);
}

public async restoreResource(item: ContentItem) {
return await this.contentAdapter.restoreItem(item);
return await this.contentAdapter?.restoreItem(item);
}
}
Loading