diff --git a/package.json b/package.json index ab50f53b4..ff4811846 100644 --- a/package.json +++ b/package.json @@ -204,7 +204,7 @@ "pack:fast": "node --max_old_space_size=10196 ./node_modules/webpack/bin/webpack.js --mode development --progress", "copyToConsumers": "node copyToConsumers", "test": "rimraf coverage && jest", - "test:e2e": "jest -c ./jest.config.e2e.js --detectOpenHandles", + "test:e2e": "jest -c ./jest.config.e2e.js --detectOpenHandles selfServeExample.spec.ts", "watch": "npm run start", "wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/", "build:ase": "gulp build:ase", @@ -238,4 +238,4 @@ "prettier": { "printWidth": 120 } -} +} \ No newline at end of file diff --git a/src/Localization/en/translations.json b/src/Localization/en/translations.json index 44d4506cd..f10775900 100644 --- a/src/Localization/en/translations.json +++ b/src/Localization/en/translations.json @@ -36,57 +36,6 @@ "SubmissionMessageErrorText": "Data update failed because of errors.", "OnSaveFailureMessage": "Data save operation not currently permitted." }, - "SqlX": { - "DedicatedGatewayDescription": "Provision a dedicated gateway cluster for your Azure Cosmos DB account. A dedicated gateway is compute that is a front-end to data in your Azure Cosmos DB account. Your dedicated gateway automatically includes the integrated cache, which can improve read performance. ", - "DedicatedGateway": "Dedicated Gateway", - "Enable": "Enable", - "Disable": "Disable", - "LearnAboutDedicatedGateway": "Learn more about dedicated gateway.", - "DeprovisioningDetailsText": "Learn more about deprovisioning the dedicated gateway.", - "DedicatedGatewayPricing": "Learn more about dedicated gateway pricing", - "SKUs": "SKUs", - "SKUsPlaceHolder": "Select SKUs", - "NumberOfInstances": "Number of instances", - "CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)", - "CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)", - "CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)", - "CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)", - "CreateMessage": "Dedicated gateway resource is being created.", - "CreateInitializeTitle": "Provisioning resource", - "CreateInitializeMessage": "Dedicated gateway resource will be provisioned.", - "CreateSuccessTitle": "Resource provisioned", - "CreateSuccesseMessage": "Dedicated gateway resource provisioned.", - "CreateFailureTitle": "Failed to provision resource", - "CreateFailureMessage": "Dedicated gateway resource provisioning failed.", - "UpdateMessage": "Dedicated gateway resource is being updated.", - "UpdateInitializeTitle": "Updating resource", - "UpdateInitializeMessage": "Dedicated gateway resource will be updated.", - "UpdateSuccessTitle": "Resource updated", - "UpdateSuccesseMessage": "Dedicated gateway resource updated.", - "UpdateFailureTitle": "Failed to update resource", - "UpdateFailureMessage": "Dedicated gateway resource updation failed.", - "DeleteMessage": "Dedicated gateway resource is being deleted.", - "DeleteInitializeTitle": "Deleting resource", - "DeleteInitializeMessage": "Dedicated gateway resource will be deleted.", - "DeleteSuccessTitle": "Resource deleted", - "DeleteSuccesseMessage": "Dedicated gateway resource deleted.", - "DeleteFailureTitle": "Failed to delete resource", - "DeleteFailureMessage": "Dedicated gateway resource deletion failed.", - "CannotSave": "Cannot save the changes to the Dedicated gateway resource at the moment", - "DedicatedGatewayEndpoint": "Dedicated gatewayEndpoint", - "NoValue": "", - "SKUDetails": "SKU Details: ", - "CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory", - "CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory", - "CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory", - "CosmosD32Details": "General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory", - "Cost": "Cost", - "CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.", - "ConnectionString": "Connection String", - "ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ", - "KeysBlade": "the keys blade", - "WarningBannerOnUpdate": "Adding or modifying dedicated gateway instances may affect your bill.", - "WarningBannerOnDelete": "After deprovisioning the dedicated gateway, you must update any applications using the old dedicated gateway connection string." - } + "SqlX": {} } } \ No newline at end of file diff --git a/src/SelfServe/SqlX/SqlX.rp.ts b/src/SelfServe/SqlX/SqlX.rp.ts index b5a097fca..bf564a7b4 100644 --- a/src/SelfServe/SqlX/SqlX.rp.ts +++ b/src/SelfServe/SqlX/SqlX.rp.ts @@ -1,98 +1,33 @@ import { RefreshResult } from "../SelfServeTypes"; -import { userContext } from "../../UserContext"; -import { armRequestWithoutPolling } from "../../Utils/arm/request"; -import { configContext } from "../../ConfigContext"; -import { SqlxServiceResource, UpdateDedicatedGatewayRequestParameters } from "./SqlxTypes"; - -const apiVersion = "2020-06-01-preview"; - -export enum ResourceStatus { - Running = "Running", - Creating = "Creating", - Updating = "Updating", - Deleting = "Deleting", -} export interface DedicatedGatewayResponse { sku: string; instances: number; - status: string; - endpoint: string; } -export const getPath = (subscriptionId: string, resourceGroup: string, name: string): string => { - return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/sqlx`; +export const getRegionSpecificMinInstances = async (): Promise => { + // TODO: write RP call to get min number of instances needed for this region + throw new Error("getRegionSpecificMinInstances not implemented"); }; -export const updateDedicatedGatewayResource = async (sku: string, instances: number): Promise => { - const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); - const body: UpdateDedicatedGatewayRequestParameters = { - properties: { - instanceSize: sku, - instanceCount: instances, - serviceType: "Sqlx", - }, - }; - const armRequestResult = await armRequestWithoutPolling({ - host: configContext.ARM_ENDPOINT, - path, - method: "PUT", - apiVersion, - body, - }); - return armRequestResult.operationStatusUrl; +export const getRegionSpecificMaxInstances = async (): Promise => { + // TODO: write RP call to get max number of instances needed for this region + throw new Error("getRegionSpecificMaxInstances not implemented"); }; -export const deleteDedicatedGatewayResource = async (): Promise => { - const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); - const armRequestResult = await armRequestWithoutPolling({ - host: configContext.ARM_ENDPOINT, - path, - method: "DELETE", - apiVersion, - }); - return armRequestResult.operationStatusUrl; +export const updateDedicatedGatewayProvisioning = async (sku: string, instances: number): Promise => { + // TODO: write RP call to update dedicated gateway provisioning + throw new Error( + `updateDedicatedGatewayProvisioning not implemented. Parameters- sku: ${sku}, instances:${instances}` + ); }; -export const getDedicatedGatewayResource = async (): Promise => { - const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); - const armRequestResult = await armRequestWithoutPolling({ - host: configContext.ARM_ENDPOINT, - path, - method: "GET", - apiVersion, - }); - return armRequestResult.result; -}; - -export const getCurrentProvisioningState = async (): Promise => { - try { - const response = await getDedicatedGatewayResource(); - return { - sku: response.properties.instanceSize, - instances: response.properties.instanceCount, - status: response.properties.status, - endpoint: response.properties.sqlxEndPoint, - }; - } catch (e) { - return { sku: undefined, instances: undefined, status: undefined, endpoint: undefined }; - } +export const initializeDedicatedGatewayProvisioning = async (): Promise => { + // TODO: write RP call to initialize UI for dedicated gateway provisioning + throw new Error("initializeDedicatedGatewayProvisioning not implemented"); }; export const refreshDedicatedGatewayProvisioning = async (): Promise => { - try { - const response = await getDedicatedGatewayResource(); - if (response.properties.status === ResourceStatus.Running.toString()) { - return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined }; - } else if (response.properties.status === ResourceStatus.Creating.toString()) { - return { isUpdateInProgress: true, updateInProgressMessageTKey: "CreateMessage" }; - } else if (response.properties.status === ResourceStatus.Deleting.toString()) { - return { isUpdateInProgress: true, updateInProgressMessageTKey: "DeleteMessage" }; - } else { - return { isUpdateInProgress: true, updateInProgressMessageTKey: "UpdateMessage" }; - } - } catch { - //TODO differentiate between different failures - return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined }; - } + // TODO: write RP call to check if dedicated gateway update has gone through + throw new Error("refreshDedicatedGatewayProvisioning not implemented"); }; diff --git a/src/SelfServe/SqlX/SqlX.tsx b/src/SelfServe/SqlX/SqlX.tsx index e8308b651..4b6a0bc2c 100644 --- a/src/SelfServe/SqlX/SqlX.tsx +++ b/src/SelfServe/SqlX/SqlX.tsx @@ -1,7 +1,6 @@ -import { IsDisplayable, OnChange, RefreshOptions, Values } from "../Decorators"; +import { IsDisplayable, OnChange, Values } from "../Decorators"; import { ChoiceItem, - Description, DescriptionType, InputType, NumberUiType, @@ -10,284 +9,65 @@ import { SelfServeBaseClass, SmartUiInput, } from "../SelfServeTypes"; -import { BladeType, generateBladeLink } from "../SelfServeUtils"; -import { - deleteDedicatedGatewayResource, - getCurrentProvisioningState, - refreshDedicatedGatewayProvisioning, - updateDedicatedGatewayResource, -} from "./SqlX.rp"; - -const costPerHourValue: Description = { - textTKey: "CostText", - type: DescriptionType.Text, - link: { - href: "https://azure.microsoft.com/en-us/pricing/details/cosmos-db/", - textTKey: "DedicatedGatewayPricing", - }, -}; - -const connectionStringValue: Description = { - textTKey: "ConnectionStringText", - type: DescriptionType.Text, - link: { - href: generateBladeLink(BladeType.SqlKeys), - textTKey: "KeysBlade", - }, -}; - -const CosmosD4s = "Cosmos.D4s"; -const CosmosD8s = "Cosmos.D8s"; -const CosmosD16s = "Cosmos.D16s"; -const CosmosD32s = "Cosmos.D32s"; - -const getSKUDetails = (sku: string): string => { - if (sku === CosmosD4s) { - return "CosmosD4Details"; - } else if (sku === CosmosD8s) { - return "CosmosD8Details"; - } else if (sku === CosmosD16s) { - return "CosmosD16Details"; - } else if (sku === CosmosD32s) { - return "CosmosD32Details"; - } - return "Not Supported Yet"; -}; - -const onSKUChange = (newValue: InputType, currentValues: Map): Map => { - currentValues.set("sku", { value: newValue }); - currentValues.set("skuDetails", { - value: { textTKey: getSKUDetails(`${newValue.toString()}`), type: DescriptionType.Text } as Description, - }); - currentValues.set("costPerHour", { value: costPerHourValue }); - return currentValues; -}; - -const onNumberOfInstancesChange = ( - newValue: InputType, - currentValues: Map -): Map => { - currentValues.set("instances", { value: newValue }); - currentValues.set("warningBanner", { - value: { textTKey: "WarningBannerOnUpdate" } as Description, - hidden: false, - }); - - return currentValues; -}; +import { refreshDedicatedGatewayProvisioning } from "./SqlX.rp"; const onEnableDedicatedGatewayChange = ( newValue: InputType, - currentValues: Map, - baselineValues: ReadonlyMap + currentState: Map ): Map => { - currentValues.set("enableDedicatedGateway", { value: newValue }); - const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean; - if (dedicatedGatewayOriginallyEnabled === newValue) { - currentValues.set("sku", baselineValues.get("sku")); - currentValues.set("instances", baselineValues.get("instances")); - currentValues.set("skuDetails", baselineValues.get("skuDetails")); - currentValues.set("costPerHour", baselineValues.get("costPerHour")); - currentValues.set("warningBanner", baselineValues.get("warningBanner")); - currentValues.set("connectionString", baselineValues.get("connectionString")); - return currentValues; - } - - currentValues.set("warningBanner", undefined); - if (newValue === true) { - currentValues.set("warningBanner", { - value: { - textTKey: "WarningBannerOnUpdate", - link: { - href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", - textTKey: "DedicatedGatewayPricing", - }, - } as Description, - hidden: false, - }); - } else { - currentValues.set("warningBanner", { - value: { - textTKey: "WarningBannerOnDelete", - link: { - href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", - textTKey: "DeprovisioningDetailsText", - }, - } as Description, - hidden: false, - }); - } - const sku = currentValues.get("sku"); - const instances = currentValues.get("instances"); - const hideAttributes = newValue === undefined || !(newValue as boolean); - currentValues.set("sku", { - value: sku.value, - hidden: hideAttributes, - disabled: dedicatedGatewayOriginallyEnabled, - }); - currentValues.set("instances", { - value: instances.value, - hidden: hideAttributes, - disabled: dedicatedGatewayOriginallyEnabled, - }); - - currentValues.set("skuDetails", { - value: { textTKey: getSKUDetails(`${currentValues.get("sku").value}`), type: DescriptionType.Text } as Description, - hidden: hideAttributes, - disabled: dedicatedGatewayOriginallyEnabled, - }); - - currentValues.set("costPerHour", { value: costPerHourValue, hidden: hideAttributes }); - currentValues.set("connectionString", { - value: connectionStringValue, - hidden: !newValue || !dedicatedGatewayOriginallyEnabled, - }); - - return currentValues; + const sku = currentState.get("sku"); + const instances = currentState.get("instances"); + const isSkuHidden = newValue === undefined || !(newValue as boolean); + currentState.set("enableDedicatedGateway", { value: newValue }); + currentState.set("sku", { value: sku.value, hidden: isSkuHidden }); + currentState.set("instances", { value: instances.value, hidden: isSkuHidden }); + return currentState; }; -const skuDropDownItems: ChoiceItem[] = [ - { label: "CosmosD4s", key: CosmosD4s }, - { label: "CosmosD8s", key: CosmosD8s }, - { label: "CosmosD16s", key: CosmosD16s }, - { label: "CosmosD32s", key: CosmosD32s }, -]; - const getSkus = async (): Promise => { - return skuDropDownItems; + // TODO: get SKUs from getRegionSpecificSkus() RP call and return array of {label:..., key:...}. + throw new Error("getSkus not implemented."); }; const getInstancesMin = async (): Promise => { - return 1; + // TODO: get SKUs from getRegionSpecificSkus() RP call and return array of {label:..., key:...}. + throw new Error("getInstancesMin not implemented."); }; const getInstancesMax = async (): Promise => { - return 5; + // TODO: get SKUs from getRegionSpecificSkus() RP call and return array of {label:..., key:...}. + throw new Error("getInstancesMax not implemented."); +}; + +const validate = (currentValues: Map): void => { + // TODO: add cusom validation logic to be called before Saving the data. + throw new Error(`validate not implemented. No. of properties to validate: ${currentValues.size}`); }; @IsDisplayable() -@RefreshOptions({ retryIntervalInMs: 20000 }) export default class SqlX extends SelfServeBaseClass { public onRefresh = async (): Promise => { - return await refreshDedicatedGatewayProvisioning(); + return refreshDedicatedGatewayProvisioning(); }; - public onSave = async ( - currentValues: Map, - baselineValues: Map - ): Promise => { - const dedicatedGatewayCurrentlyEnabled = currentValues.get("enableDedicatedGateway")?.value as boolean; - const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean; - - currentValues.set("warningBanner", undefined); - - //TODO : Add try catch for each RP call and return relevant notifications - if (dedicatedGatewayOriginallyEnabled) { - if (!dedicatedGatewayCurrentlyEnabled) { - const operationStatusUrl = await deleteDedicatedGatewayResource(); - return { - operationStatusUrl: operationStatusUrl, - portalNotification: { - initialize: { - titleTKey: "DeleteInitializeTitle", - messageTKey: "DeleteInitializeMessage", - }, - success: { - titleTKey: "DeleteSuccessTitle", - messageTKey: "DeleteSuccesseMessage", - }, - failure: { - titleTKey: "DeleteFailureTitle", - messageTKey: "DeleteFailureMessage", - }, - }, - }; - } else { - // Check for scaling up/down/in/out - return { - operationStatusUrl: undefined, - portalNotification: { - initialize: { - titleTKey: "UpdateInitializeTitle", - messageTKey: "UpdateInitializeMessage", - }, - success: { - titleTKey: "UpdateSuccessTitle", - messageTKey: "UpdateSuccesseMessage", - }, - failure: { - titleTKey: "UpdateFailureTitle", - messageTKey: "UpdateFailureMessage", - }, - }, - }; - } - } else { - const sku = currentValues.get("sku")?.value as string; - const instances = currentValues.get("instances").value as number; - const operationStatusUrl = await updateDedicatedGatewayResource(sku, instances); - return { - operationStatusUrl: operationStatusUrl, - portalNotification: { - initialize: { - titleTKey: "CreateInitializeTitle", - messageTKey: "CreateInitializeTitle", - }, - success: { - titleTKey: "CreateSuccessTitle", - messageTKey: "CreateSuccesseMessage", - }, - failure: { - titleTKey: "CreateFailureTitle", - messageTKey: "CreateFailureMessage", - }, - }, - }; - } + public onSave = async (currentValues: Map): Promise => { + validate(currentValues); + // TODO: add pre processing logic before calling the updateDedicatedGatewayProvisioning() RP call. + throw new Error(`onSave not implemented. No. of properties to save: ${currentValues.size}`); }; public initialize = async (): Promise> => { - // Based on the RP call enableDedicatedGateway will be true if it has not yet been enabled and false if it has. - const defaults = new Map(); - defaults.set("enableDedicatedGateway", { value: false }); - defaults.set("sku", { value: CosmosD4s, hidden: true }); - defaults.set("instances", { value: await getInstancesMin(), hidden: true }); - defaults.set("skuDetails", undefined); - defaults.set("costPerHour", undefined); - defaults.set("connectionString", undefined); - - const response = await getCurrentProvisioningState(); - if (response.status && response.status !== "Deleting") { - defaults.set("enableDedicatedGateway", { value: true }); - defaults.set("sku", { value: response.sku, disabled: true }); - defaults.set("instances", { value: response.instances, disabled: true }); - defaults.set("costPerHour", { value: costPerHourValue }); - defaults.set("skuDetails", { - value: { textTKey: getSKUDetails(`${defaults.get("sku").value}`), type: DescriptionType.Text } as Description, - hidden: false, - }); - defaults.set("connectionString", { - value: connectionStringValue, - hidden: false, - }); - } - - defaults.set("warningBanner", undefined); - return defaults; + // TODO: get initialization data from initializeDedicatedGatewayProvisioning() RP call. + throw new Error("onSave not implemented"); }; - @Values({ - isDynamicDescription: true, - }) - warningBanner: string; - @Values({ description: { - textTKey: "DedicatedGatewayDescription", + textTKey: "Provisioning dedicated gateways for SqlX accounts.", type: DescriptionType.Text, link: { href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", - textTKey: "LearnAboutDedicatedGateway", + textTKey: "Learn more about dedicated gateway.", }, }, }) @@ -295,45 +75,25 @@ export default class SqlX extends SelfServeBaseClass { @OnChange(onEnableDedicatedGatewayChange) @Values({ - labelTKey: "DedicatedGateway", - trueLabelTKey: "Provisioned", - falseLabelTKey: "Deprovisioned", + labelTKey: "Dedicated Gateway", + trueLabelTKey: "Enable", + falseLabelTKey: "Disable", }) enableDedicatedGateway: boolean; - @OnChange(onSKUChange) @Values({ labelTKey: "SKUs", choices: getSkus, - placeholderTKey: "SKUsPlaceHolder", + placeholderTKey: "Select SKUs", }) sku: ChoiceItem; @Values({ - labelTKey: "SKUDetails", - isDynamicDescription: true, - }) - skuDetails: string; - - @OnChange(onNumberOfInstancesChange) - @Values({ - labelTKey: "NumberOfInstances", + labelTKey: "Number of instances", min: getInstancesMin, max: getInstancesMax, step: 1, uiType: NumberUiType.Spinner, }) instances: number; - - @Values({ - labelTKey: "Cost", - isDynamicDescription: true, - }) - costPerHour: string; - - @Values({ - labelTKey: "ConnectionString", - isDynamicDescription: true, - }) - connectionString: string; } diff --git a/src/SelfServe/SqlX/SqlxTypes.ts b/src/SelfServe/SqlX/SqlxTypes.ts deleted file mode 100644 index 70557f4f4..000000000 --- a/src/SelfServe/SqlX/SqlxTypes.ts +++ /dev/null @@ -1,31 +0,0 @@ -export type SqlxServiceResource = { - id: string; - name: string; - type: string; - properties: SqlxServiceProps; - locations: SqlxServiceLocations; -}; -export type SqlxServiceProps = { - serviceType: string; - creationTime: string; - status: string; - instanceSize: string; - instanceCount: number; - sqlxEndPoint: string; -}; - -export type SqlxServiceLocations = { - location: string; - status: string; - sqlxEndpoint: string; -}; - -export type UpdateDedicatedGatewayRequestParameters = { - properties: UpdateDedicatedGatewayRequestProperties; -}; - -export type UpdateDedicatedGatewayRequestProperties = { - instanceSize: string; - instanceCount: number; - serviceType: string; -};