diff --git a/src/Localization/en/GraphAPICompute.json b/src/Localization/en/GraphAPICompute.json new file mode 100644 index 000000000..a55102f37 --- /dev/null +++ b/src/Localization/en/GraphAPICompute.json @@ -0,0 +1,57 @@ +{ + "GraphAPIDescription": "Provision a Graph API Compute for your Azure Cosmos DB account.", + "GraphAPICompute": "Graph GraphAPI Compute", + "Provisioned": "Provisioned", + "Deprovisioned": "Deprovisioned", + "Compute": "Compute", + "GremlinV2": "GremlinV2", + "LearnAboutCompute": "Learn more about GraphAPI Compute.", + "DeprovisioningDetailsText": "Learn more about deprovisioning the GraphAPI Compute.", + "ComputePricing": "Learn more about GraphAPI Compute 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": "Graph GraphAPI Compute resource is being created.", + "CreateInitializeTitle": "Provisioning resource", + "CreateInitializeMessage": "GraphAPI Compute resource will be provisioned.", + "CreateSuccessTitle": "Resource provisioned", + "CreateSuccesseMessage": "GraphAPI Compute resource provisioned.", + "CreateFailureTitle": "Failed to provision resource", + "CreateFailureMessage": "GraphAPI Compute resource provisioning failed.", + "UpdateMessage": "GraphAPI Compute resource is being updated.", + "UpdateInitializeTitle": "Updating resource", + "UpdateInitializeMessage": "GraphAPI Compute resource will be updated.", + "UpdateSuccessTitle": "Resource updated", + "UpdateSuccesseMessage": "GraphAPI Compute resource updated.", + "UpdateFailureTitle": "Failed to update resource", + "UpdateFailureMessage": "GraphAPI Compute resource updation failed.", + "DeleteMessage": "GraphAPI Compute resource is being deleted.", + "DeleteInitializeTitle": "Deleting resource", + "DeleteInitializeMessage": "GraphAPI Compute resource will be deleted.", + "DeleteSuccessTitle": "Resource deleted", + "DeleteSuccesseMessage": "GraphAPI Compute resource deleted.", + "DeleteFailureTitle": "Failed to delete resource", + "DeleteFailureMessage": "GraphAPI Compute resource deletion failed.", + "CannotSave": "Cannot save the changes to the GraphAPI Compute resource at the moment.", + "GraphAccountEndpoint": "Graph Account Endpoint", + "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", + "ApproximateCost": "Approximate Cost Per Hour", + "CostText": "Hourly cost of the GraphAPI Compute resource depends on the SKU selection, number of instances per region, and number of regions.", + "ConnectionString": "Connection String", + "ConnectionStringText": "To use the GraphAPI Compute, use the connection string shown in ", + "KeysBlade": "the keys blade.", + "MetricsString": "Metrics", + "MetricsText": "Monitor the CPU and memory usage for the GraphAPI Compute instances in ", + "MetricsBlade": "the metrics blade.", + "MonitorUsage": "Monitor Usage", + "ResizingDecisionText": "Number of instances has to be 1 during provisioning. Instances can only be incremented by 1 at once. ", + "ResizingDecisionLink": "Learn more about GraphAPI Compute sizing.", + "WarningBannerOnUpdate": "Adding or modifying GraphAPI Compute instances may affect your bill.", + "WarningBannerOnDelete": "After deprovisioning the GraphAPI Compute, you will not be able to connect to the Graph API account." +} diff --git a/src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts b/src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts new file mode 100644 index 000000000..a80ebff4e --- /dev/null +++ b/src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts @@ -0,0 +1,200 @@ +import { configContext } from "../../ConfigContext"; +import { userContext } from "../../UserContext"; +import { armRequestWithoutPolling } from "../../Utils/arm/request"; +import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor"; +import { RefreshResult } from "../SelfServeTypes"; +import GraphAPICompute from "./GraphAPICompute"; +import { + FetchPricesResponse, + RegionsResponse, + GraphAPIComputeServiceResource, + UpdateComputeRequestParameters, +} from "./GraphAPICompute.types"; + +const apiVersion = "2021-04-01-preview"; +const gremlinV2 = "GremlinV2"; + +export enum ResourceStatus { + Running = "Running", + Creating = "Creating", + Updating = "Updating", + Deleting = "Deleting", +} + +export interface ComputeResponse { + 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/${gremlinV2}`; +}; + +export const updateComputeResource = async (sku: string, instances: number): Promise => { + const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); + const body: UpdateComputeRequestParameters = { + properties: { + instanceSize: sku, + instanceCount: instances, + serviceType: gremlinV2, + }, + }; + const telemetryData = { ...body, httpMethod: "PUT", selfServeClassName: GraphAPICompute.name }; + const updateTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "PUT", + apiVersion, + body, + }); + selfServeTraceSuccess(telemetryData, updateTimeStamp); + } catch (e) { + const failureTelemetry = { ...body, e, selfServeClassName: GraphAPICompute.name }; + selfServeTraceFailure(failureTelemetry, updateTimeStamp); + throw e; + } + return armRequestResult?.operationStatusUrl; +}; + +export const deleteComputeResource = async (): Promise => { + const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); + const telemetryData = { httpMethod: "DELETE", selfServeClassName: GraphAPICompute.name }; + const deleteTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "DELETE", + apiVersion, + }); + selfServeTraceSuccess(telemetryData, deleteTimeStamp); + } catch (e) { + const failureTelemetry = { e, selfServeClassName: GraphAPICompute.name }; + selfServeTraceFailure(failureTelemetry, deleteTimeStamp); + throw e; + } + return armRequestResult?.operationStatusUrl; +}; + +export const getComputeResource = async (): Promise => { + const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name); + const telemetryData = { httpMethod: "GET", selfServeClassName: GraphAPICompute.name }; + const getResourceTimeStamp = selfServeTraceStart(telemetryData); + let armRequestResult; + try { + armRequestResult = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path, + method: "GET", + apiVersion, + }); + selfServeTraceSuccess(telemetryData, getResourceTimeStamp); + } catch (e) { + const failureTelemetry = { e, selfServeClassName: GraphAPICompute.name }; + selfServeTraceFailure(failureTelemetry, getResourceTimeStamp); + throw e; + } + return armRequestResult?.result; +}; + +export const getCurrentProvisioningState = async (): Promise => { + try { + const response = await getComputeResource(); + return { + sku: response.properties.instanceSize, + instances: response.properties.instanceCount, + status: response.properties.status, + endpoint: response.properties.GraphAPIComputeEndPoint, + }; + } catch (e) { + return { sku: undefined, instances: undefined, status: undefined, endpoint: undefined }; + } +}; + +export const refreshComputeProvisioning = async (): Promise => { + try { + const response = await getComputeResource(); + 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 }; + } +}; + +const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: string): string => { + return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`; +}; + +export const getReadRegions = async (): Promise> => { + try { + const readRegions = new Array(); + + const response = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name), + method: "GET", + apiVersion: "2021-04-01-preview", + }); + + if (response.result.location !== undefined) { + readRegions.push(response.result.location.replace(" ", "").toLowerCase()); + } else { + for (const location of response.result.locations) { + readRegions.push(location.locationName.replace(" ", "").toLowerCase()); + } + } + return readRegions; + } catch (err) { + return new Array(); + } +}; + +const getFetchPricesPathForRegion = (subscriptionId: string): string => { + return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`; +}; + +export const getPriceMap = async (regions: Array): Promise>> => { + try { + const priceMap = new Map>(); + + for (const region of regions) { + const regionPriceMap = new Map(); + + const response = await armRequestWithoutPolling({ + host: configContext.ARM_ENDPOINT, + path: getFetchPricesPathForRegion(userContext.subscriptionId), + method: "POST", + apiVersion: "2020-01-01-preview", + queryParams: { + filter: + "armRegionName eq '" + + region + + "' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'", + }, + }); + + for (const item of response.result.Items) { + regionPriceMap.set(item.skuName, item.retailPrice); + } + priceMap.set(region, regionPriceMap); + } + + return priceMap; + } catch (err) { + return undefined; + } +}; diff --git a/src/SelfServe/GraphAPICompute/GraphAPICompute.tsx b/src/SelfServe/GraphAPICompute/GraphAPICompute.tsx new file mode 100644 index 000000000..677395838 --- /dev/null +++ b/src/SelfServe/GraphAPICompute/GraphAPICompute.tsx @@ -0,0 +1,423 @@ +import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators"; +import { selfServeTrace } from "../SelfServeTelemetryProcessor"; +import { + ChoiceItem, + Description, + DescriptionType, + Info, + InputType, + NumberUiType, + OnSaveResult, + RefreshResult, + SelfServeBaseClass, + SmartUiInput, +} from "../SelfServeTypes"; +import { BladeType, generateBladeLink } from "../SelfServeUtils"; +import { + deleteComputeResource, + getCurrentProvisioningState, + getPriceMap, + getReadRegions, + refreshComputeProvisioning, + updateComputeResource, +} from "./GraphAPICompute.rp"; + +const costPerHourDefaultValue: Description = { + textTKey: "CostText", + type: DescriptionType.Text, + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", + textTKey: "ComputePricing", + }, +}; + +const connectionStringValue: Description = { + textTKey: "ConnectionStringText", + type: DescriptionType.Text, + link: { + href: generateBladeLink(BladeType.SqlKeys), + textTKey: "KeysBlade", + }, +}; + +const metricsStringValue: Description = { + textTKey: "MetricsText", + type: DescriptionType.Text, + link: { + href: generateBladeLink(BladeType.Metrics), + textTKey: "MetricsBlade", + }, +}; + +const CosmosD4s = "Cosmos.D4s"; +const CosmosD8s = "Cosmos.D8s"; +const CosmosD16s = "Cosmos.D16s"; + +const onSKUChange = (newValue: InputType, currentValues: Map): Map => { + currentValues.set("sku", { value: newValue }); + currentValues.set("costPerHour", { + value: calculateCost(newValue as string, currentValues.get("instances").value as number), + }); + + return currentValues; +}; + +const onNumberOfInstancesChange = ( + newValue: InputType, + currentValues: Map, + baselineValues: Map +): Map => { + currentValues.set("instances", { value: newValue }); + const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean; + const baselineInstances = baselineValues.get("instances")?.value as number; + if (!ComputeOriginallyEnabled || baselineInstances !== newValue) { + currentValues.set("warningBanner", { + value: { + textTKey: "WarningBannerOnUpdate", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", + textTKey: "ComputePricing", + }, + } as Description, + hidden: false, + }); + } else { + currentValues.set("warningBanner", undefined); + } + + currentValues.set("costPerHour", { + value: calculateCost(currentValues.get("sku").value as string, newValue as number), + }); + + return currentValues; +}; + +const onEnableComputeChange = ( + newValue: InputType, + currentValues: Map, + baselineValues: ReadonlyMap +): Map => { + currentValues.set("enableCompute", { value: newValue }); + const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean; + if (ComputeOriginallyEnabled === newValue) { + currentValues.set("sku", baselineValues.get("sku")); + currentValues.set("instances", baselineValues.get("instances")); + currentValues.set("costPerHour", baselineValues.get("costPerHour")); + currentValues.set("warningBanner", baselineValues.get("warningBanner")); + currentValues.set("connectionString", baselineValues.get("connectionString")); + currentValues.set("metricsString", baselineValues.get("metricsString")); + return currentValues; + } + + currentValues.set("warningBanner", undefined); + if (newValue === true) { + currentValues.set("warningBanner", { + value: { + textTKey: "WarningBannerOnUpdate", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", //needs updating + textTKey: "ComputePricing", + }, + } as Description, + hidden: false, + }); + + currentValues.set("costPerHour", { + value: calculateCost(baselineValues.get("sku").value as string, baselineValues.get("instances").value as number), + hidden: false, + }); + } else { + currentValues.set("warningBanner", { + value: { + textTKey: "WarningBannerOnDelete", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", // needs updating + textTKey: "DeprovisioningDetailsText", + }, + } as Description, + hidden: false, + }); + + currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true }); + } + const sku = currentValues.get("sku"); + const hideAttributes = newValue === undefined || !(newValue as boolean); + currentValues.set("sku", { + value: sku.value, + hidden: hideAttributes, + disabled: ComputeOriginallyEnabled, + }); + currentValues.set("instances", { + value: 1, + hidden: hideAttributes, + disabled: true, + }); + + currentValues.set("connectionString", { + value: connectionStringValue, + hidden: !newValue || !ComputeOriginallyEnabled, + }); + + currentValues.set("metricsString", { + value: metricsStringValue, + hidden: !newValue || !ComputeOriginallyEnabled, + }); + + return currentValues; +}; + +const skuDropDownItems: ChoiceItem[] = [ + { labelTKey: "CosmosD4s", key: CosmosD4s }, + { labelTKey: "CosmosD8s", key: CosmosD8s }, + { labelTKey: "CosmosD16s", key: CosmosD16s }, +]; + +const getSkus = async (): Promise => { + return skuDropDownItems; +}; + +const NumberOfInstancesDropdownInfo: Info = { + messageTKey: "ResizingDecisionText", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-size", // todo + textTKey: "ResizingDecisionLink", + }, +}; + +const getInstancesMin = async (): Promise => { + return 1; +}; + +const getInstancesMax = async (): Promise => { + return 5; +}; + +const ApproximateCostDropDownInfo: Info = { + messageTKey: "CostText", + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", //todo + textTKey: "ComputePricing", + }, +}; + +let priceMap: Map>; +let regions: Array; + +const calculateCost = (skuName: string, instanceCount: number): Description => { + try { + let costPerHour = 0; + for (const region of regions) { + const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", "")); + if (incrementalCost === undefined) { + throw new Error("Value not found in map."); + } + costPerHour += incrementalCost; + } + + costPerHour *= instanceCount; + costPerHour = Math.round(costPerHour * 100) / 100; + + return { + textTKey: `${costPerHour} USD`, + type: DescriptionType.Text, + }; + } catch (err) { + return costPerHourDefaultValue; + } +}; + +@IsDisplayable() +@RefreshOptions({ retryIntervalInMs: 20000 }) +export default class GraphAPICompute extends SelfServeBaseClass { + public onRefresh = async (): Promise => { + return await refreshComputeProvisioning(); + }; + + public onSave = async ( + currentValues: Map, + baselineValues: Map + ): Promise => { + selfServeTrace({ selfServeClassName: GraphAPICompute.name }); + + const ComputeCurrentlyEnabled = currentValues.get("enableCompute")?.value as boolean; + const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean; + + currentValues.set("warningBanner", undefined); + + if (ComputeOriginallyEnabled) { + if (!ComputeCurrentlyEnabled) { + const operationStatusUrl = await deleteComputeResource(); + return { + operationStatusUrl: operationStatusUrl, + portalNotification: { + initialize: { + titleTKey: "DeleteInitializeTitle", + messageTKey: "DeleteInitializeMessage", + }, + success: { + titleTKey: "DeleteSuccessTitle", + messageTKey: "DeleteSuccesseMessage", + }, + failure: { + titleTKey: "DeleteFailureTitle", + messageTKey: "DeleteFailureMessage", + }, + }, + }; + } else { + const sku = currentValues.get("sku")?.value as string; + const instances = currentValues.get("instances").value as number; + const operationStatusUrl = await updateComputeResource(sku, instances); + return { + operationStatusUrl: operationStatusUrl, + 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 updateComputeResource(sku, instances); + return { + operationStatusUrl: operationStatusUrl, + portalNotification: { + initialize: { + titleTKey: "CreateInitializeTitle", + messageTKey: "CreateInitializeMessage", + }, + success: { + titleTKey: "CreateSuccessTitle", + messageTKey: "CreateSuccesseMessage", + }, + failure: { + titleTKey: "CreateFailureTitle", + messageTKey: "CreateFailureMessage", + }, + }, + }; + } + }; + + public initialize = async (): Promise> => { + // Based on the RP call enableCompute will be true if it has not yet been enabled and false if it has. + const defaults = new Map(); + defaults.set("enableCompute", { value: false }); + defaults.set("sku", { value: CosmosD4s, hidden: true }); + defaults.set("instances", { value: 1, hidden: true }); + defaults.set("costPerHour", undefined); + defaults.set("connectionString", undefined); + defaults.set("metricsString", { + value: undefined, + hidden: true, + }); + + regions = await getReadRegions(); + priceMap = await getPriceMap(regions); + const response = await getCurrentProvisioningState(); + if (response.status && response.status === "Creating") { + defaults.set("enableCompute", { value: true }); + defaults.set("sku", { value: response.sku, disabled: true }); + defaults.set("instances", { value: response.instances, disabled: true }); + defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) }); + defaults.set("connectionString", { + value: connectionStringValue, + hidden: true, + }); + defaults.set("metricsString", { + value: metricsStringValue, + hidden: true, + }); + } else if (response.status && response.status !== "Deleting") { + defaults.set("enableCompute", { value: true }); + defaults.set("sku", { value: response.sku, disabled: true }); + defaults.set("instances", { value: response.instances }); + defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) }); + defaults.set("connectionString", { + value: connectionStringValue, + hidden: false, + }); + defaults.set("metricsString", { + value: metricsStringValue, + hidden: false, + }); + } + + defaults.set("warningBanner", undefined); + return defaults; + }; + + @Values({ + isDynamicDescription: true, + }) + warningBanner: string; + + @Values({ + description: { + textTKey: "GraphAPIDescription", + type: DescriptionType.Text, + link: { + href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", //todo + textTKey: "LearnAboutCompute", + }, + }, + }) + description: string; + + @OnChange(onEnableComputeChange) + @Values({ + labelTKey: "Compute", + trueLabelTKey: "Provisioned", + falseLabelTKey: "Deprovisioned", + }) + enableCompute: boolean; + + @OnChange(onSKUChange) + @Values({ + labelTKey: "SKUs", + choices: getSkus, + placeholderTKey: "SKUsPlaceHolder", + }) + sku: ChoiceItem; + + @OnChange(onNumberOfInstancesChange) + @PropertyInfo(NumberOfInstancesDropdownInfo) + @Values({ + labelTKey: "NumberOfInstances", + min: getInstancesMin, + max: getInstancesMax, + step: 1, + uiType: NumberUiType.Spinner, + }) + instances: number; + + @PropertyInfo(ApproximateCostDropDownInfo) + @Values({ + labelTKey: "ApproximateCost", + isDynamicDescription: true, + }) + costPerHour: string; + + @Values({ + labelTKey: "ConnectionString", + isDynamicDescription: true, + }) + connectionString: string; + + @Values({ + labelTKey: "MonitorUsage", + description: metricsStringValue, + }) + metricsString: string; +} diff --git a/src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts b/src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts new file mode 100644 index 000000000..76e61c435 --- /dev/null +++ b/src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts @@ -0,0 +1,65 @@ +export enum Regions { + NorthCentralUS = "NorthCentralUS", + WestUS = "WestUS", + EastUS2 = "EastUS2", +} + +export interface AccountProps { + regions: Regions; + enableLogging: boolean; + accountName: string; + collectionThroughput: number; + dbThroughput: number; +} + +export type GraphAPIComputeServiceResource = { + id: string; + name: string; + type: string; + properties: GraphAPIComputeServiceProps; + locations: GraphAPIComputeServiceLocations; +}; +export type GraphAPIComputeServiceProps = { + serviceType: string; + creationTime: string; + status: string; + instanceSize: string; + instanceCount: number; + GraphAPIComputeEndPoint: string; +}; + +export type GraphAPIComputeServiceLocations = { + location: string; + status: string; + GraphAPIComputeEndpoint: string; +}; + +export type UpdateComputeRequestParameters = { + properties: UpdateComputeRequestProperties; +}; + +export type UpdateComputeRequestProperties = { + instanceSize: string; + instanceCount: number; + serviceType: string; +}; + +export type FetchPricesResponse = { + Items: Array; + NextPageLink: string | undefined; + Count: number; +}; + +export type PriceItem = { + retailPrice: number; + skuName: string; +}; + +export type RegionsResponse = { + locations: Array; + location: string; +}; + +export type RegionItem = { + locationName: string; +}; diff --git a/src/SelfServe/SelfServe.tsx b/src/SelfServe/SelfServe.tsx index 50f6eecf8..99e6e51c9 100644 --- a/src/SelfServe/SelfServe.tsx +++ b/src/SelfServe/SelfServe.tsx @@ -50,6 +50,14 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise