mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-02-18 10:17:16 +00:00
Self-Server for GraphAPI Compute (#1017)
* Self-Server for GraphAPI Compute * Update GraphAPICompute.json
This commit is contained in:
parent
95c9b7ee31
commit
65882ea831
57
src/Localization/en/GraphAPICompute.json
Normal file
57
src/Localization/en/GraphAPICompute.json
Normal file
@ -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."
|
||||||
|
}
|
200
src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts
Normal file
200
src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts
Normal file
@ -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<string> => {
|
||||||
|
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<string> => {
|
||||||
|
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<GraphAPIComputeServiceResource> => {
|
||||||
|
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<GraphAPIComputeServiceResource>({
|
||||||
|
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<ComputeResponse> => {
|
||||||
|
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<RefreshResult> => {
|
||||||
|
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<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;
|
||||||
|
}
|
||||||
|
};
|
423
src/SelfServe/GraphAPICompute/GraphAPICompute.tsx
Normal file
423
src/SelfServe/GraphAPICompute/GraphAPICompute.tsx
Normal file
@ -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<string, SmartUiInput>): Map<string, SmartUiInput> => {
|
||||||
|
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<string, SmartUiInput>,
|
||||||
|
baselineValues: Map<string, SmartUiInput>
|
||||||
|
): Map<string, SmartUiInput> => {
|
||||||
|
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<string, SmartUiInput>,
|
||||||
|
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||||
|
): Map<string, SmartUiInput> => {
|
||||||
|
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<ChoiceItem[]> => {
|
||||||
|
return skuDropDownItems;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NumberOfInstancesDropdownInfo: Info = {
|
||||||
|
messageTKey: "ResizingDecisionText",
|
||||||
|
link: {
|
||||||
|
href: "https://aka.ms/cosmos-db-dedicated-gateway-size", // todo
|
||||||
|
textTKey: "ResizingDecisionLink",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInstancesMin = async (): Promise<number> => {
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInstancesMax = async (): Promise<number> => {
|
||||||
|
return 5;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ApproximateCostDropDownInfo: Info = {
|
||||||
|
messageTKey: "CostText",
|
||||||
|
link: {
|
||||||
|
href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", //todo
|
||||||
|
textTKey: "ComputePricing",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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()
|
||||||
|
@RefreshOptions({ retryIntervalInMs: 20000 })
|
||||||
|
export default class GraphAPICompute extends SelfServeBaseClass {
|
||||||
|
public onRefresh = async (): Promise<RefreshResult> => {
|
||||||
|
return await refreshComputeProvisioning();
|
||||||
|
};
|
||||||
|
|
||||||
|
public onSave = async (
|
||||||
|
currentValues: Map<string, SmartUiInput>,
|
||||||
|
baselineValues: Map<string, SmartUiInput>
|
||||||
|
): Promise<OnSaveResult> => {
|
||||||
|
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<Map<string, SmartUiInput>> => {
|
||||||
|
// 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<string, SmartUiInput>();
|
||||||
|
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;
|
||||||
|
}
|
65
src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts
Normal file
65
src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts
Normal file
@ -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<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;
|
||||||
|
};
|
@ -50,6 +50,14 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
|||||||
await loadTranslations(sqlX.constructor.name);
|
await loadTranslations(sqlX.constructor.name);
|
||||||
return sqlX.toSelfServeDescriptor();
|
return sqlX.toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
|
case SelfServeType.graphapicompute: {
|
||||||
|
const GraphAPICompute = await import(
|
||||||
|
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
|
||||||
|
);
|
||||||
|
const graphAPICompute = new GraphAPICompute.default();
|
||||||
|
await loadTranslations(graphAPICompute.constructor.name);
|
||||||
|
return graphAPICompute.toSelfServeDescriptor();
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ export enum SelfServeType {
|
|||||||
// Add your self serve types here
|
// Add your self serve types here
|
||||||
example = "example",
|
example = "example",
|
||||||
sqlx = "sqlx",
|
sqlx = "sqlx",
|
||||||
|
graphapicompute = "graphapicompute",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user