-
Notifications
You must be signed in to change notification settings - Fork 577
Expand file tree
/
Copy pathobjectExplorerService.ts
More file actions
1144 lines (1015 loc) · 44.8 KB
/
objectExplorerService.ts
File metadata and controls
1144 lines (1015 loc) · 44.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from "vscode";
import SqlToolsServiceClient from "../languageservice/serviceclient";
import ConnectionManager from "../controllers/connectionManager";
import {
CreateSessionCompleteNotification,
SessionCreatedParameters,
CreateSessionRequest,
CreateSessionResponse,
} from "../models/contracts/objectExplorer/createSessionRequest";
import {
ExpandRequest,
ExpandParams,
ExpandCompleteNotification,
ExpandResponse,
} from "../models/contracts/objectExplorer/expandNodeRequest";
import { RefreshRequest } from "../models/contracts/objectExplorer/refreshSessionRequest";
import {
CloseSessionRequest,
CloseSessionParams,
CloseSessionResponse,
} from "../models/contracts/objectExplorer/closeSessionRequest";
import { TreeNodeInfo } from "./nodes/treeNodeInfo";
import { IConnectionGroup, IConnectionProfile } from "../models/interfaces";
import * as LocalizedConstants from "../constants/locConstants";
import { AddConnectionTreeNode } from "./nodes/addConnectionTreeNode";
import { AccountSignInTreeNode } from "./nodes/accountSignInTreeNode";
import { ConnectTreeNode, TreeNodeType } from "./nodes/connectTreeNode";
import { Deferred } from "../protocol";
import * as Constants from "../constants/constants";
import { ObjectExplorerUtils } from "./objectExplorerUtils";
import { ConnectionCredentials } from "../models/connectionCredentials";
import { IConnectionInfo } from "vscode-mssql";
import { sendActionEvent, startActivity } from "../telemetry/telemetry";
import {
ActivityObject,
ActivityStatus,
TelemetryActions,
TelemetryViews,
} from "../sharedInterfaces/telemetry";
import {
GetSessionIdRequest,
GetSessionIdResponse,
} from "../models/contracts/objectExplorer/getSessionIdRequest";
import { Logger } from "../models/logger";
import VscodeWrapper from "../controllers/vscodeWrapper";
import { restartSqlServerContainer } from "../deployment/sqlServerContainer";
import { ExpandErrorNode } from "./nodes/expandErrorNode";
import { NoItemsNode } from "./nodes/noItemNode";
import { ConnectionNode } from "./nodes/connectionNode";
import { ConnectionGroupNode } from "./nodes/connectionGroupNode";
import { getConnectionDisplayName } from "../models/connectionInfo";
import { NewDeploymentTreeNode } from "../deployment/newDeploymentTreeNode";
import { getErrorMessage, uuid } from "../utils/utils";
import { ConnectionConfig } from "../connectionconfig/connectionconfig";
import { MissingVsCodeEntraAuthError } from "../azure/vscodeEntraMfaUtils";
import { VsCodeAzureHelper } from "../connectionconfig/azureHelpers";
import { PreviewFeature, previewService } from "../previews/previewService";
export interface CreateSessionResult {
sessionId?: string;
connectionNode?: ConnectionNode;
shouldRetryOnFailure?: boolean;
}
export class ObjectExplorerService {
private _client: SqlToolsServiceClient;
private _logger: Logger;
public initialized: Deferred<void> = new Deferred<void>();
/**
* Flat map of tree nodes to their children
* This is used to cache the children of a node so that we don't have to re-query them every time
* we expand a node. The key is the node and the value is the array of children.
*/
private _treeNodeToChildrenMap: Map<vscode.TreeItem, vscode.TreeItem[]>;
private _connectionNodes = new Map<string, ConnectionNode>();
private _connectionGroupNodes = new Map<string, ConnectionGroupNode>();
private get _rootTreeNodeArray(): Array<TreeNodeInfo> {
const result = [];
if (!this._connectionGroupNodes.has(ConnectionConfig.ROOT_GROUP_ID)) {
this._logger.verbose(
"Root server group is not defined. Cannot get root nodes for Object Explorer.",
);
return [];
}
for (const child of this._connectionGroupNodes.get(ConnectionConfig.ROOT_GROUP_ID)
?.children || []) {
result.push(child);
}
return result;
}
/**
* Map of pending session creations
*/
private _pendingSessionCreations: Map<string, Deferred<SessionCreatedParameters>> = new Map<
string,
Deferred<SessionCreatedParameters>
>();
/**
* Map of pending expands
*/
private _pendingExpands: Map<string, Deferred<ExpandResponse>> = new Map<
string,
Deferred<ExpandResponse>
>();
constructor(
private _vscodeWrapper: VscodeWrapper,
private _connectionManager: ConnectionManager,
private _refreshCallback: (node: TreeNodeInfo) => void,
) {
if (!_vscodeWrapper) {
this._vscodeWrapper = new VscodeWrapper();
}
this._client = this._connectionManager.client;
this._logger = Logger.create(this._vscodeWrapper.outputChannel, "ObjectExplorerService");
this._treeNodeToChildrenMap = new Map<vscode.TreeItem, vscode.TreeItem[]>();
this._client.onNotification(CreateSessionCompleteNotification.type, (e) =>
this.handleSessionCreatedNotification(e),
);
this._client.onNotification(ExpandCompleteNotification.type, (e) =>
this.handleExpandNodeNotification(e),
);
void this.initialize();
}
/**
* Handles the session created notification from the SQL Tools Service.
* @param result The result of the session creation request.
*/
public handleSessionCreatedNotification(result: SessionCreatedParameters): void {
const promise = this._pendingSessionCreations.get(result.sessionId);
if (promise) {
promise.resolve(result);
} else {
this._logger.error(
`Session created notification received for sessionId ${result.sessionId} but no promise found.`,
);
}
}
/**
* Handles the expand node notification from the SQL Tools Service.
* @param result The result of the expand node request.
*/
public handleExpandNodeNotification(result: ExpandResponse): void {
const promise = this._pendingExpands.get(`${result.sessionId}${result.nodePath}`);
if (promise) {
promise.resolve(result);
} else {
this._logger.error(
`Expand node notification received for sessionId ${result.sessionId} but no promise found.`,
);
}
}
/**
* Adds a connection node to the OE tree at the right position based on its label.
* @param profile The connection profile to reconnect.
*/
private async reconnectProfile(profile: IConnectionProfile): Promise<void> {
const node = this.getConnectionNodeFromProfile(profile);
if (node) {
node.updateConnectionProfile(profile);
this.cleanNodeChildren(node);
this._refreshCallback(node);
}
}
/**
* Expands a node in the Object Explorer tree. If the node has the shouldRefresh flag set, it will be refreshed.
* @param node The node to expand
* @param sessionId The session ID to use for the expansion
* @returns The children of the expanded node
*/
public async expandNode(
node: TreeNodeInfo,
sessionId: string,
): Promise<vscode.TreeItem[] | undefined> {
const expandActivity = startActivity(
TelemetryViews.ObjectExplorer,
TelemetryActions.ExpandNode,
undefined,
{
nodeType: node.nodeType,
nodeSubType: node.nodeSubType,
isRefresh: node.shouldRefresh.toString(),
},
);
this._logger.verbose(`Expanding node ${node.label} with session ID ${sessionId}`);
try {
const expandParams: ExpandParams = {
sessionId: sessionId,
nodePath: node.nodePath,
filters: node.filters,
};
const expandResponse = new Deferred<ExpandResponse>();
this._pendingExpands.set(`${sessionId}${node.nodePath}`, expandResponse);
let response: boolean;
if (node.shouldRefresh) {
this._logger.verbose(`Refreshing node ${node.label} with session ID ${sessionId}`);
response = await this._connectionManager.client.sendRequest(
RefreshRequest.type,
expandParams,
);
} else {
this._logger.verbose(`Expanding node ${node.label} with session ID ${sessionId}`);
response = await this._connectionManager.client.sendRequest(
ExpandRequest.type,
expandParams,
);
}
if (response) {
const result = await expandResponse;
this._logger.verbose(
`Expand node response: ${JSON.stringify(result)} for sessionId ${sessionId}`,
);
if (!result) {
return undefined;
}
if (result.nodes && !result.errorMessage) {
this._logger.verbose(
`Received ${result.nodes.length} children for node ${node.label} for sessionId ${sessionId}`,
);
// successfully received children from SQL Tools Service
const children = result.nodes.map((n) =>
TreeNodeInfo.fromNodeInfo(
n,
result.sessionId,
node,
node.connectionProfile,
),
);
this._treeNodeToChildrenMap.set(node, children);
expandActivity.end(ActivityStatus.Succeeded, undefined, {
childrenCount: children.length,
});
return children;
} else {
// failure to expand node; display error
if (result.errorMessage) {
this._logger.error(
`Expand node failed: ${result.errorMessage} for sessionId ${sessionId}`,
);
this._vscodeWrapper.showErrorMessage(result.errorMessage);
}
const errorNode = new ExpandErrorNode(node, result.errorMessage);
this._treeNodeToChildrenMap.set(node, [errorNode]);
expandActivity.endFailed(new Error(result.errorMessage), false);
return [errorNode];
}
} else {
this._logger.error(
`Expand node failed: Didn't receive a response from SQL Tools Service for sessionId ${sessionId}`,
);
await this._vscodeWrapper.showErrorMessage(LocalizedConstants.msgUnableToExpand);
return undefined;
}
} finally {
node.shouldRefresh = false;
}
}
/**
* Clean all children of the node
* @param node Node to cleanup
*/
public cleanNodeChildren(node: vscode.TreeItem): void {
if (this._treeNodeToChildrenMap.has(node)) {
let stack = this._treeNodeToChildrenMap.get(node);
while (stack.length > 0) {
let child = stack.pop();
if (this._treeNodeToChildrenMap.has(child)) {
stack.push(...this._treeNodeToChildrenMap.get(child));
}
this._treeNodeToChildrenMap.delete(child);
}
this._treeNodeToChildrenMap.delete(node);
}
}
/**
* Sort the array based on server names
* Public only for testing purposes
* @param array array that needs to be sorted
*/
public sortByServerName(array: TreeNodeInfo[]): TreeNodeInfo[] {
const sortedNodeArray = array.sort((a, b) => {
const labelA = typeof a.label === "string" ? a.label : a.label.label;
const labelB = typeof b.label === "string" ? b.label : b.label.label;
return labelA.toLowerCase().localeCompare(labelB.toLowerCase());
});
return sortedNodeArray;
}
/**
* Helper to show the Add Connection node; only displayed when there are no saved connections under the node
* @returns An array containing the Add Connection node
*/
private getAddConnectionNodes(parent?: TreeNodeInfo): AddConnectionTreeNode[] {
const nodeList: AddConnectionTreeNode[] = [];
nodeList.push(new AddConnectionTreeNode(parent));
nodeList.push(new NewDeploymentTreeNode(parent));
return nodeList;
}
/**
* Handles a generic OE create session failure by creating a
* sign in node
*/
private createSignInNode(element: TreeNodeInfo): AccountSignInTreeNode[] {
const signInNode = new AccountSignInTreeNode(element);
this._treeNodeToChildrenMap.set(element, [signInNode]);
return [signInNode];
}
// Main method that routes to the appropriate handler
public async getChildren(element?: TreeNodeInfo): Promise<vscode.TreeItem[]> {
await this.initialized;
if (!element) {
return this.getRootNodes();
}
if (element instanceof ConnectionGroupNode) {
// If the connection group has no children, show the add connection nodes
// so users can easily add a new connection under an empty group (same behavior
// as when there are no saved connections in the root).
if (!element.children || element.children.length === 0) {
return this.getAddConnectionNodes(element);
}
return element.children;
}
return this.getNodeChildren(element);
}
/**
* Handle getting root node children.
* @returns The root node children
*/
private async getRootNodes(): Promise<vscode.TreeItem[]> {
const getConnectionActivity = startActivity(
TelemetryViews.ObjectExplorer,
TelemetryActions.ExpandNode,
undefined,
{
nodeType: "root",
},
);
const serverGroups =
await this._connectionManager.connectionStore.readAllConnectionGroups();
let savedConnections = await this._connectionManager.connectionStore.readAllConnections();
// if there are no saved connections, show the add connection node
if (
savedConnections.length === 0 &&
serverGroups.length === 1 &&
serverGroups[0].id === ConnectionConfig.ROOT_GROUP_ID
) {
this._logger.verbose(
"No saved connections or groups found. Showing add connection node.",
);
getConnectionActivity.end(ActivityStatus.Succeeded, undefined, {
childrenCount: 0,
});
return this.getAddConnectionNodes();
}
const newConnectionGroupNodes = new Map<string, ConnectionGroupNode>();
const newConnectionNodes = new Map<string, ConnectionNode>();
// Add all group nodes from settings first
// Read the user setting for collapsed/expanded state
const config = vscode.workspace.getConfiguration(Constants.extensionName);
const collapseGroups = config.get<boolean>(
Constants.cmdObjectExplorerCollapseOrExpandByDefault,
false,
);
for (const group of serverGroups) {
// Pass the desired collapsible state to the ConnectionGroupNode constructor
const initialState = collapseGroups
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.Expanded;
const groupNode = new ConnectionGroupNode(group, initialState);
if (this._connectionGroupNodes.has(group.id)) {
groupNode.id = this._connectionGroupNodes.get(group.id).id;
}
newConnectionGroupNodes.set(group.id, groupNode);
}
// Populate group hierarchy - add each group as a child to its parent
for (const group of serverGroups) {
// Skip the root group as it has no parent
if (group.id === ConnectionConfig.ROOT_GROUP_ID) {
continue;
}
if (group.parentId && newConnectionGroupNodes.has(group.parentId)) {
const parentNode = newConnectionGroupNodes.get(group.parentId);
const childNode = newConnectionGroupNodes.get(group.id);
if (parentNode && childNode) {
parentNode.addChild(childNode);
if (parentNode.id !== ConnectionConfig.ROOT_GROUP_ID) {
// set the parent node for the child group unless the parent is the root group
// parent property is used to
childNode.parentNode = parentNode;
}
} else {
this._logger.error(
`Child group '${group.name}' with ID '${group.id}' does not have a valid parent group (${group.parentId}).`,
);
}
} else {
this._logger.error(
`Group '${group.name}' with ID '${group.id}' does not have a valid parent group ID. This should have been corrected when reading server groups from settings.`,
);
}
}
// Add connections as children of their respective groups
for (const connection of savedConnections) {
if (connection.groupId && newConnectionGroupNodes.has(connection.groupId)) {
const groupNode = newConnectionGroupNodes.get(connection.groupId);
let connectionNode: ConnectionNode;
if (this._connectionNodes.has(connection.id)) {
connectionNode = this._connectionNodes.get(connection.id);
connectionNode.updateConnectionProfile(connection);
connectionNode.label = getConnectionDisplayName(connection);
} else {
connectionNode = new ConnectionNode(
connection,
groupNode.id === ConnectionConfig.ROOT_GROUP_ID ? undefined : groupNode,
);
}
connectionNode.parentNode =
groupNode.id === ConnectionConfig.ROOT_GROUP_ID ? undefined : groupNode;
newConnectionNodes.set(connection.id, connectionNode);
groupNode.addChild(connectionNode);
} else {
this._logger.error(
`Connection '${getConnectionDisplayName(connection)}' with ID '${connection.id}' does not have a valid group ID. This should have been corrected when reading connections from settings.`,
);
}
}
this._connectionGroupNodes = newConnectionGroupNodes;
this._connectionNodes = newConnectionNodes;
const result = [...this._rootTreeNodeArray];
getConnectionActivity.end(ActivityStatus.Succeeded, undefined, {
nodeCount: result.length,
});
return result;
}
/**
* Handles getting children for all nodes other than the root node
* @param element The node to get children for
* @returns The children of the node
*/
private async getNodeChildren(element: TreeNodeInfo): Promise<vscode.TreeItem[]> {
if (element.shouldRefresh) {
this.cleanNodeChildren(element);
} else {
if (this._treeNodeToChildrenMap.has(element)) {
return this._treeNodeToChildrenMap.get(element);
}
}
/**
* If no children are cached, return a temporary loading node to keep the UI responsive
* and trigger the async call to fetch real children.
* This node will be replaced once the data is retrieved and the tree is refreshed.
* Tree expansion is queued, so without this if multiple connections are expanding,
* one blocked operation can delay the other.
*/
void this.getOrCreateNodeChildrenWithSession(element);
return this.setLoadingUiForNode(element);
}
/**
* Sets a loading UI for the given node.
* This is used to show a loading spinner while the children are being fetched/ other node operations are being performed.
* @param element The node to set the loading UI for
* @returns A loading node that will be displayed in the tree
*/
public async setLoadingUiForNode(element: TreeNodeInfo): Promise<vscode.TreeItem[]> {
const loadingNode = new vscode.TreeItem(
element.loadingLabel ?? LocalizedConstants.ObjectExplorer.LoadingNodeLabel,
vscode.TreeItemCollapsibleState.None,
);
loadingNode.iconPath = new vscode.ThemeIcon("loading~spin");
this._treeNodeToChildrenMap.set(element, [loadingNode]);
this._refreshCallback(element);
return this._treeNodeToChildrenMap.get(element);
}
/**
* Get or create the children of a node. If the node has a session ID, expand it.
* If it doesn't, create a new session and expand it.
* @param element The node to get or create children for
* @returns The children of the node
*/
private async getOrCreateNodeChildrenWithSession(element: TreeNodeInfo): Promise<void> {
if (element.sessionId) {
await this.expandExistingNode(element);
} else {
await this.createSessionAndExpandNode(element);
}
element.shouldRefresh = false;
this._refreshCallback(element);
}
/**
* Expand a node that already has a session ID.
* @param element The node to expand
* @returns The children of the node
*/
private async expandExistingNode(element: TreeNodeInfo): Promise<vscode.TreeItem[]> {
const children = await this.expandNode(element, element.sessionId);
if (children?.length === 0) {
const noItemsNode = [new NoItemsNode(element)];
this._treeNodeToChildrenMap.set(element, noItemsNode);
return noItemsNode;
}
return children;
}
/**
* Create a new session for the given node and expand it.
* If the session was not created, show the sign in node.
* If the session was created but the connected node was not created, show the sign in node.
* Otherwise, expand the existing node.
* @param element The node to create a session for and expand
* @returns The children of the node
*/
private async createSessionAndExpandNode(element: TreeNodeInfo): Promise<vscode.TreeItem[]> {
const sessionResult = await this.createSession(element.connectionProfile);
if (sessionResult?.shouldRetryOnFailure) {
setTimeout(() => void this.reconnectProfile(element.connectionProfile), 0);
return undefined;
}
// if the session was not created, show the sign in node
if (!sessionResult?.sessionId) {
return this.createSignInNode(element);
}
// If the session was created but the connected node was not created, show sign in node
if (!sessionResult.connectionNode) {
return this.createSignInNode(element);
} else {
const children = this.expandExistingNode(element);
setTimeout(() => this._refreshCallback(element), 0);
return children;
}
}
private async initialize(): Promise<void> {
// Pre-load root nodes to ensure connection/group maps are populated
await this.getRootNodes();
this.initialized.resolve();
}
/**
* Create an OE session for the given connection credentials
* otherwise prompt the user to create a new connection profile
* After the session is created, the connection node is added to the tree
* @param connectionProfile Connection Credentials for a node
* @returns The session ID and connection node. If undefined, the session was not created.
*/
public async createSession(
connectionInfo?: IConnectionInfo,
): Promise<CreateSessionResult | undefined> {
await this.initialized;
if (!this._rootTreeNodeArray) {
// Ensure root nodes are loaded.
// This is needed when connection attempts are made before OE has been activated
// e.g. User clicks connect button from Editor before ever viewing the OE panel
await this.getRootNodes();
}
const createSessionActivity = startActivity(
TelemetryViews.ObjectExplorer,
TelemetryActions.CreateSession,
undefined,
{
connectionType: connectionInfo?.authenticationType ?? "newConnection",
},
undefined,
);
const connectionProfile = await this.prepareConnectionProfile(connectionInfo);
if (!connectionProfile) {
this._logger.error("Failed to prepare connection profile");
return undefined;
}
const connectionDetails = ConnectionCredentials.createConnectionDetails(connectionProfile);
const sessionIdResponse: GetSessionIdResponse =
await this._connectionManager.client.sendRequest(
GetSessionIdRequest.type,
connectionDetails,
);
const sessionCreatedResponse: Deferred<SessionCreatedParameters> =
new Deferred<SessionCreatedParameters>();
this._pendingSessionCreations.set(sessionIdResponse.sessionId, sessionCreatedResponse);
const createSessionResponse: CreateSessionResponse =
await this._connectionManager.client.sendRequest(
CreateSessionRequest.type,
connectionDetails,
);
if (createSessionResponse) {
const sessionCreationResult = await sessionCreatedResponse;
if (sessionCreationResult.success) {
this._logger.verbose(
`Session created successfully with session ID ${sessionCreationResult.sessionId}`,
);
this._pendingSessionCreations.delete(sessionIdResponse.sessionId);
const successResponse = await this.handleSessionCreationSuccess(
sessionCreationResult,
connectionProfile,
);
createSessionActivity.end(ActivityStatus.Succeeded, {
connectionType: connectionProfile.authenticationType,
});
return successResponse;
} else {
this._logger.error(
`Session creation failed with error: ${sessionCreationResult.errorMessage}`,
);
const shouldReconnect = await this.handleSessionCreationFailure(
sessionCreationResult,
connectionProfile,
createSessionActivity,
);
createSessionActivity.endFailed();
return {
sessionId: undefined,
connectionNode: undefined,
shouldRetryOnFailure: shouldReconnect,
};
}
} else {
return undefined;
}
}
/**
* Prepares the connection profile for session creation.
* @param connectionInfo The connection info to prepare.
* @returns The prepared connection profile. If undefined, the connection was not prepared properly.
*/
private async prepareConnectionProfile(
connectionInfo?: IConnectionInfo,
): Promise<IConnectionProfile | undefined> {
let connectionProfile: IConnectionProfile = connectionInfo as IConnectionProfile;
if (!connectionProfile) {
const connectionUI = this._connectionManager.connectionUI;
connectionUI.openConnectionDialog();
sendActionEvent(
TelemetryViews.ObjectExplorer,
TelemetryActions.CreateConnection,
undefined,
undefined,
connectionInfo as IConnectionProfile,
this._connectionManager.getServerInfo(connectionInfo),
);
}
if (!connectionProfile) {
return undefined;
}
if (!connectionProfile.id) {
connectionProfile.id = uuid();
}
// Local container, ensure it is started
if (connectionProfile.containerName) {
sendActionEvent(TelemetryViews.LocalContainers, TelemetryActions.ConnectToContainer);
try {
const containerNode = this.getConnectionNodeFromProfile(connectionProfile);
// start docker and docker container
const successfullyRunning = await restartSqlServerContainer(
connectionProfile.containerName,
containerNode,
this,
);
this._logger.verbose(
successfullyRunning
? `Failed to restart Docker container "${connectionProfile.containerName}".`
: `Docker container "${connectionProfile.containerName}" has been restarted.`,
);
} catch (error) {
this._logger.error(
`Error when attempting to ensure container "${connectionProfile.containerName}" is started. Attempting to proceed normally.\n\nError:\n${getErrorMessage(error)}`,
);
}
}
const self = this;
async function prepareConnectionProfile(): Promise<IConnectionProfile | undefined> {
return (await self._connectionManager.prepareConnectionInfo(
connectionProfile,
)) as IConnectionProfile;
}
try {
return await prepareConnectionProfile();
} catch (error) {
if (!previewService.isFeatureEnabled(PreviewFeature.UseVscodeAccountsForEntraMFA)) {
this._logger.error(
`Error when attempting to prepare connection profile. Attempting to proceed normally.\n\nError:\n${getErrorMessage(error)}`,
);
return undefined;
}
// Handle case where the user isn't signed into VS Code with the necessary auth account
if (error instanceof MissingVsCodeEntraAuthError) {
const choice = await this._vscodeWrapper.showErrorMessage(
getErrorMessage(error),
LocalizedConstants.ObjectExplorer.FailedOEConnectionErrorSignIn,
LocalizedConstants.ObjectExplorer.FailedOEConnectionErrorUpdate,
);
if (choice === LocalizedConstants.ObjectExplorer.FailedOEConnectionErrorSignIn) {
try {
await VsCodeAzureHelper.signIn(true); // User chose to sign in to the missing account; try again.
return await prepareConnectionProfile();
} catch (retryError) {
this._logger.error(
`Error when signing in or attempting to prepare connection profile after VS Code sign-in.\n\nError:\n${getErrorMessage(retryError)}`,
);
}
} else if (
choice === LocalizedConstants.ObjectExplorer.FailedOEConnectionErrorUpdate
) {
// User chose to edit the connection profile; open in Connection Dialog
await vscode.commands.executeCommand(
Constants.cmdAddObjectExplorer,
connectionInfo as IConnectionProfile,
);
}
return undefined;
} else {
this._logger.error(
`Error when attempting to prepare connection profile. Attempting to proceed normally.\n\nError:\n${getErrorMessage(error)}`,
);
return undefined;
}
}
}
/**
* Handles the success of session creation.
* @param successResponse The response from the session creation request.
* @param connectionProfile The connection profile used to create the session.
* @returns The session ID and corresponding connection node. If undefined, the session was not created.
*/
private async handleSessionCreationSuccess(
successResponse: SessionCreatedParameters,
connectionProfile: IConnectionProfile,
) {
if (!successResponse.success) {
return;
}
let connectionNode = this.getConnectionNodeFromProfile(connectionProfile);
let isNewConnection = false;
if (!connectionNode) {
isNewConnection = true;
connectionNode = new ConnectionNode(connectionProfile);
this._connectionNodes.set(connectionProfile.id, connectionNode);
} else {
connectionNode.updateConnectionProfile(connectionProfile);
}
connectionNode.updateToConnectedState({
nodeInfo: successResponse.rootNode,
sessionId: successResponse.sessionId,
parentNode: connectionNode.parentNode,
connectionProfile: connectionProfile,
});
// make a connection if not connected already
const nodeUri = this.getNodeIdentifier(connectionNode);
if (
!this._connectionManager.isConnected(nodeUri) &&
!this._connectionManager.isConnecting(nodeUri)
) {
await this._connectionManager.connect(nodeUri, connectionNode.connectionProfile);
}
const dockerConnectionContainerName =
await this._connectionManager.checkForDockerConnection(connectionProfile);
if (dockerConnectionContainerName) {
connectionNode = connectionNode.updateToDockerConnection(dockerConnectionContainerName);
}
if (isNewConnection || dockerConnectionContainerName) {
this.addConnectionNode(connectionNode);
}
await this._connectionManager.handlePasswordStorageOnConnect(connectionProfile);
// remove the sign in node once the session is created
if (this._treeNodeToChildrenMap.has(connectionNode)) {
this._treeNodeToChildrenMap.delete(connectionNode);
}
const finalNode = this.getConnectionNodeFromProfile(connectionProfile);
return {
sessionId: successResponse.sessionId,
connectionNode: finalNode,
};
}
/**
* Handles the failure of session creation.
* @param failureResponse The response from the session creation request.
* @param connectionProfile The connection profile used to create the session.
* @returns True if the session creation should be retried, false otherwise.
*/
private async handleSessionCreationFailure(
failureResponse: SessionCreatedParameters,
connectionProfile: IConnectionProfile,
telemetryActivty: ActivityObject,
): Promise<boolean> {
if (failureResponse.errorNumber) {
telemetryActivty.update(
{
connectionType: connectionProfile.authenticationType,
},
{
errorNumber: failureResponse.errorNumber,
},
);
}
const errorHandlingResult = await this._connectionManager.handleConnectionErrors(
failureResponse,
connectionProfile,
);
telemetryActivty.update({
connectionType: connectionProfile.authenticationType,
errorHandled: errorHandlingResult.errorHandled,
isFixed: errorHandlingResult.errorHandled ? "true" : "false",
});
if (errorHandlingResult.isHandled) {
const connectionNode = this.getConnectionNodeFromProfile(connectionProfile);
if (connectionNode) {
connectionNode.updateConnectionProfile(
errorHandlingResult.updatedCredentials as IConnectionProfile,
);
}
}
return errorHandlingResult.isHandled;
}
/**
* Removes a node from the OE tree. It will also disconnect the node from the server before removing it.
* @param node The connection node to remove.
* @param showUserConfirmationPrompt Whether to show a confirmation prompt to the user before removing the node.
*/
public async removeNode(
node: ConnectionNode,
showUserConfirmationPrompt: boolean = true,
): Promise<void> {
if (showUserConfirmationPrompt) {
const response = await vscode.window.showInformationMessage(
LocalizedConstants.ObjectExplorer.NodeDeletionConfirmation(node.label as string),
{
modal: true,
},
LocalizedConstants.ObjectExplorer.NodeDeletionConfirmationYes,
LocalizedConstants.ObjectExplorer.NodeDeletionConfirmationNo,
);
if (response !== LocalizedConstants.ObjectExplorer.NodeDeletionConfirmationYes) {
return;
}
}
await this.disconnectNode(node);
if (this._connectionNodes.has(node.connectionProfile.id)) {
this._connectionNodes.delete(node.connectionProfile.id);
} else {
this._logger.error(
`Connection node with ID ${node.connectionProfile.id} not found in connection nodes map.`,
);
}
this._refreshCallback(undefined); // Refresh tree root.
await this._connectionManager.connectionStore.removeProfile(node.connectionProfile, false);
}
/**
* Disconnects a connection node and cleans up its cached children.
* @param node The connection node to disconnect.
*/
public async disconnectNode(node: ConnectionNode): Promise<void> {
await this.closeSession(node);
const nodeUri = this.getNodeIdentifier(node);
await this._connectionManager.disconnect(nodeUri);
this.cleanNodeChildren(node);
node.updateToDisconnectedState();
this._treeNodeToChildrenMap.set(node, [new ConnectTreeNode(node)]);
}
/**
* Remove multiple connection nodes from the OE tree.
* @param connections Connection info of the nodes to remove.
* @returns True if ALL provided connections were removed successfully, false otherwise.
*/
public async removeConnectionNodes(connections: IConnectionInfo[]): Promise<boolean> {
const notFound: string[] = [];
for (let conn of connections) {
const node = this.getConnectionNodeFromProfile(conn as IConnectionProfile);
if (node) {
await this.removeNode(node as ConnectionNode, false);
} else {
notFound.push((conn as IConnectionProfile).id);
}
}
if (notFound.length > 0) {
this._logger.error(
`Expected to remove ${connections.length} nodes, but did not find: ${notFound.join(", ")}.`,
);
}
return notFound.length === 0;
}
/**
* Adds a new disconnected node to the OE tree.
* @param connectionCredentials The connection credentials for the new node.
*/
public addDisconnectedNode(connectionCredentials: IConnectionProfile): void {
const connectionNode = new ConnectionNode(connectionCredentials);
this.updateNode(connectionNode);
}
/**
* Adds a connection node to the OE tree.
* @param connectionNode The connection node to add.
* This will replace any existing node with the same connection profile.
*/
private addConnectionNode(connectionNode: ConnectionNode): void {
const oldNode = this._connectionNodes.get(connectionNode.connectionProfile.id);
this._logger.verbose(
`${oldNode ? "Updating" : "Adding"} connection node: ${connectionNode.label}`,
);
if (oldNode) {
this._connectionGroupNodes.get(oldNode.connectionProfile.groupId)?.removeChild(oldNode);
}