diff --git a/build/builtInExtensions-insiders.json b/build/builtInExtensions-insiders.json new file mode 100644 index 0000000000..fc6bc1f24c --- /dev/null +++ b/build/builtInExtensions-insiders.json @@ -0,0 +1,7 @@ +[ + { + "name": "Microsoft.sqlservernotebook", + "version": "0.1.0", + "repo": "https://github.com/Microsoft/azuredatastudio" + } +] diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js index 72fa076f81..ae8fa2735d 100644 --- a/build/lib/builtInExtensions.js +++ b/build/lib/builtInExtensions.js @@ -18,7 +18,9 @@ const fancyLog = require('fancy-log'); const ansiColors = require('ansi-colors'); const root = path.dirname(path.dirname(__dirname)); -const builtInExtensions = require('../builtInExtensions.json'); +// {{SQL CARBON EDIT}} +const builtInExtensions = require('../builtInExtensions-insiders.json'); +// {{SQL CARBON EDIT}} - END const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); function getExtensionPath(extension) { diff --git a/build/lib/extensions.js b/build/lib/extensions.js index a97c65b0b4..dd09148894 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -151,8 +151,9 @@ const baseHeaders = { 'X-Market-User-Id': '291C1CD0-051A-4123-9B4B-30D60EF52EE2', }; function fromMarketplace(extensionName, version, metadata) { - const [publisher, name] = extensionName.split('.'); - const url = `https://marketplace.visualstudio.com/_apis/public/gallery/publishers/${publisher}/vsextensions/${name}/${version}/vspackage`; + // {{SQL CARBON EDIT}} + const [, name] = extensionName.split('.'); + const url = `https://sqlopsextensions.blob.core.windows.net/extensions/${name}/${name}-${version}.vsix`; fancyLog('Downloading extension:', ansiColors.yellow(`${extensionName}@${version}`), '...'); const options = { base: url, @@ -199,7 +200,8 @@ const sqlBuiltInExtensions = [ if (process.env['VSCODE_QUALITY'] === 'stable') { sqlBuiltInExtensions.push('resource-deployment'); } -const builtInExtensions = require('../builtInExtensions.json'); +const builtInExtensions = process.env['VSCODE_QUALITY'] === 'stable' ? require('../builtInExtensions.json') : require('../builtInExtensions-insiders.json'); +// {{SQL CARBON EDIT}} - End function packageLocalExtensionsStream() { const localExtensionDescriptions = glob.sync('extensions/*/package.json') .map(manifestPath => { diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index e1c4c77dac..8314839266 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -179,8 +179,9 @@ const baseHeaders = { }; export function fromMarketplace(extensionName: string, version: string, metadata: any): Stream { - const [publisher, name] = extensionName.split('.'); - const url = `https://marketplace.visualstudio.com/_apis/public/gallery/publishers/${publisher}/vsextensions/${name}/${version}/vspackage`; + // {{SQL CARBON EDIT}} + const [, name] = extensionName.split('.'); + const url = `https://sqlopsextensions.blob.core.windows.net/extensions/${name}/${name}-${version}.vsix`; fancyLog('Downloading extension:', ansiColors.yellow(`${extensionName}@${version}`), '...'); @@ -235,8 +236,6 @@ if (process.env['VSCODE_QUALITY'] === 'stable') { } -// {{SQL CARBON EDIT}} - End - interface IBuiltInExtension { name: string; version: string; @@ -244,7 +243,10 @@ interface IBuiltInExtension { metadata: any; } -const builtInExtensions: IBuiltInExtension[] = require('../builtInExtensions.json'); +const builtInExtensions: IBuiltInExtension[] = process.env['VSCODE_QUALITY'] === 'stable' ? require('../builtInExtensions.json') : require('../builtInExtensions-insiders.json'); + +// {{SQL CARBON EDIT}} - End + export function packageLocalExtensionsStream(): NodeJS.ReadWriteStream { const localExtensionDescriptions = (glob.sync('extensions/*/package.json')) diff --git a/extensions/mssql/src/dashboard/bookExtensions.ts b/extensions/mssql/src/dashboard/bookExtensions.ts new file mode 100644 index 0000000000..1926e584f7 --- /dev/null +++ b/extensions/mssql/src/dashboard/bookExtensions.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as arrays from '../util/arrays'; +import { Disposable } from '../util/dispose'; + +const resolveExtensionResource = (extension: vscode.Extension, resourcePath: string): vscode.Uri => { + return vscode.Uri.file(path.join(extension.extensionPath, resourcePath)); +}; + +const resolveBookResources = (extension: vscode.Extension, books: BookContribution | BookContribution[]): BookContribution[] => { + if (!books) { + return []; + } + if (!Array.isArray(books)) { + books = [books]; + } + let result = books.map(book => { + try { + book.path = resolveExtensionResource(extension, book.path).fsPath; + } catch (e) { + // noop + } + return book; + }); + return result; +}; + +export interface BookContribution { + name: string; + path: string; + when?: string; +} + +export namespace BookContributions { + + export function merge(a: BookContribution[], b: BookContribution[]): BookContribution[] { + if (!a) { + return b; + } else if (!b) { + return a; + } + return a.concat(b); + } + + export function equal(a: BookContribution, b: BookContribution): boolean { + return (a.name === b.name) + && (a.path === b.path) + && (a.when === b.when); + } + + export function fromExtension( + extension: vscode.Extension + ): BookContribution[] { + const contributions = extension.packageJSON && extension.packageJSON.contributes; + if (!contributions) { + return []; + } + + return getContributedBooks(contributions, extension); + } + + + function getContributedBooks( + contributes: any, + extension: vscode.Extension + ): BookContribution[] { + if (contributes['notebook.books']) { + return resolveBookResources(extension, contributes['notebook.books']); + } + return []; + } +} + +export interface BookContributionProvider { + readonly extensionPath: string; + readonly contributions: BookContribution[]; + readonly onContributionsChanged: vscode.Event; + + dispose(): void; +} + +class AzdataExtensionBookContributionProvider extends Disposable implements BookContributionProvider { + private _contributions?: BookContribution[]; + + public constructor( + public readonly extensionPath: string, + ) { + super(); + + vscode.extensions.onDidChange(() => { + const currentContributions = this.getCurrentContributions(); + const existingContributions = this._contributions || undefined; + if (!arrays.equals(existingContributions, currentContributions, BookContributions.equal)) { + this._contributions = currentContributions; + this._onContributionsChanged.fire(this); + } + }, undefined, this._disposables); + } + + private readonly _onContributionsChanged = this._register(new vscode.EventEmitter()); + public readonly onContributionsChanged = this._onContributionsChanged.event; + + public get contributions(): BookContribution[] { + if (!this._contributions) { + this._contributions = this.getCurrentContributions(); + } + return this._contributions; + } + + private getCurrentContributions(): BookContribution[] { + return vscode.extensions.all + .map(BookContributions.fromExtension) + .reduce(BookContributions.merge, []); + } +} + +export function getBookExtensionContributions(context: vscode.ExtensionContext): BookContributionProvider { + return new AzdataExtensionBookContributionProvider(context.extensionPath); +} \ No newline at end of file diff --git a/extensions/mssql/src/dashboard/serviceEndpoints.ts b/extensions/mssql/src/dashboard/serviceEndpoints.ts new file mode 100644 index 0000000000..eebf0f5c60 --- /dev/null +++ b/extensions/mssql/src/dashboard/serviceEndpoints.ts @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import * as Utils from '../utils'; + +export function registerServiceEndpoints(context: vscode.ExtensionContext): void { + azdata.ui.registerModelViewProvider('bdc-endpoints', async (view) => { + + const endpointsArray: Array = Object.assign([], view.serverInfo.options['clusterEndpoints']); + endpointsArray.forEach(endpointInfo => { + endpointInfo.isHyperlink = true; + endpointInfo.hyperlink = 'https://' + endpointInfo.ipAddress + ':' + endpointInfo.port; + + }); + if (endpointsArray.length > 0) { + const managementProxyEp = endpointsArray.find(e => e.serviceName === 'management-proxy' || e.serviceName === 'mgmtproxy'); + if (managementProxyEp) { + endpointsArray.push(getCustomEndpoint(managementProxyEp, localize("grafana", "Metrics Dashboard"), '/grafana/d/wZx3OUdmz')); + endpointsArray.push(getCustomEndpoint(managementProxyEp, localize("kibana", "Log Search Dashboard"), '/kibana/app/kibana#/discover')); + } + + const gatewayEp = endpointsArray.find(e => e.serviceName === 'gateway'); + if (gatewayEp) { + endpointsArray.push(getCustomEndpoint(gatewayEp, localize("sparkHostory", "Spark Job Monitoring"), '/gateway/default/sparkhistory')); + endpointsArray.push(getCustomEndpoint(gatewayEp, localize("yarnHistory", "Spark Resource Management"), '/gateway/default/yarn')); + } + + const container = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '100%', height: '100%', alignItems: 'left' }).component(); + endpointsArray.forEach(endpointInfo => { + const endPointRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); + const nameCell = view.modelBuilder.text().withProperties({ value: getFriendlyEndpointNames(endpointInfo.serviceName) }).component(); + endPointRow.addItem(nameCell, { CSSStyles: { 'width': '35%', 'font-weight': '600', 'user-select': 'text' } }); + if (endpointInfo.isHyperlink) { + const linkCell = view.modelBuilder.hyperlink().withProperties({ label: endpointInfo.hyperlink, url: endpointInfo.hyperlink }).component(); + endPointRow.addItem(linkCell, { CSSStyles: { 'width': '62%', 'color': '#0078d4', 'text-decoration': 'underline', 'padding-top': '10px' } }); + } + else { + const endpointCell = view.modelBuilder.text().withProperties({ value: endpointInfo.ipAddress + ':' + endpointInfo.port }).component(); + endPointRow.addItem(endpointCell, { CSSStyles: { 'width': '62%', 'user-select': 'text' } }); + } + const copyValueCell = view.modelBuilder.button().component(); + copyValueCell.iconPath = { light: context.asAbsolutePath('resources/light/copy.png'), dark: context.asAbsolutePath('resources/dark/copy_inverse.png') }; + copyValueCell.onDidClick(() => { + vscode.env.clipboard.writeText(endpointInfo.hyperlink); + }); + copyValueCell.title = localize("copyText", "Copy"); + copyValueCell.iconHeight = '14px'; + copyValueCell.iconWidth = '14px'; + endPointRow.addItem(copyValueCell, { CSSStyles: { 'width': '3%', 'padding-top': '10px' } }); + + container.addItem(endPointRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } }); + }); + const endpointsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '540px', height: '100%', alignItems: 'left', position: 'absolute' }).component(); + endpointsContainer.addItem(container, { CSSStyles: { 'padding-top': '25px', 'padding-left': '5px' } }); + + await view.initializeModel(endpointsContainer); + } + }); +} + +function getCustomEndpoint(parentEndpoint: Utils.IEndpoint, serviceName: string, serviceUrl?: string): Utils.IEndpoint { + if (parentEndpoint) { + let endpoint: Utils.IEndpoint = { + serviceName: serviceName, + ipAddress: parentEndpoint.ipAddress, + port: parentEndpoint.port, + isHyperlink: serviceUrl ? true : false, + hyperlink: 'https://' + parentEndpoint.ipAddress + ':' + parentEndpoint.port + serviceUrl + }; + return endpoint; + } + return null; +} + +function getFriendlyEndpointNames(name: string): string { + let friendlyName: string = name; + switch (name) { + case 'app-proxy': + friendlyName = localize("appproxy", "Application Proxy"); + break; + case 'controller': + friendlyName = localize("controller", "Cluster Management Service"); + break; + case 'gateway': + friendlyName = localize("gateway", "HDFS and Spark"); + break; + case 'management-proxy': + friendlyName = localize("managementproxy", "Management Proxy"); + break; + case 'mgmtproxy': + friendlyName = localize("mgmtproxy", "Management Proxy"); + break; + default: + break; + } + return friendlyName; +} \ No newline at end of file diff --git a/extensions/mssql/src/main.ts b/extensions/mssql/src/main.ts index 548fb2aedd..6761fb3545 100644 --- a/extensions/mssql/src/main.ts +++ b/extensions/mssql/src/main.ts @@ -33,6 +33,8 @@ import { MssqlObjectExplorerNodeProvider, mssqlOutputChannel } from './objectExp import { CmsService } from './cms/cmsService'; import { registerSearchServerCommand } from './objectExplorerNodeProvider/command'; import { MssqlIconProvider } from './iconProvider'; +import { registerServiceEndpoints } from './dashboard/serviceEndpoints'; +import { getBookExtensionContributions } from './dashboard/bookExtensions'; const baseConfig = require('./config.json'); const outputChannel = vscode.window.createOutputChannel(Constants.serviceName); @@ -63,23 +65,7 @@ export async function activate(context: vscode.ExtensionContext): Promise languageClient.stop() }); - azdata.ui.registerModelViewProvider('bdc-endpoints', async (view) => { - - const endpointsArray: Array = Object.assign([], view.serverInfo.options['clusterEndpoints']); - endpointsArray.forEach(endpointInfo => { - endpointInfo.isHyperlink = true; - endpointInfo.hyperlink = 'https://' + endpointInfo.ipAddress + ':' + endpointInfo.port; - - }); - if (endpointsArray.length > 0) { - const managementProxyEp = endpointsArray.find(e => e.serviceName === 'management-proxy' || e.serviceName === 'mgmtproxy'); - if (managementProxyEp) { - endpointsArray.push(getCustomEndpoint(managementProxyEp, localize("grafana", "Metrics Dashboard"), '/grafana/d/wZx3OUdmz')); - endpointsArray.push(getCustomEndpoint(managementProxyEp, localize("kibana", "Log Search Dashboard"), '/kibana/app/kibana#/discover')); - } - - const gatewayEp = endpointsArray.find(e => e.serviceName === 'gateway'); - if (gatewayEp) { - endpointsArray.push(getCustomEndpoint(gatewayEp, localize("sparkHostory", "Spark Job Monitoring"), '/gateway/default/sparkhistory')); - endpointsArray.push(getCustomEndpoint(gatewayEp, localize("yarnHistory", "Spark Resource Management"), '/gateway/default/yarn')); - } - - const container = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '100%', height: '100%', alignItems: 'left' }).component(); - endpointsArray.forEach(endpointInfo => { - const endPointRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); - const nameCell = view.modelBuilder.text().withProperties({ value: getFriendlyEndpointNames(endpointInfo.serviceName) }).component(); - endPointRow.addItem(nameCell, { CSSStyles: { 'width': '35%', 'font-weight': '600', 'user-select': 'text' } }); - if (endpointInfo.isHyperlink) { - const linkCell = view.modelBuilder.hyperlink().withProperties({ label: endpointInfo.hyperlink, url: endpointInfo.hyperlink }).component(); - endPointRow.addItem(linkCell, { CSSStyles: { 'width': '62%', 'color': '#0078d4', 'text-decoration': 'underline', 'padding-top': '10px' } }); - } - else { - const endpointCell = view.modelBuilder.text().withProperties({ value: endpointInfo.ipAddress + ':' + endpointInfo.port }).component(); - endPointRow.addItem(endpointCell, { CSSStyles: { 'width': '62%', 'user-select': 'text' } }); - } - const copyValueCell = view.modelBuilder.button().component(); - copyValueCell.iconPath = { light: context.asAbsolutePath('resources/light/copy.png'), dark: context.asAbsolutePath('resources/dark/copy_inverse.png') }; - copyValueCell.onDidClick(() => { - vscode.env.clipboard.writeText(endpointInfo.hyperlink); - }); - copyValueCell.title = localize("copyText", "Copy"); - copyValueCell.iconHeight = '14px'; - copyValueCell.iconWidth = '14px'; - endPointRow.addItem(copyValueCell, { CSSStyles: { 'width': '3%', 'padding-top': '10px' } }); - - container.addItem(endPointRow, { CSSStyles: { 'padding-left': '10px', 'border-top': 'solid 1px #ccc', 'box-sizing': 'border-box', 'user-select': 'text' } }); - }); - const endpointsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '540px', height: '100%', alignItems: 'left', position: 'absolute' }).component(); - endpointsContainer.addItem(container, { CSSStyles: { 'padding-top': '25px', 'padding-left': '5px' } }); - - await view.initializeModel(endpointsContainer); - } - - }); - - function getCustomEndpoint(parentEndpoint: Utils.IEndpoint, serviceName: string, serviceUrl?: string): Utils.IEndpoint { - if (parentEndpoint) { - let endpoint: Utils.IEndpoint = { - serviceName: serviceName, - ipAddress: parentEndpoint.ipAddress, - port: parentEndpoint.port, - isHyperlink: serviceUrl ? true : false, - hyperlink: 'https://' + parentEndpoint.ipAddress + ':' + parentEndpoint.port + serviceUrl - }; - return endpoint; - } - return null; - } - - function getFriendlyEndpointNames(name: string): string { - let friendlyName: string = name; - switch (name) { - case 'app-proxy': - friendlyName = localize("appproxy", "Application Proxy"); - break; - case 'controller': - friendlyName = localize("controller", "Cluster Management Service"); - break; - case 'gateway': - friendlyName = localize("gateway", "HDFS and Spark"); - break; - case 'management-proxy': - friendlyName = localize("managementproxy", "Management Proxy"); - break; - case 'mgmtproxy': - friendlyName = localize("mgmtproxy", "Management Proxy"); - break; - default: - break; - } - return friendlyName; - } + registerServiceEndpoints(context); + // Get book contributions - in the future this will be integrated with the Books/Notebook widget to show as a dashboard widget + const bookContributionProvider = getBookExtensionContributions(context); + context.subscriptions.push(bookContributionProvider); let api: MssqlExtensionApi = { getMssqlObjectExplorerBrowser(): MssqlObjectExplorerBrowser { @@ -242,6 +137,35 @@ export async function activate(context: vscode.ExtensionContext): Promise(one: ReadonlyArray, other: ReadonlyArray, itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean { + if (one.length !== other.length) { + return false; + } + + for (let i = 0, len = one.length; i < len; i++) { + if (!itemEquals(one[i], other[i])) { + return false; + } + } + + return true; +} + +export function flatten(arr: ReadonlyArray[]): T[] { + return ([] as T[]).concat.apply([], arr); +} \ No newline at end of file diff --git a/extensions/mssql/src/util/dispose.ts b/extensions/mssql/src/util/dispose.ts new file mode 100644 index 0000000000..83ac3bf543 --- /dev/null +++ b/extensions/mssql/src/util/dispose.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export function disposeAll(disposables: vscode.Disposable[]) { + while (disposables.length) { + const item = disposables.pop(); + if (item) { + item.dispose(); + } + } +} + +export abstract class Disposable { + private _isDisposed = false; + + protected _disposables: vscode.Disposable[] = []; + + public dispose(): any { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + disposeAll(this._disposables); + } + + protected _register(value: T): T { + if (this._isDisposed) { + value.dispose(); + } else { + this._disposables.push(value); + } + return value; + } + + protected get isDisposed() { + return this._isDisposed; + } +} \ No newline at end of file diff --git a/scripts/sql.bat b/scripts/sql.bat index efce9a2888..5cffe2323d 100644 --- a/scripts/sql.bat +++ b/scripts/sql.bat @@ -17,6 +17,9 @@ set CODE=".build\electron\%NAMESHORT%" node build\lib\electron.js if %errorlevel% neq 0 node .\node_modules\gulp\bin\gulp.js electron +:: Sync built-in extensions +node build\lib\builtInExtensions.js + :: Build if not exist out node .\node_modules\gulp\bin\gulp.js compile diff --git a/scripts/sql.sh b/scripts/sql.sh index fe3d014c63..fcade57786 100755 --- a/scripts/sql.sh +++ b/scripts/sql.sh @@ -27,6 +27,9 @@ function code() { # Get electron (test -f "$CODE" && [ $INTENDED_VERSION == $INSTALLED_VERSION ]) || ./node_modules/.bin/gulp electron + # Sync built-in extensions + node build/lib/builtInExtensions.js + # Build test -d out || ./node_modules/.bin/gulp compile diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index e71e2ded79..bbebf60281 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -951,7 +951,9 @@ export class ExtensionManagementService extends Disposable implements IExtension private _devSystemExtensionsFilePath: string | null = null; private get devSystemExtensionsFilePath(): string { if (!this._devSystemExtensionsFilePath) { - this._devSystemExtensionsFilePath = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', 'build', 'builtInExtensions.json')); + // {{SQL CARBON EDIT} + let builtInPath = product.quality === 'stable' ? 'builtInExtensions' : 'builtInExtensions-insiders'; + this._devSystemExtensionsFilePath = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', 'build', `${builtInPath}.json`)); } return this._devSystemExtensionsFilePath; } diff --git a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts index 2a16c37dfd..da42951505 100644 --- a/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts @@ -264,7 +264,10 @@ export class CachedExtensionScanner { let finalBuiltinExtensions: Promise = builtinExtensions; if (devMode) { - const builtInExtensionsFilePath = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', 'build', 'builtInExtensions.json')); + // {{SQL CARBON EDIT}} + let builtInFilename = product.quality === 'stable' ? 'builtInExtensions.json' : 'builtInExtensions-insiders.json'; + const builtInExtensionsFilePath = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', 'build', builtInFilename)); + // {{SQL CARBON EDIT}} - END const builtInExtensions = pfs.readFile(builtInExtensionsFilePath, 'utf8') .then(raw => JSON.parse(raw));