diff --git a/src/sql/platform/connection/common/connectionConfig.ts b/src/sql/platform/connection/common/connectionConfig.ts index 4c33d9c49c..b566915643 100644 --- a/src/sql/platform/connection/common/connectionConfig.ts +++ b/src/sql/platform/connection/common/connectionConfig.ts @@ -14,8 +14,14 @@ import * as nls from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { deepClone } from 'vs/base/common/objects'; -const GROUPS_CONFIG_KEY = 'datasource.connectionGroups'; -const CONNECTIONS_CONFIG_KEY = 'datasource.connections'; +export const GROUPS_CONFIG_KEY = 'datasource.connectionGroups'; +export const CONNECTIONS_CONFIG_KEY = 'datasource.connections'; +export const CONNECTIONS_SORT_BY_CONFIG_KEY = 'datasource.connections.sortBy'; + +export const enum ConnectionsSortBy { + dateAdded = 'dateAdded', + displayName = 'displayName' +} export interface ISaveGroupResult { groups: IConnectionProfileGroup[]; @@ -49,6 +55,26 @@ export class ConnectionConfig { } allGroups = allGroups.concat(userValue); } + + const sortBy = this.configurationService.getValue(CONNECTIONS_SORT_BY_CONFIG_KEY); + let sortFunc: (a: IConnectionProfileGroup, b: IConnectionProfileGroup) => number; + + if (sortBy === ConnectionsSortBy.displayName) { + sortFunc = ((a, b) => { + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } else { + return 0; + } + }); + } + + if (sortFunc) { + allGroups.sort(sortFunc); + } + return deepClone(allGroups).map(g => { if (g.parentId === '' || !g.parentId) { g.parentId = undefined; @@ -214,6 +240,25 @@ export class ConnectionConfig { return ConnectionProfile.createFromStoredProfile(p, this._capabilitiesService); }); + const sortBy = this.configurationService.getValue(CONNECTIONS_SORT_BY_CONFIG_KEY); + let sortFunc: (a: ConnectionProfile, b: ConnectionProfile) => number; + + if (sortBy === ConnectionsSortBy.displayName) { + sortFunc = ((a, b) => { + if (a.title < b.title) { + return -1; + } else if (a.title > b.title) { + return 1; + } else { + return 0; + } + }); + } + + if (sortFunc) { + connectionProfiles.sort(sortFunc); + } + return connectionProfiles; } diff --git a/src/sql/platform/connection/test/common/connectionConfig.test.ts b/src/sql/platform/connection/test/common/connectionConfig.test.ts index 378babaadf..c0af0381b5 100644 --- a/src/sql/platform/connection/test/common/connectionConfig.test.ts +++ b/src/sql/platform/connection/test/common/connectionConfig.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import * as azdata from 'azdata'; import { ProviderFeatures } from 'sql/platform/capabilities/common/capabilitiesService'; -import { ConnectionConfig, ISaveGroupResult } from 'sql/platform/connection/common/connectionConfig'; +import { ConnectionConfig, ISaveGroupResult, CONNECTIONS_SORT_BY_CONFIG_KEY, ConnectionsSortBy } from 'sql/platform/connection/common/connectionConfig'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { ConnectionProfileGroup, IConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; import { IConnectionProfile, IConnectionProfileStore, ConnectionOptionSpecialType, ServiceOptionType } from 'sql/platform/connection/common/interfaces'; @@ -239,6 +239,33 @@ suite('ConnectionConfig', () => { assert.ok(groupsAreEqual(allGroups, testGroups), 'the groups returned did not match expectation'); }); + test('getAllGroups should return groups sorted alphabetically by display name given datasource.connections.sortBy is set to \'' + ConnectionsSortBy.displayName + '\'', () => { + let configurationService = new TestConfigurationService(); + configurationService.updateValue('datasource.connectionGroups', deepClone(testGroups).slice(0, 3), ConfigurationTarget.USER); + configurationService.updateValue('datasource.connectionGroups', deepClone(testGroups).slice(2, testGroups.length), ConfigurationTarget.WORKSPACE); + configurationService.updateValue(CONNECTIONS_SORT_BY_CONFIG_KEY, ConnectionsSortBy.displayName, ConfigurationTarget.USER); + + let config = new ConnectionConfig(configurationService, capabilitiesService.object); + let allGroups = config.getAllGroups(); + + assert.equal(allGroups.length, testGroups.length, 'did not meet the expected length'); + assert.ok(groupsAreEqual(allGroups, testGroups), 'the groups returned did not match expectation'); + assert.ok(allGroups.slice(1).every((item, i) => allGroups[i].name <= item.name), 'the groups are not sorted correctly'); + }); + + test('getAllGroups should return groups sorted by date added given datasource.connections.sortBy is set to \'' + ConnectionsSortBy.dateAdded + '\'', () => { + let configurationService = new TestConfigurationService(); + configurationService.updateValue('datasource.connectionGroups', deepClone(testGroups).slice(0, 3).reverse(), ConfigurationTarget.USER); + configurationService.updateValue('datasource.connectionGroups', deepClone(testGroups).slice(2, testGroups.length).reverse(), ConfigurationTarget.WORKSPACE); + configurationService.updateValue(CONNECTIONS_SORT_BY_CONFIG_KEY, ConnectionsSortBy.dateAdded, ConfigurationTarget.USER); + + let config = new ConnectionConfig(configurationService, capabilitiesService.object); + let allGroups = config.getAllGroups(); + let expectedGroups = deepClone(testGroups).slice(0, 3).reverse().concat(deepClone(testGroups).slice(3, testGroups.length).reverse()); + assert.equal(allGroups.length, expectedGroups.length, 'The result groups length is invalid'); + assert.ok(allGroups.every((item, i) => item.id === allGroups[i].id)); + }); + test('addConnection should add the new profile to user settings', async () => { let newProfile: IConnectionProfile = { serverName: 'new server', @@ -384,6 +411,30 @@ suite('ConnectionConfig', () => { }); }); + test('getConnections should return connections sorted alphabetically by title given datasource.connections.sortBy is set to \'' + ConnectionsSortBy.displayName + '\'', () => { + let configurationService = new TestConfigurationService(); + configurationService.updateValue('datasource.connections', deepClone(testConnections).slice(0, 2).reverse(), ConfigurationTarget.USER); + configurationService.updateValue('datasource.connections', deepClone(testConnections).slice(2, testConnections.length).reverse(), ConfigurationTarget.WORKSPACE); + configurationService.updateValue(CONNECTIONS_SORT_BY_CONFIG_KEY, ConnectionsSortBy.displayName, ConfigurationTarget.USER); + + let config = new ConnectionConfig(configurationService, capabilitiesService.object); + let allConnections = config.getConnections(true); + assert.equal(allConnections.length, testConnections.length, 'The result connections length is invalid'); + assert.ok(allConnections.slice(1).every((item, i) => allConnections[i].title <= item.title), 'The connections are not sorted correctly'); + }); + + test('getConnections should return connections sorted by date added given datasource.connections.sortBy is set to \'' + ConnectionsSortBy.dateAdded + '\'', () => { + let configurationService = new TestConfigurationService(); + configurationService.updateValue('datasource.connections', deepClone(testConnections).reverse(), ConfigurationTarget.USER); + configurationService.updateValue(CONNECTIONS_SORT_BY_CONFIG_KEY, ConnectionsSortBy.dateAdded, ConfigurationTarget.USER); + + let config = new ConnectionConfig(configurationService, capabilitiesService.object); + let allConnections = config.getConnections(false); + let expectedConnections = deepClone(testConnections).reverse(); + assert.equal(allConnections.length, expectedConnections.length, 'The result connections length is invalid'); + assert.ok(allConnections.every((item, i) => item.id === expectedConnections[i].id)); + }); + test('saveGroup should save the new groups to tree and return the id of the last group name', () => { let config = new ConnectionConfig(undefined!, undefined!); let groups: IConnectionProfileGroup[] = deepClone(testGroups); diff --git a/src/sql/workbench/contrib/dataExplorer/browser/dataExplorer.contribution.ts b/src/sql/workbench/contrib/dataExplorer/browser/dataExplorer.contribution.ts index d0f549f55f..1067626b5d 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/dataExplorer.contribution.ts +++ b/src/sql/workbench/contrib/dataExplorer/browser/dataExplorer.contribution.ts @@ -11,6 +11,7 @@ import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/co import { DataExplorerContainerExtensionHandler } from 'sql/workbench/contrib/dataExplorer/browser/dataExplorerExtensionPoint'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { DataExplorerViewletViewsContribution } from 'sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet'; +import { GROUPS_CONFIG_KEY, CONNECTIONS_CONFIG_KEY, CONNECTIONS_SORT_BY_CONFIG_KEY, ConnectionsSortBy } from 'sql/platform/connection/common/connectionConfig'; const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(DataExplorerViewletViewsContribution, LifecyclePhase.Starting); @@ -22,13 +23,23 @@ configurationRegistry.registerConfiguration({ 'title': localize('databaseConnections', "Database Connections"), 'type': 'object', 'properties': { - 'datasource.connections': { + [CONNECTIONS_CONFIG_KEY]: { 'description': localize('datasource.connections', "data source connections"), 'type': 'array' }, - 'datasource.connectionGroups': { + [GROUPS_CONFIG_KEY]: { 'description': localize('datasource.connectionGroups', "data source groups"), 'type': 'array' + }, + [CONNECTIONS_SORT_BY_CONFIG_KEY]: { + 'type': 'string', + 'enum': [ConnectionsSortBy.dateAdded, ConnectionsSortBy.displayName], + 'enumDescriptions': [ + localize('connections.sortBy.dateAdded', 'Saved connections are sorted by the dates they were added.'), + localize('connections.sortBy.displayName', 'Saved connections are sorted by their display names alphabetically.') + ], + 'default': ConnectionsSortBy.dateAdded, + 'description': localize('datasource.connections.sortBy', "Order used for sorting saved connections and connection groups") } } }); diff --git a/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts b/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts index 138116af88..ae26232d9e 100644 --- a/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts +++ b/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts @@ -41,6 +41,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { AsyncServerTree, ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; import { coalesce } from 'vs/base/common/arrays'; +import { CONNECTIONS_SORT_BY_CONFIG_KEY } from 'sql/platform/connection/common/connectionConfig'; /** * ServerTreeview implements the dynamic tree view. @@ -196,6 +197,11 @@ export class ServerTreeView extends Disposable implements IServerTreeView { this.deleteObjectExplorerNodeAndRefreshTree(connectionParams.connectionProfile).catch(errors.onUnexpectedError); } })); + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(CONNECTIONS_SORT_BY_CONFIG_KEY)) { + this.refreshTree().catch(err => errors.onUnexpectedError); + } + })); if (this._objectExplorerService && this._objectExplorerService.onUpdateObjectExplorerNodes) { this._register(this._objectExplorerService.onUpdateObjectExplorerNodes(args => { diff --git a/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts b/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts index ea09f65cef..2e0d81ca3e 100644 --- a/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts +++ b/src/sql/workbench/services/connection/browser/connectionBrowseTab.ts @@ -12,6 +12,7 @@ import { ConnectionProfile } from 'sql/platform/connection/common/connectionProf import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; import { attachInputBoxStyler } from 'sql/platform/theme/common/styler'; import { ITreeItem } from 'sql/workbench/common/views'; +import { CONNECTIONS_SORT_BY_CONFIG_KEY } from 'sql/platform/connection/common/connectionConfig'; import { IConnectionTreeDescriptor, IConnectionTreeService } from 'sql/workbench/services/connection/common/connectionTreeService'; import { AsyncRecentConnectionTreeDataSource } from 'sql/workbench/services/objectExplorer/browser/asyncRecentConnectionTreeDataSource'; import { ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; @@ -91,7 +92,8 @@ export class ConnectionBrowserView extends Disposable implements IPanelView { @ICommandService private readonly commandService: ICommandService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IConnectionManagementService private readonly connectionManagementService: IConnectionManagementService, - @ICapabilitiesService private readonly capabilitiesService: ICapabilitiesService + @ICapabilitiesService private readonly capabilitiesService: ICapabilitiesService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); this.connectionTreeService.setView(this); @@ -224,6 +226,13 @@ export class ConnectionBrowserView extends Disposable implements IPanelView { this._register(this.themeService.onDidColorThemeChange(async () => { await this.refresh(); })); + + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(CONNECTIONS_SORT_BY_CONFIG_KEY)) { + this.updateSavedConnectionsNode(); + } + })); + } private handleTreeElementSelection(selectedNode: TreeElement, connect: boolean): void {