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:
Charles Gagnon
2020-02-04 08:03:34 -08:00
committed by GitHub
parent 5a2bbc0375
commit 62df5359e2
4 changed files with 89 additions and 48 deletions

View File

@@ -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> {

View File

@@ -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) {

View File

@@ -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');
}); });
}); });

View 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));
}