diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 89ffadd309..7ffc8e99b3 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -78,6 +78,7 @@ const sqlBuiltInExtensions = [ // Add SQL built-in extensions here. // the extension will be excluded from SQLOps package and will have separate vsix packages 'agent', + 'import', 'profiler' ]; diff --git a/extensions/import/.gitignore b/extensions/import/.gitignore new file mode 100644 index 0000000000..30b09e6cd9 --- /dev/null +++ b/extensions/import/.gitignore @@ -0,0 +1 @@ +flatfileimportservice/ diff --git a/extensions/import/.vscodeignore b/extensions/import/.vscodeignore new file mode 100644 index 0000000000..a48535cae1 --- /dev/null +++ b/extensions/import/.vscodeignore @@ -0,0 +1,2 @@ +client/src/** +client/tsconfig.json diff --git a/extensions/import/README.md b/extensions/import/README.md new file mode 100644 index 0000000000..154df058a1 --- /dev/null +++ b/extensions/import/README.md @@ -0,0 +1,16 @@ +# Microsoft SQL Server Import for SQL Operations Studio + +-- +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Privacy Statement + +The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) describes the privacy statement of this software. + +## License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the [Source EULA](https://raw.githubusercontent.com/Microsoft/sqlopsstudio/master/LICENSE.txt). diff --git a/extensions/import/images/sqlserver.png b/extensions/import/images/sqlserver.png new file mode 100644 index 0000000000..d884faa14a Binary files /dev/null and b/extensions/import/images/sqlserver.png differ diff --git a/extensions/import/package.json b/extensions/import/package.json new file mode 100644 index 0000000000..d5e171c1c8 --- /dev/null +++ b/extensions/import/package.json @@ -0,0 +1,105 @@ +{ + "name": "import", + "displayName": "SQL Server Import", + "description": "Imports data from a flat file.", + "version": "0.0.1", + "publisher": "Microsoft", + "preview": true, + "engines": { + "vscode": "^1.25.0", + "sqlops": "*" + }, + "license": "https://raw.githubusercontent.com/Microsoft/sqlopsstudio/master/LICENSE.txt", + "icon": "images/sqlserver.png", + "aiKey": "AIF-5574968e-856d-40d2-af67-c89a14e76412", + "activationEvents": [ + "*" + ], + "main": "./out/main", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/sqlopsstudio.git" + }, + "extensionDependencies": [ + "Microsoft.mssql" + ], + "contributes": { + "commands": [ + { + "command": "flatFileImport.start", + "title": "Import wizard", + "category": "Flat File Import", + "icon": { + "light": "./images/light_icon.svg", + "dark": "./images/dark_icon.svg" + } + }, + { + "command": "flatFileImport.importFlatFile" + }, + { + "command": "flatFileImport.listDatabases" + } + ], + "keybindings": [ + { + "command": "flatFileImport.start", + "key": "ctrl+i", + "mac": "ctrl+i" + } + ], + "dashboard.tabs": [ + { + "id": "flat-file-import", + "title": "Flat File Import", + "description": "The flat file importer.", + "container": { + "flat-file-import-container": {} + } + } + ], + "dashboard.containers": [ + { + "id": "flat-file-import-container", + "container": { + "widgets-container": [ + { + "name": "Tasks", + "widget": { + "tasks-widget": [ + "flatFileImport.start" + ] + } + } + ] + } + } + ], + "menus": { + "objectExplorer/item/context": [ + { + "command": "flatFileImport.start", + "when": "connectionProvider == MSSQL && nodeType && nodeType == Database", + "group": "import" + } + ] + } + }, + "dependencies": { + "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#0.1.5", + "opener": "^1.4.3", + "service-downloader": "github:anthonydresser/service-downloader#0.1.4", + "vscode-extension-telemetry": "^0.0.5", + "vscode-nls": "^3.2.1" + }, + "devDependencies": { + "mocha-junit-reporter": "^1.17.0", + "mocha-multi-reporters": "^1.1.7" + }, + "resolutions": { + "vscode-jsonrpc": "3.5.0", + "vscode-languageclient": "3.5.0", + "vscode-languageserver-protocol": "3.5.0", + "vscode-languageserver-types": "3.5.0" + } +} diff --git a/extensions/import/src/constants.ts b/extensions/import/src/constants.ts new file mode 100644 index 0000000000..481a4dec1c --- /dev/null +++ b/extensions/import/src/constants.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +export const extensionConfigSectionName = 'flatFileImport'; +export const serviceName = 'Flat File Import Service'; +export const providerId = 'FlatFileImport'; +export const configLogDebugInfo = 'logDebugInfo'; +export const sqlConfigSectionName = 'sql'; + +export const serviceCrashLink = 'https://github.com/Microsoft/sqlopsstudio/issues/2090'; + diff --git a/extensions/import/src/controllers/controllerBase.ts b/extensions/import/src/controllers/controllerBase.ts new file mode 100644 index 0000000000..df8045dfb1 --- /dev/null +++ b/extensions/import/src/controllers/controllerBase.ts @@ -0,0 +1,30 @@ + +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; + +export default abstract class ControllerBase implements vscode.Disposable { + protected _context: vscode.ExtensionContext; + + public constructor(context: vscode.ExtensionContext) { + this._context = context; + } + + public get extensionContext(): vscode.ExtensionContext { + return this._context; + } + + abstract activate(): Promise; + + abstract deactivate(): void; + + public dispose(): void { + this.deactivate(); + } +} + diff --git a/extensions/import/src/controllers/mainController.ts b/extensions/import/src/controllers/mainController.ts new file mode 100644 index 0000000000..01e9013397 --- /dev/null +++ b/extensions/import/src/controllers/mainController.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * 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 constants from '../constants'; +import * as sqlops from 'sqlops'; +import ControllerBase from './controllerBase'; +import * as vscode from 'vscode'; +import { FlatFileWizard } from '../wizard/flatFileWizard'; +import { ServiceClient } from '../services/serviceClient'; +import { ApiType, managerInstance } from '../services/serviceApiManager'; +import { FlatFileProvider } from '../services/contracts'; + +/** + * The main controller class that initializes the extension + */ +export default class MainController extends ControllerBase { + + /** + */ + public deactivate(): void { + } + + public activate(): Promise { + const outputChannel = vscode.window.createOutputChannel(constants.serviceName); + new ServiceClient(outputChannel).startService(this._context); + + managerInstance.onRegisteredApi(ApiType.FlatFileProvider)(provider => { + this.initializeFlatFileProvider(provider); + }); + + return Promise.resolve(true); + } + + private initializeFlatFileProvider(provider: FlatFileProvider) { + sqlops.tasks.registerTask('flatFileImport.start', () => new FlatFileWizard(provider).start()); + } +} diff --git a/extensions/import/src/main.ts b/extensions/import/src/main.ts new file mode 100644 index 0000000000..cac1908f61 --- /dev/null +++ b/extensions/import/src/main.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; + +import ControllerBase from './controllers/controllerBase'; +import MainController from './controllers/mainController'; + +let controllers: ControllerBase[] = []; + +export function activate(context: vscode.ExtensionContext) { + let activations: Promise[] = []; + + // Start the main controller + let mainController = new MainController(context); + controllers.push(mainController); + context.subscriptions.push(mainController); + activations.push(mainController.activate()); + + return Promise.all(activations) + .then((results: boolean[]) => { + for (let result of results) { + if (!result) { + return false; + } + } + return true; + }); +} + +export function deactivate() { + for (let controller of controllers) { + controller.deactivate(); + } +} diff --git a/extensions/import/src/services/config.json b/extensions/import/src/services/config.json new file mode 100644 index 0000000000..35df2ac6b3 --- /dev/null +++ b/extensions/import/src/services/config.json @@ -0,0 +1,16 @@ +{ + "downloadUrl": "https://sqlopsextensions.blob.core.windows.net/extensions/import/{#fileName#}", + "useDefaultLinuxRuntime": true, + "version": "0.0.1", + "downloadFileNames": { + "Windows_64": "win-x64.zip", + "Windows_86": "win-x86.zip", + "OSX": "osx.zip", + "Linux_64": "linux-x64.tar.gz" + }, + "installDirectory": "flatfileimportservice/{#platform#}/{#version#}", + "executableFiles": [ + "MicrosoftSqlToolsFlatFileImport", + "MicrosoftSqlToolsFlatFileImport.exe" + ] +} diff --git a/extensions/import/src/services/contracts.ts b/extensions/import/src/services/contracts.ts new file mode 100644 index 0000000000..4e85730891 --- /dev/null +++ b/extensions/import/src/services/contracts.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { RequestType, NotificationType } from 'vscode-languageclient'; + +/** + * @interface IMessage + */ +export interface IMessage { + jsonrpc: string; +} + +// ------------------------------- < Telemetry Sent Event > ------------------------------------ + +/** + * Event sent when the language service send a telemetry event + */ +export namespace TelemetryNotification { + export const type = new NotificationType('telemetry/sqlevent'); +} + +/** + * Update event parameters + */ +export class TelemetryParams { + public params: { + eventName: string; + properties: ITelemetryEventProperties; + measures: ITelemetryEventMeasures; + }; +} + +export interface ITelemetryEventProperties { + [key: string]: string; +} + +export interface ITelemetryEventMeasures { + [key: string]: number; +} + +/** + * Contract Classes + */ +export interface Result { + success: boolean; + errorMessage: string; +} + +export interface ColumnInfo { + name: string; + sqlType: string; + isNullable: boolean; +} + + +/** + * PROSEDiscoveryRequest + * Send this request to create a new PROSE session with a new file and preview it + */ +const proseDiscoveryRequestName = 'flatfile/proseDiscovery'; + +export interface PROSEDiscoveryParams { + filePath: string; + tableName: string; + schemaName?: string; + fileType?: string; +} + +export interface PROSEDiscoveryResponse { + dataPreview: string[][]; + columnInfo: ColumnInfo[]; +} + +/** + * InsertDataRequest + */ +const insertDataRequestName = 'flatfile/insertData'; + +export interface InsertDataParams { + connectionString: string; + batchSize: number; +} + +export interface InsertDataResponse { + result: Result; +} + + +/** + * GetColumnInfoRequest + */ +const getColumnInfoRequestName = 'flatfile/getColumnInfo'; + +export interface GetColumnInfoParams { +} + +export interface GetColumnInfoResponse { + columnInfo: ColumnInfo[]; +} + + +/** + * ChangeColumnSettingsRequest + */ +const changeColumnSettingsRequestName = 'flatfile/changeColumnSettings'; + +export interface ChangeColumnSettingsParams { + index: number; + newName?: string; + newDataType?: string; + newNullable?: boolean; + newInPrimaryKey?: boolean; +} + +export interface ChangeColumnSettingsResponse { + result: Result; +} + +/** + * Requests + */ +export namespace PROSEDiscoveryRequest { + export const type = new RequestType(proseDiscoveryRequestName); +} + +export namespace InsertDataRequest { + export const type = new RequestType(insertDataRequestName); +} + +export namespace GetColumnInfoRequest { + export const type = new RequestType(getColumnInfoRequestName); +} + +export namespace ChangeColumnSettingsRequest { + export const type = new RequestType(changeColumnSettingsRequestName); +} + + +export interface FlatFileProvider { + providerId?: string; + + sendPROSEDiscoveryRequest(params: PROSEDiscoveryParams): Thenable; + sendInsertDataRequest(params: InsertDataParams): Thenable; + sendGetColumnInfoRequest(params: GetColumnInfoParams): Thenable; + sendChangeColumnSettingsRequest(params: ChangeColumnSettingsParams): Thenable; +} diff --git a/extensions/import/src/services/features.ts b/extensions/import/src/services/features.ts new file mode 100644 index 0000000000..15ccfec04c --- /dev/null +++ b/extensions/import/src/services/features.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { SqlOpsDataClient, SqlOpsFeature } from 'dataprotocol-client'; +import { + ClientCapabilities, + StaticFeature, + RPCMessageType, + ServerCapabilities +} from 'vscode-languageclient'; +import * as UUID from 'vscode-languageclient/lib/utils/uuid'; +import { Disposable } from 'vscode'; + +import { Telemetry } from './telemetry'; +import * as serviceUtils from './serviceUtils'; +import * as Contracts from './contracts'; +import { managerInstance, ApiType } from './serviceApiManager'; + +export class TelemetryFeature implements StaticFeature { + + constructor(private _client: SqlOpsDataClient) { + } + + fillClientCapabilities(capabilities: ClientCapabilities): void { + serviceUtils.ensure(capabilities, 'telemetry')!.telemetry = true; + } + + initialize(): void { + this._client.onNotification(Contracts.TelemetryNotification.type, e => { + Telemetry.sendTelemetryEvent(e.params.eventName, e.params.properties, e.params.measures); + }); + } +} + +export class FlatFileImportFeature extends SqlOpsFeature { + private static readonly messagesTypes: RPCMessageType[] = [ + Contracts.PROSEDiscoveryRequest.type + ]; + + constructor(client: SqlOpsDataClient) { + super(client, FlatFileImportFeature.messagesTypes); + } + + public fillClientCapabilities(capabilities: ClientCapabilities): void { + } + + public initialize(capabilities: ServerCapabilities): void { + this.register(this.messages, { + id: UUID.generateUuid(), + registerOptions: undefined + }); + } + + protected registerProvider(options: undefined): Disposable { + const client = this._client; + + let requestSender = (requestType, params) => { + return client.sendRequest(requestType, params).then( + r => { + return r as any; + }, + e => { + client.logFailedRequest(requestType, e); + return Promise.reject(e); + } + ); + }; + + let sendPROSEDiscoveryRequest = (params: Contracts.PROSEDiscoveryParams): Thenable => { + return requestSender(Contracts.PROSEDiscoveryRequest.type, params); + }; + + let sendInsertDataRequest = (params: Contracts.InsertDataParams): Thenable => { + return requestSender(Contracts.InsertDataRequest.type, params); + }; + + let sendGetColumnInfoRequest = (params: Contracts.GetColumnInfoParams): Thenable => { + return requestSender(Contracts.GetColumnInfoRequest.type, params); + }; + + let sendChangeColumnSettingsRequest = (params: Contracts.ChangeColumnSettingsParams): Thenable => { + return requestSender(Contracts.ChangeColumnSettingsRequest.type, params); + }; + + return managerInstance.registerApi(ApiType.FlatFileProvider, { + providerId: client.providerId, + sendPROSEDiscoveryRequest, + sendChangeColumnSettingsRequest, + sendGetColumnInfoRequest, + sendInsertDataRequest + }); + } +} diff --git a/extensions/import/src/services/serviceApiManager.ts b/extensions/import/src/services/serviceApiManager.ts new file mode 100644 index 0000000000..a0e9b3e915 --- /dev/null +++ b/extensions/import/src/services/serviceApiManager.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sqlops from 'sqlops'; +import * as vscode from 'vscode'; +import * as contracts from './contracts'; +import { SqlOpsDataClient } from 'dataprotocol-client/lib/main'; + +export enum ApiType { + FlatFileProvider = 'FlatFileProvider' +} + +export interface IServiceApi { + onRegisteredApi(type: ApiType): vscode.Event; + registerApi(type: ApiType, feature: T): vscode.Disposable; +} + +export interface IModelViewDefinition { + id: string; + modelView: sqlops.ModelView; +} + +export class ServiceApiManager implements IServiceApi { + private modelViewRegistrations: { [id: string]: boolean } = {}; + private featureEventChannels: { [type: string]: vscode.EventEmitter } = {}; + private _onRegisteredModelView = new vscode.EventEmitter(); + + public onRegisteredApi(type: ApiType): vscode.Event { + let featureEmitter = this.featureEventChannels[type]; + if (!featureEmitter) { + featureEmitter = new vscode.EventEmitter(); + this.featureEventChannels[type] = featureEmitter; + } + return featureEmitter.event; + } + + public registerApi(type: ApiType, feature: T): vscode.Disposable { + let featureEmitter = this.featureEventChannels[type]; + if (featureEmitter) { + featureEmitter.fire(feature); + } + // TODO handle unregistering API on close + return { + dispose: () => undefined + }; + } + + public get onRegisteredModelView(): vscode.Event { + return this._onRegisteredModelView.event; + } + + public registerModelView(id: string, modelView: sqlops.ModelView): void { + this._onRegisteredModelView.fire({ + id: id, + modelView: modelView + }); + } +} + +export let managerInstance = new ServiceApiManager(); diff --git a/extensions/import/src/services/serviceClient.ts b/extensions/import/src/services/serviceClient.ts new file mode 100644 index 0000000000..6b94191a6c --- /dev/null +++ b/extensions/import/src/services/serviceClient.ts @@ -0,0 +1,165 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { SqlOpsDataClient, ClientOptions } from 'dataprotocol-client'; +import { IConfig, ServerProvider, Events } from 'service-downloader'; +import { ServerOptions, TransportKind } from 'vscode-languageclient'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); +import * as path from 'path'; +import { EventAndListener } from 'eventemitter2'; + +import { Telemetry, LanguageClientErrorHandler } from './telemetry'; +import * as Constants from '../constants'; +import { TelemetryFeature, FlatFileImportFeature } from './features'; +import * as serviceUtils from './serviceUtils'; + +const baseConfig = require('./config.json'); + +export class ServiceClient { + private statusView: vscode.StatusBarItem; + + constructor(private outputChannel: vscode.OutputChannel) { + this.statusView = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + } + + public startService(context: vscode.ExtensionContext): Promise { + let config: IConfig = JSON.parse(JSON.stringify(baseConfig)); + config.installDirectory = path.join(context.extensionPath, config.installDirectory); + config.proxy = vscode.workspace.getConfiguration('http').get('proxy'); + config.strictSSL = vscode.workspace.getConfiguration('http').get('proxyStrictSSL') || true; + + const serverdownloader = new ServerProvider(config); + serverdownloader.eventEmitter.onAny(this.generateHandleServerProviderEvent()); + + let clientOptions: ClientOptions = this.createClientOptions(); + + const installationStart = Date.now(); + let client: SqlOpsDataClient; + return new Promise((resolve, reject) => { + serverdownloader.getOrDownloadServer().then(e => { + const installationComplete = Date.now(); + let serverOptions = this.generateServerOptions(e); + client = new SqlOpsDataClient(Constants.serviceName, serverOptions, clientOptions); + const processStart = Date.now(); + client.onReady().then(() => { + const processEnd = Date.now(); + this.statusView.text = localize('serviceStarted', 'Service Started'); + setTimeout(() => { + this.statusView.hide(); + }, 1500); + Telemetry.sendTelemetryEvent('startup/LanguageClientStarted', { + installationTime: String(installationComplete - installationStart), + processStartupTime: String(processEnd - processStart), + totalTime: String(processEnd - installationStart), + beginningTimestamp: String(installationStart) + }); + }); + this.statusView.show(); + this.statusView.text = localize('serviceStarting', 'Starting service'); + let disposable = client.start(); + context.subscriptions.push(disposable); + resolve(client); + }, e => { + Telemetry.sendTelemetryEvent('ServiceInitializingFailed'); + vscode.window.showErrorMessage(localize('serviceStartFailed', 'Failed to start Scale Out Data service:{0}', e)); + // Just resolve to avoid unhandled promise. We show the error to the user. + resolve(undefined); + }); + }); + } + + private createClientOptions(): ClientOptions { + return { + providerId: Constants.providerId, + errorHandler: new LanguageClientErrorHandler(), + synchronize: { + configurationSection: [Constants.extensionConfigSectionName, Constants.sqlConfigSectionName] + }, + features: [ + // we only want to add new features + TelemetryFeature, + FlatFileImportFeature + ], + outputChannel: new CustomOutputChannel() + }; + } + + private generateServerOptions(executablePath: string): ServerOptions { + let launchArgs = []; + launchArgs.push('--log-dir'); + let logFileLocation = path.join(serviceUtils.getDefaultLogLocation(), 'flatfileimport'); + launchArgs.push(logFileLocation); + let config = vscode.workspace.getConfiguration(Constants.extensionConfigSectionName); + if (config) { + let logDebugInfo = config[Constants.configLogDebugInfo]; + if (logDebugInfo) { + launchArgs.push('--enable-logging'); + } + } + + return { command: executablePath, args: launchArgs, transport: TransportKind.stdio }; + } + + private generateHandleServerProviderEvent(): EventAndListener { + let dots = 0; + return (e: string, ...args: any[]) => { + this.outputChannel.show(); + this.statusView.show(); + switch (e) { + case Events.INSTALL_START: + this.outputChannel.appendLine(localize('installingServiceDetailed', 'Installing {0} service to {1}', Constants.serviceName, args[0])); + this.statusView.text = localize('installingService', 'Installing Service'); + break; + case Events.INSTALL_END: + this.outputChannel.appendLine(localize('serviceInstalled', 'Installed')); + break; + case Events.DOWNLOAD_START: + this.outputChannel.appendLine(localize('downloadingService', 'Downloading {0}', args[0])); + this.outputChannel.append(`(${Math.ceil(args[1] / 1024)} KB)`); + this.statusView.text = localize('downloadingServiceStatus', 'Downloading Service'); + break; + case Events.DOWNLOAD_PROGRESS: + let newDots = Math.ceil(args[0] / 5); + if (newDots > dots) { + this.outputChannel.append('.'.repeat(newDots - dots)); + dots = newDots; + } + break; + case Events.DOWNLOAD_END: + this.outputChannel.appendLine(localize('downloadingServiceComplete', 'Done!')); + break; + default: + break; + } + }; + } +} + +class CustomOutputChannel implements vscode.OutputChannel { + name: string; + append(value: string): void { + } + appendLine(value: string): void { + } + // tslint:disable-next-line:no-empty + clear(): void { + } + show(preserveFocus?: boolean): void; + show(column?: vscode.ViewColumn, preserveFocus?: boolean): void; + // tslint:disable-next-line:no-empty + show(column?: any, preserveFocus?: any): void { + } + // tslint:disable-next-line:no-empty + hide(): void { + } + // tslint:disable-next-line:no-empty + dispose(): void { + } +} + diff --git a/extensions/import/src/services/serviceUtils.ts b/extensions/import/src/services/serviceUtils.ts new file mode 100644 index 0000000000..efe740c2e1 --- /dev/null +++ b/extensions/import/src/services/serviceUtils.ts @@ -0,0 +1,156 @@ +/*--------------------------------------------------------------------------------------------- + * 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 path from 'path'; +import * as crypto from 'crypto'; +import * as os from 'os'; + +const baseConfig = require('./config.json'); + +// The function is a duplicate of \src\paths.js. IT would be better to import path.js but it doesn't +// work for now because the extension is running in different process. +export function getAppDataPath(): string { + let platform = process.platform; + switch (platform) { + case 'win32': return process.env['APPDATA'] || path.join(process.env['USERPROFILE'], 'AppData', 'Roaming'); + case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support'); + case 'linux': return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config'); + default: throw new Error('Platform not supported'); + } +} + +export function getDefaultLogLocation(): string { + return path.join(getAppDataPath(), 'sqlops'); +} + +export function ensure(target: object, key: string): any { + if (target[key] === void 0) { + target[key] = {} as any; + } + return target[key]; +} + +export interface IPackageInfo { + name: string; + version: string; + aiKey: string; +} + +export function getPackageInfo(packageJson: any): IPackageInfo { + if (packageJson) { + return { + name: packageJson.name, + version: packageJson.version, + aiKey: packageJson.aiKey + }; + } +} + +export function generateUserId(): Promise { + return new Promise(resolve => { + try { + let interfaces = os.networkInterfaces(); + let mac; + for (let key of Object.keys(interfaces)) { + let item = interfaces[key][0]; + if (!item.internal) { + mac = item.mac; + break; + } + } + if (mac) { + resolve(crypto.createHash('sha256').update(mac + os.homedir(), 'utf8').digest('hex')); + } else { + resolve(generateGuid()); + } + } catch (err) { + resolve(generateGuid()); // fallback + } + }); +} + +export function generateGuid(): string { + let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; + // c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx) + let oct: string = ''; + let tmp: number; + /* tslint:disable:no-bitwise */ + for (let a: number = 0; a < 4; a++) { + tmp = (4294967296 * Math.random()) | 0; + oct += hexValues[tmp & 0xF] + + hexValues[tmp >> 4 & 0xF] + + hexValues[tmp >> 8 & 0xF] + + hexValues[tmp >> 12 & 0xF] + + hexValues[tmp >> 16 & 0xF] + + hexValues[tmp >> 20 & 0xF] + + hexValues[tmp >> 24 & 0xF] + + hexValues[tmp >> 28 & 0xF]; + } + + // 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively' + let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0]; + return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12); + /* tslint:enable:no-bitwise */ +} + +export function verifyPlatform(): Thenable { + if (os.platform() === 'darwin' && parseFloat(os.release()) < 16.0) { + return Promise.resolve(false); + } else { + return Promise.resolve(true); + } +} + +export function getServiceInstallConfig(basePath?: string): any { + if (!basePath) { + basePath = __dirname; + } + let config = JSON.parse(JSON.stringify(baseConfig)); + config.installDirectory = path.join(basePath, config.installDirectory); + + return config; +} + +export function getResolvedServiceInstallationPath(runtime: Runtime, basePath?: string): string { + let config = getServiceInstallConfig(basePath); + let dir = config.installDirectory; + dir = dir.replace('{#version#}', config.version); + dir = dir.replace('{#platform#}', getRuntimeDisplayName(runtime)); + + return dir; +} + +export function getRuntimeDisplayName(runtime: Runtime): string { + switch (runtime) { + case Runtime.Windows_64: + return 'Windows'; + case Runtime.Windows_86: + return 'Windows'; + case Runtime.OSX: + return 'OSX'; + case Runtime.Linux_64: + return 'Linux'; + default: + return 'Unknown'; + } +} + +export enum Runtime { + Unknown = 'Unknown', + Windows_86 = 'Windows_86', + Windows_64 = 'Windows_64', + OSX = 'OSX', + CentOS_7 = 'CentOS_7', + Debian_8 = 'Debian_8', + Fedora_23 = 'Fedora_23', + OpenSUSE_13_2 = 'OpenSUSE_13_2', + SLES_12_2 = 'SLES_12_2', + RHEL_7 = 'RHEL_7', + Ubuntu_14 = 'Ubuntu_14', + Ubuntu_16 = 'Ubuntu_16', + Linux_64 = 'Linux_64', + Linux_86 = 'Linux-86' +} diff --git a/extensions/import/src/services/telemetry.ts b/extensions/import/src/services/telemetry.ts new file mode 100644 index 0000000000..c93c89d1ea --- /dev/null +++ b/extensions/import/src/services/telemetry.ts @@ -0,0 +1,216 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ErrorAction, CloseAction } from 'vscode-languageclient'; +import TelemetryReporter from 'vscode-extension-telemetry'; +import { PlatformInformation } from 'service-downloader/out/platform'; +import * as opener from 'opener'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import * as constants from '../constants'; +import * as serviceUtils from './serviceUtils'; +import { IMessage, ITelemetryEventProperties, ITelemetryEventMeasures } from './contracts'; + + +/** + * Handle Language Service client errors + * @class LanguageClientErrorHandler + */ +export class LanguageClientErrorHandler { + + /** + * Creates an instance of LanguageClientErrorHandler. + * @memberOf LanguageClientErrorHandler + */ + constructor() { + + } + + /** + * Show an error message prompt with a link to known issues wiki page + * @memberOf LanguageClientErrorHandler + */ + showOnErrorPrompt(): void { + // TODO add telemetry + // Telemetry.sendTelemetryEvent('SqlToolsServiceCrash'); + let crashButtonText = localize('import.serviceCrashButton', 'Give Feedback'); + vscode.window.showErrorMessage( + localize('serviceCrashMessage', 'service component could not start'), + crashButtonText + ).then(action => { + if (action && action === crashButtonText) { + opener(constants.serviceCrashLink); + } + }); + } + + /** + * Callback for language service client error + * + * @param {Error} error + * @param {Message} message + * @param {number} count + * @returns {ErrorAction} + * + * @memberOf LanguageClientErrorHandler + */ + error(error: Error, message: IMessage, count: number): ErrorAction { + this.showOnErrorPrompt(); + + // we don't retry running the service since crashes leave the extension + // in a bad, unrecovered state + return ErrorAction.Shutdown; + } + + /** + * Callback for language service client closed + * + * @returns {CloseAction} + * + * @memberOf LanguageClientErrorHandler + */ + closed(): CloseAction { + this.showOnErrorPrompt(); + + // we don't retry running the service since crashes leave the extension + // in a bad, unrecovered state + return CloseAction.DoNotRestart; + } +} + +/** + * Filters error paths to only include source files. Exported to support testing + */ +export function FilterErrorPath(line: string): string { + if (line) { + let values: string[] = line.split('/out/'); + if (values.length <= 1) { + // Didn't match expected format + return line; + } else { + return values[1]; + } + } +} + +export class Telemetry { + private static reporter: TelemetryReporter; + private static userId: string; + private static platformInformation: PlatformInformation; + private static disabled: boolean; + + // Get the unique ID for the current user of the extension + public static getUserId(): Promise { + return new Promise(resolve => { + // Generate the user id if it has not been created already + if (typeof this.userId === 'undefined') { + let id = serviceUtils.generateUserId(); + id.then(newId => { + this.userId = newId; + resolve(this.userId); + }); + } else { + resolve(this.userId); + } + }); + } + + public static getPlatformInformation(): Promise { + if (this.platformInformation) { + return Promise.resolve(this.platformInformation); + } else { + return new Promise(resolve => { + PlatformInformation.getCurrent().then(info => { + this.platformInformation = info; + resolve(this.platformInformation); + }); + }); + } + } + + /** + * Disable telemetry reporting + */ + public static disable(): void { + this.disabled = true; + } + + /** + * Initialize the telemetry reporter for use. + */ + public static initialize(): void { + if (typeof this.reporter === 'undefined') { + // Check if the user has opted out of telemetry + if (!vscode.workspace.getConfiguration('telemetry').get('enableTelemetry', true)) { + this.disable(); + return; + } + let packageInfo = vscode.extensions.getExtension('Microsoft.import').packageJSON; + this.reporter = new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey); + } + } + + /** + * Send a telemetry event for an exception + */ + public static sendTelemetryEventForException( + err: any, methodName: string, extensionConfigName: string): void { + try { + let stackArray: string[]; + let firstLine: string = ''; + if (err !== undefined && err.stack !== undefined) { + stackArray = err.stack.split('\n'); + if (stackArray !== undefined && stackArray.length >= 2) { + firstLine = stackArray[1]; // The fist line is the error message and we don't want to send that telemetry event + firstLine = FilterErrorPath(firstLine); + } + } + + // Only adding the method name and the fist line of the stack trace. We don't add the error message because it might have PII + this.sendTelemetryEvent('Exception', { methodName: methodName, errorLine: firstLine }); + // Utils.logDebug('Unhandled Exception occurred. error: ' + err + ' method: ' + methodName, extensionConfigName); + } catch (telemetryErr) { + // If sending telemetry event fails ignore it so it won't break the extension + // Utils.logDebug('Failed to send telemetry event. error: ' + telemetryErr, extensionConfigName); + } + } + + /** + * Send a telemetry event using application insights + */ + public static sendTelemetryEvent( + eventName: string, + properties?: ITelemetryEventProperties, + measures?: ITelemetryEventMeasures): void { + + if (typeof this.disabled === 'undefined') { + this.disabled = false; + } + + if (this.disabled || typeof (this.reporter) === 'undefined') { + // Don't do anything if telemetry is disabled + return; + } + + if (!properties || typeof properties === 'undefined') { + properties = {}; + } + + // Augment the properties structure with additional common properties before sending + Promise.all([this.getUserId(), this.getPlatformInformation()]).then(() => { + properties['userId'] = this.userId; + properties['distribution'] = (this.platformInformation && this.platformInformation.distribution) ? + `${this.platformInformation.distribution.name}, ${this.platformInformation.distribution.version}` : ''; + + this.reporter.sendTelemetryEvent(eventName, properties, measures); + }); + } +} + +Telemetry.initialize(); diff --git a/extensions/import/src/typings/ref.d.ts b/extensions/import/src/typings/ref.d.ts new file mode 100644 index 0000000000..41e273db7f --- /dev/null +++ b/extensions/import/src/typings/ref.d.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// +/// +/// \ No newline at end of file diff --git a/extensions/import/src/wizard/api/importPage.ts b/extensions/import/src/wizard/api/importPage.ts new file mode 100644 index 0000000000..9eacc453b1 --- /dev/null +++ b/extensions/import/src/wizard/api/importPage.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ImportDataModel } from './models'; +import * as sqlops from 'sqlops'; +import { FlatFileProvider } from '../../services/contracts'; +import { FlatFileWizard } from '../flatFileWizard'; + +export abstract class ImportPage { + protected readonly instance: FlatFileWizard; + protected readonly model: ImportDataModel; + protected readonly view: sqlops.ModelView; + protected readonly provider: FlatFileProvider; + + protected constructor(instance: FlatFileWizard, model: ImportDataModel, view: sqlops.ModelView, provider: FlatFileProvider) { + this.instance = instance; + this.model = model; + this.view = view; + this.provider = provider; + } + + /** + * This method constructs all the elements of the page. + * @returns {Promise} + */ + public async abstract start(): Promise; + + /** + * This method is called when the user is entering the page. + * @returns {Promise} + */ + public async abstract onPageEnter(): Promise; + + /** + * This method is called when the user is leaving the page. + * @returns {Promise} + */ + public async abstract onPageLeave(): Promise; + + /** + * Override this method to cleanup what you don't need cached in the page. + * @returns {Promise} + */ + public async cleanup(): Promise { + return true; + } +} diff --git a/extensions/import/src/wizard/api/models.ts b/extensions/import/src/wizard/api/models.ts new file mode 100644 index 0000000000..c3058e2738 --- /dev/null +++ b/extensions/import/src/wizard/api/models.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sqlops from 'sqlops'; + +/** + * The main data model that communicates between the pages. + */ +export interface ImportDataModel { + ownerUri: string; + proseColumns: ColumnMetadata[]; + proseDataPreview: string[][]; + server: sqlops.connection.Connection; + database: string; + table: string; + schema: string; + filePath: string; + fileType: string; +} + +/** + * Metadata of a column + */ +export interface ColumnMetadata { + columnName: string; + dataType: string; + primaryKey: boolean; + nullable: boolean; +} diff --git a/extensions/import/src/wizard/flatFileWizard.ts b/extensions/import/src/wizard/flatFileWizard.ts new file mode 100644 index 0000000000..038471e4f1 --- /dev/null +++ b/extensions/import/src/wizard/flatFileWizard.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import * as sqlops from 'sqlops'; +import { FlatFileProvider } from '../services/contracts'; +import { ImportDataModel } from './api/models'; +import { ImportPage } from './api/importPage'; +// pages +import { FileConfigPage } from './pages/fileConfigPage'; +import { ProsePreviewPage } from './pages/prosePreviewPage'; +import { ModifyColumnsPage } from './pages/modifyColumnsPage'; +import { SummaryPage } from './pages/summaryPage'; + +const localize = nls.loadMessageBundle(); + +export class FlatFileWizard { + private readonly provider: FlatFileProvider; + private wizard: sqlops.window.modelviewdialog.Wizard; + + private importAnotherFileButton: sqlops.window.modelviewdialog.Button; + + constructor(provider: FlatFileProvider) { + this.provider = provider; + } + + public async start() { + let model = {}; + let pages: Map = new Map(); + + + // TODO localize this + let connections = await sqlops.connection.getActiveConnections(); + if (!connections || connections.length === 0) { + vscode.window.showErrorMessage('Please connect to a server before using this wizard.'); + return; + } + + this.wizard = sqlops.window.modelviewdialog.createWizard(localize('flatFileImport.wizardName', 'Import flat file wizard')); + let page1 = sqlops.window.modelviewdialog.createWizardPage(localize('flatFileImport.page1Name', 'New Table Details')); + let page2 = sqlops.window.modelviewdialog.createWizardPage(localize('flatFileImport.page2Name', 'Preview Data')); + let page3 = sqlops.window.modelviewdialog.createWizardPage(localize('flatFileImport.page3Name', 'Modify Columns')); + let page4 = sqlops.window.modelviewdialog.createWizardPage(localize('flatFileImport.page4Name', 'Summary')); + + let fileConfigPage: FileConfigPage; + page1.registerContent(async (view) => { + fileConfigPage = new FileConfigPage(this, model, view, this.provider); + pages.set(0, fileConfigPage); + await fileConfigPage.start(); + fileConfigPage.onPageEnter(); + }); + + let prosePreviewPage: ProsePreviewPage; + page2.registerContent(async (view) => { + prosePreviewPage = new ProsePreviewPage(this, model, view, this.provider); + pages.set(1, prosePreviewPage); + await prosePreviewPage.start(); + }); + + let modifyColumnsPage: ModifyColumnsPage; + page3.registerContent(async (view) => { + modifyColumnsPage = new ModifyColumnsPage(this, model, view, this.provider); + pages.set(2, modifyColumnsPage); + await modifyColumnsPage.start(); + }); + + let summaryPage: SummaryPage; + + page4.registerContent(async (view) => { + summaryPage = new SummaryPage(this, model, view, this.provider); + pages.set(3, summaryPage); + await summaryPage.start(); + }); + + + this.importAnotherFileButton = sqlops.window.modelviewdialog.createButton(localize('flatFileImport.importNewFile', 'Import new file')); + this.importAnotherFileButton.onClick(() => { + //TODO replace this with proper cleanup for all the pages + this.wizard.close(); + pages.forEach((page) => page.cleanup()); + this.wizard.open(); + }); + + this.importAnotherFileButton.hidden = true; + this.wizard.customButtons = [this.importAnotherFileButton]; + + this.wizard.onPageChanged(async (event) => { + let idx = event.newPage; + + let page = pages.get(idx); + + if (page) { + page.onPageEnter(); + } + }); + + this.wizard.onPageChanged(async (event) => { + let idx = event.lastPage; + + let page = pages.get(idx); + if (page) { + page.onPageLeave(); + } + }); + + //not needed for this wizard + this.wizard.generateScriptButton.hidden = true; + + this.wizard.pages = [page1, page2, page3, page4]; + + this.wizard.open(); + } + + public setImportAnotherFileVisibility(visibility: boolean) { + this.importAnotherFileButton.hidden = !visibility; + } + + public registerNavigationValidator(validator: (pageChangeInfo: sqlops.window.modelviewdialog.WizardPageChangeInfo) => boolean) { + this.wizard.registerNavigationValidator(validator); + } + + +} + + + diff --git a/extensions/import/src/wizard/pages/fileConfigPage.ts b/extensions/import/src/wizard/pages/fileConfigPage.ts new file mode 100644 index 0000000000..aa973bfa99 --- /dev/null +++ b/extensions/import/src/wizard/pages/fileConfigPage.ts @@ -0,0 +1,393 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sqlops from 'sqlops'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { ImportDataModel } from '../api/models'; +import { ImportPage } from '../api/importPage'; +import { FlatFileProvider } from '../../services/contracts'; +import { FlatFileWizard } from '../flatFileWizard'; + +const localize = nls.loadMessageBundle(); + +export class FileConfigPage extends ImportPage { + + private serverDropdown: sqlops.DropDownComponent; + private databaseDropdown: sqlops.DropDownComponent; + private fileTextBox: sqlops.InputBoxComponent; + private fileButton: sqlops.ButtonComponent; + private tableNameTextBox: sqlops.InputBoxComponent; + private schemaDropdown: sqlops.DropDownComponent; + private form: sqlops.FormContainer; + + private databaseLoader: sqlops.LoadingComponent; + private schemaLoader: sqlops.LoadingComponent; + + private tableNames: string[] = []; + + public constructor(instance: FlatFileWizard, model: ImportDataModel, view: sqlops.ModelView, provider: FlatFileProvider) { + super(instance, model, view, provider); + } + + async start(): Promise { + let schemaComponent = await this.createSchemaDropdown(); + let tableNameComponent = await this.createTableNameBox(); + let fileBrowserComponent = await this.createFileBrowser(); + let databaseComponent = await this.createDatabaseDropdown(); + let serverComponent = await this.createServerDropdown(); + this.setupNavigationValidator(); + + this.form = this.view.modelBuilder.formContainer() + .withFormItems( + [ + serverComponent, + databaseComponent, + fileBrowserComponent, + tableNameComponent, + schemaComponent + ]).component(); + + await this.view.initializeModel(this.form); + return true; + } + + async onPageEnter(): Promise { + await this.populateServerDropdown(); + await this.populateDatabaseDropdown(); + await this.populateSchemaDropdown(); + return true; + } + + async onPageLeave(): Promise { + return true; + } + + public async cleanup(): Promise { + delete this.model.filePath; + delete this.model.table; + + return true; + } + + private setupNavigationValidator() { + this.instance.registerNavigationValidator((info) => { + if (this.schemaLoader.loading || this.databaseLoader.loading) { + return false; + } + return true; + }); + } + + private async createServerDropdown(): Promise { + this.serverDropdown = this.view.modelBuilder.dropDown().component(); + + // Handle server changes + this.serverDropdown.onValueChanged(async (params) => { + this.model.server = (this.serverDropdown.value as ConnectionDropdownValue).connection; + + await this.populateDatabaseDropdown(); + await this.populateSchemaDropdown(); + }); + + return { + component: this.serverDropdown, + title: localize('flatFileImport.serverDropdownTitle', 'Server the database is in') + }; + } + + private async populateServerDropdown(): Promise { + let cons = await sqlops.connection.getActiveConnections(); + // This user has no active connections ABORT MISSION + if (!cons || cons.length === 0) { + return true; + } + + + let count = -1; + let idx = -1; + + let values = cons.map(c => { + // Handle the code to remember what the user's choice was from before + count++; + if (this.model.server && c.connectionId === this.model.server.connectionId) { + idx = count; + } + + let db = c.options.databaseDisplayName; + let usr = c.options.user; + let srv = c.options.server; + + if (!db) { + db = ''; + } + + if (!usr) { + usr = 'default'; + } + + let finalName = `${srv}, ${db} (${usr})`; + return { + connection: c, + displayName: finalName, + name: c.connectionId + }; + }); + + if (idx > 0) { + let tmp = values[0]; + values[0] = values[idx]; + values[idx] = tmp; + } else { + delete this.model.server; + delete this.model.database; + delete this.model.schema; + } + + this.model.server = values[0].connection; + + + this.serverDropdown.updateProperties({ + values: values + }); + return true; + } + + private async createDatabaseDropdown(): Promise { + this.databaseDropdown = this.view.modelBuilder.dropDown().component(); + + // Handle database changes + this.databaseDropdown.onValueChanged(async (db) => { + this.model.database = (this.databaseDropdown.value).name; + //this.populateTableNames(); + this.populateSchemaDropdown(); + }); + + this.databaseLoader = this.view.modelBuilder.loadingComponent().withItem(this.databaseDropdown).component(); + + return { + component: this.databaseLoader, + title: localize('flatFileImport.databaseDropdownTitle', 'Database the table is created in') + }; + } + + private async populateDatabaseDropdown(): Promise { + this.databaseLoader.loading = true; + this.databaseDropdown.updateProperties({ values: [] }); + this.schemaDropdown.updateProperties({ values: [] }); + + if (!this.model.server) { + //TODO handle error case + this.databaseLoader.loading = false; + return false; + } + + + let idx = -1; + let count = -1; + let values = (await sqlops.connection.listDatabases(this.model.server.connectionId)).map(db => { + count++; + if (this.model.database && db === this.model.database) { + idx = count; + } + return { + displayName: db, + name: db + }; + }); + + if (idx > 0) { + let tmp = values[0]; + values[0] = values[idx]; + values[idx] = tmp; + } else { + delete this.model.database; + delete this.model.schema; + } + + this.model.database = values[0].name; + + this.databaseDropdown.updateProperties({ + values: values + }); + this.databaseLoader.loading = false; + + return true; + } + + private async createFileBrowser(): Promise { + this.fileTextBox = this.view.modelBuilder.inputBox().component(); + this.fileButton = this.view.modelBuilder.button().withProperties({ + label: localize('flatFileImport.browseFiles', 'Browse'), + }).component(); + + this.fileButton.onDidClick(async (click) => { + let fileUris = await vscode.window.showOpenDialog( + { + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + openLabel: localize('flatFileImport.openFile', 'Open'), + filters: { + 'Files': ['csv', 'txt'] + } + } + ); + + if (!fileUris || fileUris.length === 0) { + return; + } + + let fileUri = fileUris[0]; + this.fileTextBox.value = fileUri.fsPath; + + // Get the name of the file. + let nameStart = fileUri.path.lastIndexOf('/'); + let nameEnd = fileUri.path.lastIndexOf('.'); + + // Handle files without extensions + if (nameEnd === 0) { + nameEnd = fileUri.path.length; + } + this.model.fileType = 'TXT'; + let extension = fileUri.path.substring(nameEnd + 1, fileUri.path.length); + + if (extension.toLowerCase() === 'json') { + this.model.fileType = 'JSON'; + } + + this.tableNameTextBox.value = fileUri.path.substring(nameStart + 1, nameEnd); + this.model.table = this.tableNameTextBox.value; + this.tableNameTextBox.validate(); + + // Let then model know about the file path + this.model.filePath = fileUri.fsPath; + }); + + return { + component: this.fileTextBox, + title: localize('flatFileImport.fileTextboxTitle', 'Location of the file to be imported'), + actions: [this.fileButton] + }; + } + + private async createTableNameBox(): Promise { + this.tableNameTextBox = this.view.modelBuilder.inputBox().withValidation((name) => { + let tableName = name.value; + + if (!tableName || tableName.length === 0) { + return false; + } + + // This won't actually do anything until table names are brought back in. + if (this.tableNames.indexOf(tableName) !== -1) { + return false; + } + + return true; + }).component(); + + this.tableNameTextBox.onTextChanged((tableName) => { + this.model.table = tableName; + }); + + return { + component: this.tableNameTextBox, + title: localize('flatFileImport.tableTextboxTitle', 'New table name'), + }; + } + + + private async createSchemaDropdown(): Promise { + this.schemaDropdown = this.view.modelBuilder.dropDown().component(); + this.schemaLoader = this.view.modelBuilder.loadingComponent().withItem(this.schemaDropdown).component(); + + this.schemaDropdown.onValueChanged(() => { + this.model.schema = (this.schemaDropdown.value).name; + }); + + + return { + component: this.schemaLoader, + title: localize('flatFileImport.schemaTextboxTitle', 'Table schema'), + }; + + } + + private async populateSchemaDropdown(): Promise { + this.schemaLoader.loading = true; + let connectionUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId); + let queryProvider = sqlops.dataprotocol.getProvider(this.model.server.providerName, sqlops.DataProviderType.QueryProvider); + + let query = `SELECT name FROM sys.schemas`; + + let results = await queryProvider.runQueryAndReturn(connectionUri, query); + + let idx = -1; + let count = -1; + + let values = results.rows.map(row => { + let schemaName = row[0].displayValue; + count++; + if (this.model.schema && schemaName === this.model.schema) { + idx = count; + } + let val = row[0].displayValue; + + return { + name: val, + displayName: val + }; + }); + + if (idx > 0) { + let tmp = values[0]; + values[0] = values[idx]; + values[idx] = tmp; + } + + this.model.schema = values[0].name; + + this.schemaDropdown.updateProperties({ + values: values + }); + + this.schemaLoader.loading = false; + return true; + } + + // private async populateTableNames(): Promise { + // this.tableNames = []; + // let databaseName = (this.databaseDropdown.value).name; + // + // if (!databaseName || databaseName.length === 0) { + // this.tableNames = []; + // return false; + // } + // + // let connectionUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId); + // let queryProvider = sqlops.dataprotocol.getProvider(this.model.server.providerName, sqlops.DataProviderType.QueryProvider); + // let results: sqlops.SimpleExecuteResult; + // + // try { + // //let query = sqlstring.format('USE ?; SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = \'BASE TABLE\'', [databaseName]); + // //results = await queryProvider.runQueryAndReturn(connectionUri, query); + // } catch (e) { + // return false; + // } + // + // this.tableNames = results.rows.map(row => { + // return row[0].displayValue; + // }); + // + // return true; + // } +} + + +interface ConnectionDropdownValue extends sqlops.CategoryValue { + connection: sqlops.connection.Connection; +} diff --git a/extensions/import/src/wizard/pages/modifyColumnsPage.ts b/extensions/import/src/wizard/pages/modifyColumnsPage.ts new file mode 100644 index 0000000000..bee843e555 --- /dev/null +++ b/extensions/import/src/wizard/pages/modifyColumnsPage.ts @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sqlops from 'sqlops'; +import * as nls from 'vscode-nls'; +import { ColumnMetadata, ImportDataModel } from '../api/models'; +import { ImportPage } from '../api/importPage'; +import { FlatFileProvider } from '../../services/contracts'; +import { FlatFileWizard } from '../flatFileWizard'; + +const localize = nls.loadMessageBundle(); + +export class ModifyColumnsPage extends ImportPage { + private readonly categoryValues = [ + { name: 'bigint', displayName: 'bigint' }, + { name: 'binary(50)', displayName: 'binary(50)' }, + { name: 'bit', displayName: 'bit' }, + { name: 'char(10)', displayName: 'char(10)' }, + { name: 'date', displayName: 'date' }, + { name: 'datetime', displayName: 'datetime' }, + { name: 'datetime2(7)', displayName: 'datetime2(7)' }, + { name: 'datetimeoffset(7)', displayName: 'datetimeoffset(7)' }, + { name: 'decimal(18, 10)', displayName: 'decimal(18, 10)' }, + { name: 'float', displayName: 'float' }, + { name: 'geography', displayName: 'geography' }, + { name: 'geometry', displayName: 'geometry' }, + { name: 'hierarchyid', displayName: 'hierarchyid' }, + { name: 'int', displayName: 'int' }, + { name: 'money', displayName: 'money' }, + { name: 'nchar(10)', displayName: 'nchar(10)' }, + { name: 'ntext', displayName: 'ntext' }, + { name: 'numeric(18, 0)', displayName: 'numeric(18, 0)' }, + { name: 'nvarchar(50)', displayName: 'nvarchar(50)' }, + { name: 'nvarchar(MAX)', displayName: 'nvarchar(MAX)' }, + { name: 'real', displayName: 'real' }, + { name: 'smalldatetime', displayName: 'smalldatetime' }, + { name: 'smallint', displayName: 'smallint' }, + { name: 'smallmoney', displayName: 'smallmoney' }, + { name: 'sql_variant', displayName: 'sql_variant' }, + { name: 'text', displayName: 'text' }, + { name: 'time(7)', displayName: 'time(7)' }, + { name: 'timestamp', displayName: 'timestamp' }, + { name: 'tinyint', displayName: 'tinyint' }, + { name: 'uniqueidentifier', displayName: 'uniqueidentifier' }, + { name: 'varbinary(50)', displayName: 'varbinary(50)' }, + { name: 'varbinary(MAX)', displayName: 'varbinary(MAX)' }, + { name: 'varchar(50)', displayName: 'varchar(50)' }, + { name: 'varchar(MAX)', displayName: 'varchar(MAX)' } + ]; + private table: sqlops.DeclarativeTableComponent; + private loading: sqlops.LoadingComponent; + private text: sqlops.TextComponent; + private form: sqlops.FormContainer; + + public constructor(instance: FlatFileWizard, model: ImportDataModel, view: sqlops.ModelView, provider: FlatFileProvider) { + super(instance, model, view, provider); + } + + private static convertMetadata(column: ColumnMetadata): any[] { + return [column.columnName, column.dataType, false, column.nullable]; + } + + async start(): Promise { + this.loading = this.view.modelBuilder.loadingComponent().component(); + this.table = this.view.modelBuilder.declarativeTable().component(); + this.text = this.view.modelBuilder.text().component(); + + this.table.onDataChanged((e) => { + this.model.proseColumns = []; + this.table.data.forEach((row) => { + this.model.proseColumns.push({ + columnName: row[0], + dataType: row[1], + primaryKey: row[2], + nullable: row[3] + }); + }); + }); + + + this.form = this.view.modelBuilder.formContainer() + .withFormItems( + [ + { + component: this.text, + title: '' + }, + { + component: this.table, + title: '' + } + ], { + horizontal: false, + componentWidth: '100%' + }).component(); + + this.loading.component = this.form; + await this.view.initializeModel(this.form); + return true; + } + + async onPageEnter(): Promise { + this.loading.loading = true; + await this.populateTable(); + this.loading.loading = false; + + return true; + } + + async onPageLeave(): Promise { + return undefined; + } + + async cleanup(): Promise { + delete this.model.proseColumns; + + return true; + } + + private async populateTable() { + let data: any[][] = []; + + this.model.proseColumns.forEach((column) => { + data.push(ModifyColumnsPage.convertMetadata(column)); + }); + + this.table.updateProperties({ + height: 400, + columns: [{ + displayName: localize('flatFileImport.columnName', 'Column Name'), + valueType: sqlops.DeclarativeDataType.string, + width: '150px', + isReadOnly: false + }, { + displayName: localize('flatFileImport.dataType', 'Data type'), + valueType: sqlops.DeclarativeDataType.editableCategory, + width: '150px', + isReadOnly: false, + categoryValues: this.categoryValues + }, { + displayName: localize('flatFileImport.primaryKey', 'Primary key'), + valueType: sqlops.DeclarativeDataType.boolean, + width: '100px', + isReadOnly: false + }, { + displayName: localize('flatFileImport.allowNull', 'Allow null'), + valueType: sqlops.DeclarativeDataType.boolean, + isReadOnly: false, + width: '100px' + }], + data: data + }); + + + } + +} diff --git a/extensions/import/src/wizard/pages/prosePreviewPage.ts b/extensions/import/src/wizard/pages/prosePreviewPage.ts new file mode 100644 index 0000000000..7aa2ddff9b --- /dev/null +++ b/extensions/import/src/wizard/pages/prosePreviewPage.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sqlops from 'sqlops'; +import * as nls from 'vscode-nls'; +import { ImportDataModel } from '../api/models'; +import { ImportPage } from '../api/importPage'; +import { FlatFileProvider } from '../../services/contracts'; +import { FlatFileWizard } from '../flatFileWizard'; + +const localize = nls.loadMessageBundle(); + +export class ProsePreviewPage extends ImportPage { + private table: sqlops.TableComponent; + private loading: sqlops.LoadingComponent; + private form: sqlops.FormContainer; + private refresh: sqlops.ButtonComponent; + + + public constructor(instance: FlatFileWizard, model: ImportDataModel, view: sqlops.ModelView, provider: FlatFileProvider) { + super(instance, model, view, provider); + } + + async start(): Promise { + this.table = this.view.modelBuilder.table().component(); + this.refresh = this.view.modelBuilder.button().withProperties({ + label: localize('flatFileImport.refresh', 'Refresh'), + isFile: false + }).component(); + + this.refresh.onDidClick(async () => { + this.onPageEnter(); + }); + + this.loading = this.view.modelBuilder.loadingComponent().component(); + this.setupNavigationValidator(); + + this.form = this.view.modelBuilder.formContainer().withFormItems([ + { + component: this.table, + title: localize('flatFileImport.prosePreviewMessage', 'This operation analyzed the input file structure to generate the preview below'), + actions: [this.refresh] + } + ]).component(); + + this.loading.component = this.form; + + await this.view.initializeModel(this.loading); + + return true; + } + + async onPageEnter(): Promise { + this.loading.loading = true; + await this.handleProse(); + await this.populateTable(this.model.proseDataPreview, this.model.proseColumns.map(c => c.columnName)); + this.loading.loading = false; + + return true; + } + + async onPageLeave(): Promise { + await this.emptyTable(); + return true; + } + + async cleanup(): Promise { + delete this.model.proseDataPreview; + return true; + } + + private setupNavigationValidator() { + this.instance.registerNavigationValidator((info) => { + if (this.loading.loading) { + return false; + } + return true; + }); + } + + private async handleProse() { + await this.provider.sendPROSEDiscoveryRequest({ + filePath: this.model.filePath, + tableName: this.model.table, + schemaName: this.model.schema, + fileType: this.model.fileType + }).then((result) => { + this.model.proseDataPreview = result.dataPreview; + this.model.proseColumns = []; + result.columnInfo.forEach((column) => { + this.model.proseColumns.push({ + columnName: column.name, + dataType: column.sqlType, + primaryKey: false, + nullable: column.isNullable + }); + }); + }); + } + + private async populateTable(tableData: string[][], columnHeaders: string[]) { + let rows; + let rowsLength = tableData.length; + + if (rowsLength > 50) { + rows = tableData; + } + else { + rows = tableData.slice(0, rowsLength); + } + + this.table.updateProperties({ + data: rows, + columns: columnHeaders, + height: 400, + width: '700', + }); + } + + private async emptyTable() { + this.table.updateProperties([]); + } + +} diff --git a/extensions/import/src/wizard/pages/summaryPage.ts b/extensions/import/src/wizard/pages/summaryPage.ts new file mode 100644 index 0000000000..eaa4ffc975 --- /dev/null +++ b/extensions/import/src/wizard/pages/summaryPage.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sqlops from 'sqlops'; +import * as nls from 'vscode-nls'; + +import { ImportDataModel } from '../api/models'; +import { ImportPage } from '../api/importPage'; +import { FlatFileProvider, InsertDataResponse } from '../../services/contracts'; +import { FlatFileWizard } from '../flatFileWizard'; + +const localize = nls.loadMessageBundle(); + + +export class SummaryPage extends ImportPage { + private table: sqlops.TableComponent; + private statusText: sqlops.TextComponent; + private loading: sqlops.LoadingComponent; + private form: sqlops.FormContainer; + + public constructor(instance: FlatFileWizard, model: ImportDataModel, view: sqlops.ModelView, provider: FlatFileProvider) { + super(instance, model, view, provider); + } + + async start(): Promise { + this.table = this.view.modelBuilder.table().component(); + this.statusText = this.view.modelBuilder.text().component(); + this.loading = this.view.modelBuilder.loadingComponent().withItem(this.statusText).component(); + + this.form = this.view.modelBuilder.formContainer().withFormItems( + [ + { + component: this.table, + title: localize('flatFileImport.importInformation', 'Import information') + }, + { + component: this.loading, + title: localize('flatFileImport.importStatus', 'Import status') + } + ] + ).component(); + + await this.view.initializeModel(this.form); + return true; + } + + async onPageEnter(): Promise { + this.loading.loading = true; + this.populateTable(); + await this.handleImport(); + this.loading.loading = false; + this.instance.setImportAnotherFileVisibility(true); + + return true; + } + + async onPageLeave(): Promise { + this.instance.setImportAnotherFileVisibility(false); + + return true; + } + + private populateTable() { + this.table.updateProperties({ + data: [ + ['Server name', this.model.server.providerName], + ['Database name', this.model.database], + ['Table name', this.model.table], + ['Table schema', this.model.schema], + ['File to be imported', this.model.filePath]], + columns: ['Object type', 'Name'], + width: 600, + height: 200 + }); + } + + private async handleImport(): Promise { + let changeColumnResults = []; + this.model.proseColumns.forEach((val, i, arr) => { + let columnChangeParams = { + index: i, + newName: val.columnName, + newDataType: val.dataType, + newNullable: val.nullable, + newInPrimaryKey: val.primaryKey + }; + changeColumnResults.push(this.provider.sendChangeColumnSettingsRequest(columnChangeParams)); + }); + + let result: InsertDataResponse; + let err; + try { + result = await this.provider.sendInsertDataRequest({ + connectionString: await this.getConnectionString(), + //TODO check what SSMS uses as batch size + batchSize: 500 + }); + } catch (e) { + err = e.toString(); + } + + let updateText: string; + if (!result || !result.result.success) { + updateText = '✗ '; + if (!result) { + updateText += err; + } else { + updateText += result.result.errorMessage; + } + } else { + // TODO: When sql statements are in, implement this. + //let rows = await this.getCountRowsInserted(); + //if (rows < 0) { + updateText = localize('flatFileImport.success.norows', '✔ Awesome! You have successfully inserted the data into a table.'); + //} else { + //updateText = localize('flatFileImport.success.rows', '✔ Awesome! You have successfully inserted {0} rows.', rows); + //} + } + this.statusText.updateProperties({ + value: updateText + }); + return true; + } + + /** + * Gets the connection string to send to the middleware + * @returns {Promise} + */ + private async getConnectionString(): Promise { + let options = this.model.server.options; + let connectionString: string; + + if (options.authenticationType === 'Integrated') { + connectionString = `Data Source=${options.server + (options.port ? `,${options.port}` : '')};Initial Catalog=${this.model.database};Integrated Security=True`; + } else { + let credentials = await sqlops.connection.getCredentials(this.model.server.connectionId); + connectionString = `Data Source=${options.server + (options.port ? `,${options.port}` : '')};Initial Catalog=${this.model.database};Integrated Security=False;User Id=${options.user};Password=${credentials.password}`; + } + + // TODO: Fix this, it's returning undefined string. + //await sqlops.connection.getConnectionString(this.model.server.connectionId, true); + return connectionString; + } + + // private async getCountRowsInserted(): Promise { + // let connectionUri = await sqlops.connection.getUriForConnection(this.model.server.connectionId); + // let queryProvider = sqlops.dataprotocol.getProvider(this.model.server.providerName, sqlops.DataProviderType.QueryProvider); + // try { + // let query = sqlstring.format('USE ?; SELECT COUNT(*) FROM ?', [this.model.database, this.model.table]); + // let results = await queryProvider.runQueryAndReturn(connectionUri, query); + // let cell = results.rows[0][0]; + // if (!cell || cell.isNull) { + // return -1; + // } + // let numericCell = Number(cell.displayValue); + // if (isNaN(numericCell)) { + // return -1; + // } + // return numericCell; + // } catch (e) { + // return -1; + // } + // } +} diff --git a/extensions/import/tsconfig.json b/extensions/import/tsconfig.json new file mode 100644 index 0000000000..e6baa6d40d --- /dev/null +++ b/extensions/import/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "./out", + "lib": [ + "es6", "es2015.promise" + ], + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "declaration": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/extensions/import/yarn.lock b/extensions/import/yarn.lock new file mode 100644 index 0000000000..7c6bbee2e6 --- /dev/null +++ b/extensions/import/yarn.lock @@ -0,0 +1,470 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +agent-base@4, agent-base@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" + dependencies: + es6-promisify "^5.0.0" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +applicationinsights@0.15.6: + version "0.15.6" + resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-0.15.6.tgz#201a0682c0704fe4bdd9a92d0b2cbe34d2ae5972" + +base64-js@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" + +bl@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + +buffer-alloc@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + +buffer@^3.0.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-3.6.0.tgz#a72c936f77b96bf52f5f7e7b467180628551defb" + dependencies: + base64-js "0.0.8" + ieee754 "^1.1.4" + isarray "^1.0.0" + +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + +commander@~2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" + dependencies: + graceful-readlink ">= 1.0.0" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + +"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#0.1.5": + version "0.1.5" + resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/21b0bacfc759689a6c280408528c6029a21b1abf" + dependencies: + vscode-languageclient "3.5.0" + +debug@3.1.0, debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +debug@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" + dependencies: + file-type "^5.2.0" + is-stream "^1.1.0" + tar-stream "^1.5.2" + +decompress-tarbz2@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" + dependencies: + decompress-tar "^4.1.0" + file-type "^6.1.0" + is-stream "^1.1.0" + seek-bzip "^1.0.5" + unbzip2-stream "^1.0.9" + +decompress-targz@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" + dependencies: + decompress-tar "^4.1.1" + file-type "^5.2.0" + is-stream "^1.1.0" + +decompress-unzip@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69" + dependencies: + file-type "^3.8.0" + get-stream "^2.2.0" + pify "^2.3.0" + yauzl "^2.4.2" + +decompress@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.0.tgz#7aedd85427e5a92dacfe55674a7c505e96d01f9d" + dependencies: + decompress-tar "^4.0.0" + decompress-tarbz2 "^4.0.0" + decompress-targz "^4.0.0" + decompress-unzip "^4.0.1" + graceful-fs "^4.1.10" + make-dir "^1.0.0" + pify "^2.3.0" + strip-dirs "^2.0.0" + +end-of-stream@^1.0.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + dependencies: + once "^1.4.0" + +es6-promise@^4.0.3: + version "4.2.4" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29" + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + dependencies: + es6-promise "^4.0.3" + +eventemitter2@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-5.0.1.tgz#6197a095d5fb6b57e8942f6fd7eaad63a09c9452" + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + dependencies: + pend "~1.2.0" + +file-type@^3.8.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" + +file-type@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" + +file-type@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + +get-stream@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" + dependencies: + object-assign "^4.0.1" + pinkie-promise "^2.0.0" + +graceful-fs@^4.1.10: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + +http-proxy-agent@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" + dependencies: + agent-base "4" + debug "3.1.0" + +https-proxy-agent@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" + dependencies: + agent-base "^4.1.0" + debug "^3.1.0" + +ieee754@^1.1.4: + version "1.1.12" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" + +inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +is-buffer@~1.1.1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-natural-number@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +lodash@^4.16.4: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + dependencies: + pify "^3.0.0" + +md5@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +mkdirp@^0.5.1, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mocha-junit-reporter@^1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.17.0.tgz#2e5149ed40fc5d2e3ca71e42db5ab1fec9c6d85c" + dependencies: + debug "^2.2.0" + md5 "^2.1.0" + mkdirp "~0.5.1" + strip-ansi "^4.0.0" + xml "^1.0.0" + +mocha-multi-reporters@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz#cc7f3f4d32f478520941d852abb64d9988587d82" + dependencies: + debug "^3.1.0" + lodash "^4.16.4" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +opener@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + +readable-stream@^2.3.0, readable-stream@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +seek-bzip@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.5.tgz#cfe917cb3d274bcffac792758af53173eb1fabdc" + dependencies: + commander "~2.8.1" + +"service-downloader@github:anthonydresser/service-downloader#0.1.4": + version "0.1.4" + resolved "https://codeload.github.com/anthonydresser/service-downloader/tar.gz/3c0abdf8603aca85d2eacfac3c547173e41bf0c7" + dependencies: + decompress "^4.2.0" + eventemitter2 "^5.0.1" + http-proxy-agent "^2.0.0" + https-proxy-agent "^2.1.1" + mkdirp "^0.5.1" + tmp "^0.0.33" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-dirs@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" + dependencies: + is-natural-number "^4.0.1" + +tar-stream@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.1.tgz#f84ef1696269d6223ca48f6e1eeede3f7e81f395" + dependencies: + bl "^1.0.0" + buffer-alloc "^1.1.0" + end-of-stream "^1.0.0" + fs-constants "^1.0.0" + readable-stream "^2.3.0" + to-buffer "^1.1.0" + xtend "^4.0.0" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + dependencies: + os-tmpdir "~1.0.2" + +to-buffer@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" + +unbzip2-stream@^1.0.9: + version "1.2.5" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.2.5.tgz#73a033a567bbbde59654b193c44d48a7e4f43c47" + dependencies: + buffer "^3.0.1" + through "^2.3.6" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +vscode-extension-telemetry@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.0.5.tgz#21e2abb4cbce3326e469ddbb322123b3702f3f85" + dependencies: + applicationinsights "0.15.6" + winreg "0.0.13" + +vscode-jsonrpc@3.5.0, vscode-jsonrpc@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-3.5.0.tgz#87239d9e166b2d7352245b8a813597804c1d63aa" + +vscode-languageclient@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-3.5.0.tgz#36d02cc186a8365a4467719a290fb200a9ae490a" + dependencies: + vscode-languageserver-protocol "^3.5.0" + +vscode-languageserver-protocol@3.5.0, vscode-languageserver-protocol@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.5.0.tgz#067c5cbe27709795398d119692c97ebba1452209" + dependencies: + vscode-jsonrpc "^3.5.0" + vscode-languageserver-types "^3.5.0" + +vscode-languageserver-types@3.5.0, vscode-languageserver-types@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.5.0.tgz#e48d79962f0b8e02de955e3f524908e2b19c0374" + +vscode-nls@^3.2.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.2.tgz#3817eca5b985c2393de325197cf4e15eb2aa5350" + +winreg@0.0.13: + version "0.0.13" + resolved "https://registry.yarnpkg.com/winreg/-/winreg-0.0.13.tgz#76bfe02e1dd0c9c8275fb9fdf17a9f36846e3483" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +xml@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" + +xtend@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +yauzl@^2.4.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" diff --git a/product.json b/product.json index d78cfc30cd..2fa7d7fb18 100644 --- a/product.json +++ b/product.json @@ -35,6 +35,7 @@ "date": "2017-12-15T12:00:00.000Z", "recommendedExtensions": [ "Microsoft.agent", + "Microsoft.import", "Microsoft.profiler", "Microsoft.server-report", "Microsoft.whoisactive",