Differentiated server icons by server type: box, big data cluster, cloud... (#5241)

This commit is contained in:
Gene Lee
2019-05-13 14:52:56 -07:00
committed by GitHub
parent 7da0dddaa9
commit 99d00e2057
25 changed files with 693 additions and 19 deletions

View File

@@ -292,7 +292,8 @@ export enum DataProviderType {
CapabilitiesProvider = 'CapabilitiesProvider',
DacFxServicesProvider = 'DacFxServicesProvider',
SchemaCompareServicesProvider = 'SchemaCompareServicesProvider',
ObjectExplorerNodeProvider = 'ObjectExplorerNodeProvider'
ObjectExplorerNodeProvider = 'ObjectExplorerNodeProvider',
IconProvider = 'IconProvider'
}
export enum DeclarativeDataType {

View File

@@ -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<string> {
return this._resolveProvider<azdata.IconProvider>(handle).getConnectionIconId(connection, serverInfo);
}
// Metadata handlers
public $getMetadata(handle: number, connectionUri: string): Thenable<azdata.ProviderMetadata> {
return this._resolveProvider<azdata.MetadataProvider>(handle).getMetadata(connectionUri);

View File

@@ -278,6 +278,16 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape {
return undefined;
}
public $registerIconProvider(providerId: string, handle: number): Promise<any> {
const self = this;
this._connectionManagementService.registerIconProvider(providerId, <azdata.IconProvider>{
getConnectionIconId(connection: azdata.IConnectionProfile, serverInfo: azdata.ServerInfo): Thenable<string> {
return self._proxy.$getConnectionIconId(handle, connection, serverInfo);
}
});
return undefined;
}
public $registerTaskServicesProvider(providerId: string, handle: number): Promise<any> {
const self = this;
this._taskService.registerProvider(providerId, <azdata.TaskServicesProvider>{

View File

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

View File

@@ -102,6 +102,8 @@ export abstract class ExtHostDataProtocolShape {
$getServerCapabilities(handle: number, client: azdata.DataProtocolClientCapabilities): Thenable<azdata.DataProtocolServerCapabilities> { throw ni(); }
$getConnectionIconId(handle: number, connection: azdata.IConnectionProfile, serverInfo: azdata.ServerInfo): Thenable<string> { throw ni(); }
/**
* Metadata service methods
*
@@ -541,6 +543,7 @@ export interface MainThreadDataProtocolShape extends IDisposable {
$registerProfilerProvider(providerId: string, handle: number): Promise<any>;
$registerObjectExplorerProvider(providerId: string, handle: number): Promise<any>;
$registerObjectExplorerNodeProvider(providerId: string, supportedProviderId: string, group: string, handle: number): Promise<any>;
$registerIconProvider(providerId: string, handle: number): Promise<any>;
$registerMetadataProvider(providerId: string, handle: number): Promise<any>;
$registerTaskServicesProvider(providerId: string, handle: number): Promise<any>;
$registerFileBrowserProvider(providerId: string, handle: number): Promise<any>;

View File

@@ -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<ConnectionProviderProperties | Connect
for (let extension of extensions) {
const { value } = extension;
resolveIconPath(extension);
if (Array.isArray<ConnectionProviderProperties>(value)) {
for (let command of value) {
handleCommand(command, extension);
@@ -132,3 +176,39 @@ ExtensionsRegistry.registerExtensionPoint<ConnectionProviderProperties | Connect
}
}
});
function resolveIconPath(extension: IExtensionPointUser<any>): 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<ConnectionProviderProperties>(properties)) {
for (let p of properties) {
toAbsolutePath(p['iconPath'], baseDir);
}
} else {
toAbsolutePath(properties['iconPath'], baseDir);
}
}

View File

@@ -10,25 +10,35 @@ export class ServerInfoContextKey implements IContextKey<ServerInfo> {
static ServerInfo = new RawContextKey<ServerInfo>('serverInfo', undefined);
static ServerMajorVersion = new RawContextKey<string>('serverMajorVersion', undefined);
static IsCloud = new RawContextKey<boolean>('isCloud', undefined);
static IsBigDataCluster = new RawContextKey<boolean>('isBigDataCluster', undefined);
private _serverInfo: IContextKey<ServerInfo>;
private _serverMajorVersion: IContextKey<string>;
private _isCloud: IContextKey<boolean>;
private _isBigDataCluster: IContextKey<boolean>;
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 {

View File

@@ -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 */

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
viewBox="0 0 16 16"
data-name="Layer 1"
id="Layer_1">
<metadata
id="metadata15">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>server_16x16</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4">
<style
id="style2">.cls-1{fill:#212121;}.cls-2{fill:#231f20;}</style>
</defs>
<title
id="title6">server_16x16</title>
<path
style="fill:#212121"
id="path8"
d="m 2.735,0 v 16 h 10.53 V 0 Z m 1,1 h 8.53 v 9 h -8.53 z m 8.53,14 h -8.53 v -4 h 8.53 z"
class="cls-1" />
<path
style="fill:#231f20"
id="path10"
d="M 7.125,4.23 H 4.675 V 1.77 h 2.45 z m -2,-0.5 h 1.5 V 2.27 h -1.45 z"
class="cls-2" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
viewBox="0 0 16 16"
data-name="Layer 1"
id="Layer_1">
<metadata
id="metadata15">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>server_16x16</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4">
<style
id="style2">.cls-1{fill:#212121;}.cls-2{fill:#231f20;}</style>
</defs>
<title
id="title6">server_16x16</title>
<path
style="fill:#ffffff"
id="path8"
d="m 2.735,0 v 16 h 10.53 V 0 Z m 1,1 h 8.53 v 9 h -8.53 z m 8.53,14 h -8.53 v -4 h 8.53 z"
class="cls-1" />
<path
style="fill:#ffffff"
id="path10"
d="M 7.125,4.23 H 4.675 V 1.77 h 2.45 z m -2,-0.5 h 1.5 V 2.27 h -1.45 z"
class="cls-2" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -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<string> = new Set<string>();
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<string> = new Set<string>();
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 || '') +
`<div class="${badgeClass}" style="width: 0px; height: 0px;"><div>`;
}
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;
}

View File

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

View File

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