Files
azuredatastudio/extensions/arc/src/ui/dialogs/connectControllerDialog.ts
Candice Ye 77e7a90c20 Make connection mode a displayname (#20068)
* dropdown not supporting dynamic enablement

* paused for weekend

* Make connection mode display with capitalization but value lowercase

* Undo changes in modelviewutils

Co-authored-by: Candice Ye <canye@microsoft.com>
2022-07-18 17:02:31 -07:00

223 lines
8.7 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ControllerInfo, ResourceInfo } from 'arc';
import * as azdata from 'azdata';
import { v4 as uuid } from 'uuid';
import * as vscode from 'vscode';
import { Deferred } from '../../common/promise';
import * as loc from '../../localizedConstants';
import { ControllerModel } from '../../models/controllerModel';
import { InitializingComponent } from '../components/initializingComponent';
import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider';
import { RadioOptionsGroup } from '../components/radioOptionsGroup';
import { getCurrentClusterContext, getDefaultKubeConfigPath, getKubeConfigClusterContexts, KubeClusterContext } from '../../common/kubeUtils';
import { FilePicker } from '../components/filePicker';
import { ConnectionMode } from '../../constants';
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel };
abstract class ControllerDialogBase extends InitializingComponent {
protected _toDispose: vscode.Disposable[] = [];
protected modelBuilder!: azdata.ModelBuilder;
protected dialog: azdata.window.Dialog;
protected namespaceInputBox!: azdata.InputBoxComponent;
protected kubeConfigInputBox!: FilePicker;
protected clusterContextRadioGroup!: RadioOptionsGroup;
protected nameInputBox!: azdata.InputBoxComponent;
private _kubeClusters: KubeClusterContext[] = [];
protected dispose(): void {
this._toDispose.forEach(disposable => disposable.dispose());
this._toDispose.length = 0;
}
protected getComponents(): (azdata.FormComponent<azdata.Component> & { layout?: azdata.FormItemLayout | undefined; })[] {
return [
{
component: this.namespaceInputBox,
title: loc.namespace,
required: true
}, {
component: this.kubeConfigInputBox.component(),
title: loc.controllerKubeConfig,
required: true
}, {
component: this.clusterContextRadioGroup.component(),
title: loc.controllerClusterContext,
required: true
}, {
component: this.nameInputBox,
title: loc.controllerName,
required: false,
layout: {
info: loc.controllerNameDescription
}
}
];
}
protected abstract fieldToFocusOn(): azdata.Component;
protected readonlyFields(): azdata.Component[] { return []; }
protected initializeFields(controllerInfo: ControllerInfo | undefined) {
this.namespaceInputBox = this.modelBuilder.inputBox()
.withProps({
value: controllerInfo?.namespace,
}).component();
this.kubeConfigInputBox = new FilePicker(
this.modelBuilder,
controllerInfo?.kubeConfigFilePath || getDefaultKubeConfigPath(),
(disposable) => this._toDispose.push(disposable),
loc.controllerKubeConfig,
loc.invalidConfigPath,
true
);
this.modelBuilder.inputBox()
.withProps({
value: controllerInfo?.kubeConfigFilePath || getDefaultKubeConfigPath()
}).component();
this.clusterContextRadioGroup = new RadioOptionsGroup(this.modelBuilder, (disposable) => this._toDispose.push(disposable), undefined, loc.loadingClusterContextCompleted, loc.loadingClusterContextsError);
this.loadRadioGroup(controllerInfo?.kubeClusterContext);
this._toDispose.push(this.clusterContextRadioGroup.onRadioOptionChanged(newContext => this.updateNamespace(newContext)));
this._toDispose.push(this.kubeConfigInputBox.onTextChanged(() => this.loadRadioGroup(controllerInfo?.kubeClusterContext)));
this.nameInputBox = this.modelBuilder.inputBox()
.withProps({
value: controllerInfo?.name
}).component();
}
protected completionPromise = new Deferred<ConnectToControllerDialogModel | undefined>();
protected id!: string;
protected resources: ResourceInfo[] = [];
protected resourceGroup!: string;
protected connectionMode!: string;
protected location!: string;
protected customLocation!: string;
constructor(protected treeDataProvider: AzureArcTreeDataProvider, title: string) {
super();
this.dialog = azdata.window.createModelViewDialog(title);
}
private loadRadioGroup(previousClusterContext?: string): void {
this.clusterContextRadioGroup.load(() => {
this._kubeClusters = getKubeConfigClusterContexts(this.kubeConfigInputBox.value!);
const currentClusterContext = getCurrentClusterContext(this._kubeClusters, previousClusterContext, false);
this.namespaceInputBox.value = currentClusterContext.namespace || this.namespaceInputBox.value;
return {
values: this._kubeClusters.map(c => c.name),
defaultValue: currentClusterContext.name
};
});
}
private updateNamespace(currentContextName: string | undefined): void {
const currentContext = this._kubeClusters.find(cluster => cluster.name === currentContextName);
this.namespaceInputBox.value = currentContext?.namespace;
}
public showDialog(controllerInfo?: ControllerInfo): azdata.window.Dialog {
this.id = controllerInfo?.id ?? uuid();
this.resources = controllerInfo?.resources ?? [];
this._toDispose.push(this.dialog.cancelButton.onClick(() => this.handleCancel()));
this.dialog.registerContent(async (view) => {
this.modelBuilder = view.modelBuilder;
this.initializeFields(controllerInfo);
let formModel = this.modelBuilder.formContainer()
.withFormItems([{
components: this.getComponents(),
title: ''
}]).withLayout({ width: '100%' }).component();
await view.initializeModel(formModel);
await this.fieldToFocusOn().focus();
this.readonlyFields().forEach(f => f.enabled = false);
this.initialized = true;
});
this.dialog.registerCloseValidator(async () => {
const isValidated = await this.validate();
if (isValidated) {
this.dispose();
}
return isValidated;
});
this.dialog.okButton.label = loc.connect;
this.dialog.cancelButton.label = loc.cancel;
azdata.window.openDialog(this.dialog);
return this.dialog;
}
public abstract validate(): Promise<boolean>;
private handleCancel(): void {
this.completionPromise.resolve(undefined);
}
public waitForClose(): Promise<ConnectToControllerDialogModel | undefined> {
return this.completionPromise.promise;
}
protected getControllerInfo(): ControllerInfo {
return {
id: this.id,
namespace: this.namespaceInputBox.value!.trim(),
kubeConfigFilePath: this.kubeConfigInputBox.value!,
kubeClusterContext: this.clusterContextRadioGroup.value!,
name: this.nameInputBox.value ?? '',
resources: this.resources,
resourceGroup: this.resourceGroup ?? '',
connectionMode: this.connectionMode ?? '',
location: this.location ?? '',
customLocation: this.customLocation ?? ''
};
}
}
export class ConnectToControllerDialog extends ControllerDialogBase {
protected fieldToFocusOn() {
return this.namespaceInputBox;
}
constructor(treeDataProvider: AzureArcTreeDataProvider) {
super(treeDataProvider, loc.connectToController);
}
public async validate(): Promise<boolean> {
if (!this.namespaceInputBox.value) {
return false;
}
const controllerInfo: ControllerInfo = this.getControllerInfo();
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo);
try {
// Validate that we can connect to the controller, this also populates the controllerRegistration from the connection response.
await controllerModel.refresh(false, this.namespaceInputBox.value);
// default info.name to the name of the controller instance if the user did not specify their own and to a pre-canned default if for some weird reason controller endpoint returned instanceName is also not a valid value
controllerModel.info.name = controllerModel.info.name || controllerModel.controllerConfig?.metadata.name || loc.defaultControllerName;
controllerModel.info.resourceGroup = <string>controllerModel.controllerConfig?.spec.settings.azure.resourceGroup;
controllerModel.info.connectionMode = <string>controllerModel.controllerConfig?.spec.settings.azure.connectionMode.toLowerCase();
controllerModel.info.location = <string>controllerModel.controllerConfig?.spec.settings.azure.location;
if (controllerModel.info.connectionMode === ConnectionMode.direct) {
const rawCustomLocation = <string>controllerModel.controllerConfig?.metadata.annotations['management.azure.com/customLocation'];
const exp = /custom[lL]ocations\/([\S]*)/;
controllerModel.info.customLocation = <string>exp.exec(rawCustomLocation)?.pop();
}
} catch (err) {
this.dialog.message = {
text: loc.connectToControllerFailed(this.namespaceInputBox.value, err),
level: azdata.window.MessageLevel.Error
};
return false;
}
this.completionPromise.resolve({ controllerModel: controllerModel });
return true;
}
}