Compare commits

..

14 Commits

Author SHA1 Message Date
Asier Isayas
228f412406 debug 2025-03-24 16:49:21 -04:00
Asier Isayas
cad718acc4 debug 2025-03-24 16:45:58 -04:00
Asier Isayas
0559ec5cb1 debug 2025-03-24 16:23:48 -04:00
Asier Isayas
ca641b2ff5 debug 2025-03-24 16:03:02 -04:00
Asier Isayas
53836a93cd debug 2025-03-24 15:18:20 -04:00
Asier Isayas
c38e42e44b debug 2025-03-24 14:50:42 -04:00
Asier Isayas
6032b39058 debug 2025-03-24 14:06:15 -04:00
Asier Isayas
23852dcd69 debug 2025-03-24 13:03:42 -04:00
Asier Isayas
81bd0f635e decorator debug 2025-03-20 15:32:44 -04:00
Asier Isayas
3efbc57617 npm run format 2025-03-20 13:44:15 -04:00
Asier Isayas
aee8249ffa fix self serve tests 2025-03-20 13:38:51 -04:00
Asier Isayas
14db9e819a fix tests 2025-03-20 13:09:44 -04:00
Asier Isayas
f9e18cf28c Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas/self-serve-fix 2025-03-20 12:56:29 -04:00
Asier Isayas
4708722d1a explicitly set className instead of inferring from constructor 2025-03-20 12:56:00 -04:00
12 changed files with 101 additions and 107 deletions

View File

@@ -1028,7 +1028,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
);
const selectedDocumentId = documentIds[clickedRowIndex as number];
const originalPartitionKeyValue = selectedDocumentId.partitionKeyValue;
selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
onExecutionErrorChange(false);
@@ -1064,14 +1063,9 @@ 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,8 +2,9 @@
* @module SelfServe/Decorators
*/
import { TFunction } from "i18next";
import { ChoiceItem, Description, Info, NumberUiType, OnChangeCallback, RefreshParams } from "./SelfServeTypes";
import { addPropertyToMap, buildSmartUiDescriptor, DecoratorProperties } from "./SelfServeUtils";
import { addPropertyToMap, buildSmartUiDescriptor, DecoratorProperties, SelfServeType } from "./SelfServeUtils";
type ValueOf<T> = T[keyof T];
interface Decorator {
@@ -128,8 +129,9 @@ const isDescriptionDisplayOptions = (inputOptions: InputOptions): inputOptions i
};
const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
return (target, property) => {
let className = target.constructor.name;
console.log(decorators);
return async (target, property) => {
let className: string = getTargetName(target);
const propertyName = property.toString();
if (className === "Function") {
//eslint-disable-next-line @typescript-eslint/ban-types
@@ -138,6 +140,7 @@ 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);
@@ -205,7 +208,8 @@ export const Values = (inputOptions: InputOptions): PropertyDecorator => {
*/
export const IsDisplayable = (): ClassDecorator => {
return (target) => {
buildSmartUiDescriptor(target.name, target.prototype);
let targetName: string = getTargetName(target);
buildSmartUiDescriptor(targetName, target.prototype);
};
};
@@ -215,7 +219,26 @@ export const IsDisplayable = (): ClassDecorator => {
* how often the auto refresh of the page occurs.
*/
export const RefreshOptions = (refreshParams: RefreshParams): ClassDecorator => {
console.log(refreshParams);
return (target) => {
addPropertyToMap(target.prototype, "root", target.name, "refreshParams", refreshParams);
let targetName: string = getTargetName(target);
addPropertyToMap(target.prototype, "root", targetName, "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,3 +1,4 @@
import { SelfServeType } from "SelfServe/SelfServeUtils";
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
import { selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
import {
@@ -168,6 +169,10 @@ 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 } from "../SelfServeUtils";
import { BladeType, generateBladeLink, SelfServeType } from "../SelfServeUtils";
import {
deleteComputeResource,
getCurrentProvisioningState,
@@ -360,6 +360,10 @@ 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 } from "../SelfServeUtils";
import { BladeType, generateBladeLink, SelfServeType } from "../SelfServeUtils";
import {
deleteMaterializedViewsBuilderResource,
getCurrentProvisioningState,
@@ -359,6 +359,10 @@ export default class MaterializedViewsBuilder extends SelfServeBaseClass {
return defaults;
};
public getSelfServeType = (): SelfServeType => {
return SelfServeType.materializedviewsbuilder;
};
@Values({
isDynamicDescription: true,
})

View File

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

View File

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

View File

@@ -141,6 +141,9 @@ 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,11 +197,6 @@ 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 = {
@@ -242,7 +237,7 @@ export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<Pr
} catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
return { priceMap: new Map<string, Map<string, number>>(), billingCurrency: undefined };
return { priceMap: undefined, billingCurrency: undefined };
}
};
@@ -291,6 +286,6 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
} catch (err) {
const failureTelemetry = { err, selfServeClassName: SqlX.name };
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
return new Map<string, Map<string, string>>();
return undefined;
}
};

View File

@@ -20,7 +20,7 @@ import {
import type { ChoiceItem } from "../SelfServeTypes";
import { BladeType, generateBladeLink } from "../SelfServeUtils";
import { BladeType, generateBladeLink, SelfServeType } from "../SelfServeUtils";
import {
deleteDedicatedGatewayResource,
getCurrentProvisioningState,
@@ -227,13 +227,11 @@ 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;
@@ -398,6 +396,10 @@ export default class SqlX extends SelfServeBaseClass {
return defaults;
};
public getSelfServeType = (): SelfServeType => {
return SelfServeType.sqlx;
};
@Values({
isDynamicDescription: true,
})

View File

@@ -35,13 +35,6 @@ 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", () => {
@@ -96,18 +89,6 @@ 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()", () => {
@@ -220,6 +201,18 @@ 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,
@@ -232,52 +225,5 @@ 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,9 +61,8 @@ export function buildDocumentsQueryPartitionProjections(
projectedProperty += `[${projection}]`;
}
});
const fullAccess = `${collectionAlias}${projectedProperty}`;
const wrappedProjection = `IIF(IS_DEFINED(${fullAccess}), ${fullAccess}, {})`;
projections.push(wrappedProjection);
projections.push(`${collectionAlias}${projectedProperty}`);
}
return projections.join(",");
@@ -131,8 +130,6 @@ export const extractPartitionKeyValues = (
if (value !== undefined) {
partitionKeyValues.push(value);
} else {
partitionKeyValues.push({});
}
});