mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-03-29 13:09:07 +00:00
Sqlx estimated cost calculation (#925)
Adds cost estimate for the SqlX services in the Dedicated Gateway blade (queries the new FetchPrices API to retrieve price data)
This commit is contained in:
parent
ed9cf01b50
commit
39a67dbc98
@ -40,7 +40,7 @@
|
|||||||
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
|
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
|
||||||
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
|
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
|
||||||
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
|
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
|
||||||
"Cost": "Cost",
|
"ApproximateCost": "Approximate Cost Per Hour",
|
||||||
"CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.",
|
"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",
|
"ConnectionString": "Connection String",
|
||||||
"ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ",
|
"ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ",
|
||||||
|
@ -4,7 +4,12 @@ import { armRequestWithoutPolling } from "../../Utils/arm/request";
|
|||||||
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
|
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
|
||||||
import { RefreshResult } from "../SelfServeTypes";
|
import { RefreshResult } from "../SelfServeTypes";
|
||||||
import SqlX from "./SqlX";
|
import SqlX from "./SqlX";
|
||||||
import { SqlxServiceResource, UpdateDedicatedGatewayRequestParameters } from "./SqlxTypes";
|
import {
|
||||||
|
FetchPricesResponse,
|
||||||
|
RegionsResponse,
|
||||||
|
SqlxServiceResource,
|
||||||
|
UpdateDedicatedGatewayRequestParameters,
|
||||||
|
} from "./SqlxTypes";
|
||||||
|
|
||||||
const apiVersion = "2021-04-01-preview";
|
const apiVersion = "2021-04-01-preview";
|
||||||
|
|
||||||
@ -128,3 +133,67 @@ export const refreshDedicatedGatewayProvisioning = async (): Promise<RefreshResu
|
|||||||
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
|
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<Array<string>> => {
|
||||||
|
try {
|
||||||
|
const readRegions = new Array<string>();
|
||||||
|
|
||||||
|
const response = await armRequestWithoutPolling<RegionsResponse>({
|
||||||
|
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<string>();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFetchPricesPathForRegion = (subscriptionId: string): string => {
|
||||||
|
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPriceMap = async (regions: Array<string>): Promise<Map<string, Map<string, number>>> => {
|
||||||
|
try {
|
||||||
|
const priceMap = new Map<string, Map<string, number>>();
|
||||||
|
|
||||||
|
for (const region of regions) {
|
||||||
|
const regionPriceMap = new Map<string, number>();
|
||||||
|
|
||||||
|
const response = await armRequestWithoutPolling<FetchPricesResponse>({
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -16,11 +16,13 @@ import { BladeType, generateBladeLink } from "../SelfServeUtils";
|
|||||||
import {
|
import {
|
||||||
deleteDedicatedGatewayResource,
|
deleteDedicatedGatewayResource,
|
||||||
getCurrentProvisioningState,
|
getCurrentProvisioningState,
|
||||||
|
getPriceMap,
|
||||||
|
getReadRegions,
|
||||||
refreshDedicatedGatewayProvisioning,
|
refreshDedicatedGatewayProvisioning,
|
||||||
updateDedicatedGatewayResource,
|
updateDedicatedGatewayResource,
|
||||||
} from "./SqlX.rp";
|
} from "./SqlX.rp";
|
||||||
|
|
||||||
const costPerHourValue: Description = {
|
const costPerHourDefaultValue: Description = {
|
||||||
textTKey: "CostText",
|
textTKey: "CostText",
|
||||||
type: DescriptionType.Text,
|
type: DescriptionType.Text,
|
||||||
link: {
|
link: {
|
||||||
@ -53,7 +55,10 @@ const CosmosD16s = "Cosmos.D16s";
|
|||||||
|
|
||||||
const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
|
const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
|
||||||
currentValues.set("sku", { value: newValue });
|
currentValues.set("sku", { value: newValue });
|
||||||
currentValues.set("costPerHour", { value: costPerHourValue });
|
currentValues.set("costPerHour", {
|
||||||
|
value: calculateCost(newValue as string, currentValues.get("instances").value as number),
|
||||||
|
});
|
||||||
|
|
||||||
return currentValues;
|
return currentValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -79,6 +84,11 @@ const onNumberOfInstancesChange = (
|
|||||||
} else {
|
} else {
|
||||||
currentValues.set("warningBanner", undefined);
|
currentValues.set("warningBanner", undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentValues.set("costPerHour", {
|
||||||
|
value: calculateCost(currentValues.get("sku").value as string, newValue as number),
|
||||||
|
});
|
||||||
|
|
||||||
return currentValues;
|
return currentValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -111,6 +121,11 @@ const onEnableDedicatedGatewayChange = (
|
|||||||
} as Description,
|
} as Description,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
currentValues.set("costPerHour", {
|
||||||
|
value: calculateCost(baselineValues.get("sku").value as string, baselineValues.get("instances").value as number),
|
||||||
|
hidden: false,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
currentValues.set("warningBanner", {
|
currentValues.set("warningBanner", {
|
||||||
value: {
|
value: {
|
||||||
@ -122,6 +137,8 @@ const onEnableDedicatedGatewayChange = (
|
|||||||
} as Description,
|
} as Description,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true });
|
||||||
}
|
}
|
||||||
const sku = currentValues.get("sku");
|
const sku = currentValues.get("sku");
|
||||||
const instances = currentValues.get("instances");
|
const instances = currentValues.get("instances");
|
||||||
@ -137,7 +154,6 @@ const onEnableDedicatedGatewayChange = (
|
|||||||
disabled: dedicatedGatewayOriginallyEnabled,
|
disabled: dedicatedGatewayOriginallyEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
currentValues.set("costPerHour", { value: costPerHourValue, hidden: hideAttributes });
|
|
||||||
currentValues.set("connectionString", {
|
currentValues.set("connectionString", {
|
||||||
value: connectionStringValue,
|
value: connectionStringValue,
|
||||||
hidden: !newValue || !dedicatedGatewayOriginallyEnabled,
|
hidden: !newValue || !dedicatedGatewayOriginallyEnabled,
|
||||||
@ -177,6 +193,40 @@ const NumberOfInstancesDropdownInfo: Info = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ApproximateCostDropDownInfo: Info = {
|
||||||
|
messageTKey: "CostText",
|
||||||
|
link: {
|
||||||
|
href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing",
|
||||||
|
textTKey: "DedicatedGatewayPricing",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let priceMap: Map<string, Map<string, number>>;
|
||||||
|
let regions: Array<string>;
|
||||||
|
|
||||||
|
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()
|
@IsDisplayable()
|
||||||
@RefreshOptions({ retryIntervalInMs: 20000 })
|
@RefreshOptions({ retryIntervalInMs: 20000 })
|
||||||
export default class SqlX extends SelfServeBaseClass {
|
export default class SqlX extends SelfServeBaseClass {
|
||||||
@ -274,12 +324,15 @@ export default class SqlX extends SelfServeBaseClass {
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
regions = await getReadRegions();
|
||||||
|
priceMap = await getPriceMap(regions);
|
||||||
|
|
||||||
const response = await getCurrentProvisioningState();
|
const response = await getCurrentProvisioningState();
|
||||||
if (response.status && response.status !== "Deleting") {
|
if (response.status && response.status !== "Deleting") {
|
||||||
defaults.set("enableDedicatedGateway", { value: true });
|
defaults.set("enableDedicatedGateway", { value: true });
|
||||||
defaults.set("sku", { value: response.sku, disabled: true });
|
defaults.set("sku", { value: response.sku, disabled: true });
|
||||||
defaults.set("instances", { value: response.instances, disabled: false });
|
defaults.set("instances", { value: response.instances, disabled: false });
|
||||||
defaults.set("costPerHour", { value: costPerHourValue });
|
defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) });
|
||||||
defaults.set("connectionString", {
|
defaults.set("connectionString", {
|
||||||
value: connectionStringValue,
|
value: connectionStringValue,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
@ -338,8 +391,9 @@ export default class SqlX extends SelfServeBaseClass {
|
|||||||
})
|
})
|
||||||
instances: number;
|
instances: number;
|
||||||
|
|
||||||
|
@PropertyInfo(ApproximateCostDropDownInfo)
|
||||||
@Values({
|
@Values({
|
||||||
labelTKey: "Cost",
|
labelTKey: "ApproximateCost",
|
||||||
isDynamicDescription: true,
|
isDynamicDescription: true,
|
||||||
})
|
})
|
||||||
costPerHour: string;
|
costPerHour: string;
|
||||||
|
@ -29,3 +29,23 @@ export type UpdateDedicatedGatewayRequestProperties = {
|
|||||||
instanceCount: number;
|
instanceCount: number;
|
||||||
serviceType: string;
|
serviceType: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FetchPricesResponse = {
|
||||||
|
Items: Array<PriceItem>;
|
||||||
|
NextPageLink: string | undefined;
|
||||||
|
Count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PriceItem = {
|
||||||
|
retailPrice: number;
|
||||||
|
skuName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegionsResponse = {
|
||||||
|
locations: Array<RegionItem>;
|
||||||
|
location: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegionItem = {
|
||||||
|
locationName: string;
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user