mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Add kube config and kube cluster to arc data controller screens (#13551)
This commit is contained in:
91
extensions/arc/src/ui/components/filePicker.ts
Normal file
91
extensions/arc/src/ui/components/filePicker.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as azdata from 'azdata';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as loc from '../../localizedConstants';
|
||||
import { IReadOnly } from '../dialogs/connectControllerDialog';
|
||||
|
||||
export interface RadioOptionsInfo {
|
||||
values?: string[],
|
||||
defaultValue: string
|
||||
}
|
||||
|
||||
export class FilePicker implements IReadOnly {
|
||||
private _flexContainer: azdata.FlexContainer;
|
||||
private _filePathInputBox: azdata.InputBoxComponent;
|
||||
private _filePickerButton: azdata.ButtonComponent;
|
||||
constructor(
|
||||
modelBuilder: azdata.ModelBuilder,
|
||||
initialPath: string, onNewDisposableCreated: (disposable: vscode.Disposable) => void
|
||||
) {
|
||||
const buttonWidth = 80;
|
||||
this._filePathInputBox = modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
value: initialPath,
|
||||
width: 350
|
||||
}).component();
|
||||
|
||||
this._filePickerButton = modelBuilder.button()
|
||||
.withProperties<azdata.ButtonProperties>({
|
||||
label: loc.browse,
|
||||
width: buttonWidth
|
||||
}).component();
|
||||
onNewDisposableCreated(this._filePickerButton.onDidClick(async () => {
|
||||
const fileUris = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
defaultUri: this._filePathInputBox.value ? vscode.Uri.file(path.dirname(this._filePathInputBox.value)) : undefined,
|
||||
openLabel: loc.select,
|
||||
filters: undefined /* file type filters */
|
||||
});
|
||||
|
||||
if (!fileUris || fileUris.length === 0) {
|
||||
return; // This can happen when a user cancels out. we don't throw and the user just won't be able to move on until they select something.
|
||||
}
|
||||
const fileUri = fileUris[0]; //we allow the user to select only one file in the dialog
|
||||
this._filePathInputBox.value = fileUri.fsPath;
|
||||
}));
|
||||
this._flexContainer = createFlexContainer(modelBuilder, [this._filePathInputBox, this._filePickerButton]);
|
||||
}
|
||||
|
||||
component(): azdata.Component {
|
||||
return this._flexContainer;
|
||||
}
|
||||
|
||||
get onTextChanged() {
|
||||
return this._filePathInputBox.onTextChanged;
|
||||
}
|
||||
|
||||
get value(): string | undefined {
|
||||
return this._filePathInputBox?.value;
|
||||
}
|
||||
|
||||
get readOnly(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
set readOnly(value: boolean) {
|
||||
this.enabled = value;
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return !!this._flexContainer.enabled && this._flexContainer.items.every(r => r.enabled);
|
||||
}
|
||||
|
||||
set enabled(value: boolean) {
|
||||
this._flexContainer.items.forEach(r => r.enabled = value);
|
||||
this._flexContainer.enabled = value;
|
||||
}
|
||||
}
|
||||
|
||||
function createFlexContainer(modelBuilder: azdata.ModelBuilder, items: azdata.Component[], rowLayout: boolean = true, width?: string | number, height?: string | number, alignItems?: azdata.AlignItemsType, cssStyles?: { [key: string]: string }): azdata.FlexContainer {
|
||||
const flexFlow = rowLayout ? 'row' : 'column';
|
||||
alignItems = alignItems || (rowLayout ? 'center' : undefined);
|
||||
const itemsStyle = rowLayout ? { CSSStyles: { 'margin-right': '5px', } } : {};
|
||||
const flexLayout: azdata.FlexLayout = { flexFlow: flexFlow, height: height, width: width, alignItems: alignItems };
|
||||
return modelBuilder.flexContainer().withItems(items, itemsStyle).withLayout(flexLayout).withProperties<azdata.ComponentProperties>({ CSSStyles: cssStyles || {} }).component();
|
||||
}
|
||||
@@ -5,24 +5,22 @@
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { getErrorMessage } from '../../common/utils';
|
||||
import { IReadOnly } from '../dialogs/connectControllerDialog';
|
||||
|
||||
export interface RadioOptionsInfo {
|
||||
values?: string[],
|
||||
defaultValue: string
|
||||
}
|
||||
|
||||
export class RadioOptionsGroup {
|
||||
export class RadioOptionsGroup implements IReadOnly {
|
||||
static id: number = 1;
|
||||
private _divContainer!: azdata.DivContainer;
|
||||
private _loadingBuilder: azdata.LoadingComponentBuilder;
|
||||
private _currentRadioOption!: azdata.RadioButtonComponent;
|
||||
|
||||
constructor(private _view: azdata.ModelView, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void, private _groupName: string = `RadioOptionsGroup${RadioOptionsGroup.id++}`) {
|
||||
const divBuilder = this._view.modelBuilder.divContainer();
|
||||
const divBuilderWithProperties = divBuilder.withProperties<azdata.DivContainerProperties>({ clickable: false });
|
||||
this._divContainer = divBuilderWithProperties.component();
|
||||
const loadingComponentBuilder = this._view.modelBuilder.loadingComponent();
|
||||
this._loadingBuilder = loadingComponentBuilder.withItem(this._divContainer);
|
||||
constructor(private _modelBuilder: azdata.ModelBuilder, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void, private _groupName: string = `RadioOptionsGroup${RadioOptionsGroup.id++}`) {
|
||||
this._divContainer = this._modelBuilder.divContainer().withProperties<azdata.DivContainerProperties>({ clickable: false }).component();
|
||||
this._loadingBuilder = this._modelBuilder.loadingComponent().withItem(this._divContainer);
|
||||
}
|
||||
|
||||
public component(): azdata.LoadingComponent {
|
||||
@@ -37,7 +35,7 @@ export class RadioOptionsGroup {
|
||||
const options = optionsInfo.values!;
|
||||
let defaultValue: string = optionsInfo.defaultValue!;
|
||||
options.forEach((option: string) => {
|
||||
const radioOption = this._view!.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
|
||||
const radioOption = this._modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
|
||||
label: option,
|
||||
checked: option === defaultValue,
|
||||
name: this._groupName,
|
||||
@@ -60,7 +58,7 @@ export class RadioOptionsGroup {
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
const errorLabel = this._view!.modelBuilder.text().withProperties({ value: getErrorMessage(e), CSSStyles: { 'color': 'Red' } }).component();
|
||||
const errorLabel = this._modelBuilder.text().withProperties({ value: getErrorMessage(e), CSSStyles: { 'color': 'Red' } }).component();
|
||||
this._divContainer.addItem(errorLabel);
|
||||
}
|
||||
this.component().loading = false;
|
||||
@@ -69,4 +67,21 @@ export class RadioOptionsGroup {
|
||||
get value(): string | undefined {
|
||||
return this._currentRadioOption?.value;
|
||||
}
|
||||
|
||||
get readOnly(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
set readOnly(value: boolean) {
|
||||
this.enabled = value;
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return !!this._divContainer.enabled && this._divContainer.items.every(r => r.enabled);
|
||||
}
|
||||
|
||||
set enabled(value: boolean) {
|
||||
this._divContainer.items.forEach(r => r.enabled = value);
|
||||
this._divContainer.enabled = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
}).component();
|
||||
|
||||
const workerNodeslink = this.modelView.modelBuilder.hyperlink().withProperties<azdata.HyperlinkComponentProperties>({
|
||||
label: loc.addingWokerNodes,
|
||||
label: loc.addingWorkerNodes,
|
||||
url: 'https://docs.microsoft.com/azure/azure-arc/data/scale-up-down-postgresql-hyperscale-server-group-using-cli',
|
||||
CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
|
||||
@@ -14,23 +14,45 @@ import { ControllerModel } from '../../models/controllerModel';
|
||||
import { InitializingComponent } from '../components/initializingComponent';
|
||||
import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider';
|
||||
import { getErrorMessage } from '../../common/utils';
|
||||
import { RadioOptionsGroup } from '../components/radioOptionsGroup';
|
||||
import { getCurrentClusterContext, getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../../common/kubeUtils';
|
||||
import { FilePicker } from '../components/filePicker';
|
||||
|
||||
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string };
|
||||
export interface IReadOnly {
|
||||
readOnly?: boolean
|
||||
}
|
||||
abstract class ControllerDialogBase extends InitializingComponent {
|
||||
protected _toDispose: vscode.Disposable[] = [];
|
||||
protected modelBuilder!: azdata.ModelBuilder;
|
||||
protected dialog: azdata.window.Dialog;
|
||||
|
||||
protected urlInputBox!: azdata.InputBoxComponent;
|
||||
protected kubeConfigInputBox!: FilePicker;
|
||||
protected clusterContextRadioGroup!: RadioOptionsGroup;
|
||||
protected nameInputBox!: azdata.InputBoxComponent;
|
||||
protected usernameInputBox!: azdata.InputBoxComponent;
|
||||
protected passwordInputBox!: azdata.InputBoxComponent;
|
||||
|
||||
protected dispose(): void {
|
||||
this._toDispose.forEach(disposable => disposable.dispose());
|
||||
this._toDispose.length = 0; // clear the _toDispose array
|
||||
}
|
||||
|
||||
protected getComponents(): (azdata.FormComponent<azdata.Component> & { layout?: azdata.FormItemLayout | undefined; })[] {
|
||||
return [
|
||||
{
|
||||
component: this.urlInputBox,
|
||||
title: loc.controllerUrl,
|
||||
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,
|
||||
@@ -48,7 +70,7 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
}
|
||||
|
||||
protected abstract fieldToFocusOn(): azdata.Component;
|
||||
protected readonlyFields(): azdata.InputBoxComponent[] { return []; }
|
||||
protected readonlyFields(): IReadOnly[] { return []; }
|
||||
|
||||
protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
|
||||
this.urlInputBox = this.modelBuilder.inputBox()
|
||||
@@ -57,6 +79,18 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
// If we have a model then we're editing an existing connection so don't let them modify the URL
|
||||
readOnly: !!controllerInfo
|
||||
}).component();
|
||||
this.kubeConfigInputBox = new FilePicker(
|
||||
this.modelBuilder,
|
||||
controllerInfo?.kubeConfigFilePath || getDefaultKubeConfigPath(),
|
||||
(disposable) => this._toDispose.push(disposable)
|
||||
);
|
||||
this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
value: controllerInfo?.kubeConfigFilePath || getDefaultKubeConfigPath()
|
||||
}).component();
|
||||
this.clusterContextRadioGroup = new RadioOptionsGroup(this.modelBuilder, (disposable) => this._toDispose.push(disposable));
|
||||
this.loadRadioGroup(controllerInfo?.kubeClusterContext);
|
||||
this._toDispose.push(this.kubeConfigInputBox.onTextChanged(() => this.loadRadioGroup(controllerInfo?.kubeClusterContext)));
|
||||
this.nameInputBox = this.modelBuilder.inputBox()
|
||||
.withProperties<azdata.InputBoxProperties>({
|
||||
value: controllerInfo?.name
|
||||
@@ -81,10 +115,20 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
this.dialog = azdata.window.createModelViewDialog(title);
|
||||
}
|
||||
|
||||
private loadRadioGroup(previousClusterContext?: string): void {
|
||||
this.clusterContextRadioGroup.load(async () => {
|
||||
const clusters = await getKubeConfigClusterContexts(this.kubeConfigInputBox.value!);
|
||||
return {
|
||||
values: clusters.map(c => c.name),
|
||||
defaultValue: getCurrentClusterContext(clusters, previousClusterContext, false),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public showDialog(controllerInfo?: ControllerInfo, password: string | undefined = undefined): azdata.window.Dialog {
|
||||
this.id = controllerInfo?.id ?? uuid();
|
||||
this.resources = controllerInfo?.resources ?? [];
|
||||
this.dialog.cancelButton.onClick(() => this.handleCancel());
|
||||
this._toDispose.push(this.dialog.cancelButton.onClick(() => this.handleCancel()));
|
||||
this.dialog.registerContent(async (view) => {
|
||||
this.modelBuilder = view.modelBuilder;
|
||||
this.initializeFields(controllerInfo, password);
|
||||
@@ -100,7 +144,13 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
this.initialized = true;
|
||||
});
|
||||
|
||||
this.dialog.registerCloseValidator(async () => await this.validate());
|
||||
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);
|
||||
@@ -116,6 +166,19 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
||||
public waitForClose(): Promise<ConnectToControllerDialogModel | undefined> {
|
||||
return this.completionPromise.promise;
|
||||
}
|
||||
|
||||
protected getControllerInfo(url: string, rememberPassword: boolean = false): ControllerInfo {
|
||||
return {
|
||||
id: this.id,
|
||||
url: url,
|
||||
kubeConfigFilePath: this.kubeConfigInputBox.value!,
|
||||
kubeClusterContext: this.clusterContextRadioGroup.value!,
|
||||
name: this.nameInputBox.value ?? '',
|
||||
username: this.usernameInputBox.value!,
|
||||
rememberPassword: rememberPassword,
|
||||
resources: this.resources
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectToControllerDialog extends ControllerDialogBase {
|
||||
@@ -164,14 +227,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
|
||||
if (!/.*:\d*$/.test(url)) {
|
||||
url = `${url}:30080`;
|
||||
}
|
||||
const controllerInfo: ControllerInfo = {
|
||||
id: this.id,
|
||||
url: url,
|
||||
name: this.nameInputBox.value ?? '',
|
||||
username: this.usernameInputBox.value,
|
||||
rememberPassword: this.rememberPwCheckBox.checked ?? false,
|
||||
resources: this.resources
|
||||
};
|
||||
const controllerInfo: ControllerInfo = this.getControllerInfo(url, !!this.rememberPwCheckBox.checked);
|
||||
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
||||
try {
|
||||
// Validate that we can connect to the controller, this also populates the controllerRegistration from the connection response.
|
||||
@@ -202,6 +258,8 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
|
||||
protected readonlyFields() {
|
||||
return [
|
||||
this.urlInputBox,
|
||||
this.kubeConfigInputBox,
|
||||
this.clusterContextRadioGroup,
|
||||
this.nameInputBox,
|
||||
this.usernameInputBox
|
||||
];
|
||||
@@ -229,14 +287,7 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const controllerInfo: ControllerInfo = {
|
||||
id: this.id,
|
||||
url: this.urlInputBox.value!,
|
||||
name: this.nameInputBox.value!,
|
||||
username: this.usernameInputBox.value!,
|
||||
rememberPassword: false,
|
||||
resources: []
|
||||
};
|
||||
const controllerInfo: ControllerInfo = this.getControllerInfo(this.urlInputBox.value!, false);
|
||||
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
||||
this.completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
|
||||
return true;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ControllerModel } from '../../models/controllerModel';
|
||||
import { ControllerTreeNode } from './controllerTreeNode';
|
||||
import { TreeNode } from './treeNode';
|
||||
|
||||
const mementoToken = 'arcControllers';
|
||||
const mementoToken = 'arcDataControllers';
|
||||
|
||||
/**
|
||||
* The TreeDataProvider for the Azure Arc view, which displays a list of registered
|
||||
|
||||
Reference in New Issue
Block a user