Compare commits

..

15 Commits

Author SHA1 Message Date
Sung-Hyun Kang
5e8db9b539 Merge branch 'master' into missing_pk_fix 2025-03-24 10:41:14 -05:00
Sung-Hyun Kang
f94a452a98 Fix partition key missing not being able to load documents 2025-03-24 10:40:12 -05:00
tarazou9
6ce81099ef Handle catalog empty (#2082)
Handle UI errors caused by Catalog API calls returning no offering id.
2025-03-21 16:15:48 -04:00
Sung-Hyun Kang
5997fabcda moving the throughput bucket flag to the client generation level 2025-02-11 15:03:23 -06:00
Sung-Hyun Kang
f01d4a5ae2 Merge branch 'master' into throughput_bucket 2025-02-07 09:49:10 -06:00
Sung-Hyun Kang
20eeed98e4 fix unit tests 2025-02-03 11:23:49 -06:00
Sung-Hyun Kang
ac53e1b3b5 Compile build fix 2025-02-03 10:53:44 -06:00
Sung-Hyun Kang
2cab086268 Edit package-lock 2025-02-03 10:49:53 -06:00
Sung-Hyun Kang
937451d844 Fixed unit tests 2025-02-02 22:18:02 -06:00
Sung-Hyun Kang
5dfaa9f0f8 Updated to a tab 2025-01-26 18:11:50 -06:00
Sung-Hyun Kang
05e2d0ac29 change query bucket to group 2025-01-21 10:17:17 -06:00
Sung-Hyun Kang
152c995ec0 Added logic 2025-01-21 09:28:49 -06:00
Sung-Hyun Kang
07c4ca9c50 enable/disable per autoscale selection 2025-01-13 09:49:48 -06:00
Sung-Hyun Kang
80781f7c8f fix bugs 2025-01-12 22:33:02 -06:00
Sung-Hyun Kang
aa39359460 Added throughput bucketing 2025-01-12 21:28:30 -06:00
12 changed files with 107 additions and 101 deletions

View File

@@ -1028,6 +1028,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
);
const selectedDocumentId = documentIds[clickedRowIndex as number];
const originalPartitionKeyValue = selectedDocumentId.partitionKeyValue;
selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
onExecutionErrorChange(false);
@@ -1063,9 +1064,14 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
setColumnDefinitionsFromDocument(documentContent);
},
(error) => {
// in case of any kind of failures of accidently changing partition key, restore the original
// so that when user navigates away from current document and comes back,
// it doesnt fail to load due to using the invalid partition keys
selectedDocumentId.partitionKeyValue = originalPartitionKeyValue;
onExecutionErrorChange(true);
const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
TelemetryProcessor.traceFailure(
Action.UpdateDocument,
{

View File

@@ -2,9 +2,8 @@
* @module SelfServe/Decorators
*/
import { TFunction } from "i18next";
import { ChoiceItem, Description, Info, NumberUiType, OnChangeCallback, RefreshParams } from "./SelfServeTypes";
import { addPropertyToMap, buildSmartUiDescriptor, DecoratorProperties, SelfServeType } from "./SelfServeUtils";
import { addPropertyToMap, buildSmartUiDescriptor, DecoratorProperties } from "./SelfServeUtils";
type ValueOf<T> = T[keyof T];
interface Decorator {
@@ -129,9 +128,8 @@ const isDescriptionDisplayOptions = (inputOptions: InputOptions): inputOptions i
};
const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
console.log(decorators);
return async (target, property) => {
let className: string = getTargetName(target);
return (target, property) => {
let className = target.constructor.name;
const propertyName = property.toString();
if (className === "Function") {
//eslint-disable-next-line @typescript-eslint/ban-types
@@ -140,7 +138,6 @@ const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
}
const propertyType = (Reflect.getMetadata("design:type", target, property)?.name as string)?.toLowerCase();
console.log(propertyType);
addPropertyToMap(target, propertyName, className, "type", propertyType);
addPropertyToMap(target, propertyName, className, "dataFieldName", propertyName);
@@ -208,8 +205,7 @@ export const Values = (inputOptions: InputOptions): PropertyDecorator => {
*/
export const IsDisplayable = (): ClassDecorator => {
return (target) => {
let targetName: string = getTargetName(target);
buildSmartUiDescriptor(targetName, target.prototype);
buildSmartUiDescriptor(target.name, target.prototype);
};
};
@@ -219,26 +215,7 @@ export const IsDisplayable = (): ClassDecorator => {
* how often the auto refresh of the page occurs.
*/
export const RefreshOptions = (refreshParams: RefreshParams): ClassDecorator => {
console.log(refreshParams);
return (target) => {
let targetName: string = getTargetName(target);
addPropertyToMap(target.prototype, "root", targetName, "refreshParams", refreshParams);
addPropertyToMap(target.prototype, "root", target.name, "refreshParams", refreshParams);
};
};
const getTargetName = (target: TFunction | Object): string => {
const targetString: string = target.toString();
let targetName: string;
if (targetString.includes(SelfServeType.example)) {
targetName = SelfServeType.example;
} else if (targetString.includes(SelfServeType.graphapicompute)) {
targetName = SelfServeType.graphapicompute;
} else if (targetString.includes(SelfServeType.materializedviewsbuilder)) {
targetName = SelfServeType.materializedviewsbuilder;
} else if (targetString.includes(SelfServeType.sqlx)) {
targetName = SelfServeType.sqlx;
} else {
targetName = target.constructor.name;
}
return targetName;
};

View File

@@ -1,4 +1,3 @@
import { SelfServeType } from "SelfServe/SelfServeUtils";
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
import { selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
import {
@@ -169,10 +168,6 @@ export default class SelfServeExample extends SelfServeBaseClass {
return defaults;
};
public getSelfServeType = (): SelfServeType => {
return SelfServeType.example;
};
@Values({
labelTKey: "DescriptionLabel",
description: {

View File

@@ -14,7 +14,7 @@ import {
import type { ChoiceItem } from "../SelfServeTypes";
import { BladeType, generateBladeLink, SelfServeType } from "../SelfServeUtils";
import { BladeType, generateBladeLink } from "../SelfServeUtils";
import {
deleteComputeResource,
getCurrentProvisioningState,
@@ -360,10 +360,6 @@ export default class GraphAPICompute extends SelfServeBaseClass {
return defaults;
};
public getSelfServeType = (): SelfServeType => {
return SelfServeType.graphapicompute;
};
@Values({
isDynamicDescription: true,
})

View File

@@ -19,7 +19,7 @@ import {
import type { ChoiceItem } from "../SelfServeTypes";
import { BladeType, generateBladeLink, SelfServeType } from "../SelfServeUtils";
import { BladeType, generateBladeLink } from "../SelfServeUtils";
import {
deleteMaterializedViewsBuilderResource,
getCurrentProvisioningState,
@@ -359,10 +359,6 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
return defaults;
};
public getSelfServeType = (): SelfServeType => {
return SelfServeType.materializedviewsbuilder;
};
@Values({
isDynamicDescription: true,
})

View File

@@ -2,7 +2,6 @@
* @module SelfServe/SelfServeTypes
*/
import { SelfServeType } from "SelfServe/SelfServeUtils";
import { TelemetryData } from "../Shared/Telemetry/TelemetryProcessor";
interface BaseInput {
@@ -121,11 +120,9 @@ export abstract class SelfServeBaseClass {
*/
public abstract onRefresh: () => Promise<RefreshResult>;
public abstract getSelfServeType: () => SelfServeType;
public test: string = "hello";
/**@internal */
public toSelfServeDescriptor(): SelfServeDescriptor {
const className: string = this.getSelfServeType();
const className = this.constructor.name;
const selfServeDescriptor = Reflect.getMetadata(className, this) as SelfServeDescriptor;
if (!this.initialize) {

View File

@@ -1,60 +1,42 @@
import { NumberUiType, OnSaveResult, RefreshResult, SelfServeBaseClass, SmartUiInput } from "./SelfServeTypes";
import {
DecoratorProperties,
mapToSmartUiDescriptor,
SelfServeType,
updateContextWithDecorator,
} from "./SelfServeUtils";
import { DecoratorProperties, mapToSmartUiDescriptor, updateContextWithDecorator } from "./SelfServeUtils";
describe("SelfServeUtils", () => {
const getSelfServeTypeExample = (): SelfServeType => {
return SelfServeType.example;
};
it("initialize should be declared for self serve classes", () => {
class SelfServeExample extends SelfServeBaseClass {
class Test extends SelfServeBaseClass {
public initialize: () => Promise<Map<string, SmartUiInput>>;
public onSave: (currentValues: Map<string, SmartUiInput>) => Promise<OnSaveResult>;
public onRefresh: () => Promise<RefreshResult>;
public getSelfServeType = (): SelfServeType => getSelfServeTypeExample();
}
expect(() => new SelfServeExample().toSelfServeDescriptor()).toThrow(
"initialize() was not declared for the class 'SelfServeExample'",
);
expect(() => new Test().toSelfServeDescriptor()).toThrow("initialize() was not declared for the class 'Test'");
});
it("onSave should be declared for self serve classes", () => {
class SelfServeExample extends SelfServeBaseClass {
class Test extends SelfServeBaseClass {
public initialize = jest.fn();
public onSave: () => Promise<OnSaveResult>;
public onRefresh: () => Promise<RefreshResult>;
public getSelfServeType = (): SelfServeType => getSelfServeTypeExample();
}
expect(() => new SelfServeExample().toSelfServeDescriptor()).toThrow(
"onSave() was not declared for the class 'SelfServeExample'",
);
expect(() => new Test().toSelfServeDescriptor()).toThrow("onSave() was not declared for the class 'Test'");
});
it("onRefresh should be declared for self serve classes", () => {
class SelfServeExample extends SelfServeBaseClass {
class Test extends SelfServeBaseClass {
public initialize = jest.fn();
public onSave = jest.fn();
public onRefresh: () => Promise<RefreshResult>;
public getSelfServeType = (): SelfServeType => getSelfServeTypeExample();
}
expect(() => new SelfServeExample().toSelfServeDescriptor()).toThrow(
"onRefresh() was not declared for the class 'SelfServeExample'",
);
expect(() => new Test().toSelfServeDescriptor()).toThrow("onRefresh() was not declared for the class 'Test'");
});
it("@IsDisplayable decorator must be present for self serve classes", () => {
class SelfServeExample extends SelfServeBaseClass {
class Test extends SelfServeBaseClass {
public initialize = jest.fn();
public onSave = jest.fn();
public onRefresh = jest.fn();
public getSelfServeType = (): SelfServeType => getSelfServeTypeExample();
}
expect(() => new SelfServeExample().toSelfServeDescriptor()).toThrow(
"@IsDisplayable decorator was not declared for the class 'SelfServeExample'",
expect(() => new Test().toSelfServeDescriptor()).toThrow(
"@IsDisplayable decorator was not declared for the class 'Test'",
);
});

View File

@@ -141,9 +141,6 @@ export const updateContextWithDecorator = <T extends keyof DecoratorProperties,
descriptorName: keyof DecoratorProperties,
descriptorValue: K,
): void => {
console.log(context);
console.log(propertyName);
console.log(className);
if (!(context instanceof Map)) {
throw new Error(`@IsDisplayable should be the first decorator for the class '${className}'.`);
}

View File

@@ -197,6 +197,11 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
const priceMap = new Map<string, Map<string, number>>();
let billingCurrency;
for (const region of map.keys()) {
// if no offering id is found for that region, skipping calling price API
const subMap = map.get(region);
if (!subMap || subMap.size === 0) {
continue;
}
const regionPriceMap = new Map<string, number>();
const regionShortName = await getRegionShortName(region);
const requestBody: OfferingIdRequest = {
@@ -237,7 +242,7 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
} catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
return { priceMap: undefined, billingCurrency: undefined };
return { priceMap: new Map<string, Map<string, number>>(), billingCurrency: undefined };
}
};
@@ -286,6 +291,6 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
} catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
return undefined;
return new Map<string, Map<string, string>>();
}
};

View File

@@ -20,7 +20,7 @@ import {
import type { ChoiceItem } from "../SelfServeTypes";
import { BladeType, generateBladeLink, SelfServeType } from "../SelfServeUtils";
import { BladeType, generateBladeLink } from "../SelfServeUtils";
import {
deleteDedicatedGatewayResource,
getCurrentProvisioningState,
@@ -227,11 +227,13 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
let costPerHour = 0;
let costBreakdown = "";
for (const regionItem of regions) {
const incrementalCost = priceMap.get(regionItem.locationName).get(skuName.replace("Cosmos.", ""));
const incrementalCost = priceMap?.get(regionItem.locationName)?.get(skuName.replace("Cosmos.", ""));
if (incrementalCost === undefined) {
throw new Error(`${regionItem.locationName} not found in price map.`);
} else if (incrementalCost === 0) {
throw new Error(`${regionItem.locationName} cost per hour = 0`);
} else if (currencyCode === undefined) {
throw new Error(`Currency code not found in price map.`);
}
let regionalInstanceCount = instanceCount;
@@ -396,10 +398,6 @@ export default class SqlX extends SelfServeBaseClass {
return defaults;
};
public getSelfServeType = (): SelfServeType => {
return SelfServeType.sqlx;
};
@Values({
isDynamicDescription: true,
})

View File

@@ -35,6 +35,13 @@ describe("Query Utils", () => {
version: 2,
};
};
const generatePartitionKeysForPaths = (paths: string[]): DataModels.PartitionKey => {
return {
paths: paths,
kind: "Hash",
version: 2,
};
};
describe("buildDocumentsQueryPartitionProjections()", () => {
it("should return empty string if partition key is undefined", () => {
@@ -89,6 +96,18 @@ describe("Query Utils", () => {
expect(query).toContain("c.id");
});
it("should always include {} for any missing partition keys", () => {
const query = QueryUtils.buildDocumentsQuery(
"",
["a", "b", "c"],
generatePartitionKeysForPaths(["/a", "/b", "/c"]),
[],
);
expect(query).toContain('IIF(IS_DEFINED(c["a"]), c["a"], {})');
expect(query).toContain('IIF(IS_DEFINED(c["b"]), c["b"], {})');
expect(query).toContain('IIF(IS_DEFINED(c["c"]), c["c"], {})');
});
});
describe("queryPagesUntilContentPresent()", () => {
@@ -201,18 +220,6 @@ describe("Query Utils", () => {
expect(expectedPartitionKeyValues).toContain(documentContent["Category"]);
});
it("should extract no partition key values in the case nested partition key", () => {
const singlePartitionKeyDefinition: PartitionKeyDefinition = {
kind: PartitionKeyKind.Hash,
paths: ["/Location.type"],
};
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
documentContent,
singlePartitionKeyDefinition,
);
expect(partitionKeyValues.length).toBe(0);
});
it("should extract all partition key values for hierarchical and nested partition keys", () => {
const mixedPartitionKeyDefinition: PartitionKeyDefinition = {
kind: PartitionKeyKind.MultiHash,
@@ -225,5 +232,52 @@ describe("Query Utils", () => {
expect(partitionKeyValues.length).toBe(2);
expect(partitionKeyValues).toEqual(["United States", "Point"]);
});
it("if any partition key is null or empty string, the partitionKeyValues shall match", () => {
const newDocumentContent = {
...documentContent,
...{
Country: null,
Location: {
type: "",
coordinates: [-121.49, 46.206],
},
},
};
const mixedPartitionKeyDefinition: PartitionKeyDefinition = {
kind: PartitionKeyKind.MultiHash,
paths: ["/Country", "/Location/type"],
};
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
newDocumentContent,
mixedPartitionKeyDefinition,
);
expect(partitionKeyValues.length).toBe(2);
expect(partitionKeyValues).toEqual([null, ""]);
});
it("if any partition key doesn't exist, it should still set partitionkey value as {}", () => {
const newDocumentContent = {
...documentContent,
...{
Country: null,
Location: {
coordinates: [-121.49, 46.206],
},
},
};
const mixedPartitionKeyDefinition: PartitionKeyDefinition = {
kind: PartitionKeyKind.MultiHash,
paths: ["/Country", "/Location/type"],
};
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(
newDocumentContent,
mixedPartitionKeyDefinition,
);
expect(partitionKeyValues.length).toBe(2);
expect(partitionKeyValues).toEqual([null, {}]);
});
});
});

View File

@@ -61,8 +61,9 @@ export function buildDocumentsQueryPartitionProjections(
projectedProperty += `[${projection}]`;
}
});
projections.push(`${collectionAlias}${projectedProperty}`);
const fullAccess = `${collectionAlias}${projectedProperty}`;
const wrappedProjection = `IIF(IS_DEFINED(${fullAccess}), ${fullAccess}, {})`;
projections.push(wrappedProjection);
}
return projections.join(",");
@@ -130,6 +131,8 @@ export const extractPartitionKeyValues = (
if (value !== undefined) {
partitionKeyValues.push(value);
} else {
partitionKeyValues.push({});
}
});