Skip to content
This repository has been archived by the owner on Dec 7, 2021. It is now read-only.

Fix: Enables selection of azure region for custom vision export #765

Merged
merged 5 commits into from
Apr 18, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/common/localization/en-us.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,28 @@ export const english: IAppStrings = {
},
azureCV: {
displayName: "Azure Custom Vision Service",
regions: {
australiaEast: "Australia East",
centralIndia: "Central India",
eastUs: "East US",
eastUs2: "East US 2",
japanEast: "Japan East",
northCentralUs: "North Central US",
northEurope: "North Europe",
southCentralUs: "South Central US",
southeastAsia: "Southeast Asia",
ukSouth: "UK South",
westUs2: "West US 2",
westEurope: "West Europe",
},
properties: {
apiKey: {
title: "API Key",
},
region: {
title: "Region",
description: "The Azure region where your service is deployed",
},
classificationType: {
title: "Classification Type",
options: {
Expand Down
18 changes: 18 additions & 0 deletions src/common/localization/es-cl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,10 +302,28 @@ export const spanish: IAppStrings = {
},
azureCV: {
displayName: "Servicio de Visión Personalizada Azure",
regions: {
australiaEast: "Australia este",
centralIndia: "Centro de la India",
eastUs: "Este de EE.",
eastUs2: "Este US 2",
japanEast: "Japón este",
northCentralUs: "Centro norte de EE.",
northEurope: "Europa del norte",
southCentralUs: "Centro sur de EE.",
southeastAsia: "Sudeste asiático",
ukSouth: "UK sur",
westUs2: "West US 2",
westEurope: "Europa occidental",
},
properties: {
apiKey: {
title: "Clave de API",
},
region: {
title: "Región",
description: "La región de Azure donde se implementa el servicio",
},
classificationType: {
title: "Tipo de clasificación",
options: {
Expand Down
18 changes: 18 additions & 0 deletions src/common/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,10 +298,28 @@ export interface IAppStrings {
},
azureCV: {
displayName: string,
regions: {
eastUs: string,
eastUs2: string,
northCentralUs: string,
southCentralUs: string,
westUs2: string,
westEurope: string,
northEurope: string,
southeastAsia: string,
australiaEast: string,
centralIndia: string,
ukSouth: string,
japanEast: string,
},
properties: {
apiKey: {
title: string,
},
region: {
title: string,
description: string,
},
newOrExisting: {
title: string,
options: {
Expand Down
37 changes: 36 additions & 1 deletion src/providers/export/azureCustomVision.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"title": "${strings.export.providers.azureCV.displayName}",
"required": [
"assetState",
"apiKey"
"apiKey",
"region"
],
"properties": {
"assetState": {
Expand All @@ -22,6 +23,40 @@
"${strings.export.providers.common.properties.assetState.options.tagged}"
]
},
"region": {
"type": "string",
"title": "${strings.export.providers.azureCV.properties.region.title}",
"description": "${strings.export.providers.azureCV.properties.region.description}",
"default": "southcentralus",
"enum": [
"australiaeast",
"centralindia",
"eastus",
"eastus2",
"japaneast",
"northcentralus",
"northeurope",
"southcentralus",
"southeastasia",
"uksouth",
"westus2",
"westeurope"
],
"enumNames": [
"${strings.export.providers.azureCV.regions.australiaEast}",
"${strings.export.providers.azureCV.regions.centralIndia}",
"${strings.export.providers.azureCV.regions.eastUs}",
"${strings.export.providers.azureCV.regions.eastUs2}",
"${strings.export.providers.azureCV.regions.japanEast}",
"${strings.export.providers.azureCV.regions.northCentralUs}",
"${strings.export.providers.azureCV.regions.northEurope}",
"${strings.export.providers.azureCV.regions.southCentralUs}",
"${strings.export.providers.azureCV.regions.southeastAsia}",
"${strings.export.providers.azureCV.regions.ukSouth}",
"${strings.export.providers.azureCV.regions.westUs2}",
"${strings.export.providers.azureCV.regions.westEurope}"
]
},
"apiKey": {
"type": "string",
"title": "${strings.export.providers.azureCV.properties.apiKey.title}"
Expand Down
21 changes: 19 additions & 2 deletions src/providers/export/azureCustomVision.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import shortid from "shortid";
import _ from "lodash";
import { AzureCustomVisionProvider, IAzureCustomVisionExportOptions, NewOrExisting } from "./azureCustomVision";
import {
AzureCustomVisionProvider, IAzureCustomVisionExportOptions,
NewOrExisting, AzureRegion,
} from "./azureCustomVision";
import registerProviders from "../../registerProviders";
import { ExportProviderFactory } from "./exportProviderFactory";
import MockFactory from "../../common/mockFactory";
import {
IProject, AssetState, IAsset, IAssetMetadata,
RegionType, IRegion, IExportProviderOptions, AssetType,
RegionType, IRegion, IExportProviderOptions,
} from "../../models/applicationState";
import { ExportAssetState } from "./exportProvider";
jest.mock("./azureCustomVision/azureCustomVisionService");
Expand All @@ -29,6 +32,7 @@ describe("Azure Custom Vision Export Provider", () => {
let testProject: IProject = null;
const defaultOptions: IAzureCustomVisionExportOptions = {
apiKey: expect.any(String),
region: AzureRegion.SouthCentralUS,
assetState: ExportAssetState.All,
newOrExisting: NewOrExisting.New,
projectId: expect.any(String),
Expand Down Expand Up @@ -64,6 +68,7 @@ describe("Azure Custom Vision Export Provider", () => {
assetState: ExportAssetState.All,
projectId: "azure-custom-vision-project-1",
apiKey: "ABC123",
region: AzureRegion.SouthCentralUS,
},
},
};
Expand All @@ -81,6 +86,18 @@ describe("Azure Custom Vision Export Provider", () => {
expect(provider).toBeInstanceOf(AzureCustomVisionProvider);
});

it("Constructs custom vision service with correct options", () => {
const customVisionMock = AzureCustomVisionService as jest.Mocked<typeof AzureCustomVisionService>;
const providerOptions = testProject.exportFormat.providerOptions as IAzureCustomVisionExportOptions;
providerOptions.region = AzureRegion.WestEurope;
createProvider(testProject);

expect(customVisionMock).toBeCalledWith({
apiKey: providerOptions.apiKey,
baseUrl: `https://${providerOptions.region}.api.cognitive.microsoft.com/customvision/v2.2/Training`,
});
});

it("Calling save with New project creates Azure Custom Vision project", async () => {
const customVisionMock = AzureCustomVisionService as jest.Mocked<typeof AzureCustomVisionService>;
customVisionMock.prototype.create = jest.fn((project) => {
Expand Down
26 changes: 25 additions & 1 deletion src/providers/export/azureCustomVision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import HtmlFileReader from "../../common/htmlFileReader";
export interface IAzureCustomVisionExportOptions extends IExportProviderOptions {
assetState: ExportAssetState;
newOrExisting: NewOrExisting;
region: AzureRegion;
apiKey: string;
projectId?: string;
name?: string;
Expand All @@ -38,6 +39,24 @@ export enum NewOrExisting {
Existing = "existing",
}

/**
* Azure regions
*/
export enum AzureRegion {
wbreza marked this conversation as resolved.
Show resolved Hide resolved
EastUS = "eastus",
EastUS2 = "eastus2",
NorthCentralUS = "northcentralus",
SouthCentralUS = "southcentralus",
WestUS2 = "westus2",
WestEurope = "westeurope",
NorthEurope = "northeurope",
SoutheastAsia = "southeastasia",
AustraliaEast = "australiaeast",
CentralIndia = "centralindia",
UKSouth = "uksouth",
JapanEast = "japaneast",
}

/**
* @name - Azure Custom Vision Provider
* @description - Exports a VoTT project into an Azure custom vision project
Expand All @@ -49,9 +68,13 @@ export class AzureCustomVisionProvider extends ExportProvider<IAzureCustomVision
super(project, options);
Guard.null(options);

if (!options.region) {
options.region = AzureRegion.SouthCentralUS;
}

const cusomVisionServiceOptions: IAzureCustomVisionServiceOptions = {
apiKey: options.apiKey,
baseUrl: "https://southcentralus.api.cognitive.microsoft.com/customvision/v2.2/Training",
baseUrl: `https://${options.region}.api.cognitive.microsoft.com/customvision/v2.2/Training`,
};
this.customVisionService = new AzureCustomVisionService(cusomVisionServiceOptions);
}
Expand Down Expand Up @@ -111,6 +134,7 @@ export class AzureCustomVisionProvider extends ExportProvider<IAzureCustomVision

return {
assetState: customVisionOptions.assetState,
region: customVisionOptions.region,
apiKey: customVisionOptions.apiKey,
projectId: customVisionProject.id,
newOrExisting: NewOrExisting.Existing,
Expand Down
4 changes: 2 additions & 2 deletions src/providers/export/azureCustomVision.ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"ui:widget": "externalPicker",
"ui:options": {
"method": "GET",
"url": "https://southcentralus.api.cognitive.microsoft.com/customvision/v2.2/Training/projects",
"url": "https://${props.formContext.providerOptions.region}.api.cognitive.microsoft.com/customvision/v2.2/Training/projects",
"authHeaderName": "Training-key",
"authHeaderValue": "${props.formContext.providerOptions.apiKey}",
"keySelector": "${item.id}",
Expand All @@ -20,7 +20,7 @@
"ui:widget": "externalPicker",
"ui:options": {
"method": "GET",
"url": "https://southcentralus.api.cognitive.microsoft.com/customvision/v2.2/Training/domains",
"url": "https://${props.formContext.providerOptions.region}.api.cognitive.microsoft.com/customvision/v2.2/Training/domains",
"authHeaderName": "Training-key",
"authHeaderValue": "${props.formContext.providerOptions.apiKey}",
"keySelector": "${item.id}",
Expand Down
70 changes: 47 additions & 23 deletions src/react/components/common/externalPicker/externalPicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import ExternalPicker, { IExternalPickerProps, IExternalPickerState } from "./ex
import MockFactory from "../../../../common/mockFactory";

describe("External Picker", () => {
let wrapper: ReactWrapper<IExternalPickerProps, IExternalPickerState> = null;
const onChangeHandler = jest.fn();
const onChangeHandler = jest.fn(() => {
console.log("hi");
});
const defaultProps = createProps({
id: "my-custom-control",
value: "",
Expand All @@ -16,12 +17,13 @@ describe("External Picker", () => {
formContext: {
providerOptions: {
apiKey: "",
region: "",
},
},
onChange: onChangeHandler,
options: {
method: "GET",
url: "https://myserver/api",
url: "https://${props.formContext.providerOptions.region}.server.com/api",
keySelector: "${item.key}",
valueSelector: "${item.value}",
authHeaderName: "Authorization",
Expand All @@ -48,40 +50,42 @@ describe("External Picker", () => {
});
});

beforeEach(() => {
wrapper = createComponent(defaultProps as IExternalPickerProps);
});

it("Renders select element with default option", () => {
const wrapper = createComponent(defaultProps);
expect(wrapper.find("select").length).toEqual(1);
expect(wrapper.find("option").length).toEqual(1);
});

it("Does not bind external data if authorization is missing", () => {
createComponent(defaultProps);
expect(axios.request).not.toBeCalled();
});

it("Renders items bound from external data when formContext rebinds", async () => {
const expectedApiKey = "ABC123";

await MockFactory.flushUi(() => {
wrapper.setProps({
formContext: {
providerOptions: {
apiKey: expectedApiKey,
},
const expectedRegion = "southcentralus";

const props = {
...defaultProps,
formContext: {
providerOptions: {
apiKey: expectedApiKey,
region: expectedRegion,
},
});
});
},
};

const wrapper = createComponent(props);

await MockFactory.flushUi();
wrapper.update();

const expectedHeaders = {};
expectedHeaders[defaultProps.options.authHeaderName] = expectedApiKey;

expect(axios.request).toBeCalledWith({
method: defaultProps.options.method,
url: defaultProps.options.url,
url: `https://${expectedRegion}.server.com/api`,
headers: expectedHeaders,
});

Expand All @@ -93,19 +97,39 @@ describe("External Picker", () => {
});

it("Calls onChange event handler on option selection", () => {
wrapper.setProps({
formContext: {},
});
const wrapper = createComponent(defaultProps);

wrapper.find("select").simulate("change", { target: { value: testResponse[0].key } });
expect(onChangeHandler).toBeCalledWith(testResponse[0].key);
});

it("Clears items when HTTP request fails", async () => {
const requestMock = axios.request as jest.Mock;
requestMock.mockImplementationOnce(() => Promise.reject({ status: 400 }));

const expectedApiKey = "ABC123";
const expectedRegion = "southcentralus";

const props = {
...defaultProps,
formContext: {
providerOptions: {
apiKey: expectedApiKey,
region: expectedRegion,
},
},
};

const wrapper = createComponent(props);
await MockFactory.flushUi();

expect(wrapper.state().items).toEqual([]);
expect(onChangeHandler).toBeCalledWith(undefined);
});

function createProps(otherProps: any): IExternalPickerProps {
const props: IExternalPickerProps = {
return {
...otherProps,
};

return props;
}
});
Loading