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/admin-tool-ext-win/ssmsmin/**',
'!extensions/resource-deployment/notebooks/**',
'!extensions/mssql/notebooks/**'
'!extensions/mssql/notebooks/**',
'!extensions/big-data-cluster/src/bigDataCluster/controller/apiGenerated.ts'
];
const copyrightFilter = [
@@ -184,7 +185,9 @@ const tslintFilter = [
'!extensions/vscode-api-tests/testWorkspace/**',
'!extensions/vscode-api-tests/testWorkspace2/**',
'!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}}

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.
*--------------------------------------------------------------------------------------------*/
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(
url: string, username: string, password: string, ignoreSslVerification?: boolean
clusterName: string,
url: string,
username: string,
password: string,
ignoreSslVerification?: boolean
): Promise<IEndPointsResponse> {
if (!url || !username || !password) {
@@ -14,8 +41,7 @@ export async function getEndPoints(
}
url = adjustUrl(url);
let endPointApi = new EndpointRouterApi(username, password, url);
endPointApi.ignoreSslVerification = !!ignoreSslVerification;
let endPointApi = new ClusterApiWrapper(username, password, url, !!ignoreSslVerification);
let controllerResponse: IEndPointsResponse = undefined;
let controllerError: IControllerError = undefined;
@@ -27,7 +53,7 @@ export async function getEndPoints(
};
try {
let result = await endPointApi.endpointsGet();
let result = await endPointApi.endpointsGet(clusterName);
controllerResponse = <IEndPointsResponse>{
response: result.response as IHttpResponse,
endPoints: result.body as IEndPoint[],

View File

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

View File

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

View File

@@ -113,14 +113,20 @@ export class ControllerRootNode extends ControllerTreeNode {
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);
if (controllerNode) {
controllerNode.password = password;
controllerNode.rememberPassword = rememberPassword;
controllerNode.clearChildren();
} 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);
}
@@ -158,6 +164,7 @@ export class ControllerRootNode extends ControllerTreeNode {
export class ControllerNode extends ControllerTreeNode {
constructor(
private _clusterName: string,
private _url: string,
private _username: string,
private _password: string,
@@ -184,7 +191,7 @@ export class ControllerNode extends ControllerTreeNode {
}
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) {
let master = response.endPoints.find(e => e.name && e.name === 'sql-server-master');
this.addSqlMasterNode(master.endpoint, master.description);
@@ -226,6 +233,14 @@ export class ControllerNode extends ControllerTreeNode {
return item;
}
public get clusterName() {
return this._clusterName;
}
public set clusterName(clusterName: string) {
this._clusterName = clusterName;
}
public get url() {
return this._url;
}

View File

@@ -15,6 +15,12 @@ import { ControllerNode } from './bigDataCluster/tree/controllerTreeNode';
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) {
IconPath.setExtensionContext(extensionContext);
let treeDataProvider = new ControllerTreeDataProvider(extensionContext.globalState);
@@ -31,15 +37,15 @@ function registerTreeDataProvider(treeDataProvider: ControllerTreeDataProvider):
}
function registerCommands(treeDataProvider: ControllerTreeDataProvider): void {
vscode.commands.registerCommand('bigDataClusters.command.addController', (node?: TreeNode) => {
addBdcController(treeDataProvider, node);
vscode.commands.registerCommand(AddControllerCommand, (node?: TreeNode) => {
runThrottledAction(AddControllerCommand, () => addBdcController(treeDataProvider, node));
});
vscode.commands.registerCommand('bigDataClusters.command.deleteController', (node: TreeNode) => {
vscode.commands.registerCommand(DeleteControllerCommand, (node: TreeNode) => {
deleteBdcController(treeDataProvider, node);
});
vscode.commands.registerCommand('bigDataClusters.command.refreshController', (node: TreeNode) => {
vscode.commands.registerCommand(RefreshControllerCommand, (node: TreeNode) => {
if (!node) {
return;
}
@@ -83,3 +89,21 @@ function deleteControllerInternal(treeDataProvider: ControllerTreeDataProvider,
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%",
"row": 0,
"col": 4,
"row": 1,
"col": 0,
"rowspan": 1.5,
"colspan": 2,
"widget": {