mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-25 11:51:07 +00:00
Compare commits
14 Commits
missing_pk
...
users/aisa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
228f412406 | ||
|
|
cad718acc4 | ||
|
|
0559ec5cb1 | ||
|
|
ca641b2ff5 | ||
|
|
53836a93cd | ||
|
|
c38e42e44b | ||
|
|
6032b39058 | ||
|
|
23852dcd69 | ||
|
|
81bd0f635e | ||
|
|
3efbc57617 | ||
|
|
aee8249ffa | ||
|
|
14db9e819a | ||
|
|
f9e18cf28c | ||
|
|
4708722d1a |
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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}'.`);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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, {}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user