adding Materializedviews under selfserve (#1247)

* adding Materializedviews under selfserve

* ran npm format & fixed few links

* modified required key & changed URLs

* modifying links to aka.ms

* modifying few descriptions

* Delete VSWorkspaceState.json

* Delete .suo

* modifying URLs & adding missing keys

Co-authored-by: Chandra sekhar Gunturi <chguntur@microsoft.com>
This commit is contained in:
chandrasekhar gunturi
2022-06-03 12:51:54 +05:30
committed by GitHub
parent 7ab57c9ec4
commit e34f68b162
6 changed files with 758 additions and 0 deletions

View File

@@ -0,0 +1,228 @@
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 MaterializedViewsBuilder from "./MaterializedViewsBuilder";
import {
FetchPricesResponse,
PriceMapAndCurrencyCode,
RegionsResponse,
MaterializedViewsBuilderServiceResource,
UpdateMaterializedViewsBuilderRequestParameters,
} from "./MaterializedViewsBuilderTypes";
const apiVersion = "2021-07-01-preview";
export enum ResourceStatus {
Running = "Running",
Creating = "Creating",
Updating = "Updating",
Deleting = "Deleting",
}
export interface MaterializedViewsBuilderResponse {
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/materializedviewsBuilder`;
};
export const updateMaterializedViewsBuilderResource = async (sku: string, instances: number): Promise<string> => {
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
const body: UpdateMaterializedViewsBuilderRequestParameters = {
properties: {
instanceSize: sku,
instanceCount: instances,
serviceType: "materializedviewsBuilder",
},
};
const telemetryData = { ...body, httpMethod: "PUT", selfServeClassName: MaterializedViewsBuilder.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: MaterializedViewsBuilder.name };
selfServeTraceFailure(failureTelemetry, updateTimeStamp);
throw e;
}
return armRequestResult?.operationStatusUrl;
};
export const deleteMaterializedViewsBuilderResource = async (): Promise<string> => {
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
const telemetryData = { httpMethod: "DELETE", selfServeClassName: MaterializedViewsBuilder.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: MaterializedViewsBuilder.name };
selfServeTraceFailure(failureTelemetry, deleteTimeStamp);
throw e;
}
return armRequestResult?.operationStatusUrl;
};
export const getMaterializedViewsBuilderResource = async (): Promise<MaterializedViewsBuilderServiceResource> => {
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
const telemetryData = { httpMethod: "GET", selfServeClassName: MaterializedViewsBuilder.name };
const getResourceTimeStamp = selfServeTraceStart(telemetryData);
let armRequestResult;
try {
armRequestResult = await armRequestWithoutPolling<MaterializedViewsBuilderServiceResource>({
host: configContext.ARM_ENDPOINT,
path,
method: "GET",
apiVersion,
});
selfServeTraceSuccess(telemetryData, getResourceTimeStamp);
} catch (e) {
const failureTelemetry = { e, selfServeClassName: MaterializedViewsBuilder.name };
selfServeTraceFailure(failureTelemetry, getResourceTimeStamp);
throw e;
}
return armRequestResult?.result;
};
export const getCurrentProvisioningState = async (): Promise<MaterializedViewsBuilderResponse> => {
try {
const response = await getMaterializedViewsBuilderResource();
return {
sku: response.properties.instanceSize,
instances: response.properties.instanceCount,
status: response.properties.status,
endpoint: response.properties.MaterializedViewsBuilderEndPoint,
};
} catch (e) {
return { sku: undefined, instances: undefined, status: undefined, endpoint: undefined };
}
};
export const refreshMaterializedViewsBuilderProvisioning = async (): Promise<RefreshResult> => {
try {
const response = await getMaterializedViewsBuilderResource();
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 getRegions = async (): Promise<Array<string>> => {
const telemetryData = {
feature: "Calculate approximate cost",
function: "getRegions",
description: "",
selfServeClassName: MaterializedViewsBuilder.name,
};
const getRegionsTimestamp = selfServeTraceStart(telemetryData);
try {
const regions = 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-07-01-preview",
});
if (response.result.location !== undefined) {
regions.push(response.result.location.split(" ").join("").toLowerCase());
} else {
for (const location of response.result.locations) {
regions.push(location.locationName.split(" ").join("").toLowerCase());
}
}
selfServeTraceSuccess(telemetryData, getRegionsTimestamp);
return regions;
} catch (err) {
const failureTelemetry = { err, selfServeClassName: MaterializedViewsBuilder.name };
selfServeTraceFailure(failureTelemetry, getRegionsTimestamp);
return new Array<string>();
}
};
const getFetchPricesPathForRegion = (subscriptionId: string): string => {
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
};
export const getPriceMapAndCurrencyCode = async (regions: Array<string>): Promise<PriceMapAndCurrencyCode> => {
const telemetryData = {
feature: "Calculate approximate cost",
function: "getPriceMapAndCurrencyCode",
description: "fetch prices API call",
selfServeClassName: MaterializedViewsBuilder.name,
};
const getPriceMapAndCurrencyCodeTimestamp = selfServeTraceStart(telemetryData);
try {
const priceMap = new Map<string, Map<string, number>>();
let currencyCode;
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:
"armRegionNameeq '" +
region +
"'andserviceFamilyeq 'Databases' and productName eq 'Azure Cosmos DB MaterializedViews Builder - General Purpose'",
},
});
for (const item of response.result.Items) {
if (currencyCode === undefined) {
currencyCode = item.currencyCode;
} else if (item.currencyCode !== currencyCode) {
throw Error("Currency Code Mismatch: Currency code not same for all regions / skus.");
}
regionPriceMap.set(item.skuName, item.retailPrice);
}
priceMap.set(region, regionPriceMap);
}
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
return { priceMap: priceMap, currencyCode: currencyCode };
} catch (err) {
const failureTelemetry = { err, selfServeClassName: MaterializedViewsBuilder.name };
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
return { priceMap: undefined, currencyCode: undefined };
}
};

View File

@@ -0,0 +1,416 @@
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
import {
selfServeTrace,
selfServeTraceFailure,
selfServeTraceStart,
selfServeTraceSuccess,
} from "../SelfServeTelemetryProcessor";
import {
ChoiceItem,
Description,
DescriptionType,
Info,
InputType,
NumberUiType,
OnSaveResult,
RefreshResult,
SelfServeBaseClass,
SmartUiInput,
} from "../SelfServeTypes";
import { BladeType, generateBladeLink } from "../SelfServeUtils";
import {
deleteMaterializedViewsBuilderResource,
getCurrentProvisioningState,
getPriceMapAndCurrencyCode,
getRegions,
refreshMaterializedViewsBuilderProvisioning,
updateMaterializedViewsBuilderResource,
} from "./MaterializedViewsBuilder.rp";
const costPerHourDefaultValue: Description = {
textTKey: "CostText",
type: DescriptionType.Text,
link: {
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
textTKey: "MaterializedviewsBuilderPricing",
},
};
const metricsStringValue: Description = {
textTKey: "MetricsText",
type: DescriptionType.Text,
link: {
href: generateBladeLink(BladeType.Metrics),
textTKey: "MetricsBlade",
},
};
const CosmosD2s = "Cosmos.D2s";
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 MaterializedViewsBuilderOriginallyEnabled = baselineValues.get("enableMaterializedViewsBuilder")
?.value as boolean;
const baselineInstances = baselineValues.get("instances")?.value as number;
if (!MaterializedViewsBuilderOriginallyEnabled || baselineInstances !== newValue) {
currentValues.set("warningBanner", {
value: {
textTKey: "WarningBannerOnUpdate",
link: {
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
textTKey: "MaterializedviewsBuilderPricing",
},
} 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 onEnableMaterializedViewsBuilderChange = (
newValue: InputType,
currentValues: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
): Map<string, SmartUiInput> => {
currentValues.set("enableMaterializedViewsBuilder", { value: newValue });
const MaterializedViewsBuilderOriginallyEnabled = baselineValues.get("enableMaterializedViewsBuilder")
?.value as boolean;
if (MaterializedViewsBuilderOriginallyEnabled === 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("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-materializedviewsbuilder-pricing",
textTKey: "MaterializedviewsBuilderPricing",
},
} 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-materializedviews",
textTKey: "DeprovisioningDetailsText",
},
} as Description,
hidden: false,
});
currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true });
}
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: MaterializedViewsBuilderOriginallyEnabled,
});
currentValues.set("instances", {
value: instances.value,
hidden: hideAttributes,
disabled: MaterializedViewsBuilderOriginallyEnabled,
});
currentValues.set("metricsString", {
value: metricsStringValue,
hidden: !newValue || !MaterializedViewsBuilderOriginallyEnabled,
});
return currentValues;
};
const skuDropDownItems: ChoiceItem[] = [
{ labelTKey: "CosmosD2s", key: CosmosD2s },
{ labelTKey: "CosmosD4s", key: CosmosD4s },
{ labelTKey: "CosmosD8s", key: CosmosD8s },
{ labelTKey: "CosmosD16s", key: CosmosD16s },
];
const getSkus = async (): Promise<ChoiceItem[]> => {
return skuDropDownItems;
};
const getInstancesMin = async (): Promise<number> => {
return 1;
};
const getInstancesMax = async (): Promise<number> => {
return 5;
};
const NumberOfInstancesDropdownInfo: Info = {
messageTKey: "ResizingDecisionText",
link: {
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-size",
textTKey: "ResizingDecisionLink",
},
};
const ApproximateCostDropDownInfo: Info = {
messageTKey: "CostText",
link: {
href: "https://aka.ms/cosmos-db-materializedviewsbuilder-pricing",
textTKey: "MaterializedviewsBuilderPricing",
},
};
let priceMap: Map<string, Map<string, number>>;
let currencyCode: string;
let regions: Array<string>;
const calculateCost = (skuName: string, instanceCount: number): Description => {
const telemetryData = {
feature: "Calculate approximate cost",
function: "calculateCost",
description: "performs final calculation",
selfServeClassName: MaterializedViewsBuilder.name,
};
const calculateCostTimestamp = selfServeTraceStart(telemetryData);
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;
}
if (costPerHour === 0) {
throw new Error("Cost per hour = 0");
}
costPerHour *= instanceCount;
costPerHour = Math.round(costPerHour * 100) / 100;
selfServeTraceSuccess(telemetryData, calculateCostTimestamp);
return {
textTKey: `${costPerHour} ${currencyCode}`,
type: DescriptionType.Text,
};
} catch (err) {
const failureTelemetry = { err, regions, priceMap, selfServeClassName: MaterializedViewsBuilder.name };
selfServeTraceFailure(failureTelemetry, calculateCostTimestamp);
return costPerHourDefaultValue;
}
};
@IsDisplayable()
@RefreshOptions({ retryIntervalInMs: 20000 })
export default class MaterializedViewsBuilder extends SelfServeBaseClass {
public onRefresh = async (): Promise<RefreshResult> => {
return await refreshMaterializedViewsBuilderProvisioning();
};
public onSave = async (
currentValues: Map<string, SmartUiInput>,
baselineValues: Map<string, SmartUiInput>
): Promise<OnSaveResult> => {
selfServeTrace({ selfServeClassName: MaterializedViewsBuilder.name });
const MaterializedViewsBuilderCurrentlyEnabled = currentValues.get("enableMaterializedViewsBuilder")
?.value as boolean;
const MaterializedViewsBuilderOriginallyEnabled = baselineValues.get("enableMaterializedViewsBuilder")
?.value as boolean;
currentValues.set("warningBanner", undefined);
if (MaterializedViewsBuilderOriginallyEnabled) {
if (!MaterializedViewsBuilderCurrentlyEnabled) {
const operationStatusUrl = await deleteMaterializedViewsBuilderResource();
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 updateMaterializedViewsBuilderResource(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 updateMaterializedViewsBuilderResource(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 enableMaterializedViewsBuilder will be true if it has not yet been enabled and false if it has.
const defaults = new Map<string, SmartUiInput>();
defaults.set("enableMaterializedViewsBuilder", { value: false });
defaults.set("sku", { value: CosmosD2s, hidden: true });
defaults.set("instances", { value: await getInstancesMin(), hidden: true });
defaults.set("costPerHour", undefined);
defaults.set("metricsString", {
value: undefined,
hidden: true,
});
regions = await getRegions();
const priceMapAndCurrencyCode = await getPriceMapAndCurrencyCode(regions);
priceMap = priceMapAndCurrencyCode.priceMap;
currencyCode = priceMapAndCurrencyCode.currencyCode;
const response = await getCurrentProvisioningState();
if (response.status && response.status !== "Deleting") {
defaults.set("enableMaterializedViewsBuilder", { value: true });
defaults.set("sku", { value: response.sku, disabled: true });
defaults.set("instances", { value: response.instances, disabled: false });
defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) });
defaults.set("metricsString", {
value: metricsStringValue,
hidden: false,
});
}
defaults.set("warningBanner", undefined);
return defaults;
};
@Values({
isDynamicDescription: true,
})
warningBanner: string;
@Values({
description: {
textTKey: "MaterializedViewsBuilderDescription",
type: DescriptionType.Text,
link: {
href: "https://aka.ms/cosmos-db-materializedviews",
textTKey: "LearnAboutMaterializedViews",
},
},
})
description: string;
@OnChange(onEnableMaterializedViewsBuilderChange)
@Values({
labelTKey: "MaterializedViewsBuilder",
trueLabelTKey: "Provisioned",
falseLabelTKey: "Deprovisioned",
})
enableMaterializedViewsBuilder: 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: "MonitorUsage",
description: metricsStringValue,
})
metricsString: string;
}

View File

@@ -0,0 +1,57 @@
export type MaterializedViewsBuilderServiceResource = {
id: string;
name: string;
type: string;
properties: MaterializedViewsBuilderServiceProps;
locations: MaterializedViewsBuilderServiceLocations;
};
export type MaterializedViewsBuilderServiceProps = {
serviceType: string;
creationTime: string;
status: string;
instanceSize: string;
instanceCount: number;
MaterializedViewsBuilderEndPoint: string;
};
export type MaterializedViewsBuilderServiceLocations = {
location: string;
status: string;
MaterializedViewsBuilderEndpoint: string;
};
export type UpdateMaterializedViewsBuilderRequestParameters = {
properties: UpdateMaterializedViewsBuilderRequestProperties;
};
export type UpdateMaterializedViewsBuilderRequestProperties = {
instanceSize: string;
instanceCount: number;
serviceType: string;
};
export type FetchPricesResponse = {
Items: Array<PriceItem>;
NextPageLink: string | undefined;
Count: number;
};
export type PriceMapAndCurrencyCode = {
priceMap: Map<string, Map<string, number>>;
currencyCode: string;
};
export type PriceItem = {
retailPrice: number;
skuName: string;
currencyCode: string;
};
export type RegionsResponse = {
locations: Array<RegionItem>;
location: string;
};
export type RegionItem = {
locationName: string;
};

View File

@@ -58,6 +58,14 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
await loadTranslations(graphAPICompute.constructor.name);
return graphAPICompute.toSelfServeDescriptor();
}
case SelfServeType.materializedviewsbuilder: {
const MaterializedViewsBuilder = await import(
/* webpackChunkName: "MaterializedViewsBuilder" */ "./MaterializedViewsBuilder/MaterializedViewsBuilder"
);
const materializedViewsBuilder = new MaterializedViewsBuilder.default();
await loadTranslations(materializedViewsBuilder.constructor.name);
return materializedViewsBuilder.toSelfServeDescriptor();
}
default:
return undefined;
}

View File

@@ -32,6 +32,7 @@ export enum SelfServeType {
example = "example",
sqlx = "sqlx",
graphapicompute = "graphapicompute",
materializedviewsbuilder = "materializedviewsbuilder",
}
/**