From 99d00e2057619496a4a229d639735026303ccf77 Mon Sep 17 00:00:00 2001 From: Gene Lee Date: Mon, 13 May 2019 14:52:56 -0700 Subject: [PATCH] Differentiated server icons by server type: box, big data cluster, cloud... (#5241) --- extensions/mssql/package.json | 16 ++ .../mssql/resources/dark/azureDB_inverse.svg | 45 ++++++ .../dark/sql_bigdata_cluster_inverse.svg | 46 ++++++ extensions/mssql/resources/light/azureDB.svg | 43 ++++++ .../resources/light/sql_bigdata_cluster.svg | 45 ++++++ extensions/mssql/src/iconProvider.ts | 28 ++++ extensions/mssql/src/main.ts | 4 +- src/sql/azdata.proposed.d.ts | 7 + .../connection/common/connectionManagement.ts | 7 + .../common/connectionManagementService.ts | 31 +++- src/sql/sqlops.proposed.d.ts | 3 +- .../workbench/api/common/sqlExtHostTypes.ts | 3 +- .../workbench/api/node/extHostDataProtocol.ts | 10 ++ .../api/node/mainThreadDataProtocol.ts | 10 ++ .../workbench/api/node/sqlExtHost.api.impl.ts | 5 + .../workbench/api/node/sqlExtHost.protocol.ts | 3 + .../common/connectionProviderExtension.ts | 80 ++++++++++ .../connection/common/serverInfoContextKey.ts | 10 ++ .../browser/media/connectionViewletPanel.css | 19 +-- .../browser/media/default_server.svg | 41 ++++++ .../browser/media/default_server_inverse.svg | 41 ++++++ .../objectExplorer/browser/iconRenderer.ts | 137 ++++++++++++++++++ .../browser/serverTreeRenderer.ts | 61 +++++++- .../parts/objectExplorer/common/treeNode.ts | 3 + .../stubs/connectionManagementService.test.ts | 14 ++ 25 files changed, 693 insertions(+), 19 deletions(-) create mode 100644 extensions/mssql/resources/dark/azureDB_inverse.svg create mode 100644 extensions/mssql/resources/dark/sql_bigdata_cluster_inverse.svg create mode 100644 extensions/mssql/resources/light/azureDB.svg create mode 100644 extensions/mssql/resources/light/sql_bigdata_cluster.svg create mode 100644 extensions/mssql/src/iconProvider.ts create mode 100644 src/sql/workbench/parts/dataExplorer/browser/media/default_server.svg create mode 100644 src/sql/workbench/parts/dataExplorer/browser/media/default_server_inverse.svg create mode 100644 src/sql/workbench/parts/objectExplorer/browser/iconRenderer.ts diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index a88be11b06..19ffd2b219 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -418,6 +418,22 @@ "connectionProvider": { "providerId": "MSSQL", "displayName": "%mssql.provider.displayName%", + "iconPath": [ + { + "id": "mssql:cloud", + "path": { + "light": "resources/light/azureDB.svg", + "dark": "resources/dark/azureDB_inverse.svg" + } + }, + { + "id": "mssql:cluster", + "path": { + "light": "resources/light/sql_bigdata_cluster.svg", + "dark": "resources/dark/sql_bigdata_cluster_inverse.svg" + } + } + ], "connectionOptions": [ { "specialValueType": "connectionName", diff --git a/extensions/mssql/resources/dark/azureDB_inverse.svg b/extensions/mssql/resources/dark/azureDB_inverse.svg new file mode 100644 index 0000000000..5f78cf01e7 --- /dev/null +++ b/extensions/mssql/resources/dark/azureDB_inverse.svg @@ -0,0 +1,45 @@ + + + + + + image/svg+xml + + azureDB + + + + + + + azureDB + + + + + diff --git a/extensions/mssql/resources/dark/sql_bigdata_cluster_inverse.svg b/extensions/mssql/resources/dark/sql_bigdata_cluster_inverse.svg new file mode 100644 index 0000000000..dbc823e9f0 --- /dev/null +++ b/extensions/mssql/resources/dark/sql_bigdata_cluster_inverse.svg @@ -0,0 +1,46 @@ + + + + + + image/svg+xml + + sql_bigdata_cluster + + + + + + + sql_bigdata_cluster + + + + diff --git a/extensions/mssql/resources/light/azureDB.svg b/extensions/mssql/resources/light/azureDB.svg new file mode 100644 index 0000000000..b5a7e723b2 --- /dev/null +++ b/extensions/mssql/resources/light/azureDB.svg @@ -0,0 +1,43 @@ + + + + + + image/svg+xml + + azureDB + + + + + + + azureDB + + + + + diff --git a/extensions/mssql/resources/light/sql_bigdata_cluster.svg b/extensions/mssql/resources/light/sql_bigdata_cluster.svg new file mode 100644 index 0000000000..fcf1133186 --- /dev/null +++ b/extensions/mssql/resources/light/sql_bigdata_cluster.svg @@ -0,0 +1,45 @@ + + + + + + image/svg+xml + + sql_bigdata_cluster + + + + + + + sql_bigdata_cluster + + + + diff --git a/extensions/mssql/src/iconProvider.ts b/extensions/mssql/src/iconProvider.ts new file mode 100644 index 0000000000..5f06ae94b3 --- /dev/null +++ b/extensions/mssql/src/iconProvider.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as azdata from 'azdata'; +import * as constants from './constants'; + +const cloudIcon = 'mssql:cloud'; +const clusterIcon = 'mssql:cluster'; + +export class MssqlIconProvider implements azdata.IconProvider { + public readonly providerId: string = constants.sqlProviderName; + public handle: number; + getConnectionIconId(connection: azdata.IConnectionProfile, serverInfo: azdata.ServerInfo): Thenable { + let iconName: string = undefined; + if (connection.providerName === 'MSSQL') { + if (serverInfo.isCloud) { + iconName = cloudIcon; + } else if (serverInfo.options['isBigDataCluster']) { + iconName = clusterIcon; + } + } + return Promise.resolve(iconName); + } +} \ No newline at end of file diff --git a/extensions/mssql/src/main.ts b/extensions/mssql/src/main.ts index 2c551b5df5..c6d15bad12 100644 --- a/extensions/mssql/src/main.ts +++ b/extensions/mssql/src/main.ts @@ -32,6 +32,7 @@ import { OpenSparkYarnHistoryTask } from './sparkFeature/historyTask'; import { MssqlObjectExplorerNodeProvider, mssqlOutputChannel } from './objectExplorerNodeProvider/objectExplorerNodeProvider'; import { CmsService } from './cms/cmsService'; import { registerSearchServerCommand } from './objectExplorerNodeProvider/command'; +import { MssqlIconProvider } from './iconProvider'; const baseConfig = require('./config.json'); const outputChannel = vscode.window.createOutputChannel(Constants.serviceName); @@ -110,7 +111,8 @@ export async function activate(context: vscode.ExtensionContext): Promise; + } + // Admin Services interfaces ----------------------------------------------------------------------- export interface DatabaseInfo { options: {}; @@ -3963,6 +3969,7 @@ declare module 'azdata' { DacFxServicesProvider = 'DacFxServicesProvider', SchemaCompareServicesProvider = 'SchemaCompareServicesProvider', ObjectExplorerNodeProvider = 'ObjectExplorerNodeProvider', + IconProvider = 'IconProvider' } export namespace dataprotocol { diff --git a/src/sql/platform/connection/common/connectionManagement.ts b/src/sql/platform/connection/common/connectionManagement.ts index b328ded687..01e13177c6 100644 --- a/src/sql/platform/connection/common/connectionManagement.ts +++ b/src/sql/platform/connection/common/connectionManagement.ts @@ -11,6 +11,7 @@ import { ConnectionProfile } from 'sql/platform/connection/common/connectionProf import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo'; import { IServerGroupDialogCallbacks } from 'sql/platform/serverGroup/common/serverGroupController'; +import { ConnectionProviderProperties } from 'sql/workbench/parts/connection/common/connectionProviderExtension'; export const VIEWLET_ID = 'workbench.view.connections'; @@ -182,6 +183,8 @@ export interface IConnectionManagementService { */ registerProvider(providerId: string, provider: azdata.ConnectionProvider): void; + registerIconProvider(providerId: string, provider: azdata.IconProvider): void; + editGroup(group: ConnectionProfileGroup): Promise; getConnectionProfile(fileUri: string): IConnectionProfile; @@ -273,6 +276,10 @@ export interface IConnectionManagementService { * Get connection profile by id */ getConnectionProfileById(profileId: string): IConnectionProfile; + + getProviderProperties(providerName: string): ConnectionProviderProperties; + + getConnectionIconId(connectionId: string): string; } export enum RunQueryOnConnectionMode { diff --git a/src/sql/platform/connection/common/connectionManagementService.ts b/src/sql/platform/connection/common/connectionManagementService.ts index f801ae9d85..f949328a5f 100644 --- a/src/sql/platform/connection/common/connectionManagementService.ts +++ b/src/sql/platform/connection/common/connectionManagementService.ts @@ -52,12 +52,15 @@ import { IConnectionDialogService } from 'sql/workbench/services/connection/comm import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; +import * as interfaces from './interfaces'; export class ConnectionManagementService extends Disposable implements IConnectionManagementService { _serviceBrand: any; private _providers = new Map, properties: ConnectionProviderProperties }>(); + private _iconProviders = new Map(); + private _connectionIconIdCache = new Map(); private _uriToProvider: { [uri: string]: string; } = Object.create(null); @@ -176,6 +179,10 @@ export class ConnectionManagementService extends Disposable implements IConnecti (this._providers.get(providerId).onReady as Deferred).resolve(provider); } + public registerIconProvider(providerId: string, iconProvider: azdata.IconProvider): void { + this._iconProviders.set(providerId, iconProvider); + } + /** * Opens the connection dialog * @param params Include the uri, type of connection @@ -543,10 +550,27 @@ export class ConnectionManagementService extends Disposable implements IConnecti if (options.showDashboard) { this.showDashboardForConnectionManagementInfo(connectionManagementInfo.connectionProfile); } + + let connectionProfile = connectionManagementInfo.connectionProfile; this._onConnect.fire({ connectionUri: uri, - connectionProfile: connectionManagementInfo.connectionProfile + connectionProfile: connectionProfile }); + + let iconProvider = this._iconProviders.get(connectionManagementInfo.providerId); + if (iconProvider) { + let serverInfo: azdata.ServerInfo = this.getServerInfo(connectionProfile.id); + let profile: interfaces.IConnectionProfile = connectionProfile.toIConnectionProfile(); + iconProvider.getConnectionIconId(profile, serverInfo).then(iconId => { + if (iconId) { + this._connectionIconIdCache.set(connectionProfile.id, iconId); + } + }); + } + } + + public getConnectionIconId(connectionId: string): string { + return this._connectionIconIdCache.get(connectionId); } public showDashboard(connection: IConnectionProfile): Thenable { @@ -1392,4 +1416,9 @@ export class ConnectionManagementService extends Disposable implements IConnecti } return Promise.resolve(undefined); } + + public getProviderProperties(providerName: string): ConnectionProviderProperties { + let connectionProvider = this._providers.get(providerName); + return connectionProvider && connectionProvider.properties; + } } diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 3c4db2a282..13dfd3ea23 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -1598,7 +1598,8 @@ declare module 'sqlops' { CapabilitiesProvider = 'CapabilitiesProvider', DacFxServicesProvider = 'DacFxServicesProvider', SchemaCompareServicesProvider = 'SchemaCompareServicesProvider', - ObjectExplorerNodeProvider = 'ObjectExplorerNodeProvider' + ObjectExplorerNodeProvider = 'ObjectExplorerNodeProvider', + IconProvider = 'IconProvider' } export namespace dataprotocol { diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index cebe2a5b96..6661a8ced1 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -292,7 +292,8 @@ export enum DataProviderType { CapabilitiesProvider = 'CapabilitiesProvider', DacFxServicesProvider = 'DacFxServicesProvider', SchemaCompareServicesProvider = 'SchemaCompareServicesProvider', - ObjectExplorerNodeProvider = 'ObjectExplorerNodeProvider' + ObjectExplorerNodeProvider = 'ObjectExplorerNodeProvider', + IconProvider = 'IconProvider' } export enum DeclarativeDataType { diff --git a/src/sql/workbench/api/node/extHostDataProtocol.ts b/src/sql/workbench/api/node/extHostDataProtocol.ts index c532972c07..bc3860b3c5 100644 --- a/src/sql/workbench/api/node/extHostDataProtocol.ts +++ b/src/sql/workbench/api/node/extHostDataProtocol.ts @@ -136,6 +136,12 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { return rt; } + $registerIconProvider(provider: azdata.IconProvider): vscode.Disposable { + let rt = this.registerProvider(provider, DataProviderType.IconProvider); + this._proxy.$registerIconProvider(provider.providerId, provider.handle); + return rt; + } + $registerProfilerProvider(provider: azdata.ProfilerProvider): vscode.Disposable { let rt = this.registerProvider(provider, DataProviderType.ProfilerProvider); this._proxy.$registerProfilerProvider(provider.providerId, provider.handle); @@ -330,6 +336,10 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { this._proxy.$onEditSessionReady(handle, ownerUri, success, message); } + public $getConnectionIconId(handle: number, connection: azdata.IConnectionProfile, serverInfo: azdata.ServerInfo): Thenable { + return this._resolveProvider(handle).getConnectionIconId(connection, serverInfo); + } + // Metadata handlers public $getMetadata(handle: number, connectionUri: string): Thenable { return this._resolveProvider(handle).getMetadata(connectionUri); diff --git a/src/sql/workbench/api/node/mainThreadDataProtocol.ts b/src/sql/workbench/api/node/mainThreadDataProtocol.ts index ba2c6d4ed0..70fc252d10 100644 --- a/src/sql/workbench/api/node/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/node/mainThreadDataProtocol.ts @@ -278,6 +278,16 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape { return undefined; } + public $registerIconProvider(providerId: string, handle: number): Promise { + const self = this; + this._connectionManagementService.registerIconProvider(providerId, { + getConnectionIconId(connection: azdata.IConnectionProfile, serverInfo: azdata.ServerInfo): Thenable { + return self._proxy.$getConnectionIconId(handle, connection, serverInfo); + } + }); + return undefined; + } + public $registerTaskServicesProvider(providerId: string, handle: number): Promise { const self = this; this._taskService.registerProvider(providerId, { diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index 2fbded5710..8e0bf4a71f 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -269,6 +269,10 @@ export function createApiFactory( return extHostDataProvider.$registerObjectExplorerNodeProvider(provider); }; + let registerIconProvider = (provider: azdata.IconProvider): vscode.Disposable => { + return extHostDataProvider.$registerIconProvider(provider); + }; + let registerTaskServicesProvider = (provider: azdata.TaskServicesProvider): vscode.Disposable => { provider.registerOnTaskCreated((response: azdata.TaskInfo) => { extHostDataProvider.$onTaskCreated(provider.handle, response); @@ -365,6 +369,7 @@ export function createApiFactory( registerMetadataProvider, registerObjectExplorerProvider, registerObjectExplorerNodeProvider, + registerIconProvider, registerProfilerProvider, registerRestoreProvider, registerScriptingProvider, diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 1aaaa4c983..39165b542f 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -102,6 +102,8 @@ export abstract class ExtHostDataProtocolShape { $getServerCapabilities(handle: number, client: azdata.DataProtocolClientCapabilities): Thenable { throw ni(); } + $getConnectionIconId(handle: number, connection: azdata.IConnectionProfile, serverInfo: azdata.ServerInfo): Thenable { throw ni(); } + /** * Metadata service methods * @@ -541,6 +543,7 @@ export interface MainThreadDataProtocolShape extends IDisposable { $registerProfilerProvider(providerId: string, handle: number): Promise; $registerObjectExplorerProvider(providerId: string, handle: number): Promise; $registerObjectExplorerNodeProvider(providerId: string, supportedProviderId: string, group: string, handle: number): Promise; + $registerIconProvider(providerId: string, handle: number): Promise; $registerMetadataProvider(providerId: string, handle: number): Promise; $registerTaskServicesProvider(providerId: string, handle: number): Promise; $registerFileBrowserProvider(providerId: string, handle: number): Promise; diff --git a/src/sql/workbench/parts/connection/common/connectionProviderExtension.ts b/src/sql/workbench/parts/connection/common/connectionProviderExtension.ts index b2bbcf4294..21190bb79e 100644 --- a/src/sql/workbench/parts/connection/common/connectionProviderExtension.ts +++ b/src/sql/workbench/parts/connection/common/connectionProviderExtension.ts @@ -11,6 +11,8 @@ import { Event, Emitter } from 'vs/base/common/event'; import { deepClone } from 'vs/base/common/objects'; import * as azdata from 'azdata'; +import * as path from 'path'; +import { URI } from 'vs/base/common/uri'; export interface ConnectionProviderProperties { providerId: string; @@ -66,6 +68,47 @@ const ConnectionProviderContrib: IJSONSchema = { type: 'string', description: localize('schema.displayName', "Display Name for the provider") }, + iconPath: { + description: localize('schema.iconPath', 'Icon path for the server type'), + oneOf: [ + { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + }, + path: { + type: 'object', + properties: { + light: { + type: 'string', + }, + dark: { + type: 'string', + } + } + } + } + } + }, + { + type: 'object', + properties: { + light: { + type: 'string', + }, + dark: { + type: 'string', + } + } + }, + { + type: 'string' + } + ] + }, connectionOptions: { type: 'array', description: localize('schema.connectionOptions', "Options for connection"), @@ -123,6 +166,7 @@ ExtensionsRegistry.registerExtensionPoint(value)) { for (let command of value) { handleCommand(command, extension); @@ -132,3 +176,39 @@ ExtensionsRegistry.registerExtensionPoint): void { + if (!extension || !extension.value) { return undefined; } + + let toAbsolutePath = (iconPath: any, baseDir: string) => { + if (!iconPath || !baseDir) { return; } + if (Array.isArray(iconPath)) { + for (let e of iconPath) { + e.path = { + light: URI.file(path.join(baseDir, e.path.light)), + dark: URI.file(path.join(baseDir, e.path.dark)) + }; + } + } else if (typeof iconPath === 'string') { + iconPath = { + light: URI.file(path.join(baseDir, iconPath)), + dark: URI.file(path.join(baseDir, iconPath)) + }; + } else { + iconPath = { + light: URI.file(path.join(baseDir, iconPath.light)), + dark: URI.file(path.join(baseDir, iconPath.dark)) + }; + } + }; + + let baseDir = extension.description.extensionLocation.fsPath; + let properties: ConnectionProviderProperties = extension.value; + if (Array.isArray(properties)) { + for (let p of properties) { + toAbsolutePath(p['iconPath'], baseDir); + } + } else { + toAbsolutePath(properties['iconPath'], baseDir); + } +} diff --git a/src/sql/workbench/parts/connection/common/serverInfoContextKey.ts b/src/sql/workbench/parts/connection/common/serverInfoContextKey.ts index c3ba706aa4..8ca5a88e3b 100644 --- a/src/sql/workbench/parts/connection/common/serverInfoContextKey.ts +++ b/src/sql/workbench/parts/connection/common/serverInfoContextKey.ts @@ -10,25 +10,35 @@ export class ServerInfoContextKey implements IContextKey { static ServerInfo = new RawContextKey('serverInfo', undefined); static ServerMajorVersion = new RawContextKey('serverMajorVersion', undefined); + static IsCloud = new RawContextKey('isCloud', undefined); + static IsBigDataCluster = new RawContextKey('isBigDataCluster', undefined); private _serverInfo: IContextKey; private _serverMajorVersion: IContextKey; + private _isCloud: IContextKey; + private _isBigDataCluster: IContextKey; constructor( @IContextKeyService contextKeyService: IContextKeyService ) { this._serverInfo = ServerInfoContextKey.ServerInfo.bindTo(contextKeyService); this._serverMajorVersion = ServerInfoContextKey.ServerMajorVersion.bindTo(contextKeyService); + this._isCloud = ServerInfoContextKey.IsCloud.bindTo(contextKeyService); + this._isBigDataCluster = ServerInfoContextKey.IsBigDataCluster.bindTo(contextKeyService); } set(value: ServerInfo) { this._serverInfo.set(value); let majorVersion = value && value.serverMajorVersion; this._serverMajorVersion.set(majorVersion && `${majorVersion}`); + this._isCloud.set(value && value.isCloud); + this._isBigDataCluster.set(value && value.options && value.options['isBigDataCluster']); } reset(): void { this._serverMajorVersion.reset(); + this._isCloud.reset(); + this._isBigDataCluster.reset(); } public get(): ServerInfo { diff --git a/src/sql/workbench/parts/dataExplorer/browser/media/connectionViewletPanel.css b/src/sql/workbench/parts/dataExplorer/browser/media/connectionViewletPanel.css index 2b3df4148e..1eaceff820 100644 --- a/src/sql/workbench/parts/dataExplorer/browser/media/connectionViewletPanel.css +++ b/src/sql/workbench/parts/dataExplorer/browser/media/connectionViewletPanel.css @@ -98,22 +98,13 @@ margin-bottom: 2px; padding-right: 10px; } -.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.connected { - background: url('connected_active_server.svg') center center no-repeat; +.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page { + background: url('default_server.svg') center center no-repeat; } -.vs-dark .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.connected, -.hc-black .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.connected{ - background: url('connected_active_server_inverse.svg') center center no-repeat; -} - -.monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.disconnected { - background: url('disconnected_server.svg') center center no-repeat; -} - -.vs-dark .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.disconnected, -.hc-black .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page.disconnected{ - background: url('disconnected_server_inverse.svg') center center no-repeat; +.vs-dark .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page, +.hc-black .monaco-tree .monaco-tree-rows > .monaco-tree-row > .content > .connection-tile > .icon.server-page{ + background: url('default_server_inverse.svg') center center no-repeat; } /* loading for OE node */ diff --git a/src/sql/workbench/parts/dataExplorer/browser/media/default_server.svg b/src/sql/workbench/parts/dataExplorer/browser/media/default_server.svg new file mode 100644 index 0000000000..5ada1421ef --- /dev/null +++ b/src/sql/workbench/parts/dataExplorer/browser/media/default_server.svg @@ -0,0 +1,41 @@ + + + + + + image/svg+xml + + server_16x16 + + + + + + + server_16x16 + + + diff --git a/src/sql/workbench/parts/dataExplorer/browser/media/default_server_inverse.svg b/src/sql/workbench/parts/dataExplorer/browser/media/default_server_inverse.svg new file mode 100644 index 0000000000..754baf78ca --- /dev/null +++ b/src/sql/workbench/parts/dataExplorer/browser/media/default_server_inverse.svg @@ -0,0 +1,41 @@ + + + + + + image/svg+xml + + server_16x16 + + + + + + + server_16x16 + + + diff --git a/src/sql/workbench/parts/objectExplorer/browser/iconRenderer.ts b/src/sql/workbench/parts/objectExplorer/browser/iconRenderer.ts new file mode 100644 index 0000000000..2c8cf677fd --- /dev/null +++ b/src/sql/workbench/parts/objectExplorer/browser/iconRenderer.ts @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createCSSRule } from 'vs/base/browser/dom'; +import { hash } from 'vs/base/common/hash'; +import { URI } from 'vs/base/common/uri'; + +class IconRenderer { + private iconRegistered: Set = new Set(); + + public registerIcon(path: URI | IconPath): string { + if (!path) { return undefined; } + let iconPath: IconPath = this.toIconPath(path); + let iconUid: string = this.getIconUid(iconPath); + if (!this.iconRegistered.has(iconUid)) { + createCSSRule(`.icon#${iconUid}`, `background: url("${iconPath.light.toString()}") center center no-repeat`); + createCSSRule(`.vs-dark .icon#${iconUid}, .hc-black .icon#${iconUid}`, `background: url("${iconPath.dark.toString()}") center center no-repeat`); + this.iconRegistered.add(iconUid); + } + return iconUid; + } + + public getIconUid(path: URI | IconPath): string { + if (!path) { return undefined; } + let iconPath: IconPath = this.toIconPath(path); + return `icon${hash(iconPath.light.toString() + iconPath.dark.toString())}`; + } + + private toIconPath(path: URI | IconPath): IconPath { + if (path['light']) { + return path as IconPath; + } else { + let singlePath = path as URI; + return { light: singlePath, dark: singlePath }; + } + } + + public putIcon(element: HTMLElement, path: URI | IconPath): void { + if (!element || !path) { return undefined; } + let iconUid: string = this.registerIcon(path); + element.id = iconUid; + } + + public removeIcon(element: HTMLElement): void { + if (!element) { return undefined; } + element.id = undefined; + } +} + +export const iconRenderer: IconRenderer = new IconRenderer(); + +class BadgeRenderer { + public readonly serverConnected: string = 'serverConnected'; + public readonly serverDisconnected: string = 'serverDisconnected'; + public readonly newTag: string = 'newTag'; + + private badgeCreated: Set = new Set(); + + constructor() { + this.createBadge(this.serverConnected, this.getConnectionStatusBadge(true)); + this.createBadge(this.serverDisconnected, this.getConnectionStatusBadge(false)); + this.createBadge(this.newTag, this.getNewTagBadge()); + } + + private getConnectionStatusBadge(isConnected: boolean) { + let circleColor: string = isConnected ? 'rgba(59, 180, 74, 100%)' : 'rgba(208, 46, 0, 100%)'; + let bgColor: string = isConnected ? 'rgba(59, 180, 74, 100%)' : 'rgba(255, 255, 255, 80%)'; + return `position: absolute; + height: 0.25rem; + width: 0.25rem; + top: 14px; + left: 19px; + border: 0.12rem solid ${circleColor}; + border-radius: 100%; + background: ${bgColor}; + content:""; + font-size: 100%; + line-height: 100%; + color:white; + text-align:center; + vertical-align:middle;` + .replace(/\t/g, ' ').replace(/\r?\n/g, ' ').replace(/ +/g, ' '); + } + + private getNewTagBadge(): string { + return `position: absolute; + height: 0.4rem; + width: 0.4rem; + top: 3px; + left: 5px; + border: 1px solid green; + border-radius: 15%; + background: green; + content:"N"; + font-size: 0.3rem; + font-weight: bold; + line-height: 0.4rem; + color: white; + text-align:center; + vertical-align:middle;` + .replace(/\t/g, ' ').replace(/\r?\n/g, ' ').replace(/ +/g, ' '); + } + + private createBadge(badgeClass: string, badge: string): void { + if (!this.badgeCreated.has(badgeClass)) { + createCSSRule(`.${badgeClass}:after`, badge); + this.badgeCreated.add(badgeClass); + } + } + + public addBadge(element: HTMLElement, badgeClass: string): void { + element.innerHTML = (element.innerHTML || '') + + `
`; + } + + public removeBadge(element: HTMLElement, badgeClass: string): void { + let children: HTMLCollection = element.children; + let current = children[0]; + while (current) { + let next = current.nextElementSibling; + if (current.classList.contains(badgeClass)) { + current.remove(); + break; + } + current = next; + } + } +} + +export const badgeRenderer: BadgeRenderer = new BadgeRenderer(); + +interface IconPath { + light: URI; + dark: URI; +} diff --git a/src/sql/workbench/parts/objectExplorer/browser/serverTreeRenderer.ts b/src/sql/workbench/parts/objectExplorer/browser/serverTreeRenderer.ts index a4e309c329..e5c5312bd7 100644 --- a/src/sql/workbench/parts/objectExplorer/browser/serverTreeRenderer.ts +++ b/src/sql/workbench/parts/objectExplorer/browser/serverTreeRenderer.ts @@ -14,6 +14,9 @@ import { ITree, IRenderer } from 'vs/base/parts/tree/browser/tree'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; import { TreeNode } from 'sql/workbench/parts/objectExplorer/common/treeNode'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { badgeRenderer, iconRenderer } from 'sql/workbench/parts/objectExplorer/browser/iconRenderer'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { URI } from 'vs/base/common/uri'; export interface IConnectionTemplateData { root: HTMLElement; @@ -56,7 +59,8 @@ export class ServerTreeRenderer implements IRenderer { constructor( isCompact: boolean, - @IConnectionManagementService private _connectionManagementService: IConnectionManagementService + @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, + @IContextKeyService private _contextKeyService: IContextKeyService ) { // isCompact defaults to false unless explicitly set by instantiation call. if (isCompact) { @@ -152,19 +156,70 @@ export class ServerTreeRenderer implements IRenderer { let iconLowerCaseName = iconName.toLocaleLowerCase(); templateData.icon.classList.add(iconLowerCaseName); + if (treeNode.iconPath) { + iconRenderer.putIcon(templateData.icon, treeNode.iconPath); + } + templateData.label.textContent = treeNode.label; templateData.root.title = treeNode.label; } + private getIconPath(connection: ConnectionProfile): IconPath { + if (!connection) { return undefined; } + + if (connection['iconPath']) { + return connection['iconPath']; + } + + let iconId = this._connectionManagementService.getConnectionIconId(connection.id); + if (!iconId) { return undefined; } + + let providerProperties = this._connectionManagementService.getProviderProperties(connection.providerName); + if (!providerProperties) { return undefined; } + + let iconPath: IconPath = undefined; + let pathConfig: URI | IconPath | { id: string, path: IconPath }[] = providerProperties['iconPath']; + if (Array.isArray(pathConfig)) { + for (const e of pathConfig) { + if (!e.id || e.id === iconId) { + iconPath = e.path; + connection['iconPath'] = iconPath; + break; + } + } + } else if (pathConfig['light']) { + iconPath = pathConfig as IconPath; + connection['iconPath'] = iconPath; + } else { + let singlePath = pathConfig as URI; + iconPath = { light: singlePath, dark: singlePath }; + connection['iconPath'] = iconPath; + } + return iconPath; + } + + private renderServerIcon(element: HTMLElement, iconPath: IconPath, isConnected: boolean): void { + if (!element) { return; } + if (iconPath) { + iconRenderer.putIcon(element, iconPath); + } + let badgeToRemove: string = isConnected ? badgeRenderer.serverDisconnected : badgeRenderer.serverConnected; + let badgeToAdd: string = isConnected ? badgeRenderer.serverConnected : badgeRenderer.serverDisconnected; + badgeRenderer.removeBadge(element, badgeToRemove); + badgeRenderer.addBadge(element, badgeToAdd); + } private renderConnection(connection: ConnectionProfile, templateData: IConnectionTemplateData): void { if (!this._isCompact) { + let iconPath: IconPath = this.getIconPath(connection); if (this._connectionManagementService.isConnected(undefined, connection)) { templateData.icon.classList.remove('disconnected'); templateData.icon.classList.add('connected'); + this.renderServerIcon(templateData.icon, iconPath, true); } else { templateData.icon.classList.remove('connected'); templateData.icon.classList.add('disconnected'); + this.renderServerIcon(templateData.icon, iconPath, false); } } @@ -217,3 +272,7 @@ export class ServerTreeRenderer implements IRenderer { } } +interface IconPath { + light: URI; + dark: URI; +} diff --git a/src/sql/workbench/parts/objectExplorer/common/treeNode.ts b/src/sql/workbench/parts/objectExplorer/common/treeNode.ts index 833efc8fcd..58a863a8b8 100644 --- a/src/sql/workbench/parts/objectExplorer/common/treeNode.ts +++ b/src/sql/workbench/parts/objectExplorer/common/treeNode.ts @@ -8,6 +8,7 @@ import { NodeType, SqlThemeIcon } from 'sql/workbench/parts/objectExplorer/commo import * as azdata from 'sqlops'; import * as UUID from 'vs/base/common/uuid'; +import { URI } from 'vs/base/common/uri'; export enum TreeItemCollapsibleState { None = 0, @@ -91,6 +92,8 @@ export class TreeNode { public iconType: string | SqlThemeIcon; + public iconPath: URI | { light: URI, dark: URI }; + constructor(nodeTypeId: string, label: string, isAlwaysLeaf: boolean, nodePath: string, nodeSubType: string, nodeStatus: string, parent: TreeNode, metadata: azdata.ObjectMetadata, iconType: string | SqlThemeIcon, diff --git a/src/sqltest/stubs/connectionManagementService.test.ts b/src/sqltest/stubs/connectionManagementService.test.ts index fdaf10752d..19effabca3 100644 --- a/src/sqltest/stubs/connectionManagementService.test.ts +++ b/src/sqltest/stubs/connectionManagementService.test.ts @@ -11,6 +11,8 @@ import { ConnectionProfile } from 'sql/platform/connection/common/connectionProf import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo'; import * as azdata from 'azdata'; import { Event, Emitter } from 'vs/base/common/event'; +import { isUndefinedOrNull } from 'vs/base/common/types'; +import { ConnectionProviderProperties } from 'sql/workbench/parts/connection/common/connectionProviderExtension'; // Test stubs for commonly used objects @@ -35,6 +37,10 @@ export class TestConnectionManagementService implements IConnectionManagementSer } + registerIconProvider(providerId: string, provider: azdata.IconProvider): void { + + } + showConnectionDialog(params?: INewConnectionParams, model?: IConnectionProfile, connectionResult?: IConnectionResult): Promise { return undefined; } @@ -273,4 +279,12 @@ export class TestConnectionManagementService implements IConnectionManagementSer getConnectionProfileById(profileId: string): IConnectionProfile { return undefined; } + + getProviderProperties(providerName: string): ConnectionProviderProperties { + return undefined; + } + + getConnectionIconId(connectionId: string): string { + return undefined; + } } \ No newline at end of file