Add Notebook <-> SQL convert (#11590)

* Add Notebook <-> SQL convert

* Update STS
This commit is contained in:
Charles Gagnon
2020-08-03 14:50:24 -07:00
committed by GitHub
parent fbbb9ce529
commit 694f34a4cd
17 changed files with 248 additions and 7 deletions

View File

@@ -1,6 +1,6 @@
{
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "3.0.0-release.8",
"version": "3.0.0-release.11",
"downloadFileNames": {
"Windows_86": "win-x86-netcoreapp3.1.zip",
"Windows_64": "win-x64-netcoreapp3.1.zip",

View File

@@ -21,6 +21,18 @@
},
"contributes": {
"commands": [
{
"command": "mssql.exportSqlAsNotebook",
"title": "%mssql.exportSqlAsNotebook%"
},
{
"command": "mssql.exportNotebookToSql",
"title": "%mssql.exportNotebookToSql%",
"icon": {
"dark": "resources/dark/export_blue_dark.svg",
"light": "resources/light/export_blue_light.svg"
}
},
{
"command": "mssqlCluster.uploadFiles",
"title": "%mssqlCluster.uploadFiles%"
@@ -355,6 +367,14 @@
},
"menus": {
"commandPalette": [
{
"command": "mssql.exportSqlAsNotebook",
"when": "false"
},
{
"command": "mssql.exportNotebookToSql",
"when": "false"
},
{
"command": "mssqlCluster.uploadFiles",
"when": "false"
@@ -450,6 +470,12 @@
"when": "nodeType == mssqlCluster:file && nodeSubType =~/:spark:/",
"group": "1mssqlCluster@6"
}
],
"notebook/toolbar": [
{
"command": "mssql.exportNotebookToSql",
"when": "providerId == sql"
}
]
},
"dashboard": {

View File

@@ -39,6 +39,8 @@
"mssql.disabled": "Disabled",
"mssql.enabled": "Enabled",
"mssql.exportNotebookToSql": "Export Notebook as SQL",
"mssql.exportSqlAsNotebook": "Export SQL as Notebook",
"mssql.configuration.title": "MSSQL configuration",
"mssql.query.displayBitAsNumber": "Should BIT columns be displayed as numbers (1 or 0)? If false, BIT columns will be displayed as 'true' or 'false'",
"mssql.query.maxXmlCharsToStore": "Number of XML characters to store after running a query",

View File

@@ -0,0 +1,15 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<g clip-path="url(#clip1)">
<path d="M11.6943 4.60156L14.1006 7.01562H2.0459V8.01562H14.0771L11.6943 10.3984L12.3975 11.1016L15.999 7.5L12.3975 3.89844L11.6943 4.60156ZM1.0459 4H0.0458984V11H1.0459V4Z" fill="#3AA0F3"/>
</g>
</g>
<defs>
<clipPath id="clip0">
<rect width="16" height="16" fill="white"/>
</clipPath>
<clipPath id="clip1">
<rect width="16" height="16" fill="white" transform="translate(-0.000976562)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 567 B

View File

@@ -0,0 +1,15 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<g clip-path="url(#clip1)">
<path d="M11.6943 4.60156L14.1006 7.01562H2.0459V8.01562H14.0771L11.6943 10.3984L12.3975 11.1016L15.999 7.5L12.3975 3.89844L11.6943 4.60156ZM1.0459 4H0.0458984V11H1.0459V4Z" fill="#0078D4"/>
</g>
</g>
<defs>
<clipPath id="clip0">
<rect width="16" height="16" fill="white"/>
</clipPath>
<clipPath id="clip1">
<rect width="16" height="16" fill="white" transform="translate(-0.000976562)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 567 B

View File

@@ -31,6 +31,8 @@ export const hdfsRootPath = '/';
export const clusterEndpointsProperty = 'clusterEndpoints';
export const isBigDataClusterProperty = 'isBigDataCluster';
export const ViewType = 'view';
// SERVICE NAMES //////////////////////////////////////////////////////////
export const ObjectExplorerService = 'objectexplorer';
export const CmsService = 'cmsService';
@@ -38,8 +40,8 @@ export const DacFxService = 'dacfxService';
export const SchemaCompareService = 'schemaCompareService';
export const LanguageExtensionService = 'languageExtensionService';
export const objectExplorerPrefix: string = 'objectexplorer://';
export const ViewType = 'view';
export const SqlAssessmentService = 'sqlAssessmentService';
export const NotebookConvertService = 'notebookConvertService';
export enum BuiltInCommands {
SetContext = 'setContext'

View File

@@ -747,3 +747,33 @@ export class CompletionExtensionParams {
export namespace CompletionExtLoadRequest {
export const type = new RequestType<CompletionExtensionParams, boolean, void, void>('completion/extLoad');
}
// ------------------------------- < Load Completion Extension Request > ------------------------------------
/// ------------------------------- <Convert Notebook> -----------------------------
export interface ConvertNotebookToSqlParams {
content: string;
}
export namespace ConvertNotebookToSqlRequest {
export const type = new RequestType<ConvertNotebookToSqlParams, ConvertNotebookToSqlResult, void, void>('notebookconvert/convertnotebooktosql');
}
export interface ConvertNotebookToSqlResult extends azdata.ResultStatus {
content: string;
}
export interface ConvertSqlToNotebookParams {
clientUri: string;
}
export namespace ConvertSqlToNotebookRequest {
export const type = new RequestType<ConvertSqlToNotebookParams, ConvertSqlToNotebookResult, void, void>('notebookconvert/convertsqltonotebook');
}
export interface ConvertSqlToNotebookResult extends azdata.ResultStatus {
content: string;
}
// ------------------------------- <Convert Notebook> -----------------------------

View File

@@ -30,6 +30,7 @@ import { SqlToolsServer } from './sqlToolsServer';
import { promises as fs } from 'fs';
import { IconPathHelper } from './iconHelper';
import * as nls from 'vscode-nls';
import { INotebookConvertService } from './notebookConvert/notebookConvertService';
const localize = nls.loadMessageBundle();
const msgSampleCodeDataFrame = localize('msgSampleCodeDataFrame', "This sample code loads the file into a data frame and shows the first 10 results.");
@@ -79,6 +80,24 @@ export async function activate(context: vscode.ExtensionContext): Promise<IExten
context.subscriptions.push(server);
await server.start(appContext);
vscode.commands.registerCommand('mssql.exportSqlAsNotebook', async (uri: vscode.Uri) => {
const result = await appContext.getService<INotebookConvertService>(Constants.NotebookConvertService).convertSqlToNotebook(uri.toString());
const title = findNextUntitledEditorName();
const untitledUri = vscode.Uri.parse(`untitled:${title}`);
await azdata.nb.showNotebookDocument(untitledUri, { initialContent: result.content });
});
vscode.commands.registerCommand('mssql.exportNotebookToSql', async (uri: vscode.Uri) => {
// SqlToolsService doesn't currently store anything about Notebook documents so we have to pass the raw JSON to it directly
// We use vscode.workspace.textDocuments here because the azdata.nb.notebookDocuments don't actually contain their contents
// (they're left out for perf purposes)
const doc = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString());
const result = await appContext.getService<INotebookConvertService>(Constants.NotebookConvertService).convertNotebookToSql(doc.getText());
const sqlDoc = await vscode.workspace.openTextDocument({ language: 'sql', content: result.content });
await vscode.commands.executeCommand('vscode.open', sqlDoc.uri);
});
return createMssqlApi(appContext);
}

View File

@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AppContext } from '../appContext';
import { SqlOpsDataClient, ISqlOpsFeature } from 'dataprotocol-client';
import { ClientCapabilities } from 'vscode-languageclient';
import * as constants from '../constants';
import * as contracts from '../contracts';
export interface INotebookConvertService {
convertNotebookToSql(content: string): Promise<contracts.ConvertNotebookToSqlResult | undefined>;
convertSqlToNotebook(content: string): Promise<contracts.ConvertSqlToNotebookResult | undefined>;
}
export class NotebookConvertService implements INotebookConvertService {
public static asFeature(context: AppContext): ISqlOpsFeature {
return class extends NotebookConvertService {
constructor(client: SqlOpsDataClient) {
super(context, client);
}
fillClientCapabilities(capabilities: ClientCapabilities): void {
}
initialize(): void {
}
};
}
private constructor(context: AppContext, protected readonly client: SqlOpsDataClient) {
context.registerService(constants.NotebookConvertService, this);
}
async convertNotebookToSql(content: string): Promise<contracts.ConvertNotebookToSqlResult | undefined> {
let params: contracts.ConvertNotebookToSqlParams = { content: content };
try {
return this.client.sendRequest(contracts.ConvertNotebookToSqlRequest.type, params);
}
catch (e) {
this.client.logFailedRequest(contracts.ConvertNotebookToSqlRequest.type, e);
}
return undefined;
}
async convertSqlToNotebook(content: string): Promise<contracts.ConvertSqlToNotebookResult | undefined> {
let params: contracts.ConvertSqlToNotebookParams = { clientUri: content };
try {
return this.client.sendRequest(contracts.ConvertSqlToNotebookRequest.type, params);
}
catch (e) {
this.client.logFailedRequest(contracts.ConvertSqlToNotebookRequest.type, e);
}
return undefined;
}
}

View File

@@ -23,6 +23,7 @@ import { promises as fs } from 'fs';
import * as nls from 'vscode-nls';
import { LanguageExtensionService } from './languageExtension/languageExtensionService';
import { SqlAssessmentService } from './sqlAssessment/sqlAssessmentService';
import { NotebookConvertService } from './notebookConvert/notebookConvertService';
const localize = nls.loadMessageBundle();
const outputChannel = vscode.window.createOutputChannel(Constants.serviceName);
@@ -160,7 +161,8 @@ function getClientOptions(context: AppContext): ClientOptions {
LanguageExtensionService.asFeature(context),
DacFxService.asFeature(context),
CmsService.asFeature(context),
SqlAssessmentService.asFeature(context)
SqlAssessmentService.asFeature(context),
NotebookConvertService.asFeature(context)
],
outputChannel: new CustomOutputChannel()
};

View File

@@ -118,3 +118,9 @@
background-image: url('disable_sqlcmd_inverse.svg');
background-repeat: no-repeat;
}
.carbon-taskbar .codicon.export {
background-origin: initial;
background-position: left;
background-size: 11px
}

View File

@@ -287,6 +287,15 @@
background-image: url("stop_inverse.svg");
}
.hc-black .codicon.export,
.vs-dark .codicon.export {
background: url("export_inverse.svg") center center no-repeat;
}
.vs .codicon.export {
background: url("export.svg") center center no-repeat;
}
/* Notebook cells */
.codicon.toolbarIconRunInactive {
background-image: url("execute_cell_grey.svg");

View File

@@ -0,0 +1,15 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<g clip-path="url(#clip1)">
<path d="M11.6943 4.60156L14.1006 7.01562H2.0459V8.01562H14.0771L11.6943 10.3984L12.3975 11.1016L15.999 7.5L12.3975 3.89844L11.6943 4.60156ZM1.0459 4H0.0458984V11H1.0459V4Z" fill="#323130"/>
</g>
</g>
<defs>
<clipPath id="clip0">
<rect width="16" height="16" fill="white"/>
</clipPath>
<clipPath id="clip1">
<rect width="16" height="16" fill="white" transform="translate(-0.000976562)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 567 B

View File

@@ -0,0 +1,15 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<g clip-path="url(#clip1)">
<path d="M11.6943 4.60156L14.1006 7.01562H2.0459V8.01562H14.0771L11.6943 10.3984L12.3975 11.1016L15.999 7.5L12.3975 3.89844L11.6943 4.60156ZM1.0459 4H0.0458984V11H1.0459V4Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0">
<rect width="16" height="16" fill="white"/>
</clipPath>
<clipPath id="clip1">
<rect width="16" height="16" fill="white" transform="translate(-0.000976562)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 565 B

View File

@@ -553,7 +553,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
// This is similar behavior that exists in MenuItemActionItem
if (action instanceof MenuItemAction) {
if (action.item.id.includes('jupyter.cmd') && this.previewFeaturesEnabled) {
if ((action.item.id.includes('jupyter.cmd') && this.previewFeaturesEnabled) || action.item.id.includes('mssql')) {
action.tooltip = action.label;
action.label = '';
}

View File

@@ -36,7 +36,7 @@ import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/q
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { getCurrentGlobalConnection } from 'sql/workbench/browser/taskUtilities';
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
import { OEAction } from 'sql/workbench/services/objectExplorer/browser/objectExplorerActions';
import { TreeViewItemHandleArg } from 'sql/workbench/common/views';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
@@ -813,3 +813,27 @@ export class ListDatabasesActionItem extends Disposable implements IActionViewIt
}
}
/**
* Action class that sends the request to convert the contents of the sql editor
* into a Notebook document
*/
export class ExportAsNotebookAction extends QueryTaskbarAction {
public static IconClass = 'export';
public static ID = 'exportAsNotebookAction';
constructor(
editor: QueryEditor,
@IConnectionManagementService connectionManagementService: IConnectionManagementService,
@ICommandService private _commandService: ICommandService
) {
super(connectionManagementService, editor, ConnectDatabaseAction.ID, ExportAsNotebookAction.IconClass);
this.label = nls.localize('queryEditor.exportSqlAsNotebook', "Export as Notebook");
}
public async run(): Promise<void> {
this._commandService.executeCommand('mssql.exportSqlAsNotebook', this.editor.input.uri);
}
}

View File

@@ -86,6 +86,7 @@ export class QueryEditor extends BaseEditor {
private _actualQueryPlanAction: actions.ActualQueryPlanAction;
private _listDatabasesActionItem: actions.ListDatabasesActionItem;
private _toggleSqlcmdMode: actions.ToggleSqlCmdModeAction;
private _exportAsNotebookAction: actions.ExportAsNotebookAction;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@@ -183,6 +184,7 @@ export class QueryEditor extends BaseEditor {
this._estimatedQueryPlanAction = this.instantiationService.createInstance(actions.EstimatedQueryPlanAction, this);
this._actualQueryPlanAction = this.instantiationService.createInstance(actions.ActualQueryPlanAction, this);
this._toggleSqlcmdMode = this.instantiationService.createInstance(actions.ToggleSqlCmdModeAction, this, false);
this._exportAsNotebookAction = this.instantiationService.createInstance(actions.ExportAsNotebookAction, this);
this.setTaskbarContent();
@@ -266,13 +268,14 @@ export class QueryEditor extends BaseEditor {
{ action: this._listDatabasesAction },
{ element: separator },
{ action: this._estimatedQueryPlanAction },
{ action: this._toggleSqlcmdMode }
{ action: this._toggleSqlcmdMode },
{ action: this._exportAsNotebookAction }
];
// Remove the estimated query plan action if preview features are not enabled
let previewFeaturesEnabled = this.configurationService.getValue('workbench')['enablePreviewFeatures'];
if (!previewFeaturesEnabled) {
content = content.slice(0, -2);
content.splice(7, 1);
}
this.taskbar.setContent(content);