Skip to content

Commit ffc47fe

Browse files
authored
Better error handling when expanding InterSystems and Projects Explorers (#1652)
1 parent f6a635f commit ffc47fe

File tree

2 files changed

+99
-87
lines changed

2 files changed

+99
-87
lines changed

src/commands/export.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,13 @@ Would you like to continue?`,
274274
return;
275275
}
276276
}
277-
return Promise.all(nodes.map((node) => node.getItems4Export())).then((items) => {
278-
return exportList(items.flat(), node.workspaceFolder, nodeNs).then(() => explorerProvider.refresh());
279-
});
277+
return Promise.all(nodes.map((node) => node.getItems4Export()))
278+
.then((items) => {
279+
return exportList(items.flat(), node.workspaceFolder, nodeNs).then(() => explorerProvider.refresh());
280+
})
281+
.catch((error) => {
282+
handleError(error, "Error exporting Explorer items.");
283+
});
280284
}
281285

282286
export async function exportCurrentFile(): Promise<any> {

src/explorer/nodes.ts

Lines changed: 92 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
11
import * as vscode from "vscode";
22
import { AtelierAPI } from "../api";
3-
import { cspApps, currentWorkspaceFolder, notIsfs, uriOfWorkspaceFolder } from "../utils";
3+
import { cspApps, currentWorkspaceFolder, notIsfs, stringifyError, uriOfWorkspaceFolder } from "../utils";
44
import { StudioActions, OtherStudioAction } from "../commands/studio";
55
import { config, workspaceState } from "../extension";
66
import { DocumentContentProvider } from "../providers/DocumentContentProvider";
77

8-
type IconPath =
9-
| string
10-
| vscode.Uri
11-
| {
12-
light: string | vscode.Uri;
13-
dark: string | vscode.Uri;
14-
}
15-
| vscode.ThemeIcon;
16-
178
interface NodeOptions {
189
extraNode?: boolean;
1910
generated?: boolean;
@@ -49,6 +40,8 @@ function getLeafNodeUri(node: NodeBase, forceServerCopy = false): vscode.Uri {
4940
}
5041
}
5142

43+
const inactiveMsg = "Server connection is inactive";
44+
5245
export class NodeBase {
5346
public readonly options: NodeOptions;
5447
public readonly label: string;
@@ -104,7 +97,7 @@ export class RootNode extends NodeBase {
10497
public readonly contextValue: string;
10598
private readonly _category: string;
10699
private readonly isCsp: boolean;
107-
private readonly iconPath: IconPath;
100+
private readonly iconPath: vscode.ThemeIcon;
108101

109102
public constructor(
110103
label: string,
@@ -113,7 +106,7 @@ export class RootNode extends NodeBase {
113106
category: string,
114107
options: NodeOptions,
115108
isCsp = false,
116-
iconPath?: IconPath
109+
iconPath?: vscode.ThemeIcon
117110
) {
118111
super(label, fullName, options);
119112
this.contextValue = contextValue;
@@ -138,7 +131,51 @@ export class RootNode extends NodeBase {
138131

139132
public async getChildren(element: NodeBase): Promise<NodeBase[]> {
140133
const path = this instanceof PackageNode || this.isCsp ? this.fullName + "/" : "";
141-
return this.getItems(path, this._category);
134+
return this.getList(path, this._category, false)
135+
.then((data) =>
136+
data
137+
.filter((el) => {
138+
if (this._category === "OTH") {
139+
return el.Type === "100";
140+
} else if (this._category === "CSP") {
141+
return el.Type === "10" || el.Type === "5";
142+
} else {
143+
return true;
144+
}
145+
})
146+
.map((el) => {
147+
switch (el.Type) {
148+
case "9":
149+
return new PackageNode(el.Name, el.fullName, this._category, this.options);
150+
case "4":
151+
case "100":
152+
return new ClassNode(el.Name, el.fullName, this.options);
153+
case "5":
154+
return new CSPFileNode(el.Name, el.fullName, this.options);
155+
case "0":
156+
case "1":
157+
case "2":
158+
case "3":
159+
case "11":
160+
return new RoutineNode(el.Name, el.fullName, this.options);
161+
case "10":
162+
return new RootNode(
163+
el.Name,
164+
el.fullName,
165+
"dataNode:cspApplication",
166+
this._category,
167+
this.options,
168+
true
169+
);
170+
default:
171+
return null;
172+
}
173+
})
174+
.filter((el) => el !== null)
175+
)
176+
.catch((error) => [
177+
error == inactiveMsg ? new InactiveNode("", "", {}) : new ErrorNode(stringifyError(error), "", {}),
178+
]);
142179
}
143180

144181
public async getList(
@@ -180,6 +217,7 @@ export class RootNode extends NodeBase {
180217
const systemFiles = this.options.system || this.namespace === "%SYS" ? "1" : "0";
181218

182219
const api = new AtelierAPI(this.workspaceFolder);
220+
if (!api.active) throw inactiveMsg;
183221
api.setNamespace(this.namespace);
184222
if (category == "CSP" && path == "") {
185223
// Use the results from the getCSPApps() API
@@ -219,43 +257,6 @@ export class RootNode extends NodeBase {
219257
}
220258
}
221259

222-
public getItems(path: string, category: string): Promise<NodeBase[]> {
223-
return this.getList(path, category, false).then((data) =>
224-
data
225-
.filter((el) => {
226-
if (category === "OTH") {
227-
return el.Type === "100";
228-
} else if (category === "CSP") {
229-
return el.Type === "10" || el.Type === "5";
230-
} else {
231-
return true;
232-
}
233-
})
234-
.map((el) => {
235-
switch (el.Type) {
236-
case "9":
237-
return new PackageNode(el.Name, el.fullName, category, this.options);
238-
case "4":
239-
case "100":
240-
return new ClassNode(el.Name, el.fullName, this.options);
241-
case "5":
242-
return new CSPFileNode(el.Name, el.fullName, this.options);
243-
case "0":
244-
case "1":
245-
case "2":
246-
case "3":
247-
case "11":
248-
return new RoutineNode(el.Name, el.fullName, this.options);
249-
case "10":
250-
return new RootNode(el.Name, el.fullName, "dataNode:cspApplication", this._category, this.options, true);
251-
default:
252-
return null;
253-
}
254-
})
255-
.filter((el) => el !== null)
256-
);
257-
}
258-
259260
public getItems4Export(): Promise<string[]> {
260261
const path = this instanceof PackageNode || this.isCsp ? this.fullName + "/" : "";
261262
const cat = this.isCsp ? "CSP" : "ALL";
@@ -399,6 +400,7 @@ export class WorkspaceNode extends NodeBase {
399400
}
400401

401402
public async getChildren(_element: NodeBase): Promise<NodeBase[]> {
403+
if (!new AtelierAPI(this.workspaceFolder).active) return [new InactiveNode("", "", {})];
402404
const children = [];
403405
let node: RootNode;
404406

@@ -553,8 +555,9 @@ export class ProjectNode extends NodeBase {
553555
}
554556

555557
export class ProjectRootNode extends RootNode {
556-
public getChildren(element: NodeBase): Promise<NodeBase[]> {
558+
public async getChildren(element: NodeBase): Promise<NodeBase[]> {
557559
const api = new AtelierAPI(this.workspaceFolderUri);
560+
if (!api.active) return [new InactiveNode("", "", {})];
558561
api.setNamespace(this.namespace);
559562
let query: string;
560563
let parameters: string[];
@@ -646,27 +649,27 @@ export class ProjectRootNode extends RootNode {
646649
}
647650
}
648651
})
649-
);
652+
)
653+
.catch((error) => [new ErrorNode(stringifyError(error), "", {})]);
650654
}
651655
public getItems4Export(): Promise<string[]> {
652656
return Promise.resolve([]);
653657
}
654658
}
655659

656-
export class ProjectsServerNode extends NodeBase {
660+
export class ProjectsServerNsNode extends NodeBase {
657661
public eventEmitter: vscode.EventEmitter<NodeBase>;
658-
public uniqueId: string;
659-
public constructor(label: string, eventEmitter: vscode.EventEmitter<NodeBase>, wsUri: vscode.Uri) {
660-
super(label, label, { workspaceFolderUri: wsUri });
661-
this.uniqueId = `projectsServerNode:${this.workspaceFolder}`;
662+
663+
public constructor(label: string, eventEmitter: vscode.EventEmitter<NodeBase>, wsUri: vscode.Uri, extra = false) {
664+
super(label, label, { workspaceFolderUri: wsUri, extraNode: extra });
662665
this.eventEmitter = eventEmitter;
663666
}
664667

665668
public getTreeItem(): vscode.TreeItem {
666669
const { host, port, pathPrefix, serverName } = this.conn;
667670
return {
668671
collapsibleState: vscode.TreeItemCollapsibleState.Expanded,
669-
contextValue: this.uniqueId,
672+
contextValue: `projectsServerNsNode${this.extraNode ? ":extra" : ""}`,
670673
label: `${
671674
serverName && serverName.length ? serverName : `${host}:${port}${pathPrefix}`
672675
}:${this.namespace.toUpperCase()}`,
@@ -677,47 +680,52 @@ export class ProjectsServerNode extends NodeBase {
677680

678681
public async getChildren(element: NodeBase): Promise<NodeBase[]> {
679682
const api = new AtelierAPI(this.workspaceFolderUri);
683+
if (!api.active) return [new InactiveNode("", "", {})];
680684
api.setNamespace(this.namespace);
681685
return api
682686
.actionQuery("SELECT Name, Description FROM %Studio.Project", [])
683687
.then((data) =>
684688
data.result.content.map(
685689
(project) => new ProjectNode(project.Name, { project: project.Name, ...this.options }, project.Description)
686690
)
687-
);
691+
)
692+
.catch((error) => [new ErrorNode(stringifyError(error), "", {})]);
688693
}
689694
}
690695

691-
export class ProjectsServerNsNode extends NodeBase {
692-
public eventEmitter: vscode.EventEmitter<NodeBase>;
693-
694-
public constructor(label: string, eventEmitter: vscode.EventEmitter<NodeBase>, wsUri: vscode.Uri, extra = false) {
695-
super(label, label, { workspaceFolderUri: wsUri, extraNode: extra });
696-
this.eventEmitter = eventEmitter;
696+
/** Used to show that a server connection is inactive */
697+
class InactiveNode extends NodeBase {
698+
public constructor(label: string, fullName: string, options: NodeOptions) {
699+
super(label, fullName, options);
697700
}
698-
699701
public getTreeItem(): vscode.TreeItem {
700-
const { host, port, pathPrefix, serverName } = this.conn;
701702
return {
702-
collapsibleState: vscode.TreeItemCollapsibleState.Expanded,
703-
contextValue: `projectsServerNsNode${this.extraNode ? ":extra" : ""}`,
704-
label: `${
705-
serverName && serverName.length ? serverName : `${host}:${port}${pathPrefix}`
706-
}:${this.namespace.toUpperCase()}`,
707-
iconPath: new vscode.ThemeIcon("server-environment"),
708-
tooltip: "Explore projects in this server namespace",
703+
collapsibleState: vscode.TreeItemCollapsibleState.None,
704+
contextValue: "inactiveNode",
705+
label: inactiveMsg,
706+
iconPath: new vscode.ThemeIcon("warning", new vscode.ThemeColor("problemsWarningIcon.foreground")),
709707
};
710708
}
709+
public getItems4Export(): Promise<string[]> {
710+
return Promise.resolve([]);
711+
}
712+
}
711713

712-
public async getChildren(element: NodeBase): Promise<NodeBase[]> {
713-
const api = new AtelierAPI(this.workspaceFolderUri);
714-
api.setNamespace(this.namespace);
715-
return api
716-
.actionQuery("SELECT Name, Description FROM %Studio.Project", [])
717-
.then((data) =>
718-
data.result.content.map(
719-
(project) => new ProjectNode(project.Name, { project: project.Name, ...this.options }, project.Description)
720-
)
721-
);
714+
/** Used to bubble up an error to the user */
715+
class ErrorNode extends NodeBase {
716+
public constructor(label: string, fullName: string, options: NodeOptions) {
717+
super(label, fullName, options);
718+
}
719+
public getTreeItem(): vscode.TreeItem {
720+
return {
721+
collapsibleState: vscode.TreeItemCollapsibleState.None,
722+
contextValue: "errorNode",
723+
label: "Error fetching children",
724+
tooltip: new vscode.MarkdownString(this.label),
725+
iconPath: new vscode.ThemeIcon("error", new vscode.ThemeColor("problemsErrorIcon.foreground")),
726+
};
727+
}
728+
public getItems4Export(): Promise<string[]> {
729+
return Promise.resolve([]);
722730
}
723731
}

0 commit comments

Comments
 (0)