Fix #6477 controller login + fix dashboard layout (#6478)

* Fix #6477 controller login + fix dashboard layout
- Service endpoints shoudl be on own column, cut off smaller screen
- Controller login not working due to 404 error
This is due to a breaking API change
We have requested fixes to help mitigate need for cluster name,
but for now have a default value for this

Finally, modified code so it's easier to update swagger API
and also added instructions on how to update in future
This commit is contained in:
Kevin Cunnane
2019-07-23 19:14:18 -07:00
committed by GitHub
parent a92b2e0691
commit a1a67b1a86
9 changed files with 1157 additions and 1186 deletions

View File

@@ -97,7 +97,8 @@ const indentationFilter = [
'!extensions/import/flatfileimportservice/**', '!extensions/import/flatfileimportservice/**',
'!extensions/admin-tool-ext-win/ssmsmin/**', '!extensions/admin-tool-ext-win/ssmsmin/**',
'!extensions/resource-deployment/notebooks/**', '!extensions/resource-deployment/notebooks/**',
'!extensions/mssql/notebooks/**' '!extensions/mssql/notebooks/**',
'!extensions/big-data-cluster/src/bigDataCluster/controller/apiGenerated.ts'
]; ];
const copyrightFilter = [ const copyrightFilter = [
@@ -184,7 +185,9 @@ const tslintFilter = [
'!extensions/vscode-api-tests/testWorkspace/**', '!extensions/vscode-api-tests/testWorkspace/**',
'!extensions/vscode-api-tests/testWorkspace2/**', '!extensions/vscode-api-tests/testWorkspace2/**',
'!extensions/**/*.test.ts', '!extensions/**/*.test.ts',
'!extensions/html-language-features/server/lib/jquery.d.ts' '!extensions/html-language-features/server/lib/jquery.d.ts',
// {{SQL CARBON EDIT}}
'!extensions/big-data-cluster/src/bigDataCluster/controller/apiGenerated.ts'
]; ];
// {{SQL CARBON EDIT}} // {{SQL CARBON EDIT}}

View File

@@ -0,0 +1,12 @@
How to update the Swagger-generated API to contact the controller
1. You need to get the API specification. Long-term you should be able to get from the server,
but for now go to the internal repository and find the checked in SwaggerClient.yaml there.
2. Copy the content from there, and add into https://editor.swagger.io/
3. Choose Generate Client, and choose Typescript-Node as the client to generate
4. This will download a zip file. Open it and copy contents of api.ts
5. Copy this content to apiGenerated.ts
- keep the copyright header and everything above the let defaultBasePath = xyz line,
- Override the rest of the file
6. Format the apiGenerated.ts file so it passes gulp hygiene

View File

@@ -3,10 +3,37 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { EndpointRouterApi } from './apiGenerated'; import * as request from 'request';
import { ClusterRouterApi, Authentication } from './apiGenerated';
class AuthConfiguration implements Authentication {
public username: string = '';
public password: string = '';
constructor(private _ignoreSslVerification: boolean) {
}
applyToRequest(requestOptions: request.Options): void {
requestOptions['agentOptions'] = {
rejectUnauthorized: !this._ignoreSslVerification
};
}
}
class ClusterApiWrapper extends ClusterRouterApi {
constructor(basePathOrUsername: string, password?: string, basePath?: string, ignoreSslVerification?: boolean) {
super(basePathOrUsername, password, basePath);
this.authentications.default = new AuthConfiguration(!!ignoreSslVerification);
}
}
export async function getEndPoints( export async function getEndPoints(
url: string, username: string, password: string, ignoreSslVerification?: boolean clusterName: string,
url: string,
username: string,
password: string,
ignoreSslVerification?: boolean
): Promise<IEndPointsResponse> { ): Promise<IEndPointsResponse> {
if (!url || !username || !password) { if (!url || !username || !password) {
@@ -14,8 +41,7 @@ export async function getEndPoints(
} }
url = adjustUrl(url); url = adjustUrl(url);
let endPointApi = new EndpointRouterApi(username, password, url); let endPointApi = new ClusterApiWrapper(username, password, url, !!ignoreSslVerification);
endPointApi.ignoreSslVerification = !!ignoreSslVerification;
let controllerResponse: IEndPointsResponse = undefined; let controllerResponse: IEndPointsResponse = undefined;
let controllerError: IControllerError = undefined; let controllerError: IControllerError = undefined;
@@ -27,7 +53,7 @@ export async function getEndPoints(
}; };
try { try {
let result = await endPointApi.endpointsGet(); let result = await endPointApi.endpointsGet(clusterName);
controllerResponse = <IEndPointsResponse>{ controllerResponse = <IEndPointsResponse>{
response: result.response as IHttpResponse, response: result.response as IHttpResponse,
endPoints: result.body as IEndPoint[], endPoints: result.body as IEndPoint[],

View File

@@ -19,25 +19,27 @@ export class AddControllerDialogModel {
constructor( constructor(
public treeDataProvider: ControllerTreeDataProvider, public treeDataProvider: ControllerTreeDataProvider,
public node?: TreeNode, public node?: TreeNode,
public prefilledClusterName?: string,
public prefilledUrl?: string, public prefilledUrl?: string,
public prefilledUsername?: string, public prefilledUsername?: string,
public prefilledPassword?: string, public prefilledPassword?: string,
public prefilledRememberPassword?: boolean public prefilledRememberPassword?: boolean
) { ) {
this.prefilledClusterName = prefilledClusterName || (node && node['clusterName']);
this.prefilledUrl = prefilledUrl || (node && node['url']); this.prefilledUrl = prefilledUrl || (node && node['url']);
this.prefilledUsername = prefilledUsername || (node && node['username']); this.prefilledUsername = prefilledUsername || (node && node['username']);
this.prefilledPassword = prefilledPassword || (node && node['password']); this.prefilledPassword = prefilledPassword || (node && node['password']);
this.prefilledRememberPassword = prefilledRememberPassword || (node && node['rememberPassword']); this.prefilledRememberPassword = prefilledRememberPassword || (node && node['rememberPassword']);
} }
public async onComplete(url: string, username: string, password: string, rememberPassword: boolean): Promise<void> { public async onComplete(clusterName: string, url: string, username: string, password: string, rememberPassword: boolean): Promise<void> {
let response = await getEndPoints(url, username, password, true); let response = await getEndPoints(clusterName, url, username, password, true);
if (response && response.endPoints) { if (response && response.endPoints) {
let masterInstance: IEndPoint = undefined; let masterInstance: IEndPoint = undefined;
if (response.endPoints) { if (response.endPoints) {
masterInstance = response.endPoints.find(e => e.name && e.name === 'sql-server-master'); masterInstance = response.endPoints.find(e => e.name && e.name === 'sql-server-master');
} }
this.treeDataProvider.addController(url, username, password, rememberPassword, masterInstance); this.treeDataProvider.addController(clusterName, url, username, password, rememberPassword, masterInstance);
await this.treeDataProvider.saveControllers(); await this.treeDataProvider.saveControllers();
} }
} }
@@ -58,6 +60,7 @@ export class AddControllerDialog {
private dialog: azdata.window.Dialog; private dialog: azdata.window.Dialog;
private uiModelBuilder: azdata.ModelBuilder; private uiModelBuilder: azdata.ModelBuilder;
private clusterNameInputBox: azdata.InputBoxComponent;
private urlInputBox: azdata.InputBoxComponent; private urlInputBox: azdata.InputBoxComponent;
private usernameInputBox: azdata.InputBoxComponent; private usernameInputBox: azdata.InputBoxComponent;
private passwordInputBox: azdata.InputBoxComponent; private passwordInputBox: azdata.InputBoxComponent;
@@ -76,6 +79,11 @@ export class AddControllerDialog {
this.dialog.registerContent(async view => { this.dialog.registerContent(async view => {
this.uiModelBuilder = view.modelBuilder; this.uiModelBuilder = view.modelBuilder;
this.clusterNameInputBox = this.uiModelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
placeHolder: localize('textClusterNameLower', 'mssql-cluster'),
value: this.model.prefilledUrl || 'mssql-cluster'
}).component();
this.urlInputBox = this.uiModelBuilder.inputBox() this.urlInputBox = this.uiModelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({ .withProperties<azdata.InputBoxProperties>({
placeHolder: localize('textUrlLower', 'url'), placeHolder: localize('textUrlLower', 'url'),
@@ -101,7 +109,12 @@ export class AddControllerDialog {
let formModel = this.uiModelBuilder.formContainer() let formModel = this.uiModelBuilder.formContainer()
.withFormItems([{ .withFormItems([{
components: [{ components: [
{
component: this.clusterNameInputBox,
title: localize('textClusterNameCapital', 'Cluster Name'),
required: true
}, {
component: this.urlInputBox, component: this.urlInputBox,
title: localize('textUrlCapital', 'URL'), title: localize('textUrlCapital', 'URL'),
required: true required: true
@@ -131,13 +144,14 @@ export class AddControllerDialog {
} }
private async validate(): Promise<boolean> { private async validate(): Promise<boolean> {
let clusterName = this.clusterNameInputBox && this.clusterNameInputBox.value;
let url = this.urlInputBox && this.urlInputBox.value; let url = this.urlInputBox && this.urlInputBox.value;
let username = this.usernameInputBox && this.usernameInputBox.value; let username = this.usernameInputBox && this.usernameInputBox.value;
let password = this.passwordInputBox && this.passwordInputBox.value; let password = this.passwordInputBox && this.passwordInputBox.value;
let rememberPassword = this.passwordInputBox && !!this.rememberPwCheckBox.checked; let rememberPassword = this.passwordInputBox && !!this.rememberPwCheckBox.checked;
try { try {
await this.model.onComplete(url, username, password, rememberPassword); await this.model.onComplete(clusterName, url, username, password, rememberPassword);
return true; return true;
} catch (error) { } catch (error) {
showErrorMessage(error); showErrorMessage(error);

View File

@@ -18,6 +18,7 @@ import { LoadingControllerNode } from './loadingControllerNode';
const CredentialNamespace = 'clusterControllerCredentials'; const CredentialNamespace = 'clusterControllerCredentials';
interface IControllerInfoSlim { interface IControllerInfoSlim {
clusterName: string;
url: string; url: string;
username: string; username: string;
password?: string; password?: string;
@@ -57,6 +58,7 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
} }
public addController( public addController(
clusterName: string,
url: string, url: string,
username: string, username: string,
password: string, password: string,
@@ -64,7 +66,7 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
masterInstance?: IEndPoint masterInstance?: IEndPoint
): void { ): void {
this.removeNonControllerNodes(); this.removeNonControllerNodes();
this.root.addControllerNode(url, username, password, rememberPassword, masterInstance); this.root.addControllerNode(clusterName, url, username, password, rememberPassword, masterInstance);
this.notifyNodeChanged(); this.notifyNodeChanged();
} }
@@ -118,7 +120,7 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
password = await this.getPassword(c.url, c.username); password = await this.getPassword(c.url, c.username);
} }
this.root.addChild(new ControllerNode( this.root.addChild(new ControllerNode(
c.url, c.username, password, c.rememberPassword, c.clusterName, c.url, c.username, password, c.rememberPassword,
undefined, this.root, this, undefined undefined, this.root, this, undefined
)); ));
} }
@@ -136,6 +138,7 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
let controllers = this.root.children.map(e => { let controllers = this.root.children.map(e => {
let controller = e as ControllerNode; let controller = e as ControllerNode;
return <IControllerInfoSlim>{ return <IControllerInfoSlim>{
clusterName: controller.clusterName,
url: controller.url, url: controller.url,
username: controller.username, username: controller.username,
password: controller.password, password: controller.password,
@@ -145,6 +148,7 @@ export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeN
let controllersWithoutPassword = controllers.map(e => { let controllersWithoutPassword = controllers.map(e => {
return <IControllerInfoSlim>{ return <IControllerInfoSlim>{
clusterName: e.clusterName,
url: e.url, url: e.url,
username: e.username, username: e.username,
rememberPassword: e.rememberPassword rememberPassword: e.rememberPassword

View File

@@ -113,14 +113,20 @@ export class ControllerRootNode extends ControllerTreeNode {
return this.children as ControllerNode[]; return this.children as ControllerNode[];
} }
public addControllerNode(url: string, username: string, password: string, rememberPassword: boolean, masterInstance?: IEndPoint): void { public addControllerNode(clusterName: string,
url: string,
username: string,
password: string,
rememberPassword: boolean,
masterInstance?: IEndPoint
): void {
let controllerNode = this.getExistingControllerNode(url, username); let controllerNode = this.getExistingControllerNode(url, username);
if (controllerNode) { if (controllerNode) {
controllerNode.password = password; controllerNode.password = password;
controllerNode.rememberPassword = rememberPassword; controllerNode.rememberPassword = rememberPassword;
controllerNode.clearChildren(); controllerNode.clearChildren();
} else { } else {
controllerNode = new ControllerNode(url, username, password, rememberPassword, undefined, this, this.treeChangeHandler, undefined); controllerNode = new ControllerNode(clusterName, url, username, password, rememberPassword, undefined, this, this.treeChangeHandler, undefined);
this.addChild(controllerNode); this.addChild(controllerNode);
} }
@@ -158,6 +164,7 @@ export class ControllerRootNode extends ControllerTreeNode {
export class ControllerNode extends ControllerTreeNode { export class ControllerNode extends ControllerTreeNode {
constructor( constructor(
private _clusterName: string,
private _url: string, private _url: string,
private _username: string, private _username: string,
private _password: string, private _password: string,
@@ -184,7 +191,7 @@ export class ControllerNode extends ControllerTreeNode {
} }
try { try {
let response = await getEndPoints(this._url, this._username, this._password, true); let response = await getEndPoints(this._clusterName, this._url, this._username, this._password, true);
if (response && response.endPoints) { if (response && response.endPoints) {
let master = response.endPoints.find(e => e.name && e.name === 'sql-server-master'); let master = response.endPoints.find(e => e.name && e.name === 'sql-server-master');
this.addSqlMasterNode(master.endpoint, master.description); this.addSqlMasterNode(master.endpoint, master.description);
@@ -226,6 +233,14 @@ export class ControllerNode extends ControllerTreeNode {
return item; return item;
} }
public get clusterName() {
return this._clusterName;
}
public set clusterName(clusterName: string) {
this._clusterName = clusterName;
}
public get url() { public get url() {
return this._url; return this._url;
} }

View File

@@ -15,6 +15,12 @@ import { ControllerNode } from './bigDataCluster/tree/controllerTreeNode';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
const AddControllerCommand = 'bigDataClusters.command.addController';
const DeleteControllerCommand = 'bigDataClusters.command.deleteController';
const RefreshControllerCommand = 'bigDataClusters.command.refreshController';
let throttleTimers: { [key: string]: any } = {};
export function activate(extensionContext: vscode.ExtensionContext) { export function activate(extensionContext: vscode.ExtensionContext) {
IconPath.setExtensionContext(extensionContext); IconPath.setExtensionContext(extensionContext);
let treeDataProvider = new ControllerTreeDataProvider(extensionContext.globalState); let treeDataProvider = new ControllerTreeDataProvider(extensionContext.globalState);
@@ -31,15 +37,15 @@ function registerTreeDataProvider(treeDataProvider: ControllerTreeDataProvider):
} }
function registerCommands(treeDataProvider: ControllerTreeDataProvider): void { function registerCommands(treeDataProvider: ControllerTreeDataProvider): void {
vscode.commands.registerCommand('bigDataClusters.command.addController', (node?: TreeNode) => { vscode.commands.registerCommand(AddControllerCommand, (node?: TreeNode) => {
addBdcController(treeDataProvider, node); runThrottledAction(AddControllerCommand, () => addBdcController(treeDataProvider, node));
}); });
vscode.commands.registerCommand('bigDataClusters.command.deleteController', (node: TreeNode) => { vscode.commands.registerCommand(DeleteControllerCommand, (node: TreeNode) => {
deleteBdcController(treeDataProvider, node); deleteBdcController(treeDataProvider, node);
}); });
vscode.commands.registerCommand('bigDataClusters.command.refreshController', (node: TreeNode) => { vscode.commands.registerCommand(RefreshControllerCommand, (node: TreeNode) => {
if (!node) { if (!node) {
return; return;
} }
@@ -83,3 +89,21 @@ function deleteControllerInternal(treeDataProvider: ControllerTreeDataProvider,
treeDataProvider.saveControllers(); treeDataProvider.saveControllers();
} }
} }
/**
* Throttles actions to avoid bug where on clicking in tree, action gets called twice
* instead of once. Any right-click action is safe, just the default on-click action in a tree
*/
function runThrottledAction(id: string, action: () => void) {
let timer = throttleTimers[id];
if (!timer) {
throttleTimers[id] = timer = setTimeout(() => {
action();
clearTimeout(timer);
throttleTimers[id] = undefined;
}, 150);
}
// else ignore this as we got an identical action in the last 150ms
}

View File

@@ -416,8 +416,8 @@
}, },
{ {
"name": "%title.endpoints%", "name": "%title.endpoints%",
"row": 0, "row": 1,
"col": 4, "col": 0,
"rowspan": 1.5, "rowspan": 1.5,
"colspan": 2, "colspan": 2,
"widget": { "widget": {