mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
Even more robust handling of extension tree loading (#9007)
* Even more robust handling of extension tree loading * Fix initial loading and add new test for CMS * Fix compile errors * Fix logic * Remove debug log statements
This commit is contained in:
@@ -32,9 +32,11 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
|
|||||||
public readonly onDidChangeTreeData: vscode.Event<TreeNode> = this._onDidChangeTreeData.event;
|
public readonly onDidChangeTreeData: vscode.Event<TreeNode> = this._onDidChangeTreeData.event;
|
||||||
private root: ControllerRootNode;
|
private root: ControllerRootNode;
|
||||||
private credentialProvider: azdata.CredentialProvider;
|
private credentialProvider: azdata.CredentialProvider;
|
||||||
|
private initialized: boolean = false;
|
||||||
|
|
||||||
constructor(private memento: vscode.Memento) {
|
constructor(private memento: vscode.Memento) {
|
||||||
this.root = new ControllerRootNode(this);
|
this.root = new ControllerRootNode(this);
|
||||||
|
this.root.addChild(new LoadingControllerNode());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getChildren(element?: TreeNode): Promise<TreeNode[]> {
|
public async getChildren(element?: TreeNode): Promise<TreeNode[]> {
|
||||||
@@ -42,14 +44,11 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
|
|||||||
return element.getChildren();
|
return element.getChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.root.hasChildren) {
|
if (!this.initialized) {
|
||||||
return this.root.getChildren();
|
this.loadSavedControllers().catch(err => { vscode.window.showErrorMessage(localize('bdc.controllerTreeDataProvider.error', "Unexpected error loading saved controllers: {0}", err)); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kick off loading the saved controllers but then immediately return the loading node so
|
return this.root.getChildren();
|
||||||
// the user isn't left with an empty tree while we load the nodes
|
|
||||||
this.loadSavedControllers().catch(err => { vscode.window.showErrorMessage(localize('bdc.controllerTreeDataProvider.error', "Unexpected error loading saved controllers: {0}", err)); });
|
|
||||||
return [new LoadingControllerNode()];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTreeItem(element: TreeNode): vscode.TreeItem | Thenable<vscode.TreeItem> {
|
public getTreeItem(element: TreeNode): vscode.TreeItem | Thenable<vscode.TreeItem> {
|
||||||
@@ -89,12 +88,11 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
|
|||||||
}
|
}
|
||||||
|
|
||||||
private removeNonControllerNodes(): void {
|
private removeNonControllerNodes(): void {
|
||||||
this.removePlaceholderNodes();
|
this.removePlaceholderNodes(this.root.children);
|
||||||
this.removeDefectiveControllerNodes();
|
this.removeDefectiveControllerNodes(this.root.children);
|
||||||
}
|
}
|
||||||
|
|
||||||
private removePlaceholderNodes(): void {
|
private removePlaceholderNodes(nodes: TreeNode[]): void {
|
||||||
let nodes = this.root.children;
|
|
||||||
if (nodes.length > 0) {
|
if (nodes.length > 0) {
|
||||||
for (let i = 0; i < nodes.length; ++i) {
|
for (let i = 0; i < nodes.length; ++i) {
|
||||||
if (nodes[i] instanceof AddControllerNode ||
|
if (nodes[i] instanceof AddControllerNode ||
|
||||||
@@ -106,8 +104,7 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeDefectiveControllerNodes(): void {
|
private removeDefectiveControllerNodes(nodes: TreeNode[]): void {
|
||||||
let nodes = this.root.children;
|
|
||||||
if (nodes.length > 0) {
|
if (nodes.length > 0) {
|
||||||
for (let i = 0; i < nodes.length; ++i) {
|
for (let i = 0; i < nodes.length; ++i) {
|
||||||
if (nodes[i] instanceof ControllerNode) {
|
if (nodes[i] instanceof ControllerNode) {
|
||||||
@@ -121,8 +118,11 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async loadSavedControllers(): Promise<void> {
|
private async loadSavedControllers(): Promise<void> {
|
||||||
this.root.clearChildren();
|
// Optimistically set to true so we don't double-load the tree
|
||||||
|
this.initialized = true;
|
||||||
|
try {
|
||||||
let controllers: IControllerInfoSlim[] = this.memento.get('controllers');
|
let controllers: IControllerInfoSlim[] = this.memento.get('controllers');
|
||||||
|
let treeNodes: TreeNode[] = [];
|
||||||
if (controllers) {
|
if (controllers) {
|
||||||
for (const c of controllers) {
|
for (const c of controllers) {
|
||||||
let password = undefined;
|
let password = undefined;
|
||||||
@@ -133,19 +133,27 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
|
|||||||
// Added before we had added authentication
|
// Added before we had added authentication
|
||||||
c.auth = 'basic';
|
c.auth = 'basic';
|
||||||
}
|
}
|
||||||
this.root.addChild(new ControllerNode(
|
treeNodes.push(new ControllerNode(
|
||||||
c.url, c.auth, c.username, password, c.rememberPassword,
|
c.url, c.auth, c.username, password, c.rememberPassword,
|
||||||
undefined, this.root, this, undefined
|
undefined, this.root, this, undefined
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
this.removeDefectiveControllerNodes();
|
this.removeDefectiveControllerNodes(treeNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.root.hasChildren) {
|
this.root.clearChildren();
|
||||||
|
if (treeNodes.length === 0) {
|
||||||
this.root.addChild(new AddControllerNode());
|
this.root.addChild(new AddControllerNode());
|
||||||
|
} else {
|
||||||
|
treeNodes.forEach(node => this.root.addChild(node));
|
||||||
|
}
|
||||||
|
this.notifyNodeChanged();
|
||||||
|
} catch (err) {
|
||||||
|
// Reset so we can try again if the tree refreshes
|
||||||
|
this.initialized = false;
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.notifyNodeChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async saveControllers(): Promise<void> {
|
public async saveControllers(): Promise<void> {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { CmsResourceTreeNode } from './cmsResourceTreeNode';
|
|||||||
export class CmsResourceTreeProvider implements TreeDataProvider<TreeNode>, ICmsResourceTreeChangeHandler {
|
export class CmsResourceTreeProvider implements TreeDataProvider<TreeNode>, ICmsResourceTreeChangeHandler {
|
||||||
|
|
||||||
private _appContext: AppContext;
|
private _appContext: AppContext;
|
||||||
|
private _children: TreeNode[] = [CmsResourceMessageTreeNode.create(CmsResourceTreeProvider.loadingLabel, undefined)];
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly appContext: AppContext
|
public readonly appContext: AppContext
|
||||||
@@ -31,16 +32,14 @@ export class CmsResourceTreeProvider implements TreeDataProvider<TreeNode>, ICms
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isSystemInitialized) {
|
if (!this.isSystemInitialized) {
|
||||||
// Kick off loading the saved servers but then immediately return the loading node so
|
this.loadSavedServers().catch(err => this._appContext.apiWrapper.showErrorMessage(localize('cms.resource.tree.treeProvider.loadError', "Unexpected error occurred while loading saved servers {0}", err)));
|
||||||
// the user isn't left with an empty tree while we load the nodes
|
return this._children;
|
||||||
this.loadSavedServers().catch(err => this._appContext.apiWrapper.showErrorMessage(localize('cms.resource.tree.treeProvider.loadError', "Unexpected error occured while loading saved servers {0}", err)));
|
|
||||||
return [CmsResourceMessageTreeNode.create(CmsResourceTreeProvider.loadingLabel, undefined)];
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let registeredCmsServers = this.appContext.cmsUtils.registeredCmsServers;
|
let registeredCmsServers = this.appContext.cmsUtils.registeredCmsServers;
|
||||||
if (registeredCmsServers && registeredCmsServers.length > 0) {
|
if (registeredCmsServers && registeredCmsServers.length > 0) {
|
||||||
this.isSystemInitialized = true;
|
this.isSystemInitialized = true;
|
||||||
return registeredCmsServers.map((server) => {
|
this._children = registeredCmsServers.map((server) => {
|
||||||
return new CmsResourceTreeNode(
|
return new CmsResourceTreeNode(
|
||||||
server.name,
|
server.name,
|
||||||
server.description,
|
server.description,
|
||||||
@@ -49,11 +48,12 @@ export class CmsResourceTreeProvider implements TreeDataProvider<TreeNode>, ICms
|
|||||||
this._appContext, this, null);
|
this._appContext, this, null);
|
||||||
}).sort((a, b) => a.name.localeCompare(b.name));
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
||||||
} else {
|
} else {
|
||||||
return [new CmsResourceEmptyTreeNode()];
|
this._children = [new CmsResourceEmptyTreeNode()];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [new CmsResourceEmptyTreeNode()];
|
this._children = [new CmsResourceEmptyTreeNode()];
|
||||||
}
|
}
|
||||||
|
return this._children;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get onDidChangeTreeData(): Event<TreeNode> {
|
public get onDidChangeTreeData(): Event<TreeNode> {
|
||||||
@@ -93,6 +93,10 @@ export class CmsResourceTreeProvider implements TreeDataProvider<TreeNode>, ICms
|
|||||||
await this.appContext.cmsUtils.cacheRegisteredCmsServer(server.name, server.description,
|
await this.appContext.cmsUtils.cacheRegisteredCmsServer(server.name, server.description,
|
||||||
server.ownerUri, server.connection);
|
server.ownerUri, server.connection);
|
||||||
}
|
}
|
||||||
|
this._children = servers;
|
||||||
|
} else {
|
||||||
|
// No saved servers so just show the Add Server node since we're done loading
|
||||||
|
this._children = [new CmsResourceEmptyTreeNode()];
|
||||||
}
|
}
|
||||||
this._onDidChangeTreeData.fire(undefined);
|
this._onDidChangeTreeData.fire(undefined);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { CmsResourceTreeProvider } from '../../../cmsResource/tree/treeProvider'
|
|||||||
import { CmsResourceMessageTreeNode } from '../../../cmsResource/messageTreeNode';
|
import { CmsResourceMessageTreeNode } from '../../../cmsResource/messageTreeNode';
|
||||||
import { CmsResourceEmptyTreeNode } from '../../../cmsResource/tree/cmsResourceEmptyTreeNode';
|
import { CmsResourceEmptyTreeNode } from '../../../cmsResource/tree/cmsResourceEmptyTreeNode';
|
||||||
import { CmsUtils } from '../../../cmsUtils';
|
import { CmsUtils } from '../../../cmsUtils';
|
||||||
|
import { sleep } from '../../utils';
|
||||||
|
|
||||||
// Mock services
|
// Mock services
|
||||||
let mockAppContext: AppContext;
|
let mockAppContext: AppContext;
|
||||||
@@ -29,22 +30,42 @@ describe('CmsResourceTreeProvider.getChildren', function (): void {
|
|||||||
mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object, mockCmsUtils.object);
|
mockAppContext = new AppContext(mockExtensionContext.object, mockApiWrapper.object, mockCmsUtils.object);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should not be initialized.', async function (): Promise<void> {
|
it('Should be loading while waiting for saved servers to load', async function (): Promise<void> {
|
||||||
const treeProvider = new CmsResourceTreeProvider(mockAppContext);
|
const treeProvider = new CmsResourceTreeProvider(mockAppContext);
|
||||||
should.notEqual(treeProvider.isSystemInitialized, true);
|
// We need to return at least one node so the async loading part is hit
|
||||||
|
mockCmsUtils.setup(x => x.getSavedServers).returns(() => {
|
||||||
|
return () => [{name: 'name',
|
||||||
|
description: 'desc',
|
||||||
|
ownerUri: 'uri',
|
||||||
|
connection: undefined}];
|
||||||
|
});
|
||||||
|
// Set up so loading the servers doesn't return immediately - thus we'll still have the Loading node
|
||||||
|
mockCmsUtils.setup(x => x.cacheRegisteredCmsServer).returns(() => {
|
||||||
|
return async () => { await sleep(600000); return undefined; };
|
||||||
|
});
|
||||||
|
should.notEqual(treeProvider.isSystemInitialized, true, 'Expected isSystemInitialized not to be true');
|
||||||
const children = await treeProvider.getChildren(undefined);
|
const children = await treeProvider.getChildren(undefined);
|
||||||
should.equal(children.length, 1);
|
should.equal(children.length, 1, 'Expected exactly one child node');
|
||||||
should.equal(children[0].parent, undefined);
|
should.equal(children[0].parent, undefined, 'Expected node to not have a parent');
|
||||||
should.equal(children[0] instanceof CmsResourceMessageTreeNode, true);
|
should.equal(children[0] instanceof CmsResourceMessageTreeNode, true, 'Expected node to be a CmsResourceMessageTreeNode');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should be empty resource node when no servers to load', async function (): Promise<void> {
|
||||||
|
const treeProvider = new CmsResourceTreeProvider(mockAppContext);
|
||||||
|
should.notEqual(treeProvider.isSystemInitialized, true, 'Expected isSystemInitialized not to be true');
|
||||||
|
const children = await treeProvider.getChildren(undefined);
|
||||||
|
should.equal(children.length, 1, 'Expected exactly one child node');
|
||||||
|
should.equal(children[0].parent, undefined, 'Expected node to not have a parent');
|
||||||
|
should.equal(children[0] instanceof CmsResourceEmptyTreeNode, true, 'Expected node to be a CmsResourceEmptyTreeNode');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should not be loading after initialized.', async function (): Promise<void> {
|
it('Should not be loading after initialized.', async function (): Promise<void> {
|
||||||
const treeProvider = new CmsResourceTreeProvider(mockAppContext);
|
const treeProvider = new CmsResourceTreeProvider(mockAppContext);
|
||||||
treeProvider.isSystemInitialized = true;
|
treeProvider.isSystemInitialized = true;
|
||||||
should.equal(true, treeProvider.isSystemInitialized);
|
should.equal(true, treeProvider.isSystemInitialized, 'Expected isSystemInitialized to be true');
|
||||||
mockCmsUtils.setup(x => x.registeredCmsServers).returns(() => []);
|
mockCmsUtils.setup(x => x.registeredCmsServers).returns(() => []);
|
||||||
const children = await treeProvider.getChildren(undefined);
|
const children = await treeProvider.getChildren(undefined);
|
||||||
should.equal(children[0] instanceof CmsResourceEmptyTreeNode, true);
|
should.equal(children[0] instanceof CmsResourceEmptyTreeNode, true, 'Expected child node to be CmsResourceEmptyTreeNode');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should show CMS nodes if there are cached servers', async function (): Promise<void> {
|
it('Should show CMS nodes if there are cached servers', async function (): Promise<void> {
|
||||||
@@ -59,6 +80,6 @@ describe('CmsResourceTreeProvider.getChildren', function (): void {
|
|||||||
}];
|
}];
|
||||||
});
|
});
|
||||||
const children = await treeProvider.getChildren(undefined);
|
const children = await treeProvider.getChildren(undefined);
|
||||||
should.equal(children[0] !== null, true);
|
should.exist(children[0], 'Child node did not exist');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
8
extensions/cms/src/test/utils.ts
Normal file
8
extensions/cms/src/test/utils.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
export async function sleep(ms: number): Promise<{}> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user