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:
@@ -13,6 +13,11 @@ export interface KubeClusterContext {
|
|||||||
isCurrentContext: boolean;
|
isCurrentContext: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the cluster context defined in the {@see configFile}
|
||||||
|
*
|
||||||
|
* @param configFile
|
||||||
|
*/
|
||||||
export function getKubeConfigClusterContexts(configFile: string): Promise<KubeClusterContext[]> {
|
export function getKubeConfigClusterContexts(configFile: string): Promise<KubeClusterContext[]> {
|
||||||
const config: any = yamljs.load(configFile);
|
const config: any = yamljs.load(configFile);
|
||||||
const rawContexts = <any[]>config['contexts'];
|
const rawContexts = <any[]>config['contexts'];
|
||||||
@@ -33,6 +38,38 @@ export function getKubeConfigClusterContexts(configFile: string): Promise<KubeCl
|
|||||||
return Promise.resolve(contexts);
|
return Promise.resolve(contexts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* searches for {@see previousClusterContext} in the array of {@see clusterContexts}.
|
||||||
|
* if {@see previousClusterContext} was truthy and it was found in {@see clusterContexts}
|
||||||
|
* then it returns {@see previousClusterContext}
|
||||||
|
* else it returns the current cluster context from {@see clusterContexts} unless throwIfNotFound was set on input in which case an error is thrown instead.
|
||||||
|
* else it returns the current cluster context from {@see clusterContexts}
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param clusterContexts
|
||||||
|
* @param previousClusterContext
|
||||||
|
* @param throwIfNotFound
|
||||||
|
*/
|
||||||
|
export function getCurrentClusterContext(clusterContexts: KubeClusterContext[], previousClusterContext?: string, throwIfNotFound: boolean = false): string {
|
||||||
|
if (previousClusterContext) {
|
||||||
|
if (clusterContexts.find(c => c.name === previousClusterContext)) { // if previous cluster context value is found in clusters then return that value
|
||||||
|
return previousClusterContext;
|
||||||
|
} else {
|
||||||
|
if (throwIfNotFound) {
|
||||||
|
throw new Error(loc.clusterContextNotFound(previousClusterContext));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if not previousClusterContext or throwIfNotFound was false when previousCLusterContext was not found in the clusterContexts
|
||||||
|
const currentClusterContext = clusterContexts.find(c => c.isCurrentContext)?.name;
|
||||||
|
throwUnless(currentClusterContext !== undefined, loc.noCurrentClusterContext);
|
||||||
|
return currentClusterContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the default kube config file path
|
||||||
|
*/
|
||||||
export function getDefaultKubeConfigPath(): string {
|
export function getDefaultKubeConfigPath(): string {
|
||||||
return path.join(os.homedir(), '.kube', 'config');
|
return path.join(os.homedir(), '.kube', 'config');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const yes = localize('arc.yes', "Yes");
|
|||||||
export const no = localize('arc.no', "No");
|
export const no = localize('arc.no', "No");
|
||||||
export const feedback = localize('arc.feedback', "Feedback");
|
export const feedback = localize('arc.feedback', "Feedback");
|
||||||
export const selectConnectionString = localize('arc.selectConnectionString', "Select from available client connection strings below.");
|
export const selectConnectionString = localize('arc.selectConnectionString', "Select from available client connection strings below.");
|
||||||
export const addingWokerNodes = localize('arc.addingWokerNodes', "adding worker nodes");
|
export const addingWorkerNodes = localize('arc.addingWorkerNodes', "adding worker nodes");
|
||||||
export const workerNodesDescription = localize('arc.workerNodesDescription', "Expand your server group and scale your database by adding worker nodes.");
|
export const workerNodesDescription = localize('arc.workerNodesDescription', "Expand your server group and scale your database by adding worker nodes.");
|
||||||
export const postgresConfigurationInformation = localize('arc.postgres.configurationInformation', "You can configure the number of CPU cores and storage size that will apply to both worker nodes and coordinator node. Each worker node will have the same configuration. Adjust the number of CPU cores and memory settings for your server group.");
|
export const postgresConfigurationInformation = localize('arc.postgres.configurationInformation', "You can configure the number of CPU cores and storage size that will apply to both worker nodes and coordinator node. Each worker node will have the same configuration. Adjust the number of CPU cores and memory settings for your server group.");
|
||||||
export const workerNodesInformation = localize('arc.workerNodeInformation', "In preview it is not possible to reduce the number of worker nodes. Please refer to documentation linked above for more information.");
|
export const workerNodesInformation = localize('arc.workerNodeInformation', "In preview it is not possible to reduce the number of worker nodes. Please refer to documentation linked above for more information.");
|
||||||
@@ -85,6 +85,8 @@ export const passwordToController = localize('arc.passwordToController', "Provid
|
|||||||
export const controllerUrl = localize('arc.controllerUrl', "Controller URL");
|
export const controllerUrl = localize('arc.controllerUrl', "Controller URL");
|
||||||
export const serverEndpoint = localize('arc.serverEndpoint', "Server Endpoint");
|
export const serverEndpoint = localize('arc.serverEndpoint', "Server Endpoint");
|
||||||
export const controllerName = localize('arc.controllerName', "Name");
|
export const controllerName = localize('arc.controllerName', "Name");
|
||||||
|
export const controllerKubeConfig = localize('arc.controllerKubeConfig', "Kube Config File Path");
|
||||||
|
export const controllerClusterContext = localize('arc.controllerClusterContext', "Cluster Context");
|
||||||
export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc");
|
export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc");
|
||||||
export const username = localize('arc.username', "Username");
|
export const username = localize('arc.username', "Username");
|
||||||
export const password = localize('arc.password', "Password");
|
export const password = localize('arc.password', "Password");
|
||||||
@@ -202,6 +204,10 @@ export const variableValueFetchForUnsupportedVariable = (variableName: string) =
|
|||||||
export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName);
|
export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName);
|
||||||
export const noControllerInfoFound = (name: string) => localize('noControllerInfoFound', "Controller Info could not be found with name: {0}", name);
|
export const noControllerInfoFound = (name: string) => localize('noControllerInfoFound', "Controller Info could not be found with name: {0}", name);
|
||||||
export const noPasswordFound = (controllerName: string) => localize('noPasswordFound', "Password could not be retrieved for controller: {0} and user did not provide a password. Please retry later.", controllerName);
|
export const noPasswordFound = (controllerName: string) => localize('noPasswordFound', "Password could not be retrieved for controller: {0} and user did not provide a password. Please retry later.", controllerName);
|
||||||
|
export const clusterContextNotFound = (clusterContext: string) => localize('clusterContextNotFound', "Cluster Context with name: {0} not found in the Kube config file", clusterContext);
|
||||||
|
export const noCurrentClusterContext = localize('noCurrentClusterContext', "No current cluster context was found in the kube config file");
|
||||||
|
export const browse = localize('filePicker.browse', "Browse");
|
||||||
|
export const select = localize('button.label', "Select");
|
||||||
export const noContextFound = (configFile: string) => localize('noContextFound', "No 'contexts' found in the config file: {0}", configFile);
|
export const noContextFound = (configFile: string) => localize('noContextFound', "No 'contexts' found in the config file: {0}", configFile);
|
||||||
export const noCurrentContextFound = (configFile: string) => localize('noCurrentContextFound', "No context is marked as 'current-context' in the config file: {0}", configFile);
|
export const noCurrentContextFound = (configFile: string) => localize('noCurrentContextFound', "No context is marked as 'current-context' in the config file: {0}", configFile);
|
||||||
export const noNameInContext = (configFile: string) => localize('noNameInContext', "No name field was found in a cluster context in the config file: {0}", configFile);
|
export const noNameInContext = (configFile: string) => localize('noNameInContext', "No name field was found in a cluster context in the config file: {0}", configFile);
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ describe('KubeUtils', function (): void {
|
|||||||
});
|
});
|
||||||
it('throws error when unable to load config file', async () => {
|
it('throws error when unable to load config file', async () => {
|
||||||
const error = new Error('unknown error accessing file');
|
const error = new Error('unknown error accessing file');
|
||||||
sinon.stub(yamljs, 'load').throws(error); //erroring config file load
|
sinon.stub(yamljs, 'load').throws(error); // simulate an error thrown from config file load
|
||||||
((await tryExecuteAction(() => getKubeConfigClusterContexts(configFile))).error).should.equal(error, `test: getKubeConfigClusterContexts failed`);
|
((await tryExecuteAction(() => getKubeConfigClusterContexts(configFile))).error).should.equal(error, `test: getKubeConfigClusterContexts failed`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider
|
|||||||
export class FakeControllerModel extends ControllerModel {
|
export class FakeControllerModel extends ControllerModel {
|
||||||
|
|
||||||
constructor(treeDataProvider?: AzureArcTreeDataProvider, info?: Partial<ControllerInfo>, password?: string) {
|
constructor(treeDataProvider?: AzureArcTreeDataProvider, info?: Partial<ControllerInfo>, password?: string) {
|
||||||
const _info: ControllerInfo = Object.assign({ id: uuid(), url: '', name: '', username: '', rememberPassword: false, resources: [] }, info);
|
const _info: ControllerInfo = Object.assign({ id: uuid(), url: '', kubeConfigFilePath: '', kubeClusterContext: '', name: '', username: '', rememberPassword: false, resources: [] }, info);
|
||||||
super(treeDataProvider!, _info, password);
|
super(treeDataProvider!, _info, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe('ControllerModel', function (): void {
|
|||||||
it('Rejected with expected error when user cancels', async function (): Promise<void> {
|
it('Rejected with expected error when user cancels', async function (): Promise<void> {
|
||||||
// Returning an undefined model here indicates that the dialog closed without clicking "Ok" - usually through the user clicking "Cancel"
|
// Returning an undefined model here indicates that the dialog closed without clicking "Ok" - usually through the user clicking "Cancel"
|
||||||
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(undefined));
|
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(undefined));
|
||||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
||||||
await should(model.azdataLogin()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
|
await should(model.azdataLogin()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ describe('ControllerModel', function (): void {
|
|||||||
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
azdataMock.setup(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => <any>Promise.resolve(undefined));
|
||||||
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
|
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.object);
|
||||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
||||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
||||||
|
|
||||||
await model.azdataLogin();
|
await model.azdataLogin();
|
||||||
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once());
|
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once());
|
||||||
@@ -81,10 +81,10 @@ describe('ControllerModel', function (): void {
|
|||||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
||||||
|
|
||||||
// Set up dialog to return new model with our password
|
// Set up dialog to return new model with our password
|
||||||
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
||||||
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
|
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
|
||||||
|
|
||||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
||||||
|
|
||||||
await model.azdataLogin();
|
await model.azdataLogin();
|
||||||
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once());
|
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once());
|
||||||
@@ -106,10 +106,10 @@ describe('ControllerModel', function (): void {
|
|||||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
||||||
|
|
||||||
// Set up dialog to return new model with our new password from the reprompt
|
// Set up dialog to return new model with our new password from the reprompt
|
||||||
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
||||||
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
|
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
|
||||||
|
|
||||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
||||||
|
|
||||||
await model.azdataLogin(true);
|
await model.azdataLogin(true);
|
||||||
should(waitForCloseStub.called).be.true('waitForClose should have been called');
|
should(waitForCloseStub.called).be.true('waitForClose should have been called');
|
||||||
@@ -132,11 +132,11 @@ describe('ControllerModel', function (): void {
|
|||||||
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
sinon.stub(vscode.extensions, 'getExtension').returns(<any>{ exports: azdataExtApiMock.object });
|
||||||
|
|
||||||
// Set up dialog to return new model with our new password from the reprompt
|
// Set up dialog to return new model with our new password from the reprompt
|
||||||
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
const newModel = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, password);
|
||||||
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
|
const waitForCloseStub = sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve({ controllerModel: newModel, password: password }));
|
||||||
|
|
||||||
// Set up original model with a password
|
// Set up original model with a password
|
||||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, 'originalPassword');
|
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] }, 'originalPassword');
|
||||||
|
|
||||||
await model.azdataLogin(true);
|
await model.azdataLogin(true);
|
||||||
should(waitForCloseStub.called).be.true('waitForClose should have been called');
|
should(waitForCloseStub.called).be.true('waitForClose should have been called');
|
||||||
@@ -166,6 +166,8 @@ describe('ControllerModel', function (): void {
|
|||||||
{
|
{
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
url: '127.0.0.1',
|
url: '127.0.0.1',
|
||||||
|
kubeConfigFilePath: '/path/to/.kube/config',
|
||||||
|
kubeClusterContext: 'currentCluster',
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
name: 'arc',
|
name: 'arc',
|
||||||
rememberPassword: false,
|
rememberPassword: false,
|
||||||
@@ -178,6 +180,8 @@ describe('ControllerModel', function (): void {
|
|||||||
const newInfo: ControllerInfo = {
|
const newInfo: ControllerInfo = {
|
||||||
id: model.info.id, // The ID stays the same since we're just re-entering information for the same model
|
id: model.info.id, // The ID stays the same since we're just re-entering information for the same model
|
||||||
url: 'newUrl',
|
url: 'newUrl',
|
||||||
|
kubeConfigFilePath: '/path/to/.kube/config',
|
||||||
|
kubeClusterContext: 'currentCluster',
|
||||||
username: 'newUser',
|
username: 'newUser',
|
||||||
name: 'newName',
|
name: 'newName',
|
||||||
rememberPassword: true,
|
rememberPassword: true,
|
||||||
|
|||||||
@@ -7,21 +7,44 @@ import * as azdata from 'azdata';
|
|||||||
import * as TypeMoq from 'typemoq';
|
import * as TypeMoq from 'typemoq';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
export function createModelViewMock() {
|
interface ModelViewMocks {
|
||||||
|
mockModelView: TypeMoq.IMock<azdata.ModelView>,
|
||||||
|
mockModelBuilder: TypeMoq.IMock<azdata.ModelBuilder>,
|
||||||
|
mockTextBuilder: TypeMoq.IMock<azdata.ComponentBuilder<azdata.TextComponent, azdata.TextComponentProperties>>,
|
||||||
|
mockInputBoxBuilder: TypeMoq.IMock<azdata.ComponentBuilder<azdata.InputBoxComponent, azdata.InputBoxProperties>>,
|
||||||
|
mockButtonBuilder: TypeMoq.IMock<azdata.ComponentBuilder<azdata.ButtonComponent, azdata.ButtonProperties>>,
|
||||||
|
mockRadioButtonBuilder: TypeMoq.IMock<azdata.ComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>>,
|
||||||
|
mockDivBuilder: TypeMoq.IMock<azdata.DivBuilder>,
|
||||||
|
mockFlexBuilder: TypeMoq.IMock<azdata.FlexBuilder>,
|
||||||
|
mockLoadingBuilder: TypeMoq.IMock<azdata.LoadingComponentBuilder>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createModelViewMock(buttonClickEmitter?: vscode.EventEmitter<any>): ModelViewMocks {
|
||||||
const mockModelBuilder = TypeMoq.Mock.ofType<azdata.ModelBuilder>();
|
const mockModelBuilder = TypeMoq.Mock.ofType<azdata.ModelBuilder>();
|
||||||
const mockTextBuilder = setupMockComponentBuilder<azdata.TextComponent, azdata.TextComponentProperties>();
|
const mockTextBuilder = setupMockComponentBuilder<azdata.TextComponent, azdata.TextComponentProperties>();
|
||||||
const mockInputBoxBuilder = setupMockComponentBuilder<azdata.InputBoxComponent, azdata.InputBoxProperties>();
|
const mockInputBoxBuilder = setupMockComponentBuilder<azdata.InputBoxComponent, azdata.InputBoxProperties>();
|
||||||
|
buttonClickEmitter = buttonClickEmitter ?? new vscode.EventEmitter<any>();
|
||||||
|
const mockButtonBuilder = setupMockButtonBuilderWithClickEmitter(buttonClickEmitter);
|
||||||
const mockRadioButtonBuilder = setupMockComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>();
|
const mockRadioButtonBuilder = setupMockComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>();
|
||||||
const mockDivBuilder = setupMockContainerBuilder<azdata.DivContainer, azdata.DivContainerProperties, azdata.DivBuilder>();
|
const mockDivBuilder = setupMockContainerBuilder<azdata.DivContainer, azdata.DivContainerProperties, azdata.DivBuilder>();
|
||||||
|
const mockFlexBuilder = setupMockContainerBuilder<azdata.FlexContainer, azdata.ComponentProperties, azdata.FlexBuilder>();
|
||||||
const mockLoadingBuilder = setupMockLoadingBuilder();
|
const mockLoadingBuilder = setupMockLoadingBuilder();
|
||||||
mockModelBuilder.setup(b => b.loadingComponent()).returns(() => mockLoadingBuilder.object);
|
mockModelBuilder.setup(b => b.loadingComponent()).returns(() => mockLoadingBuilder.object);
|
||||||
mockModelBuilder.setup(b => b.text()).returns(() => mockTextBuilder.object);
|
mockModelBuilder.setup(b => b.text()).returns(() => mockTextBuilder.object);
|
||||||
mockModelBuilder.setup(b => b.inputBox()).returns(() => mockInputBoxBuilder.object);
|
mockModelBuilder.setup(b => b.inputBox()).returns(() => mockInputBoxBuilder.object);
|
||||||
|
mockModelBuilder.setup(b => b.button()).returns(() => mockButtonBuilder.object);
|
||||||
mockModelBuilder.setup(b => b.radioButton()).returns(() => mockRadioButtonBuilder.object);
|
mockModelBuilder.setup(b => b.radioButton()).returns(() => mockRadioButtonBuilder.object);
|
||||||
mockModelBuilder.setup(b => b.divContainer()).returns(() => mockDivBuilder.object);
|
mockModelBuilder.setup(b => b.divContainer()).returns(() => mockDivBuilder.object);
|
||||||
|
mockModelBuilder.setup(b => b.flexContainer()).returns(() => mockFlexBuilder.object);
|
||||||
const mockModelView = TypeMoq.Mock.ofType<azdata.ModelView>();
|
const mockModelView = TypeMoq.Mock.ofType<azdata.ModelView>();
|
||||||
mockModelView.setup(mv => mv.modelBuilder).returns(() => mockModelBuilder.object);
|
mockModelView.setup(mv => mv.modelBuilder).returns(() => mockModelBuilder.object);
|
||||||
return { mockModelView, mockModelBuilder, mockTextBuilder, mockInputBoxBuilder, mockRadioButtonBuilder, mockDivBuilder };
|
return { mockModelView, mockModelBuilder, mockTextBuilder, mockInputBoxBuilder, mockButtonBuilder, mockRadioButtonBuilder, mockDivBuilder, mockFlexBuilder, mockLoadingBuilder };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMockButtonBuilderWithClickEmitter(buttonClickEmitter: vscode.EventEmitter<any>): TypeMoq.IMock<azdata.ComponentBuilder<azdata.ButtonComponent, azdata.ButtonProperties>> {
|
||||||
|
const { mockComponentBuilder: mockButtonBuilder, mockComponent: mockButtonComponent } = setupMockComponentBuilderAndComponent<azdata.ButtonComponent, azdata.ButtonProperties>();
|
||||||
|
mockButtonComponent.setup(b => b.onDidClick(TypeMoq.It.isAny())).returns(buttonClickEmitter.event);
|
||||||
|
return mockButtonBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupMockLoadingBuilder(
|
function setupMockLoadingBuilder(
|
||||||
@@ -39,26 +62,44 @@ export function setupMockComponentBuilder<T extends azdata.Component, P extends
|
|||||||
mockComponentBuilder?: TypeMoq.IMock<B>,
|
mockComponentBuilder?: TypeMoq.IMock<B>,
|
||||||
): TypeMoq.IMock<B> {
|
): TypeMoq.IMock<B> {
|
||||||
mockComponentBuilder = mockComponentBuilder ?? TypeMoq.Mock.ofType<B>();
|
mockComponentBuilder = mockComponentBuilder ?? TypeMoq.Mock.ofType<B>();
|
||||||
const returnComponent = TypeMoq.Mock.ofType<T>();
|
setupMockComponentBuilderAndComponent<T, P, B>(mockComponentBuilder, componentGetter);
|
||||||
// Need to setup 'then' for when a mocked object is resolved otherwise the test will hang : https://github.com/florinn/typemoq/issues/66
|
return mockComponentBuilder;
|
||||||
returnComponent.setup((x: any) => x.then).returns(() => { });
|
}
|
||||||
|
|
||||||
|
function setupMockComponentBuilderAndComponent<T extends azdata.Component, P extends azdata.ComponentProperties, B extends azdata.ComponentBuilder<T, P> = azdata.ComponentBuilder<T, P>>(
|
||||||
|
mockComponentBuilder?: TypeMoq.IMock<B>,
|
||||||
|
componentGetter?: ((props: P) => T)
|
||||||
|
): { mockComponentBuilder: TypeMoq.IMock<B>, mockComponent: TypeMoq.IMock<T> } {
|
||||||
|
mockComponentBuilder = mockComponentBuilder ?? TypeMoq.Mock.ofType<B>();
|
||||||
|
const mockComponent = createComponentMock<T>();
|
||||||
let compProps: P;
|
let compProps: P;
|
||||||
mockComponentBuilder.setup(b => b.withProperties(TypeMoq.It.isAny())).callback((props: P) => compProps = props).returns(() => mockComponentBuilder!.object);
|
mockComponentBuilder.setup(b => b.withProperties(TypeMoq.It.isAny())).callback((props: P) => compProps = props).returns(() => mockComponentBuilder!.object);
|
||||||
mockComponentBuilder.setup(b => b.component()).returns(() => {
|
mockComponentBuilder.setup(b => b.component()).returns(() => {
|
||||||
return componentGetter ? componentGetter(compProps) : Object.assign<T, P>(Object.assign({}, returnComponent.object), compProps);
|
return componentGetter ? componentGetter(compProps) : Object.assign<T, P>(Object.assign({}, mockComponent.object), compProps);
|
||||||
});
|
});
|
||||||
|
|
||||||
// For now just have these be passthrough - can hook up additional functionality later if needed
|
// For now just have these be passthrough - can hook up additional functionality later if needed
|
||||||
mockComponentBuilder.setup(b => b.withValidation(TypeMoq.It.isAny())).returns(() => mockComponentBuilder!.object);
|
mockComponentBuilder.setup(b => b.withValidation(TypeMoq.It.isAny())).returns(() => mockComponentBuilder!.object);
|
||||||
return mockComponentBuilder;
|
return { mockComponentBuilder, mockComponent };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createComponentMock<T extends azdata.Component>(): TypeMoq.IMock<T> {
|
||||||
|
const mockComponent = TypeMoq.Mock.ofType<T>();
|
||||||
|
// Need to setup 'then' for when a mocked object is resolved otherwise the test will hang : https://github.com/florinn/typemoq/issues/66
|
||||||
|
mockComponent.setup((x: any) => x.then).returns(() => { });
|
||||||
|
return mockComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupMockContainerBuilder<T extends azdata.Container<any, any>, P extends azdata.ComponentProperties, B extends azdata.ContainerBuilder<T, any, any, any> = azdata.ContainerBuilder<T, any, any, any>>(
|
export function setupMockContainerBuilder<T extends azdata.Container<any, any>, P extends azdata.ComponentProperties, B extends azdata.ContainerBuilder<T, any, any, any> = azdata.ContainerBuilder<T, any, any, any>>(
|
||||||
mockContainerBuilder?: TypeMoq.IMock<B>
|
mockContainerBuilder?: TypeMoq.IMock<B>
|
||||||
): TypeMoq.IMock<B> {
|
): TypeMoq.IMock<B> {
|
||||||
mockContainerBuilder = mockContainerBuilder ?? setupMockComponentBuilder<T, P, B>();
|
const items: azdata.Component[] = [];
|
||||||
|
const mockContainer = createComponentMock<T>(); // T is azdata.Container type so this creates a azdata.Container mock
|
||||||
|
mockContainer.setup(c => c.items).returns(() => items);
|
||||||
|
mockContainerBuilder = mockContainerBuilder ?? setupMockComponentBuilder<T, P, B>((_props) => mockContainer.object);
|
||||||
|
|
||||||
|
mockContainerBuilder.setup(b => b.withItems(TypeMoq.It.isAny(), TypeMoq.It.isAny())).callback((_items, _itemsStyle) => items.push(..._items)).returns(() => mockContainerBuilder!.object);
|
||||||
// For now just have these be passthrough - can hook up additional functionality later if needed
|
// For now just have these be passthrough - can hook up additional functionality later if needed
|
||||||
mockContainerBuilder.setup(b => b.withItems(TypeMoq.It.isAny(), undefined)).returns(() => mockContainerBuilder!.object);
|
|
||||||
mockContainerBuilder.setup(b => b.withLayout(TypeMoq.It.isAny())).returns(() => mockContainerBuilder!.object);
|
mockContainerBuilder.setup(b => b.withLayout(TypeMoq.It.isAny())).returns(() => mockContainerBuilder!.object);
|
||||||
return mockContainerBuilder;
|
return mockContainerBuilder;
|
||||||
}
|
}
|
||||||
|
|||||||
66
extensions/arc/src/test/ui/components/filePicker.test.ts
Normal file
66
extensions/arc/src/test/ui/components/filePicker.test.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* 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 should from 'should';
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { Deferred } from '../../../common/promise';
|
||||||
|
import { FilePicker } from '../../../ui/components/filePicker';
|
||||||
|
import { createModelViewMock } from '../../stubs';
|
||||||
|
|
||||||
|
let filePicker: FilePicker;
|
||||||
|
const initialPath = '/path/to/.kube/config';
|
||||||
|
const newFilePath = '/path/to/new/.kube/config';
|
||||||
|
let filePathInputBox: azdata.InputBoxComponent;
|
||||||
|
let browseButton: azdata.ButtonComponent;
|
||||||
|
let flexContainer: azdata.FlexContainer;
|
||||||
|
const browseButtonEmitter = new vscode.EventEmitter<undefined>();
|
||||||
|
describe('filePicker', function (): void {
|
||||||
|
beforeEach(async () => {
|
||||||
|
const { mockModelBuilder, mockInputBoxBuilder, mockButtonBuilder, mockFlexBuilder } = createModelViewMock(browseButtonEmitter);
|
||||||
|
filePicker = new FilePicker(mockModelBuilder.object, initialPath, (_disposable) => { });
|
||||||
|
filePathInputBox = mockInputBoxBuilder.object.component();
|
||||||
|
browseButton = mockButtonBuilder.object.component();
|
||||||
|
flexContainer = mockFlexBuilder.object.component();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('browse Button chooses new FilePath', async () => {
|
||||||
|
should(filePathInputBox.value).should.not.be.undefined();
|
||||||
|
filePicker.value!.should.equal(initialPath);
|
||||||
|
flexContainer.items.should.deepEqual([filePathInputBox, browseButton]);
|
||||||
|
const deferred = new Deferred();
|
||||||
|
sinon.stub(vscode.window, 'showOpenDialog').callsFake(async (_options) => {
|
||||||
|
deferred.resolve();
|
||||||
|
return [vscode.Uri.file(newFilePath)];
|
||||||
|
});
|
||||||
|
browseButtonEmitter.fire(undefined); //simulate the click of the browseButton
|
||||||
|
await deferred;
|
||||||
|
filePicker.value!.should.equal(newFilePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getters and setters', async () => {
|
||||||
|
it('component getter', () => {
|
||||||
|
should(filePicker.component()).equal(flexContainer);
|
||||||
|
});
|
||||||
|
[true, false].forEach(testValue => {
|
||||||
|
it(`Test readOnly with testValue: ${testValue}`, () => {
|
||||||
|
filePicker.readOnly = testValue;
|
||||||
|
filePicker.readOnly!.should.equal(testValue);
|
||||||
|
});
|
||||||
|
it(`Test enabled with testValue: ${testValue}`, () => {
|
||||||
|
filePicker.enabled = testValue;
|
||||||
|
filePicker.enabled!.should.equal(testValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -21,11 +21,11 @@ const radioOptionsInfo = <RadioOptionsInfo>{
|
|||||||
};
|
};
|
||||||
const divItems: azdata.Component[] = [];
|
const divItems: azdata.Component[] = [];
|
||||||
let radioOptionsGroup: RadioOptionsGroup;
|
let radioOptionsGroup: RadioOptionsGroup;
|
||||||
|
let loadingComponent: azdata.LoadingComponent;
|
||||||
|
|
||||||
describe('radioOptionsGroup', function (): void {
|
describe('radioOptionsGroup', function (): void {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const { mockModelView, mockRadioButtonBuilder, mockDivBuilder } = createModelViewMock();
|
const { mockModelBuilder, mockRadioButtonBuilder, mockDivBuilder, mockLoadingBuilder } = createModelViewMock();
|
||||||
mockRadioButtonBuilder.reset(); // reset any previous mock so that we can set our own.
|
mockRadioButtonBuilder.reset(); // reset any previous mock so that we can set our own.
|
||||||
setupMockComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>(
|
setupMockComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>(
|
||||||
(props) => new FakeRadioButton(props),
|
(props) => new FakeRadioButton(props),
|
||||||
@@ -41,8 +41,9 @@ describe('radioOptionsGroup', function (): void {
|
|||||||
},
|
},
|
||||||
mockDivBuilder
|
mockDivBuilder
|
||||||
);
|
);
|
||||||
radioOptionsGroup = new RadioOptionsGroup(mockModelView.object, (_disposable) => { });
|
radioOptionsGroup = new RadioOptionsGroup(mockModelBuilder.object, (_disposable) => { });
|
||||||
await radioOptionsGroup.load(async () => radioOptionsInfo);
|
await radioOptionsGroup.load(async () => radioOptionsInfo);
|
||||||
|
loadingComponent = mockLoadingBuilder.object.component();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('verify construction and load', async () => {
|
it('verify construction and load', async () => {
|
||||||
@@ -72,6 +73,23 @@ describe('radioOptionsGroup', function (): void {
|
|||||||
should(label.CSSStyles!.color).not.be.undefined();
|
should(label.CSSStyles!.color).not.be.undefined();
|
||||||
label.CSSStyles!.color.should.equal('Red');
|
label.CSSStyles!.color.should.equal('Red');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getters and setters', async () => {
|
||||||
|
it(`component getter`, () => {
|
||||||
|
radioOptionsGroup.component().should.deepEqual(loadingComponent);
|
||||||
|
});
|
||||||
|
|
||||||
|
[true, false].forEach(testValue => {
|
||||||
|
it(`Test readOnly with testValue: ${testValue}`, () => {
|
||||||
|
radioOptionsGroup.readOnly = testValue;
|
||||||
|
radioOptionsGroup.readOnly!.should.equal(testValue);
|
||||||
|
});
|
||||||
|
it(`Test enabled with testValue: ${testValue}`, () => {
|
||||||
|
radioOptionsGroup.enabled = testValue;
|
||||||
|
radioOptionsGroup.enabled!.should.equal(testValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function verifyRadioGroup() {
|
function verifyRadioGroup() {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ describe('ConnectControllerDialog', function (): void {
|
|||||||
it('validate returns false if controller refresh fails', async function (): Promise<void> {
|
it('validate returns false if controller refresh fails', async function (): Promise<void> {
|
||||||
sinon.stub(ControllerModel.prototype, 'refresh').returns(Promise.reject('Controller refresh failed'));
|
sinon.stub(ControllerModel.prototype, 'refresh').returns(Promise.reject('Controller refresh failed'));
|
||||||
const connectControllerDialog = new ConnectToControllerDialog(undefined!);
|
const connectControllerDialog = new ConnectToControllerDialog(undefined!);
|
||||||
const info = { id: uuid(), url: 'https://127.0.0.1:30080', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
|
const info = { id: uuid(), url: 'https://127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
|
||||||
connectControllerDialog.showDialog(info, 'pwd');
|
connectControllerDialog.showDialog(info, 'pwd');
|
||||||
await connectControllerDialog.isInitialized;
|
await connectControllerDialog.isInitialized;
|
||||||
const validateResult = await connectControllerDialog.validate();
|
const validateResult = await connectControllerDialog.validate();
|
||||||
@@ -41,36 +41,36 @@ describe('ConnectControllerDialog', function (): void {
|
|||||||
|
|
||||||
it('validate replaces http with https', async function (): Promise<void> {
|
it('validate replaces http with https', async function (): Promise<void> {
|
||||||
await validateConnectControllerDialog(
|
await validateConnectControllerDialog(
|
||||||
{ id: uuid(), url: 'http://127.0.0.1:30081', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
||||||
'https://127.0.0.1:30081');
|
'https://127.0.0.1:30081');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('validate appends https if missing', async function (): Promise<void> {
|
it('validate appends https if missing', async function (): Promise<void> {
|
||||||
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1:30080', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1:30080', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
||||||
'https://127.0.0.1:30080');
|
'https://127.0.0.1:30080');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('validate appends default port if missing', async function (): Promise<void> {
|
it('validate appends default port if missing', async function (): Promise<void> {
|
||||||
await validateConnectControllerDialog({ id: uuid(), url: 'https://127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
await validateConnectControllerDialog({ id: uuid(), url: 'https://127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
||||||
'https://127.0.0.1:30080');
|
'https://127.0.0.1:30080');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('validate appends both port and https if missing', async function (): Promise<void> {
|
it('validate appends both port and https if missing', async function (): Promise<void> {
|
||||||
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
await validateConnectControllerDialog({ id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] },
|
||||||
'https://127.0.0.1:30080');
|
'https://127.0.0.1:30080');
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const name of ['', undefined]) {
|
for (const name of ['', undefined]) {
|
||||||
it.skip(`validate display name gets set to arc instance name for user chosen name of:${name}`, async function (): Promise<void> {
|
it.skip(`validate display name gets set to arc instance name for user chosen name of:${name}`, async function (): Promise<void> {
|
||||||
await validateConnectControllerDialog(
|
await validateConnectControllerDialog(
|
||||||
{ id: uuid(), url: 'http://127.0.0.1:30081', name: name!, username: 'sa', rememberPassword: true, resources: [] },
|
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: name!, username: 'sa', rememberPassword: true, resources: [] },
|
||||||
'https://127.0.0.1:30081');
|
'https://127.0.0.1:30081');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it.skip(`validate display name gets set to default data controller name for user chosen name of:'' and instanceName in explicably returned as undefined from the controller endpoint`, async function (): Promise<void> {
|
it.skip(`validate display name gets set to default data controller name for user chosen name of:'' and instanceName in explicably returned as undefined from the controller endpoint`, async function (): Promise<void> {
|
||||||
await validateConnectControllerDialog(
|
await validateConnectControllerDialog(
|
||||||
{ id: uuid(), url: 'http://127.0.0.1:30081', name: '', username: 'sa', rememberPassword: true, resources: [] },
|
{ id: uuid(), url: 'http://127.0.0.1:30081', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: '', username: 'sa', rememberPassword: true, resources: [] },
|
||||||
'https://127.0.0.1:30081',
|
'https://127.0.0.1:30081',
|
||||||
undefined);
|
undefined);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
|||||||
treeDataProvider['_loading'] = false;
|
treeDataProvider['_loading'] = false;
|
||||||
let children = await treeDataProvider.getChildren();
|
let children = await treeDataProvider.getChildren();
|
||||||
should(children.length).equal(0, 'There initially shouldn\'t be any children');
|
should(children.length).equal(0, 'There initially shouldn\'t be any children');
|
||||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||||
should(children.length).equal(1, 'Controller node should be added correctly');
|
should(children.length).equal(1, 'Controller node should be added correctly');
|
||||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||||
@@ -64,12 +64,12 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
|||||||
treeDataProvider['_loading'] = false;
|
treeDataProvider['_loading'] = false;
|
||||||
let children = await treeDataProvider.getChildren();
|
let children = await treeDataProvider.getChildren();
|
||||||
should(children.length).equal(0, 'There initially shouldn\'t be any children');
|
should(children.length).equal(0, 'There initially shouldn\'t be any children');
|
||||||
const originalInfo: ControllerInfo = { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
|
const originalInfo: ControllerInfo = { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] };
|
||||||
const controllerModel = new ControllerModel(treeDataProvider, originalInfo);
|
const controllerModel = new ControllerModel(treeDataProvider, originalInfo);
|
||||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||||
should(children.length).equal(1, 'Controller node should be added correctly');
|
should(children.length).equal(1, 'Controller node should be added correctly');
|
||||||
should((<ControllerTreeNode>children[0]).model.info).deepEqual(originalInfo);
|
should((<ControllerTreeNode>children[0]).model.info).deepEqual(originalInfo);
|
||||||
const newInfo = { id: originalInfo.id, url: '1.1.1.1', name: 'new-name', username: 'admin', rememberPassword: false, resources: [] };
|
const newInfo = { id: originalInfo.id, url: '1.1.1.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'new-name', username: 'admin', rememberPassword: false, resources: [] };
|
||||||
const controllerModel2 = new ControllerModel(treeDataProvider, newInfo);
|
const controllerModel2 = new ControllerModel(treeDataProvider, newInfo);
|
||||||
await treeDataProvider.addOrUpdateController(controllerModel2, '');
|
await treeDataProvider.addOrUpdateController(controllerModel2, '');
|
||||||
should(children.length).equal(1, 'Shouldn\'t add duplicate controller node');
|
should(children.length).equal(1, 'Shouldn\'t add duplicate controller node');
|
||||||
@@ -102,7 +102,7 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
|||||||
mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi);
|
mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi);
|
||||||
|
|
||||||
sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object);
|
sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object);
|
||||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'mypassword');
|
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'mypassword');
|
||||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||||
const controllerNode = treeDataProvider.getControllerNode(controllerModel);
|
const controllerNode = treeDataProvider.getControllerNode(controllerModel);
|
||||||
const children = await treeDataProvider.getChildren(controllerNode);
|
const children = await treeDataProvider.getChildren(controllerNode);
|
||||||
@@ -115,8 +115,8 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
|||||||
describe('removeController', function (): void {
|
describe('removeController', function (): void {
|
||||||
it('removing a controller should work as expected', async function (): Promise<void> {
|
it('removing a controller should work as expected', async function (): Promise<void> {
|
||||||
treeDataProvider['_loading'] = false;
|
treeDataProvider['_loading'] = false;
|
||||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||||
const controllerModel2 = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.2', name: 'my-arc', username: 'cloudsa', rememberPassword: true, resources: [] });
|
const controllerModel2 = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.2', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'cloudsa', rememberPassword: true, resources: [] });
|
||||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||||
await treeDataProvider.addOrUpdateController(controllerModel2, '');
|
await treeDataProvider.addOrUpdateController(controllerModel2, '');
|
||||||
const children = <ControllerTreeNode[]>(await treeDataProvider.getChildren());
|
const children = <ControllerTreeNode[]>(await treeDataProvider.getChildren());
|
||||||
@@ -133,20 +133,20 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
|||||||
|
|
||||||
describe('openResourceDashboard', function (): void {
|
describe('openResourceDashboard', function (): void {
|
||||||
it('Opening dashboard for nonexistent controller node throws', async function (): Promise<void> {
|
it('Opening dashboard for nonexistent controller node throws', async function (): Promise<void> {
|
||||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||||
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
|
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
|
||||||
await should(openDashboardPromise).be.rejected();
|
await should(openDashboardPromise).be.rejected();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Opening dashboard for nonexistent resource throws', async function (): Promise<void> {
|
it('Opening dashboard for nonexistent resource throws', async function (): Promise<void> {
|
||||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||||
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
|
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
|
||||||
await should(openDashboardPromise).be.rejected();
|
await should(openDashboardPromise).be.rejected();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Opening dashboard for existing resource node succeeds', async function (): Promise<void> {
|
it('Opening dashboard for existing resource node succeeds', async function (): Promise<void> {
|
||||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] });
|
||||||
const miaaModel = new MiaaModel(controllerModel, { name: 'miaa-1', resourceType: ResourceType.sqlManagedInstances }, undefined!, treeDataProvider);
|
const miaaModel = new MiaaModel(controllerModel, { name: 'miaa-1', resourceType: ResourceType.sqlManagedInstances }, undefined!, treeDataProvider);
|
||||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||||
const controllerNode = treeDataProvider.getControllerNode(controllerModel)!;
|
const controllerNode = treeDataProvider.getControllerNode(controllerModel)!;
|
||||||
|
|||||||
2
extensions/arc/src/typings/arc.d.ts
vendored
2
extensions/arc/src/typings/arc.d.ts
vendored
@@ -31,6 +31,8 @@ declare module 'arc' {
|
|||||||
|
|
||||||
export type ControllerInfo = {
|
export type ControllerInfo = {
|
||||||
id: string,
|
id: string,
|
||||||
|
kubeConfigFilePath: string,
|
||||||
|
kubeClusterContext: string
|
||||||
url: string,
|
url: string,
|
||||||
name: string,
|
name: string,
|
||||||
username: string,
|
username: string,
|
||||||
|
|||||||
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 azdata from 'azdata';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { getErrorMessage } from '../../common/utils';
|
import { getErrorMessage } from '../../common/utils';
|
||||||
|
import { IReadOnly } from '../dialogs/connectControllerDialog';
|
||||||
|
|
||||||
export interface RadioOptionsInfo {
|
export interface RadioOptionsInfo {
|
||||||
values?: string[],
|
values?: string[],
|
||||||
defaultValue: string
|
defaultValue: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RadioOptionsGroup {
|
export class RadioOptionsGroup implements IReadOnly {
|
||||||
static id: number = 1;
|
static id: number = 1;
|
||||||
private _divContainer!: azdata.DivContainer;
|
private _divContainer!: azdata.DivContainer;
|
||||||
private _loadingBuilder: azdata.LoadingComponentBuilder;
|
private _loadingBuilder: azdata.LoadingComponentBuilder;
|
||||||
private _currentRadioOption!: azdata.RadioButtonComponent;
|
private _currentRadioOption!: azdata.RadioButtonComponent;
|
||||||
|
|
||||||
constructor(private _view: azdata.ModelView, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void, private _groupName: string = `RadioOptionsGroup${RadioOptionsGroup.id++}`) {
|
constructor(private _modelBuilder: azdata.ModelBuilder, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void, private _groupName: string = `RadioOptionsGroup${RadioOptionsGroup.id++}`) {
|
||||||
const divBuilder = this._view.modelBuilder.divContainer();
|
this._divContainer = this._modelBuilder.divContainer().withProperties<azdata.DivContainerProperties>({ clickable: false }).component();
|
||||||
const divBuilderWithProperties = divBuilder.withProperties<azdata.DivContainerProperties>({ clickable: false });
|
this._loadingBuilder = this._modelBuilder.loadingComponent().withItem(this._divContainer);
|
||||||
this._divContainer = divBuilderWithProperties.component();
|
|
||||||
const loadingComponentBuilder = this._view.modelBuilder.loadingComponent();
|
|
||||||
this._loadingBuilder = loadingComponentBuilder.withItem(this._divContainer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public component(): azdata.LoadingComponent {
|
public component(): azdata.LoadingComponent {
|
||||||
@@ -37,7 +35,7 @@ export class RadioOptionsGroup {
|
|||||||
const options = optionsInfo.values!;
|
const options = optionsInfo.values!;
|
||||||
let defaultValue: string = optionsInfo.defaultValue!;
|
let defaultValue: string = optionsInfo.defaultValue!;
|
||||||
options.forEach((option: string) => {
|
options.forEach((option: string) => {
|
||||||
const radioOption = this._view!.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
|
const radioOption = this._modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
|
||||||
label: option,
|
label: option,
|
||||||
checked: option === defaultValue,
|
checked: option === defaultValue,
|
||||||
name: this._groupName,
|
name: this._groupName,
|
||||||
@@ -60,7 +58,7 @@ export class RadioOptionsGroup {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (e) {
|
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._divContainer.addItem(errorLabel);
|
||||||
}
|
}
|
||||||
this.component().loading = false;
|
this.component().loading = false;
|
||||||
@@ -69,4 +67,21 @@ export class RadioOptionsGroup {
|
|||||||
get value(): string | undefined {
|
get value(): string | undefined {
|
||||||
return this._currentRadioOption?.value;
|
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();
|
}).component();
|
||||||
|
|
||||||
const workerNodeslink = this.modelView.modelBuilder.hyperlink().withProperties<azdata.HyperlinkComponentProperties>({
|
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',
|
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' }
|
CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||||
}).component();
|
}).component();
|
||||||
|
|||||||
@@ -14,23 +14,45 @@ import { ControllerModel } from '../../models/controllerModel';
|
|||||||
import { InitializingComponent } from '../components/initializingComponent';
|
import { InitializingComponent } from '../components/initializingComponent';
|
||||||
import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider';
|
import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider';
|
||||||
import { getErrorMessage } from '../../common/utils';
|
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 type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string };
|
||||||
|
export interface IReadOnly {
|
||||||
|
readOnly?: boolean
|
||||||
|
}
|
||||||
abstract class ControllerDialogBase extends InitializingComponent {
|
abstract class ControllerDialogBase extends InitializingComponent {
|
||||||
|
protected _toDispose: vscode.Disposable[] = [];
|
||||||
protected modelBuilder!: azdata.ModelBuilder;
|
protected modelBuilder!: azdata.ModelBuilder;
|
||||||
protected dialog: azdata.window.Dialog;
|
protected dialog: azdata.window.Dialog;
|
||||||
|
|
||||||
protected urlInputBox!: azdata.InputBoxComponent;
|
protected urlInputBox!: azdata.InputBoxComponent;
|
||||||
|
protected kubeConfigInputBox!: FilePicker;
|
||||||
|
protected clusterContextRadioGroup!: RadioOptionsGroup;
|
||||||
protected nameInputBox!: azdata.InputBoxComponent;
|
protected nameInputBox!: azdata.InputBoxComponent;
|
||||||
protected usernameInputBox!: azdata.InputBoxComponent;
|
protected usernameInputBox!: azdata.InputBoxComponent;
|
||||||
protected passwordInputBox!: 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; })[] {
|
protected getComponents(): (azdata.FormComponent<azdata.Component> & { layout?: azdata.FormItemLayout | undefined; })[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
component: this.urlInputBox,
|
component: this.urlInputBox,
|
||||||
title: loc.controllerUrl,
|
title: loc.controllerUrl,
|
||||||
required: true
|
required: true
|
||||||
|
}, {
|
||||||
|
component: this.kubeConfigInputBox.component(),
|
||||||
|
title: loc.controllerKubeConfig,
|
||||||
|
required: true
|
||||||
|
}, {
|
||||||
|
component: this.clusterContextRadioGroup.component(),
|
||||||
|
title: loc.controllerClusterContext,
|
||||||
|
required: true
|
||||||
}, {
|
}, {
|
||||||
component: this.nameInputBox,
|
component: this.nameInputBox,
|
||||||
title: loc.controllerName,
|
title: loc.controllerName,
|
||||||
@@ -48,7 +70,7 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected abstract fieldToFocusOn(): azdata.Component;
|
protected abstract fieldToFocusOn(): azdata.Component;
|
||||||
protected readonlyFields(): azdata.InputBoxComponent[] { return []; }
|
protected readonlyFields(): IReadOnly[] { return []; }
|
||||||
|
|
||||||
protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
|
protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
|
||||||
this.urlInputBox = this.modelBuilder.inputBox()
|
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
|
// If we have a model then we're editing an existing connection so don't let them modify the URL
|
||||||
readOnly: !!controllerInfo
|
readOnly: !!controllerInfo
|
||||||
}).component();
|
}).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()
|
this.nameInputBox = this.modelBuilder.inputBox()
|
||||||
.withProperties<azdata.InputBoxProperties>({
|
.withProperties<azdata.InputBoxProperties>({
|
||||||
value: controllerInfo?.name
|
value: controllerInfo?.name
|
||||||
@@ -81,10 +115,20 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
|||||||
this.dialog = azdata.window.createModelViewDialog(title);
|
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 {
|
public showDialog(controllerInfo?: ControllerInfo, password: string | undefined = undefined): azdata.window.Dialog {
|
||||||
this.id = controllerInfo?.id ?? uuid();
|
this.id = controllerInfo?.id ?? uuid();
|
||||||
this.resources = controllerInfo?.resources ?? [];
|
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.dialog.registerContent(async (view) => {
|
||||||
this.modelBuilder = view.modelBuilder;
|
this.modelBuilder = view.modelBuilder;
|
||||||
this.initializeFields(controllerInfo, password);
|
this.initializeFields(controllerInfo, password);
|
||||||
@@ -100,7 +144,13 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
|||||||
this.initialized = true;
|
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.okButton.label = loc.connect;
|
||||||
this.dialog.cancelButton.label = loc.cancel;
|
this.dialog.cancelButton.label = loc.cancel;
|
||||||
azdata.window.openDialog(this.dialog);
|
azdata.window.openDialog(this.dialog);
|
||||||
@@ -116,6 +166,19 @@ abstract class ControllerDialogBase extends InitializingComponent {
|
|||||||
public waitForClose(): Promise<ConnectToControllerDialogModel | undefined> {
|
public waitForClose(): Promise<ConnectToControllerDialogModel | undefined> {
|
||||||
return this.completionPromise.promise;
|
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 {
|
export class ConnectToControllerDialog extends ControllerDialogBase {
|
||||||
@@ -164,14 +227,7 @@ export class ConnectToControllerDialog extends ControllerDialogBase {
|
|||||||
if (!/.*:\d*$/.test(url)) {
|
if (!/.*:\d*$/.test(url)) {
|
||||||
url = `${url}:30080`;
|
url = `${url}:30080`;
|
||||||
}
|
}
|
||||||
const controllerInfo: ControllerInfo = {
|
const controllerInfo: ControllerInfo = this.getControllerInfo(url, !!this.rememberPwCheckBox.checked);
|
||||||
id: this.id,
|
|
||||||
url: url,
|
|
||||||
name: this.nameInputBox.value ?? '',
|
|
||||||
username: this.usernameInputBox.value,
|
|
||||||
rememberPassword: this.rememberPwCheckBox.checked ?? false,
|
|
||||||
resources: this.resources
|
|
||||||
};
|
|
||||||
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
||||||
try {
|
try {
|
||||||
// Validate that we can connect to the controller, this also populates the controllerRegistration from the connection response.
|
// 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() {
|
protected readonlyFields() {
|
||||||
return [
|
return [
|
||||||
this.urlInputBox,
|
this.urlInputBox,
|
||||||
|
this.kubeConfigInputBox,
|
||||||
|
this.clusterContextRadioGroup,
|
||||||
this.nameInputBox,
|
this.nameInputBox,
|
||||||
this.usernameInputBox
|
this.usernameInputBox
|
||||||
];
|
];
|
||||||
@@ -229,14 +287,7 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const controllerInfo: ControllerInfo = {
|
const controllerInfo: ControllerInfo = this.getControllerInfo(this.urlInputBox.value!, false);
|
||||||
id: this.id,
|
|
||||||
url: this.urlInputBox.value!,
|
|
||||||
name: this.nameInputBox.value!,
|
|
||||||
username: this.usernameInputBox.value!,
|
|
||||||
rememberPassword: false,
|
|
||||||
resources: []
|
|
||||||
};
|
|
||||||
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
|
||||||
this.completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
|
this.completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ControllerModel } from '../../models/controllerModel';
|
|||||||
import { ControllerTreeNode } from './controllerTreeNode';
|
import { ControllerTreeNode } from './controllerTreeNode';
|
||||||
import { TreeNode } from './treeNode';
|
import { TreeNode } from './treeNode';
|
||||||
|
|
||||||
const mementoToken = 'arcControllers';
|
const mementoToken = 'arcDataControllers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The TreeDataProvider for the Azure Arc view, which displays a list of registered
|
* The TreeDataProvider for the Azure Arc view, which displays a list of registered
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function createViewContext(): ViewTestContext {
|
|||||||
withProps: () => checkBoxBuilder,
|
withProps: () => checkBoxBuilder,
|
||||||
withValidation: () => checkBoxBuilder
|
withValidation: () => checkBoxBuilder
|
||||||
};
|
};
|
||||||
let inputBox: () => azdata.InputBoxComponent = () => Object.assign({}, componentBase, {
|
let inputBox: () => azdata.InputBoxComponent = () => Object.assign(<azdata.InputBoxComponent>Object.assign({}, componentBase), {
|
||||||
onTextChanged: onClick.event!,
|
onTextChanged: onClick.event!,
|
||||||
onEnterKeyPressed: undefined!,
|
onEnterKeyPressed: undefined!,
|
||||||
value: ''
|
value: ''
|
||||||
|
|||||||
Reference in New Issue
Block a user