mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-18 11:01:36 -05:00
Compare commits
10 Commits
1.25.0_rel
...
1.25.1_rel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2df67c4f78 | ||
|
|
81ed7123c6 | ||
|
|
4ab0f729e1 | ||
|
|
95d22a03ae | ||
|
|
94feb1a80d | ||
|
|
254ecc4123 | ||
|
|
515b0794b0 | ||
|
|
dc8788b77f | ||
|
|
147ee53e35 | ||
|
|
e7884b8b61 |
@@ -1,5 +1,10 @@
|
||||
# Change Log
|
||||
|
||||
## Version 1.25.1
|
||||
* Release date: December 10, 2020
|
||||
* Release status: General Availability
|
||||
* Fixes https://github.com/microsoft/azuredatastudio/issues/13751
|
||||
|
||||
## Version 1.25.0
|
||||
* Release date: December 8, 2020
|
||||
* Release status: General Availability
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "arc",
|
||||
"displayName": "%arc.displayName%",
|
||||
"description": "%arc.description%",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
@@ -189,7 +189,9 @@
|
||||
"editable": false,
|
||||
"options": {
|
||||
"source": {
|
||||
"providerId": "arc.controller.config.profiles"
|
||||
"providerId": "arc.controller.config.profiles",
|
||||
"loadingText": "%arc.data.controller.cluster.config.profile.loading%",
|
||||
"loadingCompletedText": "%arc.data.controller.cluster.config.profile.loadingcompleted%"
|
||||
},
|
||||
"defaultValue": "azure-arc-aks-default-storage",
|
||||
"optionsType": "radio"
|
||||
@@ -608,7 +610,7 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.2.0"
|
||||
"version": "20.2.5"
|
||||
}
|
||||
],
|
||||
"when": true
|
||||
@@ -787,7 +789,7 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.2.0"
|
||||
"version": "20.2.5"
|
||||
}
|
||||
],
|
||||
"when": "true"
|
||||
@@ -1035,7 +1037,7 @@
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.2.0"
|
||||
"version": "20.2.5"
|
||||
}
|
||||
],
|
||||
"when": "true"
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
"arc.data.controller.kube.cluster.context": "Cluster context",
|
||||
"arc.data.controller.cluster.config.profile.title": "Choose the config profile",
|
||||
"arc.data.controller.cluster.config.profile": "Config profile",
|
||||
"arc.data.controller.cluster.config.profile.loading": "Loading config profiles",
|
||||
"arc.data.controller.cluster.config.profile.loadingcompleted": "Loading config profiles complete",
|
||||
"arc.data.controller.create.azureconfig.title": "Azure and Connectivity Configuration",
|
||||
"arc.data.controller.connectivitymode.description": "Select the connectivity mode for the controller.",
|
||||
"arc.data.controller.create.controllerconfig.title": "Controller Configuration",
|
||||
|
||||
@@ -13,6 +13,11 @@ export interface KubeClusterContext {
|
||||
isCurrentContext: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the cluster context defined in the {@see configFile}
|
||||
*
|
||||
* @param configFile
|
||||
*/
|
||||
export function getKubeConfigClusterContexts(configFile: string): Promise<KubeClusterContext[]> {
|
||||
const config: any = yamljs.load(configFile);
|
||||
const rawContexts = <any[]>config['contexts'];
|
||||
@@ -33,6 +38,38 @@ export function getKubeConfigClusterContexts(configFile: string): Promise<KubeCl
|
||||
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 {
|
||||
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 feedback = localize('arc.feedback', "Feedback");
|
||||
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 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.");
|
||||
@@ -85,6 +85,8 @@ export const passwordToController = localize('arc.passwordToController', "Provid
|
||||
export const controllerUrl = localize('arc.controllerUrl', "Controller URL");
|
||||
export const serverEndpoint = localize('arc.serverEndpoint', "Server Endpoint");
|
||||
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 username = localize('arc.username', "Username");
|
||||
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 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 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 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);
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('KubeUtils', function (): void {
|
||||
});
|
||||
it('throws error when unable to load config file', async () => {
|
||||
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`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider
|
||||
export class FakeControllerModel extends ControllerModel {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('ControllerModel', function (): 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"
|
||||
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));
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
azdataExtApiMock.setup(x => x.azdata).returns(() => azdataMock.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();
|
||||
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 });
|
||||
|
||||
// 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 }));
|
||||
|
||||
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();
|
||||
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 });
|
||||
|
||||
// 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 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);
|
||||
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 });
|
||||
|
||||
// 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 }));
|
||||
|
||||
// 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);
|
||||
should(waitForCloseStub.called).be.true('waitForClose should have been called');
|
||||
@@ -166,6 +166,8 @@ describe('ControllerModel', function (): void {
|
||||
{
|
||||
id: uuid(),
|
||||
url: '127.0.0.1',
|
||||
kubeConfigFilePath: '/path/to/.kube/config',
|
||||
kubeClusterContext: 'currentCluster',
|
||||
username: 'admin',
|
||||
name: 'arc',
|
||||
rememberPassword: false,
|
||||
@@ -178,6 +180,8 @@ describe('ControllerModel', function (): void {
|
||||
const newInfo: ControllerInfo = {
|
||||
id: model.info.id, // The ID stays the same since we're just re-entering information for the same model
|
||||
url: 'newUrl',
|
||||
kubeConfigFilePath: '/path/to/.kube/config',
|
||||
kubeClusterContext: 'currentCluster',
|
||||
username: 'newUser',
|
||||
name: 'newName',
|
||||
rememberPassword: true,
|
||||
|
||||
@@ -7,21 +7,44 @@ import * as azdata from 'azdata';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
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 mockTextBuilder = setupMockComponentBuilder<azdata.TextComponent, azdata.TextComponentProperties>();
|
||||
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 mockDivBuilder = setupMockContainerBuilder<azdata.DivContainer, azdata.DivContainerProperties, azdata.DivBuilder>();
|
||||
const mockFlexBuilder = setupMockContainerBuilder<azdata.FlexContainer, azdata.ComponentProperties, azdata.FlexBuilder>();
|
||||
const mockLoadingBuilder = setupMockLoadingBuilder();
|
||||
mockModelBuilder.setup(b => b.loadingComponent()).returns(() => mockLoadingBuilder.object);
|
||||
mockModelBuilder.setup(b => b.text()).returns(() => mockTextBuilder.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.divContainer()).returns(() => mockDivBuilder.object);
|
||||
mockModelBuilder.setup(b => b.flexContainer()).returns(() => mockFlexBuilder.object);
|
||||
const mockModelView = TypeMoq.Mock.ofType<azdata.ModelView>();
|
||||
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(
|
||||
@@ -39,26 +62,44 @@ export function setupMockComponentBuilder<T extends azdata.Component, P extends
|
||||
mockComponentBuilder?: TypeMoq.IMock<B>,
|
||||
): TypeMoq.IMock<B> {
|
||||
mockComponentBuilder = mockComponentBuilder ?? TypeMoq.Mock.ofType<B>();
|
||||
const returnComponent = 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
|
||||
returnComponent.setup((x: any) => x.then).returns(() => { });
|
||||
setupMockComponentBuilderAndComponent<T, P, B>(mockComponentBuilder, componentGetter);
|
||||
return mockComponentBuilder;
|
||||
}
|
||||
|
||||
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;
|
||||
mockComponentBuilder.setup(b => b.withProperties(TypeMoq.It.isAny())).callback((props: P) => compProps = props).returns(() => mockComponentBuilder!.object);
|
||||
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
|
||||
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>>(
|
||||
mockContainerBuilder?: 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
|
||||
mockContainerBuilder.setup(b => b.withItems(TypeMoq.It.isAny(), undefined)).returns(() => mockContainerBuilder!.object);
|
||||
mockContainerBuilder.setup(b => b.withLayout(TypeMoq.It.isAny())).returns(() => mockContainerBuilder!.object);
|
||||
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[] = [];
|
||||
let radioOptionsGroup: RadioOptionsGroup;
|
||||
|
||||
let loadingComponent: azdata.LoadingComponent;
|
||||
|
||||
describe('radioOptionsGroup', function (): void {
|
||||
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.
|
||||
setupMockComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>(
|
||||
(props) => new FakeRadioButton(props),
|
||||
@@ -41,8 +41,9 @@ describe('radioOptionsGroup', function (): void {
|
||||
},
|
||||
mockDivBuilder
|
||||
);
|
||||
radioOptionsGroup = new RadioOptionsGroup(mockModelView.object, (_disposable) => { });
|
||||
radioOptionsGroup = new RadioOptionsGroup(mockModelBuilder.object, (_disposable) => { });
|
||||
await radioOptionsGroup.load(async () => radioOptionsInfo);
|
||||
loadingComponent = mockLoadingBuilder.object.component();
|
||||
});
|
||||
|
||||
it('verify construction and load', async () => {
|
||||
@@ -72,6 +73,23 @@ describe('radioOptionsGroup', function (): void {
|
||||
should(label.CSSStyles!.color).not.be.undefined();
|
||||
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() {
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('ConnectControllerDialog', function (): void {
|
||||
it('validate returns false if controller refresh fails', async function (): Promise<void> {
|
||||
sinon.stub(ControllerModel.prototype, 'refresh').returns(Promise.reject('Controller refresh failed'));
|
||||
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');
|
||||
await connectControllerDialog.isInitialized;
|
||||
const validateResult = await connectControllerDialog.validate();
|
||||
@@ -41,36 +41,36 @@ describe('ConnectControllerDialog', function (): void {
|
||||
|
||||
it('validate replaces http with https', async function (): Promise<void> {
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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> {
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
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(
|
||||
{ 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',
|
||||
undefined);
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
treeDataProvider['_loading'] = false;
|
||||
let children = await treeDataProvider.getChildren();
|
||||
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, '');
|
||||
should(children.length).equal(1, 'Controller node should be added correctly');
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
@@ -64,12 +64,12 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
treeDataProvider['_loading'] = false;
|
||||
let children = await treeDataProvider.getChildren();
|
||||
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);
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
should(children.length).equal(1, 'Controller node should be added correctly');
|
||||
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);
|
||||
await treeDataProvider.addOrUpdateController(controllerModel2, '');
|
||||
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);
|
||||
|
||||
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, '');
|
||||
const controllerNode = treeDataProvider.getControllerNode(controllerModel);
|
||||
const children = await treeDataProvider.getChildren(controllerNode);
|
||||
@@ -115,8 +115,8 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
describe('removeController', function (): void {
|
||||
it('removing a controller should work as expected', async function (): Promise<void> {
|
||||
treeDataProvider['_loading'] = false;
|
||||
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', 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 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', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'cloudsa', rememberPassword: true, resources: [] });
|
||||
await treeDataProvider.addOrUpdateController(controllerModel, '');
|
||||
await treeDataProvider.addOrUpdateController(controllerModel2, '');
|
||||
const children = <ControllerTreeNode[]>(await treeDataProvider.getChildren());
|
||||
@@ -133,20 +133,20 @@ describe('AzureArcTreeDataProvider tests', function (): void {
|
||||
|
||||
describe('openResourceDashboard', function (): 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, '');
|
||||
await should(openDashboardPromise).be.rejected();
|
||||
});
|
||||
|
||||
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, '');
|
||||
const openDashboardPromise = treeDataProvider.openResourceDashboard(controllerModel, ResourceType.sqlManagedInstances, '');
|
||||
await should(openDashboardPromise).be.rejected();
|
||||
});
|
||||
|
||||
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);
|
||||
await treeDataProvider.addOrUpdateController(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 = {
|
||||
id: string,
|
||||
kubeConfigFilePath: string,
|
||||
kubeClusterContext: string
|
||||
url: string,
|
||||
name: 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 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
|
||||
|
||||
@@ -321,7 +321,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/arm-resourcegraph": "^2.0.0",
|
||||
"@azure/arm-storage": "^15.1.0",
|
||||
"@azure/arm-subscriptions": "1.0.0",
|
||||
"axios": "^0.19.2",
|
||||
"qs": "^6.9.1",
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
declare module 'azureResource' {
|
||||
import { TreeDataProvider } from 'vscode';
|
||||
import { DataProvider, Account, TreeItem } from 'azdata';
|
||||
import { FileShareItem, ListContainerItem } from '@azure/arm-storage/esm/models';
|
||||
export namespace azureResource {
|
||||
|
||||
export const enum AzureResourceType {
|
||||
@@ -19,8 +18,7 @@ declare module 'azureResource' {
|
||||
kustoClusters = 'microsoft.kusto/clusters',
|
||||
azureArcPostgresServer = 'microsoft.azuredata/postgresinstances',
|
||||
postgresServer = 'microsoft.dbforpostgresql/servers',
|
||||
azureArcService = 'microsoft.azuredata/datacontrollers',
|
||||
storageAccount = 'microsoft.storage/storageaccounts',
|
||||
azureArcService = 'microsoft.azuredata/datacontrollers'
|
||||
}
|
||||
|
||||
export interface IAzureResourceProvider extends DataProvider {
|
||||
@@ -77,10 +75,7 @@ declare module 'azureResource' {
|
||||
fullName: string;
|
||||
defaultDatabaseName: string;
|
||||
}
|
||||
export interface BlobContainer extends ListContainerItem {
|
||||
}
|
||||
|
||||
export interface FileShare extends FileShareItem {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,18 +5,14 @@
|
||||
|
||||
import { ResourceGraphClient } from '@azure/arm-resourcegraph';
|
||||
import { TokenCredentials } from '@azure/ms-rest-js';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import * as azdata from 'azdata';
|
||||
import { HttpGetRequestResult, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult } from 'azurecore';
|
||||
import { GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult } from 'azurecore';
|
||||
import { azureResource } from 'azureResource';
|
||||
import { EOL } from 'os';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { AppContext } from '../appContext';
|
||||
import { invalidAzureAccount, invalidTenant, unableToFetchTokenError } from '../localizedConstants';
|
||||
import { AzureResourceServiceNames } from './constants';
|
||||
import { IAzureResourceSubscriptionFilterService, IAzureResourceSubscriptionService } from './interfaces';
|
||||
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
|
||||
import { StorageManagementClient } from '@azure/arm-storage';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -110,7 +106,7 @@ export function equals(one: any, other: any): boolean {
|
||||
export async function getResourceGroups(appContext: AppContext, account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false): Promise<GetResourceGroupsResult> {
|
||||
const result: GetResourceGroupsResult = { resourceGroups: [], errors: [] };
|
||||
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants) || !subscription) {
|
||||
const error = new Error(invalidAzureAccount);
|
||||
const error = new Error(localize('azure.accounts.getResourceGroups.invalidParamsError', "Invalid account or subscription"));
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
@@ -150,7 +146,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
|
||||
query: string): Promise<ResourceQueryResult<T>> {
|
||||
const result: ResourceQueryResult<T> = { resources: [], errors: [] };
|
||||
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||
const error = new Error(invalidAzureAccount);
|
||||
const error = new Error(localize('azure.accounts.runResourceQuery.errors.invalidAccount', "Invalid account"));
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
@@ -161,7 +157,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
|
||||
// Check our subscriptions to ensure we have valid ones
|
||||
subscriptions.forEach(subscription => {
|
||||
if (!subscription.tenant) {
|
||||
const error = new Error(invalidTenant);
|
||||
const error = new Error(localize('azure.accounts.runResourceQuery.errors.noTenantSpecifiedForSubscription', "Invalid tenant for subscription"));
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
@@ -192,7 +188,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
|
||||
resourceClient = new ResourceGraphClient(credential, { baseUri: account.properties.providerSettings.settings.armResource.endpoint });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = new Error(unableToFetchTokenError(tenant.id));
|
||||
const error = new Error(localize('azure.accounts.runResourceQuery.errors.unableToFetchToken', "Unable to get token for tenant {0}", tenant.id));
|
||||
result.errors.push(error);
|
||||
continue;
|
||||
}
|
||||
@@ -231,7 +227,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
|
||||
export async function getSubscriptions(appContext: AppContext, account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> {
|
||||
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
|
||||
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||
const error = new Error(invalidAzureAccount);
|
||||
const error = new Error(localize('azure.accounts.getSubscriptions.invalidParamsError', "Invalid account"));
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
@@ -265,7 +261,7 @@ export async function getSubscriptions(appContext: AppContext, account?: azdata.
|
||||
export async function getSelectedSubscriptions(appContext: AppContext, account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> {
|
||||
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
|
||||
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||
const error = new Error(invalidAzureAccount);
|
||||
const error = new Error(localize('azure.accounts.getSelectedSubscriptions.invalidParamsError', "Invalid account"));
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
@@ -288,189 +284,3 @@ export async function getSelectedSubscriptions(appContext: AppContext, account?:
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* makes a GET request to Azure REST apis. Currently, it only supports GET ARM queries.
|
||||
*/
|
||||
export async function makeHttpGetRequest(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false, url: string): Promise<HttpGetRequestResult> {
|
||||
const result: HttpGetRequestResult = { response: {}, errors: [] };
|
||||
|
||||
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||
const error = new Error(invalidAzureAccount);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
|
||||
if (!subscription.tenant) {
|
||||
const error = new Error(invalidTenant);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
if (result.errors.length > 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
let securityToken: { token: string, tokenType?: string };
|
||||
try {
|
||||
securityToken = await azdata.accounts.getAccountSecurityToken(
|
||||
account,
|
||||
subscription.tenant!,
|
||||
azdata.AzureResource.ResourceManagement
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = new Error(unableToFetchTokenError(subscription.tenant));
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
if (result.errors.length > 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const config: AxiosRequestConfig = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${securityToken.token}`
|
||||
},
|
||||
validateStatus: () => true // Never throw
|
||||
};
|
||||
|
||||
const response = await axios.get(url, config);
|
||||
|
||||
if (response.status !== 200) {
|
||||
let errorMessage: string[] = [];
|
||||
errorMessage.push(response.status.toString());
|
||||
errorMessage.push(response.statusText);
|
||||
if (response.data && response.data.error) {
|
||||
errorMessage.push(`${response.data.error.code} : ${response.data.error.message}`);
|
||||
}
|
||||
const error = new Error(errorMessage.join(EOL));
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
|
||||
result.response = response;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccounts: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetBlobContainersResult> {
|
||||
let result: GetBlobContainersResult = { blobContainer: undefined, errors: [] };
|
||||
|
||||
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||
const error = new Error(invalidAzureAccount);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
|
||||
if (!subscription.tenant) {
|
||||
const error = new Error(invalidTenant);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
if (result.errors.length > 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
let securityToken: { token: string, tokenType?: string };
|
||||
let credential: TokenCredentials;
|
||||
try {
|
||||
securityToken = await azdata.accounts.getAccountSecurityToken(
|
||||
account,
|
||||
subscription.tenant!,
|
||||
azdata.AzureResource.ResourceManagement
|
||||
);
|
||||
const token = securityToken.token;
|
||||
const tokenType = securityToken.tokenType;
|
||||
credential = new TokenCredentials(token, tokenType);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = new Error(unableToFetchTokenError(subscription.tenant));
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new StorageManagementClient(<any>credential, subscription.id);
|
||||
result.blobContainer = await client.blobContainers.list(storageAccounts.resourceGroup, storageAccounts.name);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (!ignoreErrors) {
|
||||
throw err;
|
||||
}
|
||||
result.errors.push(err);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getFileShares(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccounts: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetFileSharesResult> {
|
||||
let result: GetFileSharesResult = { fileShares: undefined, errors: [] };
|
||||
|
||||
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||
const error = new Error(invalidAzureAccount);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
|
||||
if (!subscription.tenant) {
|
||||
const error = new Error(invalidTenant);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
if (result.errors.length > 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
let securityToken: { token: string, tokenType?: string };
|
||||
let credential: TokenCredentials;
|
||||
try {
|
||||
securityToken = await azdata.accounts.getAccountSecurityToken(
|
||||
account,
|
||||
subscription.tenant!,
|
||||
azdata.AzureResource.ResourceManagement
|
||||
);
|
||||
const token = securityToken.token;
|
||||
const tokenType = securityToken.tokenType;
|
||||
credential = new TokenCredentials(token, tokenType);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = new Error(unableToFetchTokenError(subscription.tenant));
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new StorageManagementClient(<any>credential, subscription.id);
|
||||
result.fileShares = await client.fileShares.list(storageAccounts.resourceGroup, storageAccounts.name);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (!ignoreErrors) {
|
||||
throw err;
|
||||
}
|
||||
result.errors.push(err);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
20
extensions/azurecore/src/azurecore.d.ts
vendored
20
extensions/azurecore/src/azurecore.d.ts
vendored
@@ -6,8 +6,6 @@
|
||||
declare module 'azurecore' {
|
||||
import * as azdata from 'azdata';
|
||||
import { azureResource } from 'azureResource';
|
||||
import { BlobContainersListResponse, FileSharesListResponse } from '@azure/arm-storage/esm/models';
|
||||
|
||||
/**
|
||||
* Covers defining what the azurecore extension exports to other extensions
|
||||
*
|
||||
@@ -68,14 +66,8 @@ declare module 'azurecore' {
|
||||
}
|
||||
|
||||
export interface IExtension {
|
||||
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Promise<GetSubscriptionsResult>;
|
||||
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<GetResourceGroupsResult>;
|
||||
getSqlManagedInstances(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlManagedInstancesResult>;
|
||||
getSqlServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlServersResult>;
|
||||
getSqlVMServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlVMServersResult>;
|
||||
getStorageAccounts(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetStorageAccountResult>;
|
||||
getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise<GetBlobContainersResult>;
|
||||
getFileShares(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise<GetFileSharesResult>;
|
||||
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Thenable<GetSubscriptionsResult>;
|
||||
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<GetResourceGroupsResult>;
|
||||
/**
|
||||
* Converts a region value (@see AzureRegion) into the localized Display Name
|
||||
* @param region The region value
|
||||
@@ -84,18 +76,10 @@ declare module 'azurecore' {
|
||||
provideResources(): azureResource.IAzureResourceProvider[];
|
||||
|
||||
runGraphQuery<T extends azureResource.AzureGraphResource>(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors: boolean, query: string): Promise<ResourceQueryResult<T>>;
|
||||
makeHttpGetRequest(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, ignoreErrors: boolean, url: string): Promise<HttpGetRequestResult>;
|
||||
}
|
||||
|
||||
export type GetSubscriptionsResult = { subscriptions: azureResource.AzureResourceSubscription[], errors: Error[] };
|
||||
export type GetResourceGroupsResult = { resourceGroups: azureResource.AzureResourceResourceGroup[], errors: Error[] };
|
||||
export type GetSqlManagedInstancesResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };
|
||||
export type GetSqlServersResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
|
||||
export type GetSqlVMServersResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
|
||||
export type GetStorageAccountResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
|
||||
export type GetBlobContainersResult = {blobContainer: BlobContainersListResponse | undefined, errors: Error[]};
|
||||
export type GetFileSharesResult = {fileShares: FileSharesListResponse | undefined, errors: Error[]};
|
||||
|
||||
export type ResourceQueryResult<T extends azureResource.AzureGraphResource> = { resources: T[], errors: Error[] };
|
||||
export type HttpGetRequestResult = { response: any, errors: Error[] };
|
||||
}
|
||||
|
||||
@@ -141,12 +141,12 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
|
||||
});
|
||||
|
||||
return {
|
||||
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly: boolean = false): Promise<azurecore.GetSubscriptionsResult> {
|
||||
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly: boolean = false): Thenable<azurecore.GetSubscriptionsResult> {
|
||||
return selectedOnly
|
||||
? azureResourceUtils.getSelectedSubscriptions(appContext, account, ignoreErrors)
|
||||
: azureResourceUtils.getSubscriptions(appContext, account, ignoreErrors);
|
||||
},
|
||||
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<azurecore.GetResourceGroupsResult> { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); },
|
||||
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<azurecore.GetResourceGroupsResult> { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); },
|
||||
provideResources(): azureResource.IAzureResourceProvider[] {
|
||||
const arcFeaturedEnabled = vscode.workspace.getConfiguration(constants.extensionConfigSectionName).get('enableArcFeatures');
|
||||
const providers: azureResource.IAzureResourceProvider[] = [
|
||||
@@ -164,50 +164,12 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
|
||||
}
|
||||
return providers;
|
||||
},
|
||||
getSqlManagedInstances(account: azdata.Account,
|
||||
subscriptions: azureResource.AzureResourceSubscription[],
|
||||
ignoreErrors: boolean): Promise<azurecore.GetSqlManagedInstancesResult> {
|
||||
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.sqlManagedInstance}"`);
|
||||
},
|
||||
getSqlServers(account: azdata.Account,
|
||||
subscriptions: azureResource.AzureResourceSubscription[],
|
||||
ignoreErrors: boolean): Promise<azurecore.GetSqlServersResult> {
|
||||
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.sqlServer}"`);
|
||||
},
|
||||
getSqlVMServers(account: azdata.Account,
|
||||
subscriptions: azureResource.AzureResourceSubscription[],
|
||||
ignoreErrors: boolean): Promise<azurecore.GetSqlVMServersResult> {
|
||||
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.virtualMachines}" and properties.storageProfile.imageReference.publisher == "microsoftsqlserver"`);
|
||||
},
|
||||
getStorageAccounts(account: azdata.Account,
|
||||
subscriptions: azureResource.AzureResourceSubscription[],
|
||||
ignoreErrors: boolean): Promise<azurecore.GetStorageAccountResult> {
|
||||
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.storageAccount}"`);
|
||||
},
|
||||
getBlobContainers(account: azdata.Account,
|
||||
subscription: azureResource.AzureResourceSubscription,
|
||||
storageAccount: azureResource.AzureGraphResource,
|
||||
ignoreErrors: boolean): Promise<azurecore.GetBlobContainersResult> {
|
||||
return azureResourceUtils.getBlobContainers(account, subscription, storageAccount, ignoreErrors);
|
||||
},
|
||||
getFileShares(account: azdata.Account,
|
||||
subscription: azureResource.AzureResourceSubscription,
|
||||
storageAccount: azureResource.AzureGraphResource,
|
||||
ignoreErrors: boolean): Promise<azurecore.GetFileSharesResult> {
|
||||
return azureResourceUtils.getFileShares(account, subscription, storageAccount, ignoreErrors);
|
||||
},
|
||||
getRegionDisplayName: utils.getRegionDisplayName,
|
||||
runGraphQuery<T extends azureResource.AzureGraphResource>(account: azdata.Account,
|
||||
subscriptions: azureResource.AzureResourceSubscription[],
|
||||
ignoreErrors: boolean,
|
||||
query: string): Promise<azurecore.ResourceQueryResult<T>> {
|
||||
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, query);
|
||||
},
|
||||
makeHttpGetRequest(account: azdata.Account,
|
||||
subscription: azureResource.AzureResourceSubscription,
|
||||
ignoreErrors: boolean,
|
||||
url: string) {
|
||||
return azureResourceUtils.makeHttpGetRequest(account, subscription, ignoreErrors, url);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,10 +74,3 @@ export const azureArcPostgresServer = localize('azurecore.azureArcPostgres', "Az
|
||||
|
||||
export const unableToOpenAzureLink = localize('azure.unableToOpenAzureLink', "Unable to open link, missing required values");
|
||||
export const azureResourcesGridTitle = localize('azure.azureResourcesGridTitle', "Azure Resources (Preview)");
|
||||
|
||||
// Azure Request Errors
|
||||
export const invalidAzureAccount = localize('azurecore.invalidAzureAccount', "Invalid account");
|
||||
export const invalidTenant = localize('azurecore.invalidTenant', "Invalid tenant for subscription");
|
||||
export function unableToFetchTokenError(tenant: string): string {
|
||||
return localize('azurecore.unableToFetchToken', "Unable to get token for tenant {0}", tenant);
|
||||
}
|
||||
|
||||
@@ -11,15 +11,6 @@
|
||||
"@azure/ms-rest-js" "^1.8.1"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@azure/arm-storage@^15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@azure/arm-storage/-/arm-storage-15.1.0.tgz#fa14b5e532babf39b47c5cffe89de5aa062e1f80"
|
||||
integrity sha512-IWomHlT7eEnCSMDHH/z5/XyPHhGAIPmWYgHkIyYB2YQt+Af+hWvE1NIwI79Eeiu+Am4U8BKUsXWmWKqDYh0Srg==
|
||||
dependencies:
|
||||
"@azure/ms-rest-azure-js" "^2.0.1"
|
||||
"@azure/ms-rest-js" "^2.0.4"
|
||||
tslib "^1.10.0"
|
||||
|
||||
"@azure/arm-subscriptions@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@azure/arm-subscriptions/-/arm-subscriptions-1.0.0.tgz#ab65a5cd4d8b8c878ff6621428f29137b84eb1d6"
|
||||
@@ -37,14 +28,6 @@
|
||||
"@azure/ms-rest-js" "^1.8.10"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@azure/ms-rest-azure-js@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@azure/ms-rest-azure-js/-/ms-rest-azure-js-2.0.1.tgz#fa1b38f039b3ee48a9e086a88c8a5b5b7776491c"
|
||||
integrity sha512-5e+A710O7gRFISoV4KI/ZyLQbKmjXxQZ1L8Z/sx7jSUQqmswjTnN4yyIZxs5JzfLVkobU0rXxbi5/LVzaI8QXQ==
|
||||
dependencies:
|
||||
"@azure/ms-rest-js" "^2.0.4"
|
||||
tslib "^1.10.0"
|
||||
|
||||
"@azure/ms-rest-js@^1.1.0", "@azure/ms-rest-js@^1.8.1", "@azure/ms-rest-js@^1.8.10":
|
||||
version "1.8.14"
|
||||
resolved "https://registry.yarnpkg.com/@azure/ms-rest-js/-/ms-rest-js-1.8.14.tgz#657fc145db20b6eb3d58d1a2055473aa72eb609d"
|
||||
@@ -59,22 +42,6 @@
|
||||
uuid "^3.2.1"
|
||||
xml2js "^0.4.19"
|
||||
|
||||
"@azure/ms-rest-js@^2.0.4":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@azure/ms-rest-js/-/ms-rest-js-2.1.0.tgz#41bc541984983b5242dfbcf699ea281acd045946"
|
||||
integrity sha512-4BXLVImYRt+jcUmEJ5LUWglI8RBNVQndY6IcyvQ4U8O4kIXdmlRz3cJdA/RpXf5rKT38KOoTO2T6Z1f6Z1HDBg==
|
||||
dependencies:
|
||||
"@types/node-fetch" "^2.3.7"
|
||||
"@types/tunnel" "0.0.1"
|
||||
abort-controller "^3.0.0"
|
||||
form-data "^2.5.0"
|
||||
node-fetch "^2.6.0"
|
||||
tough-cookie "^3.0.1"
|
||||
tslib "^1.10.0"
|
||||
tunnel "0.0.6"
|
||||
uuid "^3.3.2"
|
||||
xml2js "^0.4.19"
|
||||
|
||||
"@babel/code-frame@^7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
|
||||
@@ -308,14 +275,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea"
|
||||
integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==
|
||||
|
||||
"@types/node-fetch@^2.3.7":
|
||||
version "2.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c"
|
||||
integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
form-data "^3.0.0"
|
||||
|
||||
"@types/node@*":
|
||||
version "13.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.5.tgz#59738bf30b31aea1faa2df7f4a5f55613750cf00"
|
||||
@@ -365,13 +324,6 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/tunnel@0.0.1":
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/tunnel/-/tunnel-0.0.1.tgz#0d72774768b73df26f25df9184273a42da72b19c"
|
||||
integrity sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/ws@^6.0.4":
|
||||
version "6.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1"
|
||||
@@ -379,13 +331,6 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
abort-controller@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
|
||||
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
|
||||
dependencies:
|
||||
event-target-shim "^5.0.0"
|
||||
|
||||
ansi-regex@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
|
||||
@@ -521,7 +466,7 @@ color-name@1.1.3:
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||
|
||||
combined-stream@^1.0.6, combined-stream@^1.0.8:
|
||||
combined-stream@^1.0.6:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
@@ -651,11 +596,6 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
|
||||
|
||||
event-target-shim@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
||||
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
|
||||
|
||||
expand-template@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
|
||||
@@ -677,15 +617,6 @@ form-data@^2.3.2, form-data@^2.5.0:
|
||||
combined-stream "^1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
|
||||
integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fs-constants@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
@@ -802,11 +733,6 @@ ini@~1.3.0:
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
||||
|
||||
ip-regex@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
|
||||
integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
|
||||
|
||||
is-buffer@~1.1.1:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
@@ -1083,11 +1009,6 @@ node-abi@^2.7.0:
|
||||
dependencies:
|
||||
semver "^5.4.1"
|
||||
|
||||
node-fetch@^2.6.0:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
|
||||
noop-logger@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2"
|
||||
@@ -1468,20 +1389,6 @@ tough-cookie@^2.4.3:
|
||||
psl "^1.1.28"
|
||||
punycode "^2.1.1"
|
||||
|
||||
tough-cookie@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
|
||||
integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
|
||||
dependencies:
|
||||
ip-regex "^2.1.0"
|
||||
psl "^1.1.28"
|
||||
punycode "^2.1.1"
|
||||
|
||||
tslib@^1.10.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^1.9.2, tslib@^1.9.3:
|
||||
version "1.11.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
|
||||
@@ -1518,7 +1425,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
||||
uuid@^3.2.1, uuid@^3.3.2:
|
||||
uuid@^3.2.1:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ads-extension-telemetry": "^1.0.0",
|
||||
"htmlparser2": "^3.10.1",
|
||||
"vscode-nls": "^4.0.0"
|
||||
},
|
||||
|
||||
18
extensions/dacpac/src/telemetry.ts
Normal file
18
extensions/dacpac/src/telemetry.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import AdsTelemetryReporter from 'ads-extension-telemetry';
|
||||
|
||||
import * as Utils from './utils';
|
||||
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
let packageInfo = Utils.getPackageInfo(packageJson);
|
||||
|
||||
export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
|
||||
|
||||
export enum TelemetryViews {
|
||||
SelectOperationPage = 'SelectOperationPage'
|
||||
}
|
||||
35
extensions/dacpac/src/utils.ts
Normal file
35
extensions/dacpac/src/utils.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface IPackageInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
aiKey: string;
|
||||
}
|
||||
|
||||
export function getPackageInfo(packageJson: any): IPackageInfo | undefined {
|
||||
if (packageJson) {
|
||||
return {
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
aiKey: packageJson.aiKey
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an error message into a short name for the type of error.
|
||||
* @param msg The error message to map
|
||||
*/
|
||||
export function getTelemetryErrorType(msg: string): string {
|
||||
if (msg && msg.indexOf('Object reference not set to an instance of an object') !== -1) {
|
||||
return 'ObjectReferenceNotSet';
|
||||
}
|
||||
else {
|
||||
return 'Other';
|
||||
}
|
||||
}
|
||||
@@ -240,6 +240,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e"
|
||||
integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==
|
||||
|
||||
ads-extension-telemetry@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ads-extension-telemetry/-/ads-extension-telemetry-1.0.0.tgz#840b363a6ad958447819b9bc59fdad3e49de31a9"
|
||||
integrity sha512-ouxZVECe4tsO0ek0dLdnAZEz1Lrytv1uLbbGZhRbZsHITsUYNjnkKnA471uWh0Dj80s+orvv49/j3/tNBDP/SQ==
|
||||
dependencies:
|
||||
vscode-extension-telemetry "0.1.2"
|
||||
|
||||
ansi-regex@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
|
||||
@@ -259,6 +266,31 @@ append-transform@^2.0.0:
|
||||
dependencies:
|
||||
default-require-extensions "^3.0.0"
|
||||
|
||||
applicationinsights@1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.4.0.tgz#e17e436427b6e273291055181e29832cca978644"
|
||||
integrity sha512-TV8MYb0Kw9uE2cdu4V/UvTKdOABkX2+Fga9iDz0zqV7FLrNXfmAugWZmmdTx4JoynYkln3d5CUHY3oVSUEbfFw==
|
||||
dependencies:
|
||||
cls-hooked "^4.2.2"
|
||||
continuation-local-storage "^3.2.1"
|
||||
diagnostic-channel "0.2.0"
|
||||
diagnostic-channel-publishers "^0.3.2"
|
||||
|
||||
async-hook-jl@^1.7.6:
|
||||
version "1.7.6"
|
||||
resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68"
|
||||
integrity sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==
|
||||
dependencies:
|
||||
stack-chain "^1.3.7"
|
||||
|
||||
async-listener@^0.6.0:
|
||||
version "0.6.10"
|
||||
resolved "https://registry.yarnpkg.com/async-listener/-/async-listener-0.6.10.tgz#a7c97abe570ba602d782273c0de60a51e3e17cbc"
|
||||
integrity sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==
|
||||
dependencies:
|
||||
semver "^5.3.0"
|
||||
shimmer "^1.1.0"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
@@ -301,6 +333,15 @@ circular-json@^0.3.1:
|
||||
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
|
||||
integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==
|
||||
|
||||
cls-hooked@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908"
|
||||
integrity sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==
|
||||
dependencies:
|
||||
async-hook-jl "^1.7.6"
|
||||
emitter-listener "^1.0.1"
|
||||
semver "^5.4.1"
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
@@ -323,6 +364,14 @@ concat-map@0.0.1:
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
|
||||
continuation-local-storage@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz#11f613f74e914fe9b34c92ad2d28fe6ae1db7ffb"
|
||||
integrity sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==
|
||||
dependencies:
|
||||
async-listener "^0.6.0"
|
||||
emitter-listener "^1.1.1"
|
||||
|
||||
convert-source-map@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
|
||||
@@ -377,6 +426,18 @@ default-require-extensions@^3.0.0:
|
||||
dependencies:
|
||||
strip-bom "^4.0.0"
|
||||
|
||||
diagnostic-channel-publishers@^0.3.2:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.3.5.tgz#a84a05fd6cc1d7619fdd17791c17e540119a7536"
|
||||
integrity sha512-AOIjw4T7Nxl0G2BoBPhkQ6i7T4bUd9+xvdYizwvG7vVAM1dvr+SDrcUudlmzwH0kbEwdR2V1EcnKT0wAeYLQNQ==
|
||||
|
||||
diagnostic-channel@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17"
|
||||
integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=
|
||||
dependencies:
|
||||
semver "^5.3.0"
|
||||
|
||||
diff@3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
|
||||
@@ -415,6 +476,13 @@ domutils@^1.5.1:
|
||||
dom-serializer "0"
|
||||
domelementtype "1"
|
||||
|
||||
emitter-listener@^1.0.1, emitter-listener@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8"
|
||||
integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==
|
||||
dependencies:
|
||||
shimmer "^1.2.0"
|
||||
|
||||
entities@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
|
||||
@@ -793,7 +861,7 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
semver@^5.4.1, semver@^5.6.0:
|
||||
semver@^5.3.0, semver@^5.4.1, semver@^5.6.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
@@ -803,6 +871,11 @@ semver@^6.0.0, semver@^6.3.0:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
shimmer@^1.1.0, shimmer@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
|
||||
integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
|
||||
|
||||
should-equal@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3"
|
||||
@@ -870,6 +943,11 @@ source-map@^0.6.1:
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
stack-chain@^1.3.7:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
|
||||
integrity sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
|
||||
@@ -934,6 +1012,13 @@ util-deprecate@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
||||
vscode-extension-telemetry@0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.2.tgz#049207f5453930888ff68ca925b07bab08f2c955"
|
||||
integrity sha512-FSbaZKlIH3VKvBJsKw7v5bESWHXzltji2rtjaJeJglpQH4tfClzwHMzlMXUZGiblV++djEzb1gW8mb5E+wxFsg==
|
||||
dependencies:
|
||||
applicationinsights "1.4.0"
|
||||
|
||||
vscode-nls@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c"
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"ads-extension-telemetry": "^1.0.0",
|
||||
"vscode-nls": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -146,9 +147,9 @@
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-junit-reporter": "^1.17.0",
|
||||
"mocha-multi-reporters": "^1.1.7",
|
||||
"typemoq": "^2.1.0",
|
||||
"vscodetestcover": "^1.1.0",
|
||||
"should": "^13.2.3",
|
||||
"sinon": "^9.0.2"
|
||||
"sinon": "^9.0.2",
|
||||
"typemoq": "^2.1.0",
|
||||
"vscodetestcover": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
17
extensions/data-workspace/src/common/telemetry.ts
Normal file
17
extensions/data-workspace/src/common/telemetry.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import AdsTelemetryReporter from 'ads-extension-telemetry';
|
||||
|
||||
import * as Utils from './utils';
|
||||
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
let packageInfo = Utils.getPackageInfo(packageJson)!;
|
||||
|
||||
export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
|
||||
|
||||
export enum TelemetryViews {
|
||||
}
|
||||
@@ -29,3 +29,21 @@ async function getFileStatus(path: string): Promise<fs.Stats | undefined> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IPackageInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
aiKey: string;
|
||||
}
|
||||
|
||||
export function getPackageInfo(packageJson: any): IPackageInfo | undefined {
|
||||
if (packageJson) {
|
||||
return {
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
aiKey: packageJson.aiKey
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -235,6 +235,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e"
|
||||
integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==
|
||||
|
||||
ads-extension-telemetry@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ads-extension-telemetry/-/ads-extension-telemetry-1.0.0.tgz#840b363a6ad958447819b9bc59fdad3e49de31a9"
|
||||
integrity sha512-ouxZVECe4tsO0ek0dLdnAZEz1Lrytv1uLbbGZhRbZsHITsUYNjnkKnA471uWh0Dj80s+orvv49/j3/tNBDP/SQ==
|
||||
dependencies:
|
||||
vscode-extension-telemetry "0.1.2"
|
||||
|
||||
ansi-regex@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
|
||||
@@ -254,6 +261,31 @@ append-transform@^2.0.0:
|
||||
dependencies:
|
||||
default-require-extensions "^3.0.0"
|
||||
|
||||
applicationinsights@1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.4.0.tgz#e17e436427b6e273291055181e29832cca978644"
|
||||
integrity sha512-TV8MYb0Kw9uE2cdu4V/UvTKdOABkX2+Fga9iDz0zqV7FLrNXfmAugWZmmdTx4JoynYkln3d5CUHY3oVSUEbfFw==
|
||||
dependencies:
|
||||
cls-hooked "^4.2.2"
|
||||
continuation-local-storage "^3.2.1"
|
||||
diagnostic-channel "0.2.0"
|
||||
diagnostic-channel-publishers "^0.3.2"
|
||||
|
||||
async-hook-jl@^1.7.6:
|
||||
version "1.7.6"
|
||||
resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68"
|
||||
integrity sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==
|
||||
dependencies:
|
||||
stack-chain "^1.3.7"
|
||||
|
||||
async-listener@^0.6.0:
|
||||
version "0.6.10"
|
||||
resolved "https://registry.yarnpkg.com/async-listener/-/async-listener-0.6.10.tgz#a7c97abe570ba602d782273c0de60a51e3e17cbc"
|
||||
integrity sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==
|
||||
dependencies:
|
||||
semver "^5.3.0"
|
||||
shimmer "^1.1.0"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
@@ -296,6 +328,15 @@ circular-json@^0.3.1:
|
||||
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
|
||||
integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==
|
||||
|
||||
cls-hooked@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908"
|
||||
integrity sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==
|
||||
dependencies:
|
||||
async-hook-jl "^1.7.6"
|
||||
emitter-listener "^1.0.1"
|
||||
semver "^5.4.1"
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
@@ -318,6 +359,14 @@ concat-map@0.0.1:
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
|
||||
continuation-local-storage@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz#11f613f74e914fe9b34c92ad2d28fe6ae1db7ffb"
|
||||
integrity sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==
|
||||
dependencies:
|
||||
async-listener "^0.6.0"
|
||||
emitter-listener "^1.1.1"
|
||||
|
||||
convert-source-map@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
|
||||
@@ -372,6 +421,18 @@ default-require-extensions@^3.0.0:
|
||||
dependencies:
|
||||
strip-bom "^4.0.0"
|
||||
|
||||
diagnostic-channel-publishers@^0.3.2:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.3.5.tgz#a84a05fd6cc1d7619fdd17791c17e540119a7536"
|
||||
integrity sha512-AOIjw4T7Nxl0G2BoBPhkQ6i7T4bUd9+xvdYizwvG7vVAM1dvr+SDrcUudlmzwH0kbEwdR2V1EcnKT0wAeYLQNQ==
|
||||
|
||||
diagnostic-channel@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17"
|
||||
integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=
|
||||
dependencies:
|
||||
semver "^5.3.0"
|
||||
|
||||
diff@3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
|
||||
@@ -382,6 +443,13 @@ diff@^4.0.2:
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
emitter-listener@^1.0.1, emitter-listener@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8"
|
||||
integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==
|
||||
dependencies:
|
||||
shimmer "^1.2.0"
|
||||
|
||||
escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
@@ -734,7 +802,7 @@ safe-buffer@~5.1.1:
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
semver@^5.4.1, semver@^5.6.0:
|
||||
semver@^5.3.0, semver@^5.4.1, semver@^5.6.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
@@ -744,6 +812,11 @@ semver@^6.0.0, semver@^6.3.0:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
shimmer@^1.1.0, shimmer@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
|
||||
integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
|
||||
|
||||
should-equal@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3"
|
||||
@@ -811,6 +884,11 @@ source-map@^0.6.1:
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
stack-chain@^1.3.7:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
|
||||
integrity sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=
|
||||
|
||||
strip-ansi@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
|
||||
@@ -863,6 +941,13 @@ typemoq@^2.1.0:
|
||||
lodash "^4.17.4"
|
||||
postinstall-build "^5.0.1"
|
||||
|
||||
vscode-extension-telemetry@0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.2.tgz#049207f5453930888ff68ca925b07bab08f2c955"
|
||||
integrity sha512-FSbaZKlIH3VKvBJsKw7v5bESWHXzltji2rtjaJeJglpQH4tfClzwHMzlMXUZGiblV++djEzb1gW8mb5E+wxFsg==
|
||||
dependencies:
|
||||
applicationinsights "1.4.0"
|
||||
|
||||
vscode-nls@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c"
|
||||
|
||||
@@ -8,34 +8,13 @@ import * as azurecore from 'azurecore';
|
||||
import { azureResource } from 'azureResource';
|
||||
|
||||
export class AzurecoreApiStub implements azurecore.IExtension {
|
||||
getFileShares(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _storageAccount: azureResource.AzureGraphResource, _ignoreErrors?: boolean): Promise<azurecore.GetFileSharesResult> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getBlobContainers(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _storageAccount: azureResource.AzureGraphResource, _ignoreErrors?: boolean): Promise<azurecore.GetBlobContainersResult> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getSqlManagedInstances(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors?: boolean): Promise<azurecore.GetSqlManagedInstancesResult> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getSqlServers(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors?: boolean): Promise<azurecore.GetSqlServersResult> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getSqlVMServers(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors?: boolean): Promise<azurecore.GetSqlVMServersResult> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getStorageAccounts(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors?: boolean): Promise<azurecore.GetStorageAccountResult> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
makeHttpGetRequest(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _ignoreErrors: boolean, _url: string): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
runGraphQuery<T extends azureResource.AzureGraphResource>(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors: boolean, _query: string): Promise<azurecore.ResourceQueryResult<T>> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getSubscriptions(_account?: azdata.Account | undefined, _ignoreErrors?: boolean | undefined, _selectedOnly?: boolean | undefined): Promise<azurecore.GetSubscriptionsResult> {
|
||||
getSubscriptions(_account?: azdata.Account | undefined, _ignoreErrors?: boolean | undefined, _selectedOnly?: boolean | undefined): Thenable<azurecore.GetSubscriptionsResult> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getResourceGroups(_account?: azdata.Account | undefined, _subscription?: azureResource.AzureResourceSubscription | undefined, _ignoreErrors?: boolean | undefined): Promise<azurecore.GetResourceGroupsResult> {
|
||||
getResourceGroups(_account?: azdata.Account | undefined, _subscription?: azureResource.AzureResourceSubscription | undefined, _ignoreErrors?: boolean | undefined): Thenable<azurecore.GetResourceGroupsResult> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getRegionDisplayName(_region?: string | undefined): string {
|
||||
|
||||
@@ -86,7 +86,7 @@ export function createViewContext(): ViewTestContext {
|
||||
withProps: () => checkBoxBuilder,
|
||||
withValidation: () => checkBoxBuilder
|
||||
};
|
||||
let inputBox: () => azdata.InputBoxComponent = () => Object.assign({}, componentBase, {
|
||||
let inputBox: () => azdata.InputBoxComponent = () => Object.assign(<azdata.InputBoxComponent>Object.assign({}, componentBase), {
|
||||
onTextChanged: onClick.event!,
|
||||
onEnterKeyPressed: undefined!,
|
||||
value: ''
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
|
||||
"version": "3.0.0-release.61",
|
||||
"version": "3.0.0-release.64",
|
||||
"downloadFileNames": {
|
||||
"Windows_86": "win-x86-netcoreapp3.1.zip",
|
||||
"Windows_64": "win-x64-netcoreapp3.1.zip",
|
||||
|
||||
@@ -244,6 +244,8 @@ export type ComponentCSSStyles = {
|
||||
|
||||
export interface IOptionsSource {
|
||||
provider?: IOptionsSourceProvider
|
||||
loadingText?: string,
|
||||
loadingCompletedText?: string,
|
||||
readonly variableNames?: { [index: string]: string; };
|
||||
readonly providerId: string;
|
||||
}
|
||||
|
||||
@@ -614,7 +614,6 @@ async function processOptionsTypeField(context: FieldContext): Promise<void> {
|
||||
if (context.fieldInfo.options.source?.providerId) {
|
||||
try {
|
||||
context.fieldInfo.options.source.provider = optionsSourcesService.getOptionsSource(context.fieldInfo.options.source.providerId);
|
||||
context.fieldInfo.options.values = await context.fieldInfo.options.source.provider.getOptions();
|
||||
}
|
||||
catch (e) {
|
||||
disableControlButtons(context.container);
|
||||
@@ -628,16 +627,25 @@ async function processOptionsTypeField(context: FieldContext): Promise<void> {
|
||||
context.fieldInfo.subFields = context.fieldInfo.subFields || [];
|
||||
}
|
||||
let optionsComponent: RadioGroupLoadingComponentBuilder | azdata.DropDownComponent;
|
||||
const options = context.fieldInfo.options;
|
||||
const optionsSource = options.source;
|
||||
if (context.fieldInfo.options.optionsType === OptionsType.Radio) {
|
||||
optionsComponent = await processRadioOptionsTypeField(context);
|
||||
let getRadioOptions: (() => Promise<OptionsInfo>) | undefined = undefined;
|
||||
// If the options are provided for us then set up the callback to load those options async'ly
|
||||
if (optionsSource?.provider) {
|
||||
getRadioOptions = async () => {
|
||||
return { defaultValue: options.defaultValue, values: await optionsSource.provider!.getOptions() };
|
||||
};
|
||||
}
|
||||
optionsComponent = await processRadioOptionsTypeField(context, getRadioOptions);
|
||||
} else {
|
||||
throwUnless(context.fieldInfo.options.optionsType === OptionsType.Dropdown, loc.optionsTypeRadioOrDropdown);
|
||||
optionsComponent = processDropdownOptionsTypeField(context);
|
||||
}
|
||||
const optionsSource = context.fieldInfo.options.source;
|
||||
|
||||
if (optionsSource?.provider) {
|
||||
const optionsSourceProvider = optionsSource.provider;
|
||||
await Promise.all(Object.keys(context.fieldInfo.options.source?.variableNames ?? {}).map(async key => {
|
||||
await Promise.all(Object.keys(optionsSource?.variableNames ?? {}).map(async key => {
|
||||
await configureOptionsSourceSubfields(context, optionsSource, key, optionsComponent, optionsSourceProvider);
|
||||
}));
|
||||
}
|
||||
@@ -966,8 +974,8 @@ async function processKubeConfigClusterPickerField(context: KubeClusterContextFi
|
||||
|
||||
}
|
||||
|
||||
async function processRadioOptionsTypeField(context: FieldContext): Promise<RadioGroupLoadingComponentBuilder> {
|
||||
return await createRadioOptions(context);
|
||||
async function processRadioOptionsTypeField(context: FieldContext, getRadioButtonInfo?: () => Promise<OptionsInfo>): Promise<RadioGroupLoadingComponentBuilder> {
|
||||
return await createRadioOptions(context, getRadioButtonInfo);
|
||||
}
|
||||
|
||||
|
||||
@@ -978,6 +986,7 @@ async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: ((
|
||||
}
|
||||
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
|
||||
const radioGroupLoadingComponentBuilder = new RadioGroupLoadingComponentBuilder(context.view, context.onNewDisposableCreated, context.fieldInfo);
|
||||
|
||||
context.fieldInfo.labelPosition = LabelPosition.Left;
|
||||
context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, {
|
||||
component: radioGroupLoadingComponentBuilder,
|
||||
@@ -987,10 +996,21 @@ async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: ((
|
||||
getDisplayValue: async (): Promise<string> => radioGroupLoadingComponentBuilder.displayValue,
|
||||
onValueChanged: radioGroupLoadingComponentBuilder.onValueChanged,
|
||||
});
|
||||
addLabelInputPairToContainer(context.view, context.components, label, radioGroupLoadingComponentBuilder.component(), context.fieldInfo);
|
||||
const options = context.fieldInfo.options as OptionsInfo;
|
||||
await radioGroupLoadingComponentBuilder.loadOptions(
|
||||
getRadioButtonInfo || options); // wait for the radioGroup to be fully initialized
|
||||
let loadingText = options?.source?.loadingText;
|
||||
let loadingCompletedText = options?.source?.loadingCompletedText;
|
||||
if (loadingText || loadingCompletedText) {
|
||||
radioGroupLoadingComponentBuilder.withProps({
|
||||
showText: true,
|
||||
loadingText: loadingText,
|
||||
loadingCompletedText: loadingCompletedText
|
||||
});
|
||||
}
|
||||
addLabelInputPairToContainer(context.view, context.components, label, radioGroupLoadingComponentBuilder.component(), context.fieldInfo);
|
||||
// Start loading the options but continue on so that we can continue setting up the rest of the components - the group
|
||||
// will show a loading spinner while the options are loaded
|
||||
radioGroupLoadingComponentBuilder.loadOptions(
|
||||
getRadioButtonInfo || options).catch(e => console.log('Error loading options for radio group ', e));
|
||||
return radioGroupLoadingComponentBuilder;
|
||||
}
|
||||
|
||||
|
||||
@@ -365,6 +365,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/xml-formatter": "^1.1.0",
|
||||
"ads-extension-telemetry": "^1.0.0",
|
||||
"fast-glob": "^3.1.0",
|
||||
"promisify-child-process": "^3.1.1",
|
||||
"vscode-languageclient": "^5.3.0-next.1",
|
||||
|
||||
17
extensions/sql-database-projects/src/common/telemetry.ts
Normal file
17
extensions/sql-database-projects/src/common/telemetry.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import AdsTelemetryReporter from 'ads-extension-telemetry';
|
||||
|
||||
import * as Utils from './utils';
|
||||
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
let packageInfo = Utils.getPackageInfo(packageJson)!;
|
||||
|
||||
export const TelemetryReporter = new AdsTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
|
||||
|
||||
export enum TelemetryViews {
|
||||
}
|
||||
@@ -253,3 +253,21 @@ export async function GetDefaultDeploymentOptions(): Promise<mssql.DeploymentOpt
|
||||
|
||||
return result.defaultDeploymentOptions;
|
||||
}
|
||||
|
||||
export interface IPackageInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
aiKey: string;
|
||||
}
|
||||
|
||||
export function getPackageInfo(packageJson: any): IPackageInfo | undefined {
|
||||
if (packageJson) {
|
||||
return {
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
aiKey: packageJson.aiKey
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -179,12 +179,11 @@ export class PublishDatabaseDialog {
|
||||
}
|
||||
|
||||
public async publishClick(): Promise<void> {
|
||||
const sqlCmdVars = this.getSqlCmdVariablesForPublish();
|
||||
const settings: IPublishSettings = {
|
||||
databaseName: this.getTargetDatabaseName(),
|
||||
upgradeExisting: true,
|
||||
connectionUri: await this.getConnectionUri(),
|
||||
sqlCmdVariables: sqlCmdVars,
|
||||
sqlCmdVariables: this.getSqlCmdVariablesForPublish(),
|
||||
deploymentOptions: await this.getDeploymentOptions()
|
||||
};
|
||||
|
||||
@@ -212,7 +211,7 @@ export class PublishDatabaseDialog {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
private async getDeploymentOptions(): Promise<DeploymentOptions> {
|
||||
public async getDeploymentOptions(): Promise<DeploymentOptions> {
|
||||
// eventually, database options will be configurable in this dialog
|
||||
// but for now, just send the default DacFx deployment options if no options were loaded from a publish profile
|
||||
if (!this.deploymentOptions) {
|
||||
@@ -230,7 +229,7 @@ export class PublishDatabaseDialog {
|
||||
return this.deploymentOptions;
|
||||
}
|
||||
|
||||
private getSqlCmdVariablesForPublish(): Record<string, string> {
|
||||
public getSqlCmdVariablesForPublish(): Record<string, string> {
|
||||
// get SQLCMD variables from table
|
||||
let sqlCmdVariables = { ...this.sqlCmdVars };
|
||||
return sqlCmdVariables;
|
||||
|
||||
@@ -17,8 +17,9 @@ import { Project } from '../../models/project';
|
||||
import { ProjectsController } from '../../controllers/projectController';
|
||||
import { IPublishSettings, IGenerateScriptSettings } from '../../models/IPublishSettings';
|
||||
import { emptySqlDatabaseProjectTypeId } from '../../common/constants';
|
||||
import { mockDacFxOptionsResult } from '../testContext';
|
||||
|
||||
describe.skip('Publish Database Dialog', () => {
|
||||
describe('Publish Database Dialog', () => {
|
||||
before(async function (): Promise<void> {
|
||||
await templates.loadTemplates(path.join(__dirname, '..', '..', '..', 'resources', 'templates'));
|
||||
await baselines.loadBaselines();
|
||||
@@ -64,6 +65,8 @@ describe.skip('Publish Database Dialog', () => {
|
||||
const dialog = TypeMoq.Mock.ofType(PublishDatabaseDialog, undefined, undefined, proj);
|
||||
dialog.setup(x => x.getConnectionUri()).returns(() => { return Promise.resolve('Mock|Connection|Uri'); });
|
||||
dialog.setup(x => x.getTargetDatabaseName()).returns(() => 'MockDatabaseName');
|
||||
dialog.setup(x => x.getSqlCmdVariablesForPublish()).returns(() => proj.sqlCmdVariables);
|
||||
dialog.setup(x => x.getDeploymentOptions()).returns(() => { return Promise.resolve(mockDacFxOptionsResult.deploymentOptions); });
|
||||
dialog.callBase = true;
|
||||
|
||||
let profile: IPublishSettings | IGenerateScriptSettings | undefined;
|
||||
@@ -75,7 +78,8 @@ describe.skip('Publish Database Dialog', () => {
|
||||
sqlCmdVariables: {
|
||||
'ProdDatabaseName': 'MyProdDatabase',
|
||||
'BackupDatabaseName': 'MyBackupDatabase'
|
||||
}
|
||||
},
|
||||
deploymentOptions: mockDacFxOptionsResult.deploymentOptions
|
||||
};
|
||||
|
||||
dialog.object.publish = (_, prof) => { profile = prof; };
|
||||
@@ -89,7 +93,8 @@ describe.skip('Publish Database Dialog', () => {
|
||||
sqlCmdVariables: {
|
||||
'ProdDatabaseName': 'MyProdDatabase',
|
||||
'BackupDatabaseName': 'MyBackupDatabase'
|
||||
}
|
||||
},
|
||||
deploymentOptions: mockDacFxOptionsResult.deploymentOptions
|
||||
};
|
||||
|
||||
dialog.object.generateScript = (_, prof) => { profile = prof; };
|
||||
|
||||
@@ -8,20 +8,25 @@ import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
import * as sinon from 'sinon';
|
||||
import { NetCoreTool, DBProjectConfigurationKey, NetCoreInstallLocationKey, NextCoreNonWindowsDefaultPath } from '../tools/netcoreTool';
|
||||
import { getQuotedPath } from '../common/utils';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
import { generateTestFolderPath } from './testUtils';
|
||||
|
||||
describe.skip('NetCoreTool: Net core tests', function (): void {
|
||||
describe('NetCoreTool: Net core tests', function (): void {
|
||||
afterEach(function (): void {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('Should override dotnet default value with settings', async function (): Promise<void> {
|
||||
try {
|
||||
// update settings and validate
|
||||
await vscode.workspace.getConfiguration(DBProjectConfigurationKey).update(NetCoreInstallLocationKey, 'test value path', true);
|
||||
const netcoreTool = new NetCoreTool();
|
||||
sinon.stub(netcoreTool, 'showInstallDialog').returns(Promise.resolve());
|
||||
should(netcoreTool.netcoreInstallLocation).equal('test value path'); // the path in settings should be taken
|
||||
should(netcoreTool.findOrInstallNetCore()).equal(false); // dotnet can not be present at dummy path in settings
|
||||
should(await netcoreTool.findOrInstallNetCore()).equal(false); // dotnet can not be present at dummy path in settings
|
||||
}
|
||||
finally {
|
||||
// clean again
|
||||
@@ -29,9 +34,10 @@ describe.skip('NetCoreTool: Net core tests', function (): void {
|
||||
}
|
||||
});
|
||||
|
||||
it('Should find right dotnet default paths', function (): void {
|
||||
it('Should find right dotnet default paths', async function (): Promise<void> {
|
||||
const netcoreTool = new NetCoreTool();
|
||||
netcoreTool.findOrInstallNetCore();
|
||||
sinon.stub(netcoreTool, 'showInstallDialog').returns(Promise.resolve());
|
||||
await netcoreTool.findOrInstallNetCore();
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
// check that path should start with c:\program files
|
||||
|
||||
@@ -13,7 +13,7 @@ import { FolderNode, FileNode, sortFileFolderNodes } from '../models/tree/fileFo
|
||||
import { ProjectRootTreeItem } from '../models/tree/projectTreeItem';
|
||||
import { DatabaseProjectItemType } from '../common/constants';
|
||||
|
||||
describe.skip('Project Tree tests', function (): void {
|
||||
describe('Project Tree tests', function (): void {
|
||||
it('Should correctly order tree nodes by type, then by name', function (): void {
|
||||
const root = os.platform() === 'win32' ? 'Z:\\' : '/';
|
||||
|
||||
@@ -68,26 +68,24 @@ describe.skip('Project Tree tests', function (): void {
|
||||
|
||||
const tree = new ProjectRootTreeItem(proj);
|
||||
should(tree.children.map(x => x.uri.path)).deepEqual([
|
||||
'/TestProj.sqlproj/Data Sources',
|
||||
'/TestProj.sqlproj/Database References',
|
||||
'/TestProj.sqlproj/duplicateFolder',
|
||||
'/TestProj.sqlproj/someFolder',
|
||||
'/TestProj.sqlproj/duplicate.sql']);
|
||||
'/TestProj/Database References',
|
||||
'/TestProj/duplicateFolder',
|
||||
'/TestProj/someFolder',
|
||||
'/TestProj/duplicate.sql']);
|
||||
|
||||
should(tree.children.find(x => x.uri.path === '/TestProj.sqlproj/someFolder')?.children.map(y => y.uri.path)).deepEqual([
|
||||
'/TestProj.sqlproj/someFolder/aNestedFolder',
|
||||
'/TestProj.sqlproj/someFolder/bNestedFolder',
|
||||
'/TestProj.sqlproj/someFolder/aNestedTest.sql',
|
||||
'/TestProj.sqlproj/someFolder/bNestedTest.sql']);
|
||||
should(tree.children.find(x => x.uri.path === '/TestProj/someFolder')?.children.map(y => y.uri.path)).deepEqual([
|
||||
'/TestProj/someFolder/aNestedFolder',
|
||||
'/TestProj/someFolder/bNestedFolder',
|
||||
'/TestProj/someFolder/aNestedTest.sql',
|
||||
'/TestProj/someFolder/bNestedTest.sql']);
|
||||
|
||||
should(tree.children.map(x => x.treeItem.contextValue)).deepEqual([
|
||||
DatabaseProjectItemType.dataSourceRoot,
|
||||
DatabaseProjectItemType.referencesRoot,
|
||||
DatabaseProjectItemType.folder,
|
||||
DatabaseProjectItemType.folder,
|
||||
DatabaseProjectItemType.file]);
|
||||
|
||||
should(tree.children.find(x => x.uri.path === '/TestProj.sqlproj/someFolder')?.children.map(y => y.treeItem.contextValue)).deepEqual([
|
||||
should(tree.children.find(x => x.uri.path === '/TestProj/someFolder')?.children.map(y => y.treeItem.contextValue)).deepEqual([
|
||||
DatabaseProjectItemType.folder,
|
||||
DatabaseProjectItemType.folder,
|
||||
DatabaseProjectItemType.file,
|
||||
@@ -106,19 +104,18 @@ describe.skip('Project Tree tests', function (): void {
|
||||
|
||||
const tree = new ProjectRootTreeItem(proj);
|
||||
should(tree.children.map(x => x.uri.path)).deepEqual([
|
||||
'/TestProj.sqlproj/Data Sources',
|
||||
'/TestProj.sqlproj/Database References',
|
||||
'/TestProj.sqlproj/someFolder1']);
|
||||
'/TestProj/Database References',
|
||||
'/TestProj/someFolder1']);
|
||||
|
||||
// Why are we only matching names - https://github.com/microsoft/azuredatastudio/issues/11026
|
||||
should(tree.children.find(x => x.uri.path === '/TestProj.sqlproj/someFolder1')?.children.map(y => path.basename(y.uri.path))).deepEqual([
|
||||
should(tree.children.find(x => x.uri.path === '/TestProj/someFolder1')?.children.map(y => path.basename(y.uri.path))).deepEqual([
|
||||
'MyNestedFolder1',
|
||||
'MyNestedFolder2',
|
||||
'MyFile2.sql']);
|
||||
});
|
||||
|
||||
it('Should be able to parse and include relative paths outside project folder', function (): void {
|
||||
const root = os.platform() === 'win32' ? 'Z:\\Level1\\Level2\\' : '/Root/Level1/Level2';
|
||||
const root = os.platform() === 'win32' ? 'Z:\\Level1\\Level2\\' : '/Root/Level1/Level2/';
|
||||
const proj = new Project(vscode.Uri.file(`${root}TestProj.sqlproj`).fsPath);
|
||||
|
||||
// nested entries before explicit top-level folder entry
|
||||
@@ -129,9 +126,8 @@ describe.skip('Project Tree tests', function (): void {
|
||||
|
||||
const tree = new ProjectRootTreeItem(proj);
|
||||
should(tree.children.map(x => x.uri.path)).deepEqual([
|
||||
'/TestProj.sqlproj/Data Sources',
|
||||
'/TestProj.sqlproj/Database References',
|
||||
'/TestProj.sqlproj/MyFile1.sql',
|
||||
'/TestProj.sqlproj/MyFile2.sql']);
|
||||
'/TestProj/Database References',
|
||||
'/TestProj/MyFile1.sql',
|
||||
'/TestProj/MyFile2.sql']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('Tests to verify utils functions', function (): void {
|
||||
should(await exists(path.join(testFolderPath, 'folder4', 'file2.sql'))).equal(false);
|
||||
});
|
||||
|
||||
it.skip('Should get correct relative paths of files/folders', async () => {
|
||||
it('Should get correct relative paths of files/folders', async () => {
|
||||
const root = os.platform() === 'win32' ? 'Z:\\' : '/';
|
||||
let projectUri = Uri.file(path.join(root, 'project', 'folder', 'project.sqlproj'));
|
||||
let fileUri = Uri.file(path.join(root, 'project', 'folder', 'file.sql'));
|
||||
|
||||
@@ -42,7 +42,7 @@ export class NetCoreTool {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async showInstallDialog(): Promise<void> {
|
||||
public async showInstallDialog(): Promise<void> {
|
||||
let result = await vscode.window.showInformationMessage(NetCoreInstallationConfirmation, UpdateNetCoreLocation, InstallNetCore);
|
||||
if (result === UpdateNetCoreLocation) {
|
||||
//open settings
|
||||
@@ -96,7 +96,7 @@ export class NetCoreTool {
|
||||
NetCoreTool._outputChannel.appendLine(`\t[ ${options.commandTitle} ]`);
|
||||
}
|
||||
|
||||
if (!this.findOrInstallNetCore()) {
|
||||
if (!(await this.findOrInstallNetCore())) {
|
||||
throw new Error(NetCoreInstallationConfirmation);
|
||||
}
|
||||
|
||||
|
||||
@@ -284,6 +284,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/xmldom/-/xmldom-0.1.29.tgz#c4428b0ca86d3b881475726fd94980b38a27c381"
|
||||
integrity sha1-xEKLDKhtO4gUdXJv2UmAs4onw4E=
|
||||
|
||||
ads-extension-telemetry@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ads-extension-telemetry/-/ads-extension-telemetry-1.0.0.tgz#840b363a6ad958447819b9bc59fdad3e49de31a9"
|
||||
integrity sha512-ouxZVECe4tsO0ek0dLdnAZEz1Lrytv1uLbbGZhRbZsHITsUYNjnkKnA471uWh0Dj80s+orvv49/j3/tNBDP/SQ==
|
||||
dependencies:
|
||||
vscode-extension-telemetry "0.1.2"
|
||||
|
||||
ansi-regex@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
|
||||
@@ -303,6 +310,16 @@ append-transform@^2.0.0:
|
||||
dependencies:
|
||||
default-require-extensions "^3.0.0"
|
||||
|
||||
applicationinsights@1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.4.0.tgz#e17e436427b6e273291055181e29832cca978644"
|
||||
integrity sha512-TV8MYb0Kw9uE2cdu4V/UvTKdOABkX2+Fga9iDz0zqV7FLrNXfmAugWZmmdTx4JoynYkln3d5CUHY3oVSUEbfFw==
|
||||
dependencies:
|
||||
cls-hooked "^4.2.2"
|
||||
continuation-local-storage "^3.2.1"
|
||||
diagnostic-channel "0.2.0"
|
||||
diagnostic-channel-publishers "^0.3.2"
|
||||
|
||||
argparse@^1.0.7:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
||||
@@ -310,6 +327,21 @@ argparse@^1.0.7:
|
||||
dependencies:
|
||||
sprintf-js "~1.0.2"
|
||||
|
||||
async-hook-jl@^1.7.6:
|
||||
version "1.7.6"
|
||||
resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68"
|
||||
integrity sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==
|
||||
dependencies:
|
||||
stack-chain "^1.3.7"
|
||||
|
||||
async-listener@^0.6.0:
|
||||
version "0.6.10"
|
||||
resolved "https://registry.yarnpkg.com/async-listener/-/async-listener-0.6.10.tgz#a7c97abe570ba602d782273c0de60a51e3e17cbc"
|
||||
integrity sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==
|
||||
dependencies:
|
||||
semver "^5.3.0"
|
||||
shimmer "^1.1.0"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
@@ -364,6 +396,15 @@ circular-json@^0.3.1:
|
||||
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
|
||||
integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==
|
||||
|
||||
cls-hooked@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908"
|
||||
integrity sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==
|
||||
dependencies:
|
||||
async-hook-jl "^1.7.6"
|
||||
emitter-listener "^1.0.1"
|
||||
semver "^5.4.1"
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
@@ -391,6 +432,14 @@ concat-map@0.0.1:
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
|
||||
continuation-local-storage@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz#11f613f74e914fe9b34c92ad2d28fe6ae1db7ffb"
|
||||
integrity sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==
|
||||
dependencies:
|
||||
async-listener "^0.6.0"
|
||||
emitter-listener "^1.1.1"
|
||||
|
||||
convert-source-map@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
|
||||
@@ -445,6 +494,18 @@ default-require-extensions@^3.0.0:
|
||||
dependencies:
|
||||
strip-bom "^4.0.0"
|
||||
|
||||
diagnostic-channel-publishers@^0.3.2:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.3.5.tgz#a84a05fd6cc1d7619fdd17791c17e540119a7536"
|
||||
integrity sha512-AOIjw4T7Nxl0G2BoBPhkQ6i7T4bUd9+xvdYizwvG7vVAM1dvr+SDrcUudlmzwH0kbEwdR2V1EcnKT0wAeYLQNQ==
|
||||
|
||||
diagnostic-channel@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17"
|
||||
integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=
|
||||
dependencies:
|
||||
semver "^5.3.0"
|
||||
|
||||
diff@3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
|
||||
@@ -460,6 +521,13 @@ diff@^4.0.2:
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
emitter-listener@^1.0.1, emitter-listener@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8"
|
||||
integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==
|
||||
dependencies:
|
||||
shimmer "^1.2.0"
|
||||
|
||||
escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
@@ -925,6 +993,11 @@ semver@^6.0.0, semver@^6.3.0:
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
|
||||
shimmer@^1.1.0, shimmer@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337"
|
||||
integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==
|
||||
|
||||
should-equal@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3"
|
||||
@@ -997,6 +1070,11 @@ sprintf-js@~1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
||||
|
||||
stack-chain@^1.3.7:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
|
||||
integrity sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=
|
||||
|
||||
strip-ansi@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
|
||||
@@ -1092,6 +1170,13 @@ typescript@^2.6.1:
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
|
||||
integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==
|
||||
|
||||
vscode-extension-telemetry@0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.2.tgz#049207f5453930888ff68ca925b07bab08f2c955"
|
||||
integrity sha512-FSbaZKlIH3VKvBJsKw7v5bESWHXzltji2rtjaJeJglpQH4tfClzwHMzlMXUZGiblV++djEzb1gW8mb5E+wxFsg==
|
||||
dependencies:
|
||||
applicationinsights "1.4.0"
|
||||
|
||||
vscode-jsonrpc@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794"
|
||||
|
||||
@@ -39,7 +39,7 @@ export type SqlManagedInstance = AzureProduct;
|
||||
export async function getAvailableManagedInstanceProducts(account: azdata.Account, subscription: Subscription): Promise<SqlManagedInstance[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
|
||||
const result = await api.getSqlManagedInstances(account, [subscription], false);
|
||||
const result = await api.runGraphQuery<SqlManagedInstance>(account, [subscription], false, `where type == "${azureResource.AzureResourceType.sqlManagedInstance}"`);
|
||||
return result.resources;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export type SqlServer = AzureProduct;
|
||||
export async function getAvailableSqlServers(account: azdata.Account, subscription: Subscription): Promise<SqlServer[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
|
||||
const result = await api.getSqlServers(account, [subscription], false);
|
||||
const result = await api.runGraphQuery<SqlServer>(account, [subscription], false, `where type == "${azureResource.AzureResourceType.sqlServer}"`);
|
||||
return result.resources;
|
||||
}
|
||||
|
||||
@@ -55,45 +55,6 @@ export type SqlVMServer = AzureProduct;
|
||||
export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription): Promise<SqlVMServer[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
|
||||
const result = await api.getSqlVMServers(account, [subscription], false);
|
||||
const result = await api.runGraphQuery<SqlVMServer>(account, [subscription], false, `where type == "${azureResource.AzureResourceType.virtualMachines}" and properties.storageProfile.imageReference.publisher == "microsoftsqlserver"`);
|
||||
return result.resources;
|
||||
}
|
||||
|
||||
export type StorageAccount = AzureProduct;
|
||||
export async function getAvailableStorageAccounts(account: azdata.Account, subscription: Subscription): Promise<StorageAccount[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const result = await api.getStorageAccounts(account, [subscription], false);
|
||||
sortResourceArrayByName(result.resources);
|
||||
return result.resources;
|
||||
}
|
||||
|
||||
export async function getFileShares(account: azdata.Account, subscription: Subscription, storageAccount: StorageAccount): Promise<azureResource.FileShare[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
let result = await api.getFileShares(account, subscription, storageAccount, true);
|
||||
let fileShares = result.fileShares;
|
||||
sortResourceArrayByName(fileShares!);
|
||||
return fileShares!;
|
||||
}
|
||||
|
||||
export async function getBlobContainers(account: azdata.Account, subscription: Subscription, storageAccount: StorageAccount): Promise<azureResource.BlobContainer[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
let result = await api.getBlobContainers(account, subscription, storageAccount, true);
|
||||
let blobContainers = result.blobContainer;
|
||||
sortResourceArrayByName(blobContainers!);
|
||||
return blobContainers!;
|
||||
}
|
||||
|
||||
function sortResourceArrayByName(resourceArray: AzureProduct[] | azureResource.FileShare[] | azureResource.BlobContainer[] | undefined): void {
|
||||
if (!resourceArray) {
|
||||
return;
|
||||
}
|
||||
resourceArray.sort((a: AzureProduct | azureResource.BlobContainer | azureResource.FileShare, b: AzureProduct | azureResource.BlobContainer | azureResource.FileShare) => {
|
||||
if (a.name! < b.name!) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name! > b.name!) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
|
||||
import { MigrationStateModel, StateChangeEvent } from './stateMachine';
|
||||
export abstract class MigrationWizardPage {
|
||||
constructor(
|
||||
protected readonly wizard: azdata.window.Wizard,
|
||||
private readonly wizard: azdata.window.Wizard,
|
||||
protected readonly wizardPage: azdata.window.WizardPage,
|
||||
protected readonly migrationStateModel: MigrationStateModel
|
||||
) { }
|
||||
|
||||
@@ -26,51 +26,11 @@ export enum State {
|
||||
EXIT,
|
||||
}
|
||||
|
||||
export enum MigrationCutover {
|
||||
MANUAL,
|
||||
AUTOMATIC
|
||||
}
|
||||
|
||||
export enum NetworkContainerType {
|
||||
FILE_SHARE,
|
||||
BLOB_CONTAINER,
|
||||
NETWORK_SHARE
|
||||
}
|
||||
|
||||
export interface NetworkShare {
|
||||
networkShareLocation: string;
|
||||
windowsUser: string;
|
||||
password: string;
|
||||
storageSubscriptionId: string;
|
||||
storageAccountId: string;
|
||||
}
|
||||
|
||||
export interface BlobContainer {
|
||||
subscriptionId: string;
|
||||
storageAccountId: string;
|
||||
containerId: string;
|
||||
}
|
||||
|
||||
export interface FileShare {
|
||||
subscriptionId: string;
|
||||
storageAccountId: string;
|
||||
fileShareId: string;
|
||||
resourceGroupId: string;
|
||||
}
|
||||
export interface DatabaseBackupModel {
|
||||
emailNotification: boolean;
|
||||
migrationCutover: MigrationCutover;
|
||||
networkContainerType: NetworkContainerType;
|
||||
networkContainer: NetworkShare | BlobContainer | FileShare;
|
||||
azureSecurityToken: string;
|
||||
}
|
||||
export interface Model {
|
||||
readonly sourceConnection: azdata.connection.Connection;
|
||||
readonly currentState: State;
|
||||
gatheringInformationError: string | undefined;
|
||||
skuRecommendations: SKURecommendations | undefined;
|
||||
azureAccount: azdata.Account | undefined;
|
||||
databaseBackup: DatabaseBackupModel | undefined;
|
||||
}
|
||||
|
||||
export interface StateChangeEvent {
|
||||
@@ -84,8 +44,6 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
private _gatheringInformationError: string | undefined;
|
||||
private _skuRecommendations: SKURecommendations | undefined;
|
||||
private _assessmentResults: mssql.SqlMigrationAssessmentResultItem[] | undefined;
|
||||
private _azureAccount!: azdata.Account;
|
||||
private _databaseBackup!: DatabaseBackupModel;
|
||||
|
||||
constructor(
|
||||
private readonly _extensionContext: vscode.ExtensionContext,
|
||||
@@ -93,23 +51,6 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
public readonly migrationService: mssql.ISqlMigrationService
|
||||
) {
|
||||
this._currentState = State.INIT;
|
||||
this.databaseBackup = {} as DatabaseBackupModel;
|
||||
}
|
||||
|
||||
public get azureAccount(): azdata.Account {
|
||||
return this._azureAccount;
|
||||
}
|
||||
|
||||
public set azureAccount(account: azdata.Account) {
|
||||
this._azureAccount = account;
|
||||
}
|
||||
|
||||
public get databaseBackup(): DatabaseBackupModel {
|
||||
return this._databaseBackup;
|
||||
}
|
||||
|
||||
public set databaseBackup(dbBackup: DatabaseBackupModel) {
|
||||
this._databaseBackup = dbBackup;
|
||||
}
|
||||
|
||||
public get sourceConnection(): azdata.connection.Connection {
|
||||
|
||||
@@ -35,54 +35,3 @@ export const SUBSCRIPTION_SELECTION_AZURE_SUBSCRIPTION_TITLE = localize('sql.mig
|
||||
export const SUBSCRIPTION_SELECTION_AZURE_PRODUCT_TITLE = localize('sql.migration.wizard.subscription.azure.product.title', "Azure Product");
|
||||
|
||||
export const CONGRATULATIONS = localize('sql.migration.generic.congratulations', "Congratulations");
|
||||
|
||||
|
||||
// Accounts page
|
||||
export const ACCOUNTS_SELECTION_PAGE_TITLE = localize('sql.migration.wizard.account.title', "Select your Azure account");
|
||||
export const ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR = localize('sql.migration.wizard.account.noaccount.error', "There is no linked account. Please add an account.");
|
||||
export const ACCOUNT_ADD_BUTTON_LABEL = localize('sql.migration.wizard.account.add.button.label', "Add account");
|
||||
export function accountLinkedMessage(count: number): string {
|
||||
return count === 1 ? localize('sql.migration.wizard.account.count.single.message', '{0} account linked', count) : localize('sql.migration.wizard.account.count.multiple.message', '{0} accounts linked', count);
|
||||
}
|
||||
|
||||
|
||||
// database backup page
|
||||
export const DATABASE_BACKUP_PAGE_TITLE = localize('sql.migration.database.page.title', "Database Backup");
|
||||
export const DATABASE_BACKUP_PAGE_DESCRIPTION = localize('sql.migration.database.page.description', "Select the location where we can find your database backups (Full, Differential and Log) to use for migration.");
|
||||
export const DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL = localize('sql.migration.nc.network.share.radio.label', "My database backups are on a network share");
|
||||
export const DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT = localize('sql.migration.network.share.help.text', "Enter network share information");
|
||||
export const DATABASE_BACKUP_NC_BLOB_STORAGE_RADIO_LABEL = localize('sql.migration.nc.blob.storage.radio.label', "My database backups are in an Azure Storage Blob Container");
|
||||
export const DATABASE_BACKUP_NC_FILE_SHARE_RADIO_LABEL = localize('sql.migration.nc.file.share.radio.label', "My database backups are in an Azure Storage File Share");
|
||||
export const DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL = localize('sql.migration.network.share.location.label', "Network share location to read backups from.");
|
||||
export const DATABASE_BACKUP_NETWORK_SHARE_WINDOWS_USER_LABEL = localize('sql.migration.network.share.windows.user.label', "Windows user account with read access to the network share location.");
|
||||
export const DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL = localize('sql.migration.network.share.password.label', "Password");
|
||||
export const DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_PLACEHOLDER = localize('sql.migration.network.share.password.placeholder', "Enter password");
|
||||
export const DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP = localize('sql.migration.network.share.azure.help', "Enter Azure storage account information where the backup will be copied");
|
||||
export const DATABASE_BACKUP_NETWORK_SHARE_SUBSCRIPTION_LABEL = localize('sql.migration.network.share.subscription.label', "Select the subscription that contains the storage account.");
|
||||
export const DATABASE_BACKUP_SUBSCRIPTION_PLACEHOLDER = localize('sql.migration.network.share.subscription.placeholder', "Select subscription");
|
||||
export const DATABASE_BACKUP_NETWORK_SHARE_NETWORK_STORAGE_ACCOUNT_LABEL = localize('sql.migration.network.share.storage.account.label', "Select the storage account where backup files will be copied.");
|
||||
export const DATABASE_BACKUP_STORAGE_ACCOUNT_PLACEHOLDER = localize('sql.migration.network.share.storage.account.placeholder', "Select account");
|
||||
export const DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL = localize('sql.migration.blob.storage.subscription.label', "Select the subscription that contains the storage account.");
|
||||
export const DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_LABEL = localize('sql.migration.blob.storage.account.label', "Select the storage account that contains the backup files.");
|
||||
export const DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_CONTAINER_LABEL = localize('sql.migration.blob.storage.container.label', "Select the container that contains the backup files.");
|
||||
export const DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_CONTAINER_PLACEHOLDER = localize('sql.migration.blob.storage.container.placeholder', "Select container");
|
||||
export const DATABASE_BACKUP_FILE_SHARE_SUBSCRIPTION_LABEL = localize('sql.migration.file.share.subscription.label', "Select the subscription that contains the file share.");
|
||||
export const DATABASE_BACKUP_FILE_SHARE_STORAGE_ACCOUNT_LABEL = localize('sql.migration.file.share.storage.account.label', "Select the storage account that contains the file share.");
|
||||
export const DATABASE_BACKUP_FILE_SHARE_LABEL = localize('sql.migration.file.share.label', "Select the file share that contains the backup files.");
|
||||
export const DATABASE_BACKUP_FILE_SHARE_PLACEHOLDER = localize('sql.migration.file.share.placeholder', "Select share");
|
||||
export const DATABASE_BACKUP_MIGRATION_CUTOVER_LABEL = localize('sql.migration.database.migration.cutover.label', "Migration Cutover");
|
||||
export const DATABASE_BACKUP_MIGRATION_CUTOVER_DESCRIPTION = localize('sql.migration.database.migration.cutover.description', "Select how you want to cutover when the migration is complete.");
|
||||
export const DATABASE_BACKUP_MIGRATION_CUTOVER_AUTOMATIC_LABEL = localize('sql.migration.database.migration.cutover.automatic.label', "Automatically cutover when migration is complete");
|
||||
export const DATABASE_BACKUP_MIGRATION_CUTOVER_MANUAL_LABEL = localize('sql.migration.database.migration.cutover.manual.label', "Manually cutover when migration is complete");
|
||||
export const DATABASE_BACKUP_EMAIL_NOTIFICATION_LABEL = localize('sql.migration.database.backup.email.notification.label', "Email notifications");
|
||||
export const DATABASE_BACKUP_EMAIL_NOTIFICATION_CHECKBOX_LABEL = localize('sql.migration.database.backup.email.notification.checkbox.label', "Notify me when migration is complete");
|
||||
export const NO_SUBSCRIPTIONS_FOUND = localize('sql.migration.no.subscription.found', "No subscription found");
|
||||
export const NO_STORAGE_ACCOUNT_FOUND = localize('sql.migration.no.storageAccount.found', "No storage account found");
|
||||
export const NO_FILESHARES_FOUND = localize('sql.migration.no.fileShares.found', "No file shares found");
|
||||
export const NO_BLOBCONTAINERS_FOUND = localize('sql.migration.no.blobContainers.found', "No blob containers found");
|
||||
export const INVALID_SUBSCRIPTION_ERROR = localize('sql.migration.invalid.subscription.error', "Please select a valid subscription to proceed.");
|
||||
export const INVALID_STORAGE_ACCOUNT_ERROR = localize('sql.migration.invalid.storageAccout.error', "Please select a valid storage account to proceed.");
|
||||
export const INVALID_FILESHARE_ERROR = localize('sql.migration.invalid.fileShare.error', "Please select a valid file share to proceed.");
|
||||
export const INVALID_BLOBCONTAINER_ERROR = localize('sql.migration.invalid.blobContainer.error', "Please select a valid blob container to proceed.");
|
||||
export const INVALID_NETWORK_SHARE_LOCATION = localize('sql.migration.invalid.network.share.location', "Invalid network share location format. Example: {0}", '\\\\Servername.domainname.com\\Backupfolder');
|
||||
export const INVALID_USER_ACCOUNT = localize('sql.migration.invalid.user.account', "Invalid user account format. Example: {0}", 'Domain\\username');
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 vscode from 'vscode';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../models/strings';
|
||||
|
||||
export class AccountsSelectionPage extends MigrationWizardPage {
|
||||
private _azureAccountsDropdown!: azdata.DropDownComponent;
|
||||
private _accountsMap: Map<string, azdata.Account> = new Map();
|
||||
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
super(wizard, azdata.window.createWizardPage(constants.ACCOUNTS_SELECTION_PAGE_TITLE), migrationStateModel);
|
||||
}
|
||||
|
||||
protected async registerContent(view: azdata.ModelView): Promise<void> {
|
||||
const form = view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
[
|
||||
await this.createAzureAccountsDropdown(view)
|
||||
]
|
||||
);
|
||||
await view.initializeModel(form.component());
|
||||
await this.populateAzureAccountsDropdown();
|
||||
}
|
||||
|
||||
private createAzureAccountsDropdown(view: azdata.ModelView): azdata.FormComponent {
|
||||
|
||||
this._azureAccountsDropdown = view.modelBuilder.dropDown().withValidation((c) => {
|
||||
if ((<azdata.CategoryValue>c.value).displayName === constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR) {
|
||||
this.wizard.message = {
|
||||
text: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR,
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).component();
|
||||
|
||||
this._azureAccountsDropdown.onValueChanged(async (value) => {
|
||||
this.migrationStateModel.azureAccount = this._accountsMap.get((this._azureAccountsDropdown.value as azdata.CategoryValue).name)!;
|
||||
});
|
||||
|
||||
const addAccountButton = view.modelBuilder.button()
|
||||
.withProperties<azdata.ButtonProperties>({
|
||||
label: constants.ACCOUNT_ADD_BUTTON_LABEL,
|
||||
width: '100px'
|
||||
})
|
||||
.component();
|
||||
|
||||
addAccountButton.onDidClick(async (event) => {
|
||||
await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount');
|
||||
await this.populateAzureAccountsDropdown();
|
||||
});
|
||||
|
||||
const flexContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column'
|
||||
})
|
||||
.withItems([this._azureAccountsDropdown, addAccountButton], { CSSStyles: { 'margin': '10px', } })
|
||||
.component();
|
||||
|
||||
return {
|
||||
title: '',
|
||||
component: flexContainer
|
||||
};
|
||||
}
|
||||
|
||||
private async populateAzureAccountsDropdown(): Promise<void> {
|
||||
this._azureAccountsDropdown.loading = true;
|
||||
let accounts = await azdata.accounts.getAllAccounts();
|
||||
|
||||
if (accounts.length === 0) {
|
||||
this._azureAccountsDropdown.value = {
|
||||
displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR,
|
||||
name: ''
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
this._azureAccountsDropdown.values = accounts.map((account): azdata.CategoryValue => {
|
||||
let accountCategoryValue = {
|
||||
displayName: account.displayInfo.displayName,
|
||||
name: account.displayInfo.userId
|
||||
};
|
||||
this._accountsMap.set(accountCategoryValue.name, account);
|
||||
return accountCategoryValue;
|
||||
});
|
||||
|
||||
this.migrationStateModel.azureAccount = accounts[0];
|
||||
this._azureAccountsDropdown.loading = false;
|
||||
}
|
||||
|
||||
public async onPageEnter(): Promise<void> {
|
||||
}
|
||||
|
||||
public async onPageLeave(): Promise<void> {
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
}
|
||||
}
|
||||
@@ -1,702 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { azureResource } from 'azureResource';
|
||||
import { EOL } from 'os';
|
||||
import { getAvailableStorageAccounts, getBlobContainers, getFileShares, getSubscriptions, StorageAccount, Subscription } from '../api/azure';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { BlobContainer, FileShare, MigrationCutover, MigrationStateModel, NetworkContainerType, NetworkShare, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../models/strings';
|
||||
|
||||
export class DatabaseBackupPage extends MigrationWizardPage {
|
||||
|
||||
private _networkShareContainer!: azdata.FlexContainer;
|
||||
private _networkShareContainerSubscriptionDropdown!: azdata.DropDownComponent;
|
||||
private _networkShareContainerStorageAccountDropdown!: azdata.DropDownComponent;
|
||||
private _networkShareLocationText!: azdata.InputBoxComponent;
|
||||
private _windowsUserAccountText!: azdata.InputBoxComponent;
|
||||
private _passwordText!: azdata.InputBoxComponent;
|
||||
|
||||
private _blobContainer!: azdata.FlexContainer;
|
||||
private _blobContainerSubscriptionDropdown!: azdata.DropDownComponent;
|
||||
private _blobContainerStorageAccountDropdown!: azdata.DropDownComponent;
|
||||
private _blobContainerBlobDropdown!: azdata.DropDownComponent;
|
||||
|
||||
private _fileShareContainer!: azdata.FlexContainer;
|
||||
private _fileShareSubscriptionDropdown!: azdata.DropDownComponent;
|
||||
private _fileShareStorageAccountDropdown!: azdata.DropDownComponent;
|
||||
private _fileShareFileShareDropdown!: azdata.DropDownComponent;
|
||||
|
||||
private _networkShare = {} as NetworkShare;
|
||||
private _fileShare = {} as FileShare;
|
||||
private _blob = {} as BlobContainer;
|
||||
|
||||
private _subscriptionDropdownValues: azdata.CategoryValue[] = [];
|
||||
private _subscriptionMap: Map<string, Subscription> = new Map();
|
||||
private _storageAccountMap: Map<string, StorageAccount> = new Map();
|
||||
|
||||
private _errors: string[] = [];
|
||||
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
super(wizard, azdata.window.createWizardPage(constants.DATABASE_BACKUP_PAGE_TITLE), migrationStateModel);
|
||||
this.wizardPage.description = constants.DATABASE_BACKUP_PAGE_DESCRIPTION;
|
||||
}
|
||||
|
||||
protected async registerContent(view: azdata.ModelView): Promise<void> {
|
||||
|
||||
this._networkShareContainer = this.createNetworkShareContainer(view);
|
||||
this._blobContainer = this.createBlobContainer(view);
|
||||
this._fileShareContainer = this.createFileShareContainer(view);
|
||||
|
||||
const networkContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).withItems([
|
||||
this._networkShareContainer,
|
||||
this._blobContainer,
|
||||
this._fileShareContainer
|
||||
]).component();
|
||||
|
||||
const form = view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
[
|
||||
this.createBackupLocationComponent(view),
|
||||
{
|
||||
title: '',
|
||||
component: networkContainer
|
||||
},
|
||||
this.migrationCutoverContainer(view),
|
||||
this.emailNotificationContainer(view),
|
||||
]
|
||||
);
|
||||
await view.initializeModel(form.component());
|
||||
this.toggleNetworkContainerFields(NetworkContainerType.NETWORK_SHARE, this._networkShare);
|
||||
}
|
||||
|
||||
private createBackupLocationComponent(view: azdata.ModelView): azdata.FormComponent {
|
||||
const buttonGroup = 'networkContainer';
|
||||
|
||||
const networkShareButton = view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
name: buttonGroup,
|
||||
label: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL,
|
||||
checked: true
|
||||
}).component();
|
||||
|
||||
networkShareButton.onDidClick((e) => this.toggleNetworkContainerFields(NetworkContainerType.NETWORK_SHARE, this._networkShare));
|
||||
|
||||
const blobContainerButton = view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
name: buttonGroup,
|
||||
label: constants.DATABASE_BACKUP_NC_BLOB_STORAGE_RADIO_LABEL,
|
||||
}).component();
|
||||
|
||||
blobContainerButton.onDidClick((e) => this.toggleNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER, this._blob));
|
||||
|
||||
const fileShareButton = view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
name: buttonGroup,
|
||||
label: constants.DATABASE_BACKUP_NC_FILE_SHARE_RADIO_LABEL,
|
||||
}).component();
|
||||
|
||||
fileShareButton.onDidClick((e) => this.toggleNetworkContainerFields(NetworkContainerType.FILE_SHARE, this._fileShare));
|
||||
|
||||
const flexContainer = view.modelBuilder.flexContainer().withItems(
|
||||
[
|
||||
networkShareButton,
|
||||
blobContainerButton,
|
||||
fileShareButton
|
||||
]
|
||||
).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
return {
|
||||
title: '',
|
||||
component: flexContainer
|
||||
};
|
||||
}
|
||||
|
||||
private createFileShareContainer(view: azdata.ModelView): azdata.FlexContainer {
|
||||
|
||||
const subscriptionLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.DATABASE_BACKUP_FILE_SHARE_SUBSCRIPTION_LABEL,
|
||||
requiredIndicator: true,
|
||||
}).component();
|
||||
this._fileShareSubscriptionDropdown = view.modelBuilder.dropDown().withProps({
|
||||
required: true,
|
||||
}).withValidation((c) => {
|
||||
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.FILE_SHARE) {
|
||||
if ((<azdata.CategoryValue>c.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
|
||||
this.addErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR);
|
||||
return false;
|
||||
} else {
|
||||
this.removeErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).component();
|
||||
this._fileShareSubscriptionDropdown.onValueChanged(async (value) => {
|
||||
this._fileShare.subscriptionId = (this._fileShareSubscriptionDropdown.value as azdata.CategoryValue).name;
|
||||
await this.loadFileShareStorageDropdown();
|
||||
});
|
||||
|
||||
const storageAccountLabel = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DATABASE_BACKUP_FILE_SHARE_STORAGE_ACCOUNT_LABEL,
|
||||
requiredIndicator: true,
|
||||
}).component();
|
||||
this._fileShareStorageAccountDropdown = view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
required: true
|
||||
}).withValidation((c) => {
|
||||
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.FILE_SHARE) {
|
||||
if ((<azdata.CategoryValue>c.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
|
||||
this.addErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR);
|
||||
return false;
|
||||
} else {
|
||||
this.removeErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).component();
|
||||
this._fileShareStorageAccountDropdown.onValueChanged(async (value) => {
|
||||
this._fileShare.storageAccountId = (this._fileShareStorageAccountDropdown.value as azdata.CategoryValue).name;
|
||||
await this.loadFileShareDropdown();
|
||||
});
|
||||
|
||||
const fileShareLabel = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DATABASE_BACKUP_FILE_SHARE_LABEL,
|
||||
requiredIndicator: true,
|
||||
}).component();
|
||||
this._fileShareFileShareDropdown = view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
required: true
|
||||
}).withValidation((c) => {
|
||||
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.FILE_SHARE) {
|
||||
if ((<azdata.CategoryValue>c.value).displayName === constants.NO_FILESHARES_FOUND) {
|
||||
this.addErrorMessage(constants.INVALID_FILESHARE_ERROR);
|
||||
return false;
|
||||
} else {
|
||||
this.removeErrorMessage(constants.INVALID_FILESHARE_ERROR);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).component();
|
||||
this._fileShareFileShareDropdown.onValueChanged((value) => {
|
||||
this._fileShare.fileShareId = (this._fileShareFileShareDropdown.value as azdata.CategoryValue).name;
|
||||
});
|
||||
|
||||
|
||||
const flexContainer = view.modelBuilder.flexContainer()
|
||||
.withItems(
|
||||
[
|
||||
subscriptionLabel,
|
||||
this._fileShareSubscriptionDropdown,
|
||||
storageAccountLabel,
|
||||
this._fileShareStorageAccountDropdown,
|
||||
fileShareLabel,
|
||||
this._fileShareFileShareDropdown
|
||||
]
|
||||
).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
private createBlobContainer(view: azdata.ModelView): azdata.FlexContainer {
|
||||
const subscriptionLabel = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL,
|
||||
requiredIndicator: true,
|
||||
}).component();
|
||||
this._blobContainerSubscriptionDropdown = view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
required: true
|
||||
}).withValidation((c) => {
|
||||
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
|
||||
if (
|
||||
(<azdata.CategoryValue>c.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
|
||||
this.addErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR);
|
||||
return false;
|
||||
} else {
|
||||
this.removeErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).component();
|
||||
this._blobContainerSubscriptionDropdown.onValueChanged(async (value) => {
|
||||
this._blob.subscriptionId = (this._blobContainerSubscriptionDropdown.value as azdata.CategoryValue).name;
|
||||
await this.loadblobStorageDropdown();
|
||||
});
|
||||
|
||||
const storageAccountLabel = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_LABEL,
|
||||
requiredIndicator: true,
|
||||
}).component();
|
||||
this._blobContainerStorageAccountDropdown = view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
required: true
|
||||
}).withValidation((c) => {
|
||||
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
|
||||
if ((<azdata.CategoryValue>c.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
|
||||
this.addErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR);
|
||||
return false;
|
||||
} else {
|
||||
this.removeErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).component();
|
||||
this._blobContainerStorageAccountDropdown.onValueChanged(async (value) => {
|
||||
this._blob.storageAccountId = (this._blobContainerStorageAccountDropdown.value as azdata.CategoryValue).name;
|
||||
await this.loadBlobContainerDropdown();
|
||||
});
|
||||
|
||||
const containerLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_CONTAINER_LABEL,
|
||||
requiredIndicator: true,
|
||||
}).component();
|
||||
this._blobContainerBlobDropdown = view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
required: true
|
||||
}).withValidation((c) => {
|
||||
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
|
||||
if ((<azdata.CategoryValue>c.value).displayName === constants.NO_BLOBCONTAINERS_FOUND) {
|
||||
this.addErrorMessage(constants.INVALID_BLOBCONTAINER_ERROR);
|
||||
return false;
|
||||
} else {
|
||||
this.removeErrorMessage(constants.INVALID_BLOBCONTAINER_ERROR);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).component();
|
||||
this._blobContainerBlobDropdown.onValueChanged((value) => {
|
||||
this._blob.containerId = (this._blobContainerBlobDropdown.value as azdata.CategoryValue).name;
|
||||
});
|
||||
|
||||
const flexContainer = view.modelBuilder.flexContainer()
|
||||
.withItems(
|
||||
[
|
||||
subscriptionLabel,
|
||||
this._blobContainerSubscriptionDropdown,
|
||||
storageAccountLabel,
|
||||
this._blobContainerStorageAccountDropdown,
|
||||
containerLabel,
|
||||
this._blobContainerBlobDropdown
|
||||
]
|
||||
).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
private createNetworkShareContainer(view: azdata.ModelView): azdata.FlexContainer {
|
||||
const networkShareHelpText = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT,
|
||||
}).component();
|
||||
|
||||
const networkShareLocationLabel = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL,
|
||||
requiredIndicator: true,
|
||||
}).component();
|
||||
this._networkShareLocationText = view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
placeHolder: '\\\\Servername.domainname.com\\Backupfolder',
|
||||
required: true,
|
||||
validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION
|
||||
})
|
||||
.withValidation((component) => {
|
||||
if (component.value) {
|
||||
if (!/^(\\)(\\[\w\.-_]+){2,}(\\?)$/.test(component.value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).component();
|
||||
this._networkShareLocationText.onTextChanged((value) => {
|
||||
this._networkShare.networkShareLocation = value;
|
||||
});
|
||||
|
||||
const windowsUserAccountLabel = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DATABASE_BACKUP_NETWORK_SHARE_WINDOWS_USER_LABEL,
|
||||
requiredIndicator: true,
|
||||
}).component();
|
||||
this._windowsUserAccountText = view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
placeHolder: 'Domain\\username',
|
||||
required: true,
|
||||
validationErrorMessage: constants.INVALID_USER_ACCOUNT
|
||||
})
|
||||
.withValidation((component) => {
|
||||
if (component.value) {
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9\-\.]{0,61}[a-zA-Z]\\\w[\w\.\- ]*$/.test(component.value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).component();
|
||||
this._windowsUserAccountText.onTextChanged((value) => {
|
||||
this._networkShare.windowsUser = value;
|
||||
});
|
||||
|
||||
const passwordLabel = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL,
|
||||
requiredIndicator: true,
|
||||
}).component();
|
||||
this._passwordText = view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
placeHolder: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_PLACEHOLDER,
|
||||
inputType: 'password',
|
||||
required: true
|
||||
}).component();
|
||||
this._passwordText.onTextChanged((value) => {
|
||||
this._networkShare.password = value;
|
||||
});
|
||||
|
||||
const azureAccountHelpText = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP,
|
||||
}).component();
|
||||
|
||||
const subscriptionLabel = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DATABASE_BACKUP_NETWORK_SHARE_SUBSCRIPTION_LABEL,
|
||||
requiredIndicator: true,
|
||||
}).component();
|
||||
this._networkShareContainerSubscriptionDropdown = view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
required: true
|
||||
}).withValidation((c) => {
|
||||
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
|
||||
if ((<azdata.CategoryValue>c.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
|
||||
this.addErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR);
|
||||
return false;
|
||||
} else {
|
||||
this.removeErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).component();
|
||||
this._networkShareContainerSubscriptionDropdown.onValueChanged(async (value) => {
|
||||
this._networkShare.storageSubscriptionId = (this._networkShareContainerSubscriptionDropdown.value as azdata.CategoryValue).name;
|
||||
await this.loadNetworkShareStorageDropdown();
|
||||
});
|
||||
|
||||
const storageAccountLabel = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DATABASE_BACKUP_NETWORK_SHARE_NETWORK_STORAGE_ACCOUNT_LABEL,
|
||||
requiredIndicator: true,
|
||||
}).component();
|
||||
this._networkShareContainerStorageAccountDropdown = view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
required: true
|
||||
}).withValidation((c) => {
|
||||
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
|
||||
if ((<azdata.CategoryValue>c.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
|
||||
this.addErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR);
|
||||
return false;
|
||||
} else {
|
||||
this.removeErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).component();
|
||||
this._networkShareContainerStorageAccountDropdown.onValueChanged((value) => {
|
||||
this._networkShare.storageAccountId = (this._networkShareContainerStorageAccountDropdown.value as azdata.CategoryValue).name;
|
||||
});
|
||||
|
||||
const flexContainer = view.modelBuilder.flexContainer().withItems(
|
||||
[
|
||||
networkShareHelpText,
|
||||
networkShareLocationLabel,
|
||||
this._networkShareLocationText,
|
||||
windowsUserAccountLabel,
|
||||
this._windowsUserAccountText,
|
||||
passwordLabel,
|
||||
this._passwordText,
|
||||
azureAccountHelpText,
|
||||
subscriptionLabel,
|
||||
this._networkShareContainerSubscriptionDropdown,
|
||||
storageAccountLabel,
|
||||
this._networkShareContainerStorageAccountDropdown
|
||||
]
|
||||
).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
private emailNotificationContainer(view: azdata.ModelView): azdata.FormComponent {
|
||||
const emailCheckbox = view.modelBuilder.checkBox().withProps({
|
||||
label: constants.DATABASE_BACKUP_EMAIL_NOTIFICATION_CHECKBOX_LABEL
|
||||
}).component();
|
||||
|
||||
emailCheckbox.onChanged((value) => this.migrationStateModel.databaseBackup.emailNotification = value);
|
||||
|
||||
return {
|
||||
title: constants.DATABASE_BACKUP_EMAIL_NOTIFICATION_LABEL,
|
||||
component: emailCheckbox
|
||||
};
|
||||
}
|
||||
|
||||
private migrationCutoverContainer(view: azdata.ModelView): azdata.FormComponent {
|
||||
const description = view.modelBuilder.text().withProps({
|
||||
value: constants.DATABASE_BACKUP_MIGRATION_CUTOVER_DESCRIPTION
|
||||
}).component();
|
||||
|
||||
const buttonGroup = 'cutoverContainer';
|
||||
|
||||
const automaticButton = view.modelBuilder.radioButton().withProps({
|
||||
label: constants.DATABASE_BACKUP_MIGRATION_CUTOVER_AUTOMATIC_LABEL,
|
||||
name: buttonGroup,
|
||||
checked: true
|
||||
}).component();
|
||||
|
||||
this.migrationStateModel.databaseBackup.migrationCutover = MigrationCutover.AUTOMATIC;
|
||||
|
||||
automaticButton.onDidClick((e) => this.migrationStateModel.databaseBackup.migrationCutover = MigrationCutover.AUTOMATIC);
|
||||
|
||||
const manualButton = view.modelBuilder.radioButton().withProps({
|
||||
label: constants.DATABASE_BACKUP_MIGRATION_CUTOVER_MANUAL_LABEL,
|
||||
name: buttonGroup
|
||||
}).component();
|
||||
|
||||
manualButton.onDidClick((e) => this.migrationStateModel.databaseBackup.migrationCutover = MigrationCutover.MANUAL);
|
||||
|
||||
const flexContainer = view.modelBuilder.flexContainer().withItems(
|
||||
[
|
||||
description,
|
||||
automaticButton,
|
||||
manualButton
|
||||
]
|
||||
).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
return {
|
||||
title: constants.DATABASE_BACKUP_MIGRATION_CUTOVER_LABEL,
|
||||
component: flexContainer
|
||||
};
|
||||
}
|
||||
|
||||
public async onPageEnter(): Promise<void> {
|
||||
await this.getSubscriptionValues();
|
||||
}
|
||||
|
||||
public async onPageLeave(): Promise<void> {
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
}
|
||||
|
||||
private toggleNetworkContainerFields(containerType: NetworkContainerType, networkContainer: NetworkShare | BlobContainer | FileShare): void {
|
||||
this.migrationStateModel.databaseBackup.networkContainer = networkContainer;
|
||||
this.migrationStateModel.databaseBackup.networkContainerType = containerType;
|
||||
this._fileShareContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.FILE_SHARE) ? 'inline' : 'none' });
|
||||
this._blobContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.BLOB_CONTAINER) ? 'inline' : 'none' });
|
||||
this._networkShareContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.NETWORK_SHARE) ? 'inline' : 'none' });
|
||||
this._networkShareLocationText.updateProperties({
|
||||
required: containerType === NetworkContainerType.NETWORK_SHARE
|
||||
});
|
||||
this._windowsUserAccountText.updateProperties({
|
||||
required: containerType === NetworkContainerType.NETWORK_SHARE
|
||||
});
|
||||
this._passwordText.updateProperties({
|
||||
required: containerType === NetworkContainerType.NETWORK_SHARE
|
||||
});
|
||||
}
|
||||
|
||||
private async getSubscriptionValues(): Promise<void> {
|
||||
this._networkShareContainerSubscriptionDropdown.loading = true;
|
||||
this._fileShareSubscriptionDropdown.loading = true;
|
||||
this._blobContainerSubscriptionDropdown.loading = true;
|
||||
|
||||
let subscriptions: azureResource.AzureResourceSubscription[] = [];
|
||||
|
||||
try {
|
||||
subscriptions = await getSubscriptions(this.migrationStateModel.azureAccount);
|
||||
subscriptions.forEach((subscription) => {
|
||||
this._subscriptionMap.set(subscription.id, subscription);
|
||||
this._subscriptionDropdownValues.push({
|
||||
name: subscription.id,
|
||||
displayName: subscription.name + ' - ' + subscription.id,
|
||||
});
|
||||
});
|
||||
|
||||
if (!this._subscriptionDropdownValues) {
|
||||
this._subscriptionDropdownValues = [
|
||||
{
|
||||
displayName: constants.NO_SUBSCRIPTIONS_FOUND,
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
this._fileShareSubscriptionDropdown.values = this._subscriptionDropdownValues;
|
||||
this._networkShareContainerSubscriptionDropdown.values = this._subscriptionDropdownValues;
|
||||
this._blobContainerSubscriptionDropdown.values = this._subscriptionDropdownValues;
|
||||
|
||||
this._networkShare.storageSubscriptionId = this._subscriptionDropdownValues[0].name;
|
||||
this._fileShare.subscriptionId = this._subscriptionDropdownValues[0].name;
|
||||
this._blob.subscriptionId = this._subscriptionDropdownValues[0].name;
|
||||
|
||||
} catch (error) {
|
||||
|
||||
console.log(error);
|
||||
this.setEmptyDropdownPlaceHolder(this._fileShareSubscriptionDropdown, constants.NO_SUBSCRIPTIONS_FOUND);
|
||||
this.setEmptyDropdownPlaceHolder(this._networkShareContainerSubscriptionDropdown, constants.NO_SUBSCRIPTIONS_FOUND);
|
||||
this.setEmptyDropdownPlaceHolder(this._blobContainerSubscriptionDropdown, constants.NO_SUBSCRIPTIONS_FOUND);
|
||||
}
|
||||
|
||||
this._networkShareContainerSubscriptionDropdown.loading = false;
|
||||
this._fileShareSubscriptionDropdown.loading = false;
|
||||
this._blobContainerSubscriptionDropdown.loading = false;
|
||||
|
||||
await this.loadNetworkShareStorageDropdown();
|
||||
await this.loadFileShareStorageDropdown();
|
||||
await this.loadblobStorageDropdown();
|
||||
this._networkShareContainerSubscriptionDropdown.validate();
|
||||
this._networkShareContainerStorageAccountDropdown.validate();
|
||||
}
|
||||
|
||||
private async loadNetworkShareStorageDropdown(): Promise<void> {
|
||||
this._networkShareContainerStorageAccountDropdown.loading = true;
|
||||
|
||||
const subscriptionId = (<azdata.CategoryValue>this._networkShareContainerSubscriptionDropdown.value).name;
|
||||
if (!subscriptionId.length) {
|
||||
this.setEmptyDropdownPlaceHolder(this._networkShareContainerStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND);
|
||||
} else {
|
||||
const storageAccounts = await this.loadStorageAccounts(this._networkShare.storageSubscriptionId);
|
||||
|
||||
if (storageAccounts && storageAccounts.length) {
|
||||
this._networkShareContainerStorageAccountDropdown.values = storageAccounts.map(s => <azdata.CategoryValue>{ name: s.id, displayName: s.name });
|
||||
this._networkShare.storageAccountId = storageAccounts[0].id;
|
||||
}
|
||||
else {
|
||||
this.setEmptyDropdownPlaceHolder(this._networkShareContainerStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND);
|
||||
}
|
||||
}
|
||||
this._networkShareContainerStorageAccountDropdown.loading = false;
|
||||
}
|
||||
|
||||
private async loadFileShareStorageDropdown(): Promise<void> {
|
||||
this._fileShareStorageAccountDropdown.loading = true;
|
||||
this._fileShareFileShareDropdown.loading = true;
|
||||
|
||||
const subscriptionId = (<azdata.CategoryValue>this._fileShareSubscriptionDropdown.value).name;
|
||||
if (!subscriptionId.length) {
|
||||
this.setEmptyDropdownPlaceHolder(this._fileShareStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND);
|
||||
} else {
|
||||
const storageAccounts = await this.loadStorageAccounts(this._fileShare.subscriptionId);
|
||||
if (storageAccounts && storageAccounts.length) {
|
||||
this._fileShareStorageAccountDropdown.values = storageAccounts.map(s => <azdata.CategoryValue>{ name: s.id, displayName: s.name });
|
||||
this._fileShare.storageAccountId = storageAccounts[0].id;
|
||||
}
|
||||
else {
|
||||
this.setEmptyDropdownPlaceHolder(this._fileShareStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND);
|
||||
this._fileShareStorageAccountDropdown.loading = false;
|
||||
}
|
||||
}
|
||||
this._fileShareStorageAccountDropdown.loading = false;
|
||||
await this.loadFileShareDropdown();
|
||||
}
|
||||
|
||||
private async loadblobStorageDropdown(): Promise<void> {
|
||||
this._blobContainerStorageAccountDropdown.loading = true;
|
||||
this._blobContainerBlobDropdown.loading = true;
|
||||
|
||||
const subscriptionId = (<azdata.CategoryValue>this._blobContainerSubscriptionDropdown.value).name;
|
||||
if (!subscriptionId.length) {
|
||||
this.setEmptyDropdownPlaceHolder(this._blobContainerStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND);
|
||||
} else {
|
||||
const storageAccounts = await this.loadStorageAccounts(this._blob.subscriptionId);
|
||||
if (storageAccounts.length) {
|
||||
this._blobContainerStorageAccountDropdown.values = storageAccounts.map(s => <azdata.CategoryValue>{ name: s.id, displayName: s.name });
|
||||
this._blob.storageAccountId = storageAccounts[0].id;
|
||||
} else {
|
||||
this.setEmptyDropdownPlaceHolder(this._blobContainerStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND);
|
||||
}
|
||||
}
|
||||
this._blobContainerStorageAccountDropdown.loading = false;
|
||||
await this.loadBlobContainerDropdown();
|
||||
}
|
||||
|
||||
private async loadStorageAccounts(subscriptionId: string): Promise<StorageAccount[]> {
|
||||
const storageAccounts = await getAvailableStorageAccounts(this.migrationStateModel.azureAccount, this._subscriptionMap.get(subscriptionId)!);
|
||||
storageAccounts.forEach(s => {
|
||||
this._storageAccountMap.set(s.id, s);
|
||||
});
|
||||
return storageAccounts;
|
||||
}
|
||||
|
||||
private async loadFileShareDropdown(): Promise<void> {
|
||||
this._fileShareFileShareDropdown.loading = true;
|
||||
const storageAccountId = (<azdata.CategoryValue>this._fileShareStorageAccountDropdown.value).name;
|
||||
if (!storageAccountId.length) {
|
||||
this.setEmptyDropdownPlaceHolder(this._fileShareFileShareDropdown, constants.NO_FILESHARES_FOUND);
|
||||
} else {
|
||||
const fileShares = await getFileShares(this.migrationStateModel.azureAccount, this._subscriptionMap.get(this._fileShare.subscriptionId)!, this._storageAccountMap.get(storageAccountId)!);
|
||||
if (fileShares && fileShares.length) {
|
||||
this._fileShareFileShareDropdown.values = fileShares.map(f => <azdata.CategoryValue>{ name: f.id, displayName: f.name });
|
||||
this._fileShare.fileShareId = fileShares[0].id!;
|
||||
} else {
|
||||
this.setEmptyDropdownPlaceHolder(this._fileShareFileShareDropdown, constants.NO_FILESHARES_FOUND);
|
||||
}
|
||||
}
|
||||
this._fileShareFileShareDropdown.loading = false;
|
||||
}
|
||||
|
||||
private async loadBlobContainerDropdown(): Promise<void> {
|
||||
this._blobContainerBlobDropdown.loading = true;
|
||||
const storageAccountId = (<azdata.CategoryValue>this._blobContainerStorageAccountDropdown.value).name;
|
||||
if (!storageAccountId.length) {
|
||||
this.setEmptyDropdownPlaceHolder(this._blobContainerBlobDropdown, constants.NO_BLOBCONTAINERS_FOUND);
|
||||
} else {
|
||||
const blobContainer = await getBlobContainers(this.migrationStateModel.azureAccount, this._subscriptionMap.get(this._blob.subscriptionId)!, this._storageAccountMap.get(storageAccountId)!);
|
||||
if (blobContainer && blobContainer.length) {
|
||||
this._blobContainerBlobDropdown.values = blobContainer.map(f => <azdata.CategoryValue>{ name: f.id, displayName: f.name });
|
||||
this._blob.containerId = blobContainer[0].id!;
|
||||
} else {
|
||||
this.setEmptyDropdownPlaceHolder(this._blobContainerBlobDropdown, constants.NO_BLOBCONTAINERS_FOUND);
|
||||
}
|
||||
}
|
||||
this._blobContainerBlobDropdown.loading = false;
|
||||
}
|
||||
|
||||
private setEmptyDropdownPlaceHolder(dropDown: azdata.DropDownComponent, placeholder: string): void {
|
||||
dropDown.values = [{
|
||||
displayName: placeholder,
|
||||
name: ''
|
||||
}];
|
||||
}
|
||||
|
||||
private addErrorMessage(message: string) {
|
||||
if (!this._errors.includes(message)) {
|
||||
this._errors.push(message);
|
||||
}
|
||||
this.wizard.message = {
|
||||
text: this._errors.join(EOL),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
}
|
||||
|
||||
private removeErrorMessage(message: string) {
|
||||
this._errors = this._errors.filter(e => e !== message);
|
||||
this.wizard.message = {
|
||||
text: this._errors.join(EOL),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,6 @@ import { WIZARD_TITLE } from '../models/strings';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { SKURecommendationPage } from './skuRecommendationPage';
|
||||
import { SubscriptionSelectionPage } from './subscriptionSelectionPage';
|
||||
import { DatabaseBackupPage } from './databaseBackupPage';
|
||||
import { AccountsSelectionPage } from './accountsSelectionPage';
|
||||
|
||||
export class WizardController {
|
||||
constructor(private readonly extensionContext: vscode.ExtensionContext) {
|
||||
@@ -36,9 +34,8 @@ export class WizardController {
|
||||
const sourceConfigurationPage = new SourceConfigurationPage(wizard, stateModel);
|
||||
const skuRecommendationPage = new SKURecommendationPage(wizard, stateModel);
|
||||
const subscriptionSelectionPage = new SubscriptionSelectionPage(wizard, stateModel);
|
||||
const azureAccountsPage = new AccountsSelectionPage(wizard, stateModel);
|
||||
const databaseBackupPage = new DatabaseBackupPage(wizard, stateModel);
|
||||
const pages: MigrationWizardPage[] = [sourceConfigurationPage, skuRecommendationPage, subscriptionSelectionPage, azureAccountsPage, databaseBackupPage];
|
||||
|
||||
const pages: MigrationWizardPage[] = [sourceConfigurationPage, skuRecommendationPage, subscriptionSelectionPage];
|
||||
|
||||
wizard.pages = pages.map(p => p.getwizardPage());
|
||||
|
||||
|
||||
@@ -1660,8 +1660,9 @@ class ButtonWrapper extends ComponentWithIconWrapper implements azdata.ButtonCom
|
||||
class LoadingComponentWrapper extends ComponentWrapper implements azdata.LoadingComponent {
|
||||
constructor(proxy: MainThreadModelViewShape, handle: number, id: string) {
|
||||
super(proxy, handle, ModelComponentTypes.LoadingComponent, id);
|
||||
this.properties = {};
|
||||
this.loading = true;
|
||||
this.properties = {
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
public get loading(): boolean {
|
||||
|
||||
@@ -42,6 +42,9 @@ export default class LoadingComponent extends ComponentBase<azdata.LoadingCompon
|
||||
if (!this._component) {
|
||||
return true;
|
||||
}
|
||||
if (this.loading) {
|
||||
return false;
|
||||
}
|
||||
return this.modelStore.getComponent(this._component.id).validate();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export default () => `
|
||||
<div class="flex list-header-container">
|
||||
<i class="icon-document themed-icon"></i>
|
||||
<span class="list-header">${escape(localize('welcomePage.name', "Name"))}</span>
|
||||
<span class="list-header-last-opened">${escape(localize('welcomePage.lastOpened', "Last Opened"))}</span>
|
||||
<span class="list-header-last-opened">${escape(localize('welcomePage.location', "Location"))}</span>
|
||||
</div>
|
||||
<ul class="list">
|
||||
<!-- Filled programmatically -->
|
||||
|
||||
@@ -335,7 +335,7 @@ class WelcomePage extends Disposable {
|
||||
}
|
||||
const workspacesToShow = workspaces.slice(0, 5);
|
||||
clearNode(ul);
|
||||
await this.mapListEntries(workspacesToShow, fileService, container, ul);
|
||||
await this.mapListEntries(workspacesToShow, container, ul);
|
||||
}).then(undefined, onUnexpectedError);
|
||||
this.addExtensionList(container, '.extension-list');
|
||||
this.addExtensionPack(container, '.extensionPack');
|
||||
@@ -469,21 +469,16 @@ class WelcomePage extends Disposable {
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
private async createListEntries(container: HTMLElement, fileService: IFileService, fullPath: URI, windowOpenable: IWindowOpenable, relativePath: string): Promise<HTMLElement[]> {
|
||||
private async createListEntries(container: HTMLElement, fullPath: string, windowOpenable: IWindowOpenable): Promise<HTMLElement[]> {
|
||||
let result: HTMLElement[] = [];
|
||||
const value = await fileService.resolve(fullPath);
|
||||
let date = new Date(value.mtime);
|
||||
let mtime: Date = date;
|
||||
const options = { weekday: 'short', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' };
|
||||
const lastOpened: string = mtime.toLocaleDateString(undefined, options);
|
||||
const { name, parentPath } = splitName(relativePath);
|
||||
const { name, parentPath } = splitName(fullPath);
|
||||
const li = document.createElement('li');
|
||||
const icon = document.createElement('i');
|
||||
const a = document.createElement('a');
|
||||
const span = document.createElement('span');
|
||||
icon.title = relativePath;
|
||||
icon.title = fullPath;
|
||||
a.innerText = name;
|
||||
a.title = relativePath;
|
||||
a.title = fullPath;
|
||||
a.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentPath));
|
||||
a.setAttribute('role', 'button');
|
||||
a.href = 'javascript:void(0)';
|
||||
@@ -503,8 +498,8 @@ class WelcomePage extends Disposable {
|
||||
li.appendChild(a);
|
||||
span.classList.add('path');
|
||||
span.classList.add('detail');
|
||||
span.innerText = lastOpened;
|
||||
span.title = relativePath;
|
||||
span.innerText = parentPath;
|
||||
span.title = parentPath;
|
||||
li.appendChild(span);
|
||||
const ul = container.querySelector('.list');
|
||||
ul.appendChild(li);
|
||||
@@ -512,22 +507,20 @@ class WelcomePage extends Disposable {
|
||||
return result;
|
||||
}
|
||||
|
||||
private async mapListEntries(recents: (IRecentWorkspace | IRecentFolder)[], fileService: IFileService, container: HTMLElement, ul: HTMLElement): Promise<HTMLElement[]> {
|
||||
private async mapListEntries(recents: (IRecentWorkspace | IRecentFolder)[], container: HTMLElement, ul: HTMLElement): Promise<HTMLElement[]> {
|
||||
const result: HTMLElement[] = [];
|
||||
for (let i = 0; i < recents.length; i++) {
|
||||
const recent = recents[i];
|
||||
let relativePath: string;
|
||||
let fullPath: URI;
|
||||
let fullPath: string;
|
||||
let windowOpenable: IWindowOpenable;
|
||||
if (isRecentFolder(recent)) {
|
||||
windowOpenable = { folderUri: recent.folderUri };
|
||||
relativePath = recent.label || this.labelService.getWorkspaceLabel(recent.folderUri, { verbose: true });
|
||||
fullPath = recent.folderUri;
|
||||
fullPath = recent.label || this.labelService.getWorkspaceLabel(recent.folderUri, { verbose: true });
|
||||
} else {
|
||||
relativePath = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: true });
|
||||
fullPath = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: true });
|
||||
windowOpenable = { workspaceUri: recent.workspace.configPath };
|
||||
}
|
||||
const elements = await this.createListEntries(container, fileService, fullPath, windowOpenable, relativePath);
|
||||
const elements = await this.createListEntries(container, fullPath, windowOpenable);
|
||||
result.push(...elements);
|
||||
}
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user