mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-20 09:20:16 +00:00
Prettier 2.0 (#393)
This commit is contained in:
@@ -1,105 +1,105 @@
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as AuthorizationUtils from "./AuthorizationUtils";
|
||||
import { AuthType } from "../AuthType";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { Platform, updateConfigContext } from "../ConfigContext";
|
||||
jest.mock("../Explorer/Explorer");
|
||||
|
||||
describe("AuthorizationUtils", () => {
|
||||
describe("getAuthorizationHeader()", () => {
|
||||
it("should return authorization header if authentication type is AAD", () => {
|
||||
window.authType = AuthType.AAD;
|
||||
updateUserContext({
|
||||
authorizationToken: "some-token"
|
||||
});
|
||||
|
||||
expect(AuthorizationUtils.getAuthorizationHeader().header).toBe(Constants.HttpHeaders.authorization);
|
||||
expect(AuthorizationUtils.getAuthorizationHeader().token).toBe("some-token");
|
||||
});
|
||||
|
||||
it("should return guest access header if authentication type is EncryptedToken", () => {
|
||||
window.authType = AuthType.EncryptedToken;
|
||||
updateUserContext({
|
||||
accessToken: "some-token"
|
||||
});
|
||||
|
||||
expect(AuthorizationUtils.getAuthorizationHeader().header).toBe(Constants.HttpHeaders.guestAccessToken);
|
||||
expect(AuthorizationUtils.getAuthorizationHeader().token).toBe("some-token");
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptJWTToken()", () => {
|
||||
it("should throw an error if token is undefined", () => {
|
||||
expect(() => AuthorizationUtils.decryptJWTToken(undefined)).toThrowError();
|
||||
});
|
||||
|
||||
it("should throw an error if token is null", () => {
|
||||
expect(() => AuthorizationUtils.decryptJWTToken(null)).toThrowError();
|
||||
});
|
||||
|
||||
it("should throw an error if token is empty", () => {
|
||||
expect(() => AuthorizationUtils.decryptJWTToken("")).toThrowError();
|
||||
});
|
||||
|
||||
it("should throw an error if token is malformed", () => {
|
||||
expect(() =>
|
||||
AuthorizationUtils.decryptJWTToken(
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9."
|
||||
)
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
it("should return decrypted token payload", () => {
|
||||
expect(
|
||||
AuthorizationUtils.decryptJWTToken(
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiJodHRwczovL3dvcmtzcGFjZWFydGlmYWN0cy5wcm9qZWN0YXJjYWRpYS5uZXQiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTcxOTUwMjIwLCJuYmYiOjE1NzE5NTAyMjAsImV4cCI6MTU3MTk1NDEyMCwiYWNyIjoiMSIsImFpbyI6IkFWUUFxLzhOQUFBQVJ5c1pWWW1qV3lqeG1zU3VpdUdGbUZLSEwxKytFM2JBK0xhck5mMUVYUnZ1MFB6bDlERWFaMVNMdi8vSXlscG5hanFwZG1aSjFaSXNZUEN0UzJrY1lJbWdTVjFvUitsM2VlNWZlT1JZRjZvPSIsImFtciI6WyJyc2EiLCJtZmEiXSwiYXBwaWQiOiIyMDNmMTE0NS04NTZhLTQyMzItODNkNC1hNDM1NjhmYmEyM2QiLCJhcHBpZGFjciI6IjAiLCJmYW1pbHlfbmFtZSI6IlJhbmdhaXNoZW52aSIsImdpdmVuX25hbWUiOiJWaWduZXNoIiwiaGFzZ3JvdXBzIjoidHJ1ZSIsImlwYWRkciI6IjEzMS4xMDcuMTQ3LjE0NiIsIm5hbWUiOiJWaWduZXNoIFJhbmdhaXNoZW52aSIsIm9pZCI6ImJiN2Q0YjliLTZlOGYtNDg4NS05OTI4LTBhOWM5OWQwN2Q1NSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0yMTI3NTIxMTg0LTE2MDQwMTI5MjAtMTg4NzkyNzUyNy0yNzEyNTYzNiIsInB1aWQiOiIxMDAzMDAwMEEyNjJGNDE4Iiwic2NwIjoid29ya3NwYWNlYXJ0aWZhY3RzLm1hbmFnZW1lbnQiLCJzdWIiOiI0X3hzSVdTdWZncHEtN2ZBV1dxaURYT3U5bGtKbDRpWEtBV0JVeUZ0Mm5vIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidW5pcXVlX25hbWUiOiJ2aXJhbmdhaUBtaWNyb3NvZnQuY29tIiwidXBuIjoidmlyYW5nYWlAbWljcm9zb2Z0LmNvbSIsInV0aSI6InoxRldzZzlWU2tPR1BTcEdremdWQUEiLCJ2ZXIiOiIxLjAifQ.nd-CZ6jpTQ8_2wkxQzuaoJCyEeR_woFK4MGMpHEVttwTd5WBDbVOUgk6gz36Jm2fdFemrQFJ03n1MXtCJYNnMoJX37SrGD3lAzZlXs5aBQig6ZrexWkiUDaaNcbx5qVy8O5JEQPds8OGMArsfUra0DG7iW0v7rgvhInX0umeC8ugnU5C-xEMPSZ9xYj0Q7m62AQrrCIIc94nUicEpxm_cusfsbT-CJHf2yLdmLYQkSx-ewzyBca0jiIl98sm0xA9btXDcwnWcmTY9scyGZ9mlSMtz4zmVY0NUdwssysKm7Js4aWtbA_ON8tsNEElViuwy_w3havM_3RQaNv26J87eQ"
|
||||
)
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("displayTokenRenewalPromptForStatus()", () => {
|
||||
let explorer = new Explorer() as jest.Mocked<Explorer>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
window.dataExplorer = explorer;
|
||||
updateConfigContext({
|
||||
platform: Platform.Hosted
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.dataExplorer = undefined;
|
||||
});
|
||||
|
||||
it("should not open token renewal prompt if status code is undefined", () => {
|
||||
AuthorizationUtils.displayTokenRenewalPromptForStatus(undefined);
|
||||
expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not open token renewal prompt if status code is null", () => {
|
||||
AuthorizationUtils.displayTokenRenewalPromptForStatus(null);
|
||||
expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not open token renewal prompt if status code is not 401", () => {
|
||||
AuthorizationUtils.displayTokenRenewalPromptForStatus(Constants.HttpStatusCodes.Forbidden);
|
||||
expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not open token renewal prompt if running on a different platform", () => {
|
||||
updateConfigContext({
|
||||
platform: Platform.Portal
|
||||
});
|
||||
AuthorizationUtils.displayTokenRenewalPromptForStatus(Constants.HttpStatusCodes.Unauthorized);
|
||||
expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should open token renewal prompt if running on hosted platform and status code is 401", () => {
|
||||
AuthorizationUtils.displayTokenRenewalPromptForStatus(Constants.HttpStatusCodes.Unauthorized);
|
||||
expect(explorer.displayGuestAccessTokenRenewalPrompt).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as AuthorizationUtils from "./AuthorizationUtils";
|
||||
import { AuthType } from "../AuthType";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { Platform, updateConfigContext } from "../ConfigContext";
|
||||
jest.mock("../Explorer/Explorer");
|
||||
|
||||
describe("AuthorizationUtils", () => {
|
||||
describe("getAuthorizationHeader()", () => {
|
||||
it("should return authorization header if authentication type is AAD", () => {
|
||||
window.authType = AuthType.AAD;
|
||||
updateUserContext({
|
||||
authorizationToken: "some-token",
|
||||
});
|
||||
|
||||
expect(AuthorizationUtils.getAuthorizationHeader().header).toBe(Constants.HttpHeaders.authorization);
|
||||
expect(AuthorizationUtils.getAuthorizationHeader().token).toBe("some-token");
|
||||
});
|
||||
|
||||
it("should return guest access header if authentication type is EncryptedToken", () => {
|
||||
window.authType = AuthType.EncryptedToken;
|
||||
updateUserContext({
|
||||
accessToken: "some-token",
|
||||
});
|
||||
|
||||
expect(AuthorizationUtils.getAuthorizationHeader().header).toBe(Constants.HttpHeaders.guestAccessToken);
|
||||
expect(AuthorizationUtils.getAuthorizationHeader().token).toBe("some-token");
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptJWTToken()", () => {
|
||||
it("should throw an error if token is undefined", () => {
|
||||
expect(() => AuthorizationUtils.decryptJWTToken(undefined)).toThrowError();
|
||||
});
|
||||
|
||||
it("should throw an error if token is null", () => {
|
||||
expect(() => AuthorizationUtils.decryptJWTToken(null)).toThrowError();
|
||||
});
|
||||
|
||||
it("should throw an error if token is empty", () => {
|
||||
expect(() => AuthorizationUtils.decryptJWTToken("")).toThrowError();
|
||||
});
|
||||
|
||||
it("should throw an error if token is malformed", () => {
|
||||
expect(() =>
|
||||
AuthorizationUtils.decryptJWTToken(
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9."
|
||||
)
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
it("should return decrypted token payload", () => {
|
||||
expect(
|
||||
AuthorizationUtils.decryptJWTToken(
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyIsImtpZCI6ImFQY3R3X29kdlJPb0VOZzNWb09sSWgydGlFcyJ9.eyJhdWQiOiJodHRwczovL3dvcmtzcGFjZWFydGlmYWN0cy5wcm9qZWN0YXJjYWRpYS5uZXQiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNTcxOTUwMjIwLCJuYmYiOjE1NzE5NTAyMjAsImV4cCI6MTU3MTk1NDEyMCwiYWNyIjoiMSIsImFpbyI6IkFWUUFxLzhOQUFBQVJ5c1pWWW1qV3lqeG1zU3VpdUdGbUZLSEwxKytFM2JBK0xhck5mMUVYUnZ1MFB6bDlERWFaMVNMdi8vSXlscG5hanFwZG1aSjFaSXNZUEN0UzJrY1lJbWdTVjFvUitsM2VlNWZlT1JZRjZvPSIsImFtciI6WyJyc2EiLCJtZmEiXSwiYXBwaWQiOiIyMDNmMTE0NS04NTZhLTQyMzItODNkNC1hNDM1NjhmYmEyM2QiLCJhcHBpZGFjciI6IjAiLCJmYW1pbHlfbmFtZSI6IlJhbmdhaXNoZW52aSIsImdpdmVuX25hbWUiOiJWaWduZXNoIiwiaGFzZ3JvdXBzIjoidHJ1ZSIsImlwYWRkciI6IjEzMS4xMDcuMTQ3LjE0NiIsIm5hbWUiOiJWaWduZXNoIFJhbmdhaXNoZW52aSIsIm9pZCI6ImJiN2Q0YjliLTZlOGYtNDg4NS05OTI4LTBhOWM5OWQwN2Q1NSIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0yMTI3NTIxMTg0LTE2MDQwMTI5MjAtMTg4NzkyNzUyNy0yNzEyNTYzNiIsInB1aWQiOiIxMDAzMDAwMEEyNjJGNDE4Iiwic2NwIjoid29ya3NwYWNlYXJ0aWZhY3RzLm1hbmFnZW1lbnQiLCJzdWIiOiI0X3hzSVdTdWZncHEtN2ZBV1dxaURYT3U5bGtKbDRpWEtBV0JVeUZ0Mm5vIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidW5pcXVlX25hbWUiOiJ2aXJhbmdhaUBtaWNyb3NvZnQuY29tIiwidXBuIjoidmlyYW5nYWlAbWljcm9zb2Z0LmNvbSIsInV0aSI6InoxRldzZzlWU2tPR1BTcEdremdWQUEiLCJ2ZXIiOiIxLjAifQ.nd-CZ6jpTQ8_2wkxQzuaoJCyEeR_woFK4MGMpHEVttwTd5WBDbVOUgk6gz36Jm2fdFemrQFJ03n1MXtCJYNnMoJX37SrGD3lAzZlXs5aBQig6ZrexWkiUDaaNcbx5qVy8O5JEQPds8OGMArsfUra0DG7iW0v7rgvhInX0umeC8ugnU5C-xEMPSZ9xYj0Q7m62AQrrCIIc94nUicEpxm_cusfsbT-CJHf2yLdmLYQkSx-ewzyBca0jiIl98sm0xA9btXDcwnWcmTY9scyGZ9mlSMtz4zmVY0NUdwssysKm7Js4aWtbA_ON8tsNEElViuwy_w3havM_3RQaNv26J87eQ"
|
||||
)
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("displayTokenRenewalPromptForStatus()", () => {
|
||||
let explorer = new Explorer() as jest.Mocked<Explorer>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
window.dataExplorer = explorer;
|
||||
updateConfigContext({
|
||||
platform: Platform.Hosted,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.dataExplorer = undefined;
|
||||
});
|
||||
|
||||
it("should not open token renewal prompt if status code is undefined", () => {
|
||||
AuthorizationUtils.displayTokenRenewalPromptForStatus(undefined);
|
||||
expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not open token renewal prompt if status code is null", () => {
|
||||
AuthorizationUtils.displayTokenRenewalPromptForStatus(null);
|
||||
expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not open token renewal prompt if status code is not 401", () => {
|
||||
AuthorizationUtils.displayTokenRenewalPromptForStatus(Constants.HttpStatusCodes.Forbidden);
|
||||
expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not open token renewal prompt if running on a different platform", () => {
|
||||
updateConfigContext({
|
||||
platform: Platform.Portal,
|
||||
});
|
||||
AuthorizationUtils.displayTokenRenewalPromptForStatus(Constants.HttpStatusCodes.Unauthorized);
|
||||
expect(explorer.displayGuestAccessTokenRenewalPrompt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should open token renewal prompt if running on hosted platform and status code is 401", () => {
|
||||
AuthorizationUtils.displayTokenRenewalPromptForStatus(Constants.HttpStatusCodes.Unauthorized);
|
||||
expect(explorer.displayGuestAccessTokenRenewalPrompt).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
import { AuthType } from "../AuthType";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
||||
if (window.authType === AuthType.EncryptedToken) {
|
||||
return {
|
||||
header: Constants.HttpHeaders.guestAccessToken,
|
||||
token: userContext.accessToken
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
header: Constants.HttpHeaders.authorization,
|
||||
token: userContext.authorizationToken || ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function decryptJWTToken(token: string) {
|
||||
if (!token) {
|
||||
Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken");
|
||||
throw new Error("No JWT token found");
|
||||
}
|
||||
const tokenParts = token.split(".");
|
||||
if (tokenParts.length < 2) {
|
||||
Logger.logError(`Invalid JWT token: ${token}`, "AuthorizationUtils/decryptJWTToken");
|
||||
throw new Error(`Invalid JWT token: ${token}`);
|
||||
}
|
||||
let tokenPayloadBase64: string = tokenParts[1];
|
||||
tokenPayloadBase64 = tokenPayloadBase64.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const tokenPayload = decodeURIComponent(
|
||||
atob(tokenPayloadBase64)
|
||||
.split("")
|
||||
.map(p => "%" + ("00" + p.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join("")
|
||||
);
|
||||
|
||||
return JSON.parse(tokenPayload);
|
||||
}
|
||||
|
||||
export function displayTokenRenewalPromptForStatus(httpStatusCode: number): void {
|
||||
const explorer = window.dataExplorer;
|
||||
|
||||
if (
|
||||
httpStatusCode == null ||
|
||||
httpStatusCode != Constants.HttpStatusCodes.Unauthorized ||
|
||||
configContext.platform !== Platform.Hosted
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
explorer.displayGuestAccessTokenRenewalPrompt();
|
||||
}
|
||||
import { AuthType } from "../AuthType";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
||||
if (window.authType === AuthType.EncryptedToken) {
|
||||
return {
|
||||
header: Constants.HttpHeaders.guestAccessToken,
|
||||
token: userContext.accessToken,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
header: Constants.HttpHeaders.authorization,
|
||||
token: userContext.authorizationToken || "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function decryptJWTToken(token: string) {
|
||||
if (!token) {
|
||||
Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken");
|
||||
throw new Error("No JWT token found");
|
||||
}
|
||||
const tokenParts = token.split(".");
|
||||
if (tokenParts.length < 2) {
|
||||
Logger.logError(`Invalid JWT token: ${token}`, "AuthorizationUtils/decryptJWTToken");
|
||||
throw new Error(`Invalid JWT token: ${token}`);
|
||||
}
|
||||
let tokenPayloadBase64: string = tokenParts[1];
|
||||
tokenPayloadBase64 = tokenPayloadBase64.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const tokenPayload = decodeURIComponent(
|
||||
atob(tokenPayloadBase64)
|
||||
.split("")
|
||||
.map((p) => "%" + ("00" + p.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join("")
|
||||
);
|
||||
|
||||
return JSON.parse(tokenPayload);
|
||||
}
|
||||
|
||||
export function displayTokenRenewalPromptForStatus(httpStatusCode: number): void {
|
||||
const explorer = window.dataExplorer;
|
||||
|
||||
if (
|
||||
httpStatusCode == null ||
|
||||
httpStatusCode != Constants.HttpStatusCodes.Unauthorized ||
|
||||
configContext.platform !== Platform.Hosted
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
explorer.displayGuestAccessTokenRenewalPrompt();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as Base64Utils from "./Base64Utils";
|
||||
|
||||
describe("Base64Utils", () => {
|
||||
describe("utf8ToB64", () => {
|
||||
it("should convert utf8 to base64", () => {
|
||||
expect(Base64Utils.utf8ToB64("abcd")).toEqual(btoa("abcd"));
|
||||
expect(Base64Utils.utf8ToB64("小飼弾")).toEqual("5bCP6aO85by+");
|
||||
expect(Base64Utils.utf8ToB64("à mon hôpital préféré")).toEqual("w6AgbW9uIGjDtHBpdGFsIHByw6lmw6lyw6k=");
|
||||
});
|
||||
});
|
||||
});
|
||||
import * as Base64Utils from "./Base64Utils";
|
||||
|
||||
describe("Base64Utils", () => {
|
||||
describe("utf8ToB64", () => {
|
||||
it("should convert utf8 to base64", () => {
|
||||
expect(Base64Utils.utf8ToB64("abcd")).toEqual(btoa("abcd"));
|
||||
expect(Base64Utils.utf8ToB64("小飼弾")).toEqual("5bCP6aO85by+");
|
||||
expect(Base64Utils.utf8ToB64("à mon hôpital préféré")).toEqual("w6AgbW9uIGjDtHBpdGFsIHByw6lmw6lyw6k=");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const utf8ToB64 = (utf8Str: string): string => {
|
||||
return btoa(
|
||||
encodeURIComponent(utf8Str).replace(/%([0-9A-F]{2})/g, (_, args) => {
|
||||
return String.fromCharCode(parseInt(args, 16));
|
||||
})
|
||||
);
|
||||
};
|
||||
export const utf8ToB64 = (utf8Str: string): string => {
|
||||
return btoa(
|
||||
encodeURIComponent(utf8Str).replace(/%([0-9A-F]{2})/g, (_, args) => {
|
||||
return String.fromCharCode(parseInt(args, 16));
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
export const stringToBlob = (data: string, contentType: string, sliceSize = 512): Blob => {
|
||||
const byteArrays = [];
|
||||
|
||||
for (let offset = 0; offset < data.length; offset += sliceSize) {
|
||||
const slice = data.slice(offset, offset + sliceSize);
|
||||
|
||||
const byteNumbers = new Array(slice.length);
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
|
||||
return new Blob(byteArrays, { type: contentType });
|
||||
};
|
||||
export const stringToBlob = (data: string, contentType: string, sliceSize = 512): Blob => {
|
||||
const byteArrays = [];
|
||||
|
||||
for (let offset = 0; offset < data.length; offset += sliceSize) {
|
||||
const slice = data.slice(offset, offset + sliceSize);
|
||||
|
||||
const byteNumbers = new Array(slice.length);
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
|
||||
return new Blob(byteArrays, { type: contentType });
|
||||
};
|
||||
|
||||
@@ -1,111 +1,111 @@
|
||||
import * as GalleryUtils from "./GalleryUtils";
|
||||
import { JunoClient, IGalleryItem } from "../Juno/JunoClient";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
|
||||
const galleryItem: IGalleryItem = {
|
||||
id: "id",
|
||||
name: "name",
|
||||
description: "description",
|
||||
gitSha: "gitSha",
|
||||
tags: ["tag1"],
|
||||
author: "author",
|
||||
thumbnailUrl: "thumbnailUrl",
|
||||
created: "created",
|
||||
isSample: false,
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
views: 0,
|
||||
newCellId: undefined,
|
||||
policyViolations: undefined,
|
||||
pendingScanJobIds: undefined
|
||||
};
|
||||
|
||||
describe("GalleryUtils", () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("downloadItem shows dialog in data explorer", () => {
|
||||
const container = {} as Explorer;
|
||||
container.showOkCancelModalDialog = jest.fn().mockImplementation();
|
||||
|
||||
GalleryUtils.downloadItem(container, undefined, galleryItem, undefined);
|
||||
|
||||
expect(container.showOkCancelModalDialog).toBeCalled();
|
||||
});
|
||||
|
||||
it("favoriteItem favorites item", async () => {
|
||||
const container = {} as Explorer;
|
||||
const junoClient = new JunoClient();
|
||||
junoClient.favoriteNotebook = jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: galleryItem }));
|
||||
const onComplete = jest.fn().mockImplementation();
|
||||
|
||||
await GalleryUtils.favoriteItem(container, junoClient, galleryItem, onComplete);
|
||||
|
||||
expect(junoClient.favoriteNotebook).toBeCalledWith(galleryItem.id);
|
||||
expect(onComplete).toBeCalledWith(galleryItem);
|
||||
});
|
||||
|
||||
it("unfavoriteItem unfavorites item", async () => {
|
||||
const container = {} as Explorer;
|
||||
const junoClient = new JunoClient();
|
||||
junoClient.unfavoriteNotebook = jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: galleryItem }));
|
||||
const onComplete = jest.fn().mockImplementation();
|
||||
|
||||
await GalleryUtils.unfavoriteItem(container, junoClient, galleryItem, onComplete);
|
||||
|
||||
expect(junoClient.unfavoriteNotebook).toBeCalledWith(galleryItem.id);
|
||||
expect(onComplete).toBeCalledWith(galleryItem);
|
||||
});
|
||||
|
||||
it("deleteItem shows dialog in data explorer", () => {
|
||||
const container = {} as Explorer;
|
||||
container.showOkCancelModalDialog = jest.fn().mockImplementation();
|
||||
|
||||
GalleryUtils.deleteItem(container, undefined, galleryItem, undefined);
|
||||
|
||||
expect(container.showOkCancelModalDialog).toBeCalled();
|
||||
});
|
||||
|
||||
it("getGalleryViewerProps gets gallery viewer props correctly", () => {
|
||||
const selectedTab: GalleryTab = GalleryTab.OfficialSamples;
|
||||
const sortBy: SortBy = SortBy.MostDownloaded;
|
||||
const searchText = "my-complicated%20search%20query!!!";
|
||||
|
||||
const response = GalleryUtils.getGalleryViewerProps(
|
||||
`?${GalleryUtils.GalleryViewerParams.SelectedTab}=${GalleryTab[selectedTab]}&${GalleryUtils.GalleryViewerParams.SortBy}=${SortBy[sortBy]}&${GalleryUtils.GalleryViewerParams.SearchText}=${searchText}`
|
||||
);
|
||||
|
||||
expect(response).toEqual({
|
||||
selectedTab,
|
||||
sortBy,
|
||||
searchText: decodeURIComponent(searchText)
|
||||
} as GalleryUtils.GalleryViewerProps);
|
||||
});
|
||||
|
||||
it("getNotebookViewerProps gets notebook viewer props correctly", () => {
|
||||
const notebookUrl = "https%3A%2F%2Fnotebook.url";
|
||||
const galleryItemId = "1234-abcd-efgh";
|
||||
const hideInputs = "true";
|
||||
|
||||
const response = GalleryUtils.getNotebookViewerProps(
|
||||
`?${GalleryUtils.NotebookViewerParams.NotebookUrl}=${notebookUrl}&${GalleryUtils.NotebookViewerParams.GalleryItemId}=${galleryItemId}&${GalleryUtils.NotebookViewerParams.HideInputs}=${hideInputs}`
|
||||
);
|
||||
|
||||
expect(response).toEqual({
|
||||
notebookUrl: decodeURIComponent(notebookUrl),
|
||||
galleryItemId,
|
||||
hideInputs: true
|
||||
} as GalleryUtils.NotebookViewerProps);
|
||||
});
|
||||
|
||||
it("getTabTitle returns correct title for official samples", () => {
|
||||
expect(GalleryUtils.getTabTitle(GalleryTab.OfficialSamples)).toBe("Official samples");
|
||||
});
|
||||
});
|
||||
import * as GalleryUtils from "./GalleryUtils";
|
||||
import { JunoClient, IGalleryItem } from "../Juno/JunoClient";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
|
||||
const galleryItem: IGalleryItem = {
|
||||
id: "id",
|
||||
name: "name",
|
||||
description: "description",
|
||||
gitSha: "gitSha",
|
||||
tags: ["tag1"],
|
||||
author: "author",
|
||||
thumbnailUrl: "thumbnailUrl",
|
||||
created: "created",
|
||||
isSample: false,
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
views: 0,
|
||||
newCellId: undefined,
|
||||
policyViolations: undefined,
|
||||
pendingScanJobIds: undefined,
|
||||
};
|
||||
|
||||
describe("GalleryUtils", () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("downloadItem shows dialog in data explorer", () => {
|
||||
const container = {} as Explorer;
|
||||
container.showOkCancelModalDialog = jest.fn().mockImplementation();
|
||||
|
||||
GalleryUtils.downloadItem(container, undefined, galleryItem, undefined);
|
||||
|
||||
expect(container.showOkCancelModalDialog).toBeCalled();
|
||||
});
|
||||
|
||||
it("favoriteItem favorites item", async () => {
|
||||
const container = {} as Explorer;
|
||||
const junoClient = new JunoClient();
|
||||
junoClient.favoriteNotebook = jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: galleryItem }));
|
||||
const onComplete = jest.fn().mockImplementation();
|
||||
|
||||
await GalleryUtils.favoriteItem(container, junoClient, galleryItem, onComplete);
|
||||
|
||||
expect(junoClient.favoriteNotebook).toBeCalledWith(galleryItem.id);
|
||||
expect(onComplete).toBeCalledWith(galleryItem);
|
||||
});
|
||||
|
||||
it("unfavoriteItem unfavorites item", async () => {
|
||||
const container = {} as Explorer;
|
||||
const junoClient = new JunoClient();
|
||||
junoClient.unfavoriteNotebook = jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: galleryItem }));
|
||||
const onComplete = jest.fn().mockImplementation();
|
||||
|
||||
await GalleryUtils.unfavoriteItem(container, junoClient, galleryItem, onComplete);
|
||||
|
||||
expect(junoClient.unfavoriteNotebook).toBeCalledWith(galleryItem.id);
|
||||
expect(onComplete).toBeCalledWith(galleryItem);
|
||||
});
|
||||
|
||||
it("deleteItem shows dialog in data explorer", () => {
|
||||
const container = {} as Explorer;
|
||||
container.showOkCancelModalDialog = jest.fn().mockImplementation();
|
||||
|
||||
GalleryUtils.deleteItem(container, undefined, galleryItem, undefined);
|
||||
|
||||
expect(container.showOkCancelModalDialog).toBeCalled();
|
||||
});
|
||||
|
||||
it("getGalleryViewerProps gets gallery viewer props correctly", () => {
|
||||
const selectedTab: GalleryTab = GalleryTab.OfficialSamples;
|
||||
const sortBy: SortBy = SortBy.MostDownloaded;
|
||||
const searchText = "my-complicated%20search%20query!!!";
|
||||
|
||||
const response = GalleryUtils.getGalleryViewerProps(
|
||||
`?${GalleryUtils.GalleryViewerParams.SelectedTab}=${GalleryTab[selectedTab]}&${GalleryUtils.GalleryViewerParams.SortBy}=${SortBy[sortBy]}&${GalleryUtils.GalleryViewerParams.SearchText}=${searchText}`
|
||||
);
|
||||
|
||||
expect(response).toEqual({
|
||||
selectedTab,
|
||||
sortBy,
|
||||
searchText: decodeURIComponent(searchText),
|
||||
} as GalleryUtils.GalleryViewerProps);
|
||||
});
|
||||
|
||||
it("getNotebookViewerProps gets notebook viewer props correctly", () => {
|
||||
const notebookUrl = "https%3A%2F%2Fnotebook.url";
|
||||
const galleryItemId = "1234-abcd-efgh";
|
||||
const hideInputs = "true";
|
||||
|
||||
const response = GalleryUtils.getNotebookViewerProps(
|
||||
`?${GalleryUtils.NotebookViewerParams.NotebookUrl}=${notebookUrl}&${GalleryUtils.NotebookViewerParams.GalleryItemId}=${galleryItemId}&${GalleryUtils.NotebookViewerParams.HideInputs}=${hideInputs}`
|
||||
);
|
||||
|
||||
expect(response).toEqual({
|
||||
notebookUrl: decodeURIComponent(notebookUrl),
|
||||
galleryItemId,
|
||||
hideInputs: true,
|
||||
} as GalleryUtils.NotebookViewerProps);
|
||||
});
|
||||
|
||||
it("getTabTitle returns correct title for official samples", () => {
|
||||
expect(GalleryUtils.getTabTitle(GalleryTab.OfficialSamples)).toBe("Official samples");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,344 +1,344 @@
|
||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
||||
import * as NotificationConsoleUtils from "./NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import {
|
||||
GalleryTab,
|
||||
SortBy,
|
||||
GalleryViewerComponent
|
||||
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { IChoiceGroupOption, IChoiceGroupProps } from "office-ui-fabric-react";
|
||||
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
|
||||
import { handleError } from "../Common/ErrorHandlingUtils";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
|
||||
const defaultSelectedAbuseCategory = "Other";
|
||||
const abuseCategories: IChoiceGroupOption[] = [
|
||||
{
|
||||
key: "ChildEndangermentExploitation",
|
||||
text: "Child endangerment or exploitation"
|
||||
},
|
||||
{
|
||||
key: "ContentInfringement",
|
||||
text: "Content infringement"
|
||||
},
|
||||
{
|
||||
key: "OffensiveContent",
|
||||
text: "Offensive content"
|
||||
},
|
||||
{
|
||||
key: "Terrorism",
|
||||
text: "Terrorism"
|
||||
},
|
||||
{
|
||||
key: "ThreatsCyberbullyingHarassment",
|
||||
text: "Threats, cyber bullying or harassment"
|
||||
},
|
||||
{
|
||||
key: "VirusSpywareMalware",
|
||||
text: "Virus, spyware or malware"
|
||||
},
|
||||
{
|
||||
key: "Fraud",
|
||||
text: "Fraud"
|
||||
},
|
||||
{
|
||||
key: "HateSpeech",
|
||||
text: "Hate speech"
|
||||
},
|
||||
{
|
||||
key: "ImminentHarmToPersonsOrProperty",
|
||||
text: "Imminent harm to persons or property"
|
||||
},
|
||||
{
|
||||
key: "Other",
|
||||
text: "Other"
|
||||
}
|
||||
];
|
||||
|
||||
export enum NotebookViewerParams {
|
||||
NotebookUrl = "notebookUrl",
|
||||
GalleryItemId = "galleryItemId",
|
||||
HideInputs = "hideInputs"
|
||||
}
|
||||
|
||||
export interface NotebookViewerProps {
|
||||
notebookUrl: string;
|
||||
galleryItemId: string;
|
||||
hideInputs: boolean;
|
||||
}
|
||||
|
||||
export enum GalleryViewerParams {
|
||||
SelectedTab = "tab",
|
||||
SortBy = "sort",
|
||||
SearchText = "q"
|
||||
}
|
||||
|
||||
export interface GalleryViewerProps {
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
export interface DialogHost {
|
||||
showOkCancelModalDialog(
|
||||
title: string,
|
||||
msg: string,
|
||||
okLabel: string,
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void,
|
||||
choiceGroupProps?: IChoiceGroupProps,
|
||||
textFieldProps?: TextFieldProps
|
||||
): void;
|
||||
}
|
||||
|
||||
export function reportAbuse(
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
dialogHost: DialogHost,
|
||||
onComplete: (success: boolean) => void
|
||||
): void {
|
||||
const notebookId = data.id;
|
||||
let abuseCategory = defaultSelectedAbuseCategory;
|
||||
let additionalDetails: string;
|
||||
|
||||
dialogHost.showOkCancelModalDialog(
|
||||
"Report Abuse",
|
||||
undefined,
|
||||
"Report Abuse",
|
||||
async () => {
|
||||
const clearSubmitReportNotification = NotificationConsoleUtils.logConsoleProgress(
|
||||
`Submitting your report on ${data.name} violating code of conduct`
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails);
|
||||
if (response.status !== HttpStatusCodes.Accepted) {
|
||||
throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.logConsoleInfo(
|
||||
`Your report on ${data.name} has been submitted. Thank you for reporting the violation.`
|
||||
);
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
"GalleryUtils/reportAbuse",
|
||||
`Failed to submit report on ${data.name} violating code of conduct`
|
||||
);
|
||||
}
|
||||
|
||||
clearSubmitReportNotification();
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
{
|
||||
label: "How does this content violate the code of conduct?",
|
||||
options: abuseCategories,
|
||||
defaultSelectedKey: defaultSelectedAbuseCategory,
|
||||
onChange: (_event?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption) => {
|
||||
abuseCategory = option?.key;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "You can also include additional relevant details on the offensive content",
|
||||
multiline: true,
|
||||
rows: 3,
|
||||
autoAdjustHeight: false,
|
||||
onChange: (_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
|
||||
additionalDetails = newValue;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function downloadItem(
|
||||
container: Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void
|
||||
): void {
|
||||
const name = data.name;
|
||||
container.showOkCancelModalDialog(
|
||||
"Download to My Notebooks",
|
||||
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`,
|
||||
"Download",
|
||||
async () => {
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Downloading ${name} to My Notebooks`
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await junoClient.getNotebookContent(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
|
||||
}
|
||||
|
||||
await container.importAndOpenContent(data.name, response.data);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully downloaded ${name} to My Notebooks`
|
||||
);
|
||||
|
||||
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
|
||||
if (increaseDownloadResponse.data) {
|
||||
onComplete(increaseDownloadResponse.data);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
},
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
export async function favoriteItem(
|
||||
container: Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void
|
||||
): Promise<void> {
|
||||
if (container) {
|
||||
try {
|
||||
const response = await junoClient.favoriteNotebook(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when favoriting ${data.name}`);
|
||||
}
|
||||
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
handleError(error, "GalleryUtils/favoriteItem", `Failed to favorite ${data.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function unfavoriteItem(
|
||||
container: Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void
|
||||
): Promise<void> {
|
||||
if (container) {
|
||||
try {
|
||||
const response = await junoClient.unfavoriteNotebook(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when unfavoriting ${data.name}`);
|
||||
}
|
||||
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
handleError(error, "GalleryUtils/unfavoriteItem", `Failed to unfavorite ${data.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteItem(
|
||||
container: Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void
|
||||
): void {
|
||||
if (container) {
|
||||
container.showOkCancelModalDialog(
|
||||
"Remove published notebook",
|
||||
`Would you like to remove ${data.name} from the gallery?`,
|
||||
"Remove",
|
||||
async () => {
|
||||
const name = data.name;
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Removing ${name} from gallery`
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await junoClient.deleteNotebook(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} while removing ${name}`);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully removed ${name} from gallery`);
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
handleError(error, "GalleryUtils/deleteItem", `Failed to remove ${name} from gallery`);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
},
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getGalleryViewerProps(search: string): GalleryViewerProps {
|
||||
const params = new URLSearchParams(search);
|
||||
let selectedTab: GalleryTab;
|
||||
if (params.has(GalleryViewerParams.SelectedTab)) {
|
||||
selectedTab = GalleryTab[params.get(GalleryViewerParams.SelectedTab) as keyof typeof GalleryTab];
|
||||
}
|
||||
|
||||
let sortBy: SortBy;
|
||||
if (params.has(GalleryViewerParams.SortBy)) {
|
||||
sortBy = SortBy[params.get(GalleryViewerParams.SortBy) as keyof typeof SortBy];
|
||||
}
|
||||
|
||||
return {
|
||||
selectedTab,
|
||||
sortBy,
|
||||
searchText: params.get(GalleryViewerParams.SearchText)
|
||||
};
|
||||
}
|
||||
|
||||
export function getNotebookViewerProps(search: string): NotebookViewerProps {
|
||||
const params = new URLSearchParams(search);
|
||||
return {
|
||||
notebookUrl: params.get(NotebookViewerParams.NotebookUrl),
|
||||
galleryItemId: params.get(NotebookViewerParams.GalleryItemId),
|
||||
hideInputs: JSON.parse(params.get(NotebookViewerParams.HideInputs))
|
||||
};
|
||||
}
|
||||
|
||||
export function getTabTitle(tab: GalleryTab): string {
|
||||
switch (tab) {
|
||||
case GalleryTab.OfficialSamples:
|
||||
return GalleryViewerComponent.OfficialSamplesTitle;
|
||||
case GalleryTab.PublicGallery:
|
||||
return GalleryViewerComponent.PublicGalleryTitle;
|
||||
case GalleryTab.Favorites:
|
||||
return GalleryViewerComponent.FavoritesTitle;
|
||||
case GalleryTab.Published:
|
||||
return GalleryViewerComponent.PublishedTitle;
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function filterPublishedNotebooks(
|
||||
items: IGalleryItem[]
|
||||
): {
|
||||
published: IGalleryItem[];
|
||||
underReview: IGalleryItem[];
|
||||
removed: IGalleryItem[];
|
||||
} {
|
||||
const underReview: IGalleryItem[] = [];
|
||||
const removed: IGalleryItem[] = [];
|
||||
const published: IGalleryItem[] = [];
|
||||
|
||||
items?.forEach(item => {
|
||||
if (item.policyViolations?.length > 0) {
|
||||
removed.push(item);
|
||||
} else if (item.pendingScanJobIds?.length > 0) {
|
||||
underReview.push(item);
|
||||
} else {
|
||||
published.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return { published, underReview, removed };
|
||||
}
|
||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
||||
import * as NotificationConsoleUtils from "./NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import {
|
||||
GalleryTab,
|
||||
SortBy,
|
||||
GalleryViewerComponent,
|
||||
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { IChoiceGroupOption, IChoiceGroupProps } from "office-ui-fabric-react";
|
||||
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
|
||||
import { handleError } from "../Common/ErrorHandlingUtils";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
|
||||
const defaultSelectedAbuseCategory = "Other";
|
||||
const abuseCategories: IChoiceGroupOption[] = [
|
||||
{
|
||||
key: "ChildEndangermentExploitation",
|
||||
text: "Child endangerment or exploitation",
|
||||
},
|
||||
{
|
||||
key: "ContentInfringement",
|
||||
text: "Content infringement",
|
||||
},
|
||||
{
|
||||
key: "OffensiveContent",
|
||||
text: "Offensive content",
|
||||
},
|
||||
{
|
||||
key: "Terrorism",
|
||||
text: "Terrorism",
|
||||
},
|
||||
{
|
||||
key: "ThreatsCyberbullyingHarassment",
|
||||
text: "Threats, cyber bullying or harassment",
|
||||
},
|
||||
{
|
||||
key: "VirusSpywareMalware",
|
||||
text: "Virus, spyware or malware",
|
||||
},
|
||||
{
|
||||
key: "Fraud",
|
||||
text: "Fraud",
|
||||
},
|
||||
{
|
||||
key: "HateSpeech",
|
||||
text: "Hate speech",
|
||||
},
|
||||
{
|
||||
key: "ImminentHarmToPersonsOrProperty",
|
||||
text: "Imminent harm to persons or property",
|
||||
},
|
||||
{
|
||||
key: "Other",
|
||||
text: "Other",
|
||||
},
|
||||
];
|
||||
|
||||
export enum NotebookViewerParams {
|
||||
NotebookUrl = "notebookUrl",
|
||||
GalleryItemId = "galleryItemId",
|
||||
HideInputs = "hideInputs",
|
||||
}
|
||||
|
||||
export interface NotebookViewerProps {
|
||||
notebookUrl: string;
|
||||
galleryItemId: string;
|
||||
hideInputs: boolean;
|
||||
}
|
||||
|
||||
export enum GalleryViewerParams {
|
||||
SelectedTab = "tab",
|
||||
SortBy = "sort",
|
||||
SearchText = "q",
|
||||
}
|
||||
|
||||
export interface GalleryViewerProps {
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
export interface DialogHost {
|
||||
showOkCancelModalDialog(
|
||||
title: string,
|
||||
msg: string,
|
||||
okLabel: string,
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void,
|
||||
choiceGroupProps?: IChoiceGroupProps,
|
||||
textFieldProps?: TextFieldProps
|
||||
): void;
|
||||
}
|
||||
|
||||
export function reportAbuse(
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
dialogHost: DialogHost,
|
||||
onComplete: (success: boolean) => void
|
||||
): void {
|
||||
const notebookId = data.id;
|
||||
let abuseCategory = defaultSelectedAbuseCategory;
|
||||
let additionalDetails: string;
|
||||
|
||||
dialogHost.showOkCancelModalDialog(
|
||||
"Report Abuse",
|
||||
undefined,
|
||||
"Report Abuse",
|
||||
async () => {
|
||||
const clearSubmitReportNotification = NotificationConsoleUtils.logConsoleProgress(
|
||||
`Submitting your report on ${data.name} violating code of conduct`
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails);
|
||||
if (response.status !== HttpStatusCodes.Accepted) {
|
||||
throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.logConsoleInfo(
|
||||
`Your report on ${data.name} has been submitted. Thank you for reporting the violation.`
|
||||
);
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
"GalleryUtils/reportAbuse",
|
||||
`Failed to submit report on ${data.name} violating code of conduct`
|
||||
);
|
||||
}
|
||||
|
||||
clearSubmitReportNotification();
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
{
|
||||
label: "How does this content violate the code of conduct?",
|
||||
options: abuseCategories,
|
||||
defaultSelectedKey: defaultSelectedAbuseCategory,
|
||||
onChange: (_event?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption) => {
|
||||
abuseCategory = option?.key;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "You can also include additional relevant details on the offensive content",
|
||||
multiline: true,
|
||||
rows: 3,
|
||||
autoAdjustHeight: false,
|
||||
onChange: (_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
|
||||
additionalDetails = newValue;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function downloadItem(
|
||||
container: Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void
|
||||
): void {
|
||||
const name = data.name;
|
||||
container.showOkCancelModalDialog(
|
||||
"Download to My Notebooks",
|
||||
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`,
|
||||
"Download",
|
||||
async () => {
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Downloading ${name} to My Notebooks`
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await junoClient.getNotebookContent(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
|
||||
}
|
||||
|
||||
await container.importAndOpenContent(data.name, response.data);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully downloaded ${name} to My Notebooks`
|
||||
);
|
||||
|
||||
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
|
||||
if (increaseDownloadResponse.data) {
|
||||
onComplete(increaseDownloadResponse.data);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
},
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
export async function favoriteItem(
|
||||
container: Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void
|
||||
): Promise<void> {
|
||||
if (container) {
|
||||
try {
|
||||
const response = await junoClient.favoriteNotebook(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when favoriting ${data.name}`);
|
||||
}
|
||||
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
handleError(error, "GalleryUtils/favoriteItem", `Failed to favorite ${data.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function unfavoriteItem(
|
||||
container: Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void
|
||||
): Promise<void> {
|
||||
if (container) {
|
||||
try {
|
||||
const response = await junoClient.unfavoriteNotebook(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when unfavoriting ${data.name}`);
|
||||
}
|
||||
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
handleError(error, "GalleryUtils/unfavoriteItem", `Failed to unfavorite ${data.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteItem(
|
||||
container: Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void
|
||||
): void {
|
||||
if (container) {
|
||||
container.showOkCancelModalDialog(
|
||||
"Remove published notebook",
|
||||
`Would you like to remove ${data.name} from the gallery?`,
|
||||
"Remove",
|
||||
async () => {
|
||||
const name = data.name;
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Removing ${name} from gallery`
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await junoClient.deleteNotebook(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} while removing ${name}`);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully removed ${name} from gallery`);
|
||||
onComplete(response.data);
|
||||
} catch (error) {
|
||||
handleError(error, "GalleryUtils/deleteItem", `Failed to remove ${name} from gallery`);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
},
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getGalleryViewerProps(search: string): GalleryViewerProps {
|
||||
const params = new URLSearchParams(search);
|
||||
let selectedTab: GalleryTab;
|
||||
if (params.has(GalleryViewerParams.SelectedTab)) {
|
||||
selectedTab = GalleryTab[params.get(GalleryViewerParams.SelectedTab) as keyof typeof GalleryTab];
|
||||
}
|
||||
|
||||
let sortBy: SortBy;
|
||||
if (params.has(GalleryViewerParams.SortBy)) {
|
||||
sortBy = SortBy[params.get(GalleryViewerParams.SortBy) as keyof typeof SortBy];
|
||||
}
|
||||
|
||||
return {
|
||||
selectedTab,
|
||||
sortBy,
|
||||
searchText: params.get(GalleryViewerParams.SearchText),
|
||||
};
|
||||
}
|
||||
|
||||
export function getNotebookViewerProps(search: string): NotebookViewerProps {
|
||||
const params = new URLSearchParams(search);
|
||||
return {
|
||||
notebookUrl: params.get(NotebookViewerParams.NotebookUrl),
|
||||
galleryItemId: params.get(NotebookViewerParams.GalleryItemId),
|
||||
hideInputs: JSON.parse(params.get(NotebookViewerParams.HideInputs)),
|
||||
};
|
||||
}
|
||||
|
||||
export function getTabTitle(tab: GalleryTab): string {
|
||||
switch (tab) {
|
||||
case GalleryTab.OfficialSamples:
|
||||
return GalleryViewerComponent.OfficialSamplesTitle;
|
||||
case GalleryTab.PublicGallery:
|
||||
return GalleryViewerComponent.PublicGalleryTitle;
|
||||
case GalleryTab.Favorites:
|
||||
return GalleryViewerComponent.FavoritesTitle;
|
||||
case GalleryTab.Published:
|
||||
return GalleryViewerComponent.PublishedTitle;
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function filterPublishedNotebooks(
|
||||
items: IGalleryItem[]
|
||||
): {
|
||||
published: IGalleryItem[];
|
||||
underReview: IGalleryItem[];
|
||||
removed: IGalleryItem[];
|
||||
} {
|
||||
const underReview: IGalleryItem[] = [];
|
||||
const removed: IGalleryItem[] = [];
|
||||
const published: IGalleryItem[] = [];
|
||||
|
||||
items?.forEach((item) => {
|
||||
if (item.policyViolations?.length > 0) {
|
||||
removed.push(item);
|
||||
} else if (item.pendingScanJobIds?.length > 0) {
|
||||
underReview.push(item);
|
||||
} else {
|
||||
published.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return { published, underReview, removed };
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import * as GitHubUtils from "./GitHubUtils";
|
||||
|
||||
const owner = "owner-1";
|
||||
const repo = "repo-1";
|
||||
const branch = "branch/name.1-2";
|
||||
const path = "folder name/file name1:2.ipynb";
|
||||
|
||||
describe("GitHubUtils", () => {
|
||||
it("fromRepoUri parses github repo url correctly", () => {
|
||||
const repoInfo = GitHubUtils.fromRepoUri(`https://github.com/${owner}/${repo}/tree/${branch}`);
|
||||
expect(repoInfo).toEqual({
|
||||
owner,
|
||||
repo,
|
||||
branch
|
||||
});
|
||||
});
|
||||
|
||||
it("toContentUri generates github uris correctly", () => {
|
||||
const uri = GitHubUtils.toContentUri(owner, repo, branch, path);
|
||||
expect(uri).toBe(`github://${owner}/${repo}/${path}?ref=${branch}`);
|
||||
});
|
||||
|
||||
it("fromContentUri parses the github uris correctly", () => {
|
||||
const contentInfo = GitHubUtils.fromContentUri(`github://${owner}/${repo}/${path}?ref=${branch}`);
|
||||
expect(contentInfo).toEqual({
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
path
|
||||
});
|
||||
});
|
||||
});
|
||||
import * as GitHubUtils from "./GitHubUtils";
|
||||
|
||||
const owner = "owner-1";
|
||||
const repo = "repo-1";
|
||||
const branch = "branch/name.1-2";
|
||||
const path = "folder name/file name1:2.ipynb";
|
||||
|
||||
describe("GitHubUtils", () => {
|
||||
it("fromRepoUri parses github repo url correctly", () => {
|
||||
const repoInfo = GitHubUtils.fromRepoUri(`https://github.com/${owner}/${repo}/tree/${branch}`);
|
||||
expect(repoInfo).toEqual({
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
});
|
||||
});
|
||||
|
||||
it("toContentUri generates github uris correctly", () => {
|
||||
const uri = GitHubUtils.toContentUri(owner, repo, branch, path);
|
||||
expect(uri).toBe(`github://${owner}/${repo}/${path}?ref=${branch}`);
|
||||
});
|
||||
|
||||
it("fromContentUri parses the github uris correctly", () => {
|
||||
const contentInfo = GitHubUtils.fromContentUri(`github://${owner}/${repo}/${path}?ref=${branch}`);
|
||||
expect(contentInfo).toEqual({
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
path,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
// https://github.com/<owner>/<repo>/tree/<branch>
|
||||
// The url when users visit a repo/branch on github.com
|
||||
export const RepoUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/tree\/([^?]*)/;
|
||||
|
||||
// github://<owner>/<repo>/<path>?ref=<branch>
|
||||
// Custom scheme for github content
|
||||
export const ContentUriPattern = /github:\/\/([^/]*)\/([^/]*)\/([^?]*)\?ref=(.*)/;
|
||||
|
||||
// https://github.com/<owner>/<repo>/blob/<branch>/<path>
|
||||
// We need to support this until we move to newer scheme for quickstarts
|
||||
export const LegacyContentUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/blob\/([^/]*)\/([^?]*)/;
|
||||
|
||||
export function toRepoFullName(owner: string, repo: string): string {
|
||||
return `${owner}/${repo}`;
|
||||
}
|
||||
|
||||
export function fromRepoUri(repoUri: string): undefined | { owner: string; repo: string; branch: string } {
|
||||
const matches = repoUri.match(RepoUriPattern);
|
||||
if (matches && matches.length > 3) {
|
||||
return {
|
||||
owner: matches[1],
|
||||
repo: matches[2],
|
||||
branch: matches[3]
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function fromContentUri(
|
||||
contentUri: string
|
||||
): undefined | { owner: string; repo: string; branch: string; path: string } {
|
||||
let matches = contentUri.match(ContentUriPattern);
|
||||
if (matches && matches.length > 4) {
|
||||
return {
|
||||
owner: matches[1],
|
||||
repo: matches[2],
|
||||
branch: matches[4],
|
||||
path: matches[3]
|
||||
};
|
||||
}
|
||||
|
||||
matches = contentUri.match(LegacyContentUriPattern);
|
||||
if (matches && matches.length > 4) {
|
||||
return {
|
||||
owner: matches[1],
|
||||
repo: matches[2],
|
||||
branch: matches[3],
|
||||
path: matches[4]
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function toContentUri(owner: string, repo: string, branch: string, path: string): string {
|
||||
return `github://${owner}/${repo}/${path}?ref=${branch}`;
|
||||
}
|
||||
|
||||
export function toRawContentUri(owner: string, repo: string, branch: string, path: string): string {
|
||||
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
|
||||
}
|
||||
// https://github.com/<owner>/<repo>/tree/<branch>
|
||||
// The url when users visit a repo/branch on github.com
|
||||
export const RepoUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/tree\/([^?]*)/;
|
||||
|
||||
// github://<owner>/<repo>/<path>?ref=<branch>
|
||||
// Custom scheme for github content
|
||||
export const ContentUriPattern = /github:\/\/([^/]*)\/([^/]*)\/([^?]*)\?ref=(.*)/;
|
||||
|
||||
// https://github.com/<owner>/<repo>/blob/<branch>/<path>
|
||||
// We need to support this until we move to newer scheme for quickstarts
|
||||
export const LegacyContentUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/blob\/([^/]*)\/([^?]*)/;
|
||||
|
||||
export function toRepoFullName(owner: string, repo: string): string {
|
||||
return `${owner}/${repo}`;
|
||||
}
|
||||
|
||||
export function fromRepoUri(repoUri: string): undefined | { owner: string; repo: string; branch: string } {
|
||||
const matches = repoUri.match(RepoUriPattern);
|
||||
if (matches && matches.length > 3) {
|
||||
return {
|
||||
owner: matches[1],
|
||||
repo: matches[2],
|
||||
branch: matches[3],
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function fromContentUri(
|
||||
contentUri: string
|
||||
): undefined | { owner: string; repo: string; branch: string; path: string } {
|
||||
let matches = contentUri.match(ContentUriPattern);
|
||||
if (matches && matches.length > 4) {
|
||||
return {
|
||||
owner: matches[1],
|
||||
repo: matches[2],
|
||||
branch: matches[4],
|
||||
path: matches[3],
|
||||
};
|
||||
}
|
||||
|
||||
matches = contentUri.match(LegacyContentUriPattern);
|
||||
if (matches && matches.length > 4) {
|
||||
return {
|
||||
owner: matches[1],
|
||||
repo: matches[2],
|
||||
branch: matches[3],
|
||||
path: matches[4],
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function toContentUri(owner: string, repo: string, branch: string, path: string): string {
|
||||
return `github://${owner}/${repo}/${path}?ref=${branch}`;
|
||||
}
|
||||
|
||||
export function toRawContentUri(owner: string, repo: string, branch: string, path: string): string {
|
||||
return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
|
||||
}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import { RepoListItem } from "../Explorer/Controls/GitHub/GitHubReposComponent";
|
||||
import { IPinnedRepo } from "../Juno/JunoClient";
|
||||
import { JunoUtils } from "./JunoUtils";
|
||||
import { IGitHubRepo } from "../GitHub/GitHubClient";
|
||||
|
||||
const gitHubRepo: IGitHubRepo = {
|
||||
name: "repo-name",
|
||||
owner: "owner",
|
||||
private: false
|
||||
};
|
||||
|
||||
const repoListItem: RepoListItem = {
|
||||
key: "key",
|
||||
repo: {
|
||||
name: "repo-name",
|
||||
owner: "owner",
|
||||
private: false
|
||||
},
|
||||
branches: [
|
||||
{
|
||||
name: "branch-name"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const pinnedRepo: IPinnedRepo = {
|
||||
name: "repo-name",
|
||||
owner: "owner",
|
||||
private: false,
|
||||
branches: [
|
||||
{
|
||||
name: "branch-name"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
describe("JunoUtils", () => {
|
||||
it("toPinnedRepo converts RepoListItem to IPinnedRepo", () => {
|
||||
expect(JunoUtils.toPinnedRepo(repoListItem)).toEqual(pinnedRepo);
|
||||
});
|
||||
|
||||
it("toGitHubRepo converts IPinnedRepo to IGitHubRepo", () => {
|
||||
expect(JunoUtils.toGitHubRepo(pinnedRepo)).toEqual(gitHubRepo);
|
||||
});
|
||||
});
|
||||
import { RepoListItem } from "../Explorer/Controls/GitHub/GitHubReposComponent";
|
||||
import { IPinnedRepo } from "../Juno/JunoClient";
|
||||
import { JunoUtils } from "./JunoUtils";
|
||||
import { IGitHubRepo } from "../GitHub/GitHubClient";
|
||||
|
||||
const gitHubRepo: IGitHubRepo = {
|
||||
name: "repo-name",
|
||||
owner: "owner",
|
||||
private: false,
|
||||
};
|
||||
|
||||
const repoListItem: RepoListItem = {
|
||||
key: "key",
|
||||
repo: {
|
||||
name: "repo-name",
|
||||
owner: "owner",
|
||||
private: false,
|
||||
},
|
||||
branches: [
|
||||
{
|
||||
name: "branch-name",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const pinnedRepo: IPinnedRepo = {
|
||||
name: "repo-name",
|
||||
owner: "owner",
|
||||
private: false,
|
||||
branches: [
|
||||
{
|
||||
name: "branch-name",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe("JunoUtils", () => {
|
||||
it("toPinnedRepo converts RepoListItem to IPinnedRepo", () => {
|
||||
expect(JunoUtils.toPinnedRepo(repoListItem)).toEqual(pinnedRepo);
|
||||
});
|
||||
|
||||
it("toGitHubRepo converts IPinnedRepo to IGitHubRepo", () => {
|
||||
expect(JunoUtils.toGitHubRepo(pinnedRepo)).toEqual(gitHubRepo);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ export class JunoUtils {
|
||||
owner: item.repo.owner,
|
||||
name: item.repo.name,
|
||||
private: item.repo.private,
|
||||
branches: item.branches.map(element => ({ name: element.name }))
|
||||
branches: item.branches.map((element) => ({ name: element.name })),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export class JunoUtils {
|
||||
return {
|
||||
owner: pinnedRepo.owner,
|
||||
name: pinnedRepo.name,
|
||||
private: pinnedRepo.private
|
||||
private: pinnedRepo.private,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +1,92 @@
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||
|
||||
interface KernelConnectionMetadata {
|
||||
name: string;
|
||||
configurationEndpoints: DataModels.NotebookConfigurationEndpoints;
|
||||
notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||
}
|
||||
|
||||
export class NotebookConfigurationUtils {
|
||||
private constructor() {}
|
||||
|
||||
public static async configureServiceEndpoints(
|
||||
notebookPath: string,
|
||||
notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo,
|
||||
kernelName: string,
|
||||
clusterConnectionInfo: DataModels.SparkClusterConnectionInfo
|
||||
): Promise<void> {
|
||||
if (!notebookPath || !notebookConnectionInfo || !kernelName) {
|
||||
Logger.logError(
|
||||
"Invalid or missing notebook connection info/path",
|
||||
"NotebookConfigurationUtils/configureServiceEndpoints"
|
||||
);
|
||||
return Promise.reject("Invalid or missing notebook connection info");
|
||||
}
|
||||
|
||||
if (!clusterConnectionInfo || !clusterConnectionInfo.endpoints || clusterConnectionInfo.endpoints.length === 0) {
|
||||
Logger.logError(
|
||||
"Invalid or missing cluster connection info/endpoints",
|
||||
"NotebookConfigurationUtils/configureServiceEndpoints"
|
||||
);
|
||||
return Promise.reject("Invalid or missing cluster connection info");
|
||||
}
|
||||
|
||||
const dataExplorer = window.dataExplorer;
|
||||
const notebookEndpointInfo: DataModels.NotebookConfigurationEndpointInfo[] = clusterConnectionInfo.endpoints.map(
|
||||
clusterEndpoint => ({
|
||||
type: clusterEndpoint.kind.toLowerCase(),
|
||||
endpoint: clusterEndpoint && clusterEndpoint.endpoint,
|
||||
username: clusterConnectionInfo.userName,
|
||||
password: clusterConnectionInfo.password,
|
||||
token: dataExplorer && dataExplorer.arcadiaToken()
|
||||
})
|
||||
);
|
||||
const configurationEndpoints: DataModels.NotebookConfigurationEndpoints = {
|
||||
path: notebookPath,
|
||||
endpoints: notebookEndpointInfo
|
||||
};
|
||||
const kernelMetadata: KernelConnectionMetadata = {
|
||||
configurationEndpoints,
|
||||
notebookConnectionInfo,
|
||||
name: kernelName
|
||||
};
|
||||
|
||||
return await NotebookConfigurationUtils._configureServiceEndpoints(kernelMetadata);
|
||||
}
|
||||
|
||||
private static async _configureServiceEndpoints(kernelMetadata: KernelConnectionMetadata): Promise<void> {
|
||||
if (!kernelMetadata) {
|
||||
// should never get into this state
|
||||
Logger.logWarning("kernel metadata is null or undefined", "NotebookConfigurationUtils/configureServiceEndpoints");
|
||||
return;
|
||||
}
|
||||
|
||||
const notebookConnectionInfo = kernelMetadata.notebookConnectionInfo;
|
||||
const configurationEndpoints = kernelMetadata.configurationEndpoints;
|
||||
if (notebookConnectionInfo && configurationEndpoints) {
|
||||
try {
|
||||
const headers: any = { "Content-Type": "application/json" };
|
||||
if (notebookConnectionInfo.authToken) {
|
||||
headers["Authorization"] = `token ${notebookConnectionInfo.authToken}`;
|
||||
}
|
||||
const response = await fetch(`${notebookConnectionInfo.notebookServerEndpoint}/api/configureEndpoints`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(configurationEndpoints)
|
||||
});
|
||||
if (!response.ok) {
|
||||
const responseMessage = await response.json();
|
||||
Logger.logError(
|
||||
getErrorMessage(responseMessage),
|
||||
"NotebookConfigurationUtils/configureServiceEndpoints",
|
||||
response.status
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookConfigurationUtils/configureServiceEndpoints");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||
|
||||
interface KernelConnectionMetadata {
|
||||
name: string;
|
||||
configurationEndpoints: DataModels.NotebookConfigurationEndpoints;
|
||||
notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||
}
|
||||
|
||||
export class NotebookConfigurationUtils {
|
||||
private constructor() {}
|
||||
|
||||
public static async configureServiceEndpoints(
|
||||
notebookPath: string,
|
||||
notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo,
|
||||
kernelName: string,
|
||||
clusterConnectionInfo: DataModels.SparkClusterConnectionInfo
|
||||
): Promise<void> {
|
||||
if (!notebookPath || !notebookConnectionInfo || !kernelName) {
|
||||
Logger.logError(
|
||||
"Invalid or missing notebook connection info/path",
|
||||
"NotebookConfigurationUtils/configureServiceEndpoints"
|
||||
);
|
||||
return Promise.reject("Invalid or missing notebook connection info");
|
||||
}
|
||||
|
||||
if (!clusterConnectionInfo || !clusterConnectionInfo.endpoints || clusterConnectionInfo.endpoints.length === 0) {
|
||||
Logger.logError(
|
||||
"Invalid or missing cluster connection info/endpoints",
|
||||
"NotebookConfigurationUtils/configureServiceEndpoints"
|
||||
);
|
||||
return Promise.reject("Invalid or missing cluster connection info");
|
||||
}
|
||||
|
||||
const dataExplorer = window.dataExplorer;
|
||||
const notebookEndpointInfo: DataModels.NotebookConfigurationEndpointInfo[] = clusterConnectionInfo.endpoints.map(
|
||||
(clusterEndpoint) => ({
|
||||
type: clusterEndpoint.kind.toLowerCase(),
|
||||
endpoint: clusterEndpoint && clusterEndpoint.endpoint,
|
||||
username: clusterConnectionInfo.userName,
|
||||
password: clusterConnectionInfo.password,
|
||||
token: dataExplorer && dataExplorer.arcadiaToken(),
|
||||
})
|
||||
);
|
||||
const configurationEndpoints: DataModels.NotebookConfigurationEndpoints = {
|
||||
path: notebookPath,
|
||||
endpoints: notebookEndpointInfo,
|
||||
};
|
||||
const kernelMetadata: KernelConnectionMetadata = {
|
||||
configurationEndpoints,
|
||||
notebookConnectionInfo,
|
||||
name: kernelName,
|
||||
};
|
||||
|
||||
return await NotebookConfigurationUtils._configureServiceEndpoints(kernelMetadata);
|
||||
}
|
||||
|
||||
private static async _configureServiceEndpoints(kernelMetadata: KernelConnectionMetadata): Promise<void> {
|
||||
if (!kernelMetadata) {
|
||||
// should never get into this state
|
||||
Logger.logWarning("kernel metadata is null or undefined", "NotebookConfigurationUtils/configureServiceEndpoints");
|
||||
return;
|
||||
}
|
||||
|
||||
const notebookConnectionInfo = kernelMetadata.notebookConnectionInfo;
|
||||
const configurationEndpoints = kernelMetadata.configurationEndpoints;
|
||||
if (notebookConnectionInfo && configurationEndpoints) {
|
||||
try {
|
||||
const headers: any = { "Content-Type": "application/json" };
|
||||
if (notebookConnectionInfo.authToken) {
|
||||
headers["Authorization"] = `token ${notebookConnectionInfo.authToken}`;
|
||||
}
|
||||
const response = await fetch(`${notebookConnectionInfo.notebookServerEndpoint}/api/configureEndpoints`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(configurationEndpoints),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const responseMessage = await response.json();
|
||||
Logger.logError(
|
||||
getErrorMessage(responseMessage),
|
||||
"NotebookConfigurationUtils/configureServiceEndpoints",
|
||||
response.status
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookConfigurationUtils/configureServiceEndpoints");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +1,82 @@
|
||||
import * as _ from "underscore";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
|
||||
const _global = typeof self === "undefined" ? window : self;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Use logConsoleInfo, logConsoleError, logConsoleProgress instead
|
||||
* */
|
||||
export function logConsoleMessage(type: ConsoleDataType, message: string, id?: string): string {
|
||||
const dataExplorer = _global.dataExplorer;
|
||||
if (dataExplorer) {
|
||||
const date = new Date();
|
||||
const formattedDate: string = new Intl.DateTimeFormat("en-EN", {
|
||||
hour12: true,
|
||||
hour: "numeric",
|
||||
minute: "numeric"
|
||||
}).format(date);
|
||||
if (!id) {
|
||||
id = _.uniqueId();
|
||||
}
|
||||
dataExplorer.logConsoleData({ type: type, date: formattedDate, message: message, id: id });
|
||||
}
|
||||
return id || "";
|
||||
}
|
||||
|
||||
export function clearInProgressMessageWithId(id: string): void {
|
||||
const dataExplorer = _global.dataExplorer;
|
||||
dataExplorer && dataExplorer.deleteInProgressConsoleDataWithId(id);
|
||||
}
|
||||
|
||||
export function logConsoleProgress(message: string): () => void {
|
||||
const type = ConsoleDataType.InProgress;
|
||||
const dataExplorer = _global.dataExplorer;
|
||||
if (dataExplorer) {
|
||||
const id = _.uniqueId();
|
||||
const date = new Date();
|
||||
const formattedDate: string = new Intl.DateTimeFormat("en-EN", {
|
||||
hour12: true,
|
||||
hour: "numeric",
|
||||
minute: "numeric"
|
||||
}).format(date);
|
||||
dataExplorer.logConsoleData({ type, date: formattedDate, message, id });
|
||||
return () => {
|
||||
dataExplorer.deleteInProgressConsoleDataWithId(id);
|
||||
};
|
||||
} else {
|
||||
return () => {
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function logConsoleError(message: string): void {
|
||||
const type = ConsoleDataType.Error;
|
||||
const dataExplorer = _global.dataExplorer;
|
||||
if (dataExplorer) {
|
||||
const id = _.uniqueId();
|
||||
const date = new Date();
|
||||
const formattedDate: string = new Intl.DateTimeFormat("en-EN", {
|
||||
hour12: true,
|
||||
hour: "numeric",
|
||||
minute: "numeric"
|
||||
}).format(date);
|
||||
dataExplorer.logConsoleData({ type, date: formattedDate, message, id });
|
||||
}
|
||||
}
|
||||
|
||||
export function logConsoleInfo(message: string): void {
|
||||
const type = ConsoleDataType.Info;
|
||||
const dataExplorer = _global.dataExplorer;
|
||||
if (dataExplorer) {
|
||||
const id = _.uniqueId();
|
||||
const date = new Date();
|
||||
const formattedDate: string = new Intl.DateTimeFormat("en-EN", {
|
||||
hour12: true,
|
||||
hour: "numeric",
|
||||
minute: "numeric"
|
||||
}).format(date);
|
||||
dataExplorer.logConsoleData({ type, date: formattedDate, message, id });
|
||||
}
|
||||
}
|
||||
import * as _ from "underscore";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
|
||||
const _global = typeof self === "undefined" ? window : self;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Use logConsoleInfo, logConsoleError, logConsoleProgress instead
|
||||
* */
|
||||
export function logConsoleMessage(type: ConsoleDataType, message: string, id?: string): string {
|
||||
const dataExplorer = _global.dataExplorer;
|
||||
if (dataExplorer) {
|
||||
const date = new Date();
|
||||
const formattedDate: string = new Intl.DateTimeFormat("en-EN", {
|
||||
hour12: true,
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
}).format(date);
|
||||
if (!id) {
|
||||
id = _.uniqueId();
|
||||
}
|
||||
dataExplorer.logConsoleData({ type: type, date: formattedDate, message: message, id: id });
|
||||
}
|
||||
return id || "";
|
||||
}
|
||||
|
||||
export function clearInProgressMessageWithId(id: string): void {
|
||||
const dataExplorer = _global.dataExplorer;
|
||||
dataExplorer && dataExplorer.deleteInProgressConsoleDataWithId(id);
|
||||
}
|
||||
|
||||
export function logConsoleProgress(message: string): () => void {
|
||||
const type = ConsoleDataType.InProgress;
|
||||
const dataExplorer = _global.dataExplorer;
|
||||
if (dataExplorer) {
|
||||
const id = _.uniqueId();
|
||||
const date = new Date();
|
||||
const formattedDate: string = new Intl.DateTimeFormat("en-EN", {
|
||||
hour12: true,
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
}).format(date);
|
||||
dataExplorer.logConsoleData({ type, date: formattedDate, message, id });
|
||||
return () => {
|
||||
dataExplorer.deleteInProgressConsoleDataWithId(id);
|
||||
};
|
||||
} else {
|
||||
return () => {
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function logConsoleError(message: string): void {
|
||||
const type = ConsoleDataType.Error;
|
||||
const dataExplorer = _global.dataExplorer;
|
||||
if (dataExplorer) {
|
||||
const id = _.uniqueId();
|
||||
const date = new Date();
|
||||
const formattedDate: string = new Intl.DateTimeFormat("en-EN", {
|
||||
hour12: true,
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
}).format(date);
|
||||
dataExplorer.logConsoleData({ type, date: formattedDate, message, id });
|
||||
}
|
||||
}
|
||||
|
||||
export function logConsoleInfo(message: string): void {
|
||||
const type = ConsoleDataType.Info;
|
||||
const dataExplorer = _global.dataExplorer;
|
||||
if (dataExplorer) {
|
||||
const id = _.uniqueId();
|
||||
const date = new Date();
|
||||
const formattedDate: string = new Intl.DateTimeFormat("en-EN", {
|
||||
hour12: true,
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
}).format(date);
|
||||
dataExplorer.logConsoleData({ type, date: formattedDate, message, id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("PricingUtils Tests", () => {
|
||||
requestUnits: 1,
|
||||
numberOfRegions: null,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: false
|
||||
isAutoscale: false,
|
||||
});
|
||||
expect(value).toBe(0);
|
||||
});
|
||||
@@ -40,7 +40,7 @@ describe("PricingUtils Tests", () => {
|
||||
requestUnits: 1,
|
||||
numberOfRegions: null,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: true
|
||||
isAutoscale: true,
|
||||
});
|
||||
expect(value).toBe(0);
|
||||
});
|
||||
@@ -51,7 +51,7 @@ describe("PricingUtils Tests", () => {
|
||||
requestUnits: 1,
|
||||
numberOfRegions: -1,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: false
|
||||
isAutoscale: false,
|
||||
});
|
||||
expect(value).toBe(0);
|
||||
});
|
||||
@@ -61,7 +61,7 @@ describe("PricingUtils Tests", () => {
|
||||
requestUnits: 1,
|
||||
numberOfRegions: -1,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: true
|
||||
isAutoscale: true,
|
||||
});
|
||||
expect(value).toBe(0);
|
||||
});
|
||||
@@ -72,7 +72,7 @@ describe("PricingUtils Tests", () => {
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: false
|
||||
isAutoscale: false,
|
||||
});
|
||||
expect(value).toBe(0.00008);
|
||||
});
|
||||
@@ -82,7 +82,7 @@ describe("PricingUtils Tests", () => {
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: true
|
||||
isAutoscale: true,
|
||||
});
|
||||
expect(value).toBe(0.00012);
|
||||
});
|
||||
@@ -93,7 +93,7 @@ describe("PricingUtils Tests", () => {
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: false
|
||||
isAutoscale: false,
|
||||
});
|
||||
expect(value).toBe(0.00051);
|
||||
});
|
||||
@@ -103,7 +103,7 @@ describe("PricingUtils Tests", () => {
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: true
|
||||
isAutoscale: true,
|
||||
});
|
||||
expect(value).toBe(0.00076);
|
||||
});
|
||||
@@ -114,7 +114,7 @@ describe("PricingUtils Tests", () => {
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 2,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: false
|
||||
isAutoscale: false,
|
||||
});
|
||||
expect(value).toBe(0.00016);
|
||||
});
|
||||
@@ -124,7 +124,7 @@ describe("PricingUtils Tests", () => {
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 2,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: true
|
||||
isAutoscale: true,
|
||||
});
|
||||
expect(value).toBe(0.00024);
|
||||
});
|
||||
@@ -135,7 +135,7 @@ describe("PricingUtils Tests", () => {
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: true,
|
||||
isAutoscale: false
|
||||
isAutoscale: false,
|
||||
});
|
||||
expect(value).toBe(0.00008);
|
||||
});
|
||||
@@ -145,7 +145,7 @@ describe("PricingUtils Tests", () => {
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: true,
|
||||
isAutoscale: true
|
||||
isAutoscale: true,
|
||||
});
|
||||
expect(value).toBe(0.00012);
|
||||
});
|
||||
@@ -156,7 +156,7 @@ describe("PricingUtils Tests", () => {
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 2,
|
||||
multimasterEnabled: true,
|
||||
isAutoscale: false
|
||||
isAutoscale: false,
|
||||
});
|
||||
expect(value).toBe(0.00048);
|
||||
});
|
||||
@@ -166,7 +166,7 @@ describe("PricingUtils Tests", () => {
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 2,
|
||||
multimasterEnabled: true,
|
||||
isAutoscale: true
|
||||
isAutoscale: true,
|
||||
});
|
||||
expect(value).toBe(0.00096);
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ export function computeRUUsagePriceHourly({
|
||||
requestUnits,
|
||||
numberOfRegions,
|
||||
multimasterEnabled,
|
||||
isAutoscale
|
||||
isAutoscale,
|
||||
}: ComputeRUUsagePriceHourlyArgs): number {
|
||||
const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled);
|
||||
const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
||||
@@ -183,7 +183,7 @@ export function getEstimatedAutoscaleSpendHtml(
|
||||
requestUnits: throughput,
|
||||
numberOfRegions: regions,
|
||||
multimasterEnabled: multimaster,
|
||||
isAutoscale: true
|
||||
isAutoscale: true,
|
||||
});
|
||||
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
@@ -196,8 +196,9 @@ export function getEstimatedAutoscaleSpendHtml(
|
||||
`Estimated monthly cost (${currency}): <b>` +
|
||||
`${currencySign}${calculateEstimateNumber(monthlyPrice / 10)} - ` +
|
||||
`${currencySign}${calculateEstimateNumber(monthlyPrice)} </b> ` +
|
||||
`(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput /
|
||||
10} - ${throughput} RU/s, ${currencySign}${pricePerRu}/RU)`
|
||||
`(${regions} ${regions === 1 ? "region" : "regions"}, ${
|
||||
throughput / 10
|
||||
} - ${throughput} RU/s, ${currencySign}${pricePerRu}/RU)`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -212,7 +213,7 @@ export function getEstimatedSpendHtml(
|
||||
requestUnits: throughput,
|
||||
numberOfRegions: regions,
|
||||
multimasterEnabled: multimaster,
|
||||
isAutoscale: false
|
||||
isAutoscale: false,
|
||||
});
|
||||
const dailyPrice: number = hourlyPrice * 24;
|
||||
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
|
||||
@@ -243,7 +244,7 @@ export function getEstimatedSpendAcknowledgeString(
|
||||
requestUnits: throughput,
|
||||
numberOfRegions: regions,
|
||||
multimasterEnabled: multimaster,
|
||||
isAutoscale: isAutoscale
|
||||
isAutoscale: isAutoscale,
|
||||
});
|
||||
const dailyPrice: number = hourlyPrice * 24;
|
||||
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
|
||||
|
||||
@@ -1,187 +1,187 @@
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as Q from "q";
|
||||
import * as sinon from "sinon";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { QueryUtils } from "./QueryUtils";
|
||||
|
||||
describe("Query Utils", () => {
|
||||
function generatePartitionKeyForPath(path: string): DataModels.PartitionKey {
|
||||
return {
|
||||
paths: [path],
|
||||
kind: "hash",
|
||||
version: 2
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildDocumentsQueryPartitionProjections()", () => {
|
||||
it("should return empty string if partition key is undefined", () => {
|
||||
expect(QueryUtils.buildDocumentsQueryPartitionProjections("c", undefined)).toBe("");
|
||||
});
|
||||
|
||||
it("should return empty string if partition key is null", () => {
|
||||
expect(QueryUtils.buildDocumentsQueryPartitionProjections("c", null)).toBe("");
|
||||
});
|
||||
|
||||
it("should replace slashes and embed projection in square braces", () => {
|
||||
const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/a");
|
||||
const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey);
|
||||
|
||||
expect(partitionProjection).toContain('c["a"]');
|
||||
});
|
||||
|
||||
it("should embed multiple projections in individual square braces", () => {
|
||||
const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/a/b");
|
||||
const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey);
|
||||
|
||||
expect(partitionProjection).toContain('c["a"]["b"]');
|
||||
});
|
||||
|
||||
it("should not escape double quotes if partition key definition does not have single quote prefix", () => {
|
||||
const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath('/"a"');
|
||||
const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey);
|
||||
|
||||
expect(partitionProjection).toContain('c["a"]');
|
||||
});
|
||||
|
||||
it("should escape single quotes", () => {
|
||||
const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/'\"a\"'");
|
||||
const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey);
|
||||
|
||||
expect(partitionProjection).toContain('c["\\\\\\"a\\\\\\""]');
|
||||
});
|
||||
|
||||
it("should escape double quotes if partition key definition has single quote prefix", () => {
|
||||
const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/'\\\"a\\\"'");
|
||||
const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey);
|
||||
|
||||
expect(partitionProjection).toContain('c["\\\\\\"a\\\\\\""]');
|
||||
});
|
||||
});
|
||||
|
||||
describe("queryPagesUntilContentPresent()", () => {
|
||||
const queryResultWithItemsInPage: ViewModels.QueryResults = {
|
||||
documents: [{ a: "123" }],
|
||||
activityId: "123",
|
||||
requestCharge: 1,
|
||||
hasMoreResults: false,
|
||||
firstItemIndex: 0,
|
||||
lastItemIndex: 1,
|
||||
itemCount: 1
|
||||
};
|
||||
const queryResultWithNoItemsInPage: ViewModels.QueryResults = {
|
||||
documents: [],
|
||||
activityId: "123",
|
||||
requestCharge: 1,
|
||||
hasMoreResults: true,
|
||||
firstItemIndex: 0,
|
||||
lastItemIndex: 0,
|
||||
itemCount: 0
|
||||
};
|
||||
|
||||
it("should perform multiple queries until it finds a page that has items", async () => {
|
||||
const queryStub = sinon
|
||||
.stub()
|
||||
.onFirstCall()
|
||||
.returns(Q.resolve(queryResultWithNoItemsInPage))
|
||||
.returns(Q.resolve(queryResultWithItemsInPage));
|
||||
|
||||
await QueryUtils.queryPagesUntilContentPresent(0, queryStub);
|
||||
expect(queryStub.callCount).toBe(2);
|
||||
expect(queryStub.getCall(0).args[0]).toBe(0);
|
||||
expect(queryStub.getCall(1).args[0]).toBe(0);
|
||||
});
|
||||
|
||||
it("should not perform multiple queries if the first page of results has items", done => {
|
||||
const queryStub = sinon.stub().returns(Q.resolve(queryResultWithItemsInPage));
|
||||
QueryUtils.queryPagesUntilContentPresent(0, queryStub).finally(() => {
|
||||
expect(queryStub.callCount).toBe(1);
|
||||
expect(queryStub.getCall(0).args[0]).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not proceed with subsequent queries if the first one errors out", done => {
|
||||
const queryStub = sinon.stub().returns(Q.reject("Error injected for testing purposes"));
|
||||
QueryUtils.queryPagesUntilContentPresent(0, queryStub).finally(() => {
|
||||
expect(queryStub.callCount).toBe(1);
|
||||
expect(queryStub.getCall(0).args[0]).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("queryAllPages()", () => {
|
||||
const queryResultWithNoContinuation: ViewModels.QueryResults = {
|
||||
documents: [{ a: "123" }],
|
||||
activityId: "123",
|
||||
requestCharge: 1,
|
||||
hasMoreResults: false,
|
||||
firstItemIndex: 1,
|
||||
lastItemIndex: 1,
|
||||
itemCount: 1
|
||||
};
|
||||
const queryResultWithContinuation: ViewModels.QueryResults = {
|
||||
documents: [{ b: "123" }],
|
||||
activityId: "123",
|
||||
requestCharge: 1,
|
||||
hasMoreResults: true,
|
||||
firstItemIndex: 0,
|
||||
lastItemIndex: 0,
|
||||
itemCount: 1
|
||||
};
|
||||
|
||||
it("should follow continuation token to fetch all pages", done => {
|
||||
const queryStub = sinon
|
||||
.stub()
|
||||
.onFirstCall()
|
||||
.returns(Q.resolve(queryResultWithContinuation))
|
||||
.returns(Q.resolve(queryResultWithNoContinuation));
|
||||
QueryUtils.queryAllPages(queryStub).then(
|
||||
(results: ViewModels.QueryResults) => {
|
||||
expect(queryStub.callCount).toBe(2);
|
||||
expect(queryStub.getCall(0).args[0]).toBe(0);
|
||||
expect(queryStub.getCall(1).args[0]).toBe(1);
|
||||
expect(results.itemCount).toBe(
|
||||
queryResultWithContinuation.documents.length + queryResultWithNoContinuation.documents.length
|
||||
);
|
||||
expect(results.requestCharge).toBe(
|
||||
queryResultWithContinuation.requestCharge + queryResultWithNoContinuation.requestCharge
|
||||
);
|
||||
expect(results.documents).toEqual(
|
||||
queryResultWithContinuation.documents.concat(queryResultWithNoContinuation.documents)
|
||||
);
|
||||
done();
|
||||
},
|
||||
(error: any) => {
|
||||
fail(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("should not perform subsequent fetches when result has no continuation", done => {
|
||||
const queryStub = sinon.stub().returns(Q.resolve(queryResultWithNoContinuation));
|
||||
QueryUtils.queryAllPages(queryStub).then(
|
||||
(results: ViewModels.QueryResults) => {
|
||||
expect(queryStub.callCount).toBe(1);
|
||||
expect(queryStub.getCall(0).args[0]).toBe(0);
|
||||
expect(results.itemCount).toBe(queryResultWithNoContinuation.documents.length);
|
||||
expect(results.requestCharge).toBe(queryResultWithNoContinuation.requestCharge);
|
||||
expect(results.documents).toEqual(queryResultWithNoContinuation.documents);
|
||||
done();
|
||||
},
|
||||
(error: any) => {
|
||||
fail(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("should not proceed with subsequent fetches if the first one errors out", done => {
|
||||
const queryStub = sinon.stub().returns(Q.reject("Error injected for testing purposes"));
|
||||
QueryUtils.queryAllPages(queryStub).finally(() => {
|
||||
expect(queryStub.callCount).toBe(1);
|
||||
expect(queryStub.getCall(0).args[0]).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as Q from "q";
|
||||
import * as sinon from "sinon";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { QueryUtils } from "./QueryUtils";
|
||||
|
||||
describe("Query Utils", () => {
|
||||
function generatePartitionKeyForPath(path: string): DataModels.PartitionKey {
|
||||
return {
|
||||
paths: [path],
|
||||
kind: "hash",
|
||||
version: 2,
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildDocumentsQueryPartitionProjections()", () => {
|
||||
it("should return empty string if partition key is undefined", () => {
|
||||
expect(QueryUtils.buildDocumentsQueryPartitionProjections("c", undefined)).toBe("");
|
||||
});
|
||||
|
||||
it("should return empty string if partition key is null", () => {
|
||||
expect(QueryUtils.buildDocumentsQueryPartitionProjections("c", null)).toBe("");
|
||||
});
|
||||
|
||||
it("should replace slashes and embed projection in square braces", () => {
|
||||
const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/a");
|
||||
const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey);
|
||||
|
||||
expect(partitionProjection).toContain('c["a"]');
|
||||
});
|
||||
|
||||
it("should embed multiple projections in individual square braces", () => {
|
||||
const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/a/b");
|
||||
const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey);
|
||||
|
||||
expect(partitionProjection).toContain('c["a"]["b"]');
|
||||
});
|
||||
|
||||
it("should not escape double quotes if partition key definition does not have single quote prefix", () => {
|
||||
const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath('/"a"');
|
||||
const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey);
|
||||
|
||||
expect(partitionProjection).toContain('c["a"]');
|
||||
});
|
||||
|
||||
it("should escape single quotes", () => {
|
||||
const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/'\"a\"'");
|
||||
const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey);
|
||||
|
||||
expect(partitionProjection).toContain('c["\\\\\\"a\\\\\\""]');
|
||||
});
|
||||
|
||||
it("should escape double quotes if partition key definition has single quote prefix", () => {
|
||||
const partitionKey: DataModels.PartitionKey = generatePartitionKeyForPath("/'\\\"a\\\"'");
|
||||
const partitionProjection: string = QueryUtils.buildDocumentsQueryPartitionProjections("c", partitionKey);
|
||||
|
||||
expect(partitionProjection).toContain('c["\\\\\\"a\\\\\\""]');
|
||||
});
|
||||
});
|
||||
|
||||
describe("queryPagesUntilContentPresent()", () => {
|
||||
const queryResultWithItemsInPage: ViewModels.QueryResults = {
|
||||
documents: [{ a: "123" }],
|
||||
activityId: "123",
|
||||
requestCharge: 1,
|
||||
hasMoreResults: false,
|
||||
firstItemIndex: 0,
|
||||
lastItemIndex: 1,
|
||||
itemCount: 1,
|
||||
};
|
||||
const queryResultWithNoItemsInPage: ViewModels.QueryResults = {
|
||||
documents: [],
|
||||
activityId: "123",
|
||||
requestCharge: 1,
|
||||
hasMoreResults: true,
|
||||
firstItemIndex: 0,
|
||||
lastItemIndex: 0,
|
||||
itemCount: 0,
|
||||
};
|
||||
|
||||
it("should perform multiple queries until it finds a page that has items", async () => {
|
||||
const queryStub = sinon
|
||||
.stub()
|
||||
.onFirstCall()
|
||||
.returns(Q.resolve(queryResultWithNoItemsInPage))
|
||||
.returns(Q.resolve(queryResultWithItemsInPage));
|
||||
|
||||
await QueryUtils.queryPagesUntilContentPresent(0, queryStub);
|
||||
expect(queryStub.callCount).toBe(2);
|
||||
expect(queryStub.getCall(0).args[0]).toBe(0);
|
||||
expect(queryStub.getCall(1).args[0]).toBe(0);
|
||||
});
|
||||
|
||||
it("should not perform multiple queries if the first page of results has items", (done) => {
|
||||
const queryStub = sinon.stub().returns(Q.resolve(queryResultWithItemsInPage));
|
||||
QueryUtils.queryPagesUntilContentPresent(0, queryStub).finally(() => {
|
||||
expect(queryStub.callCount).toBe(1);
|
||||
expect(queryStub.getCall(0).args[0]).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not proceed with subsequent queries if the first one errors out", (done) => {
|
||||
const queryStub = sinon.stub().returns(Q.reject("Error injected for testing purposes"));
|
||||
QueryUtils.queryPagesUntilContentPresent(0, queryStub).finally(() => {
|
||||
expect(queryStub.callCount).toBe(1);
|
||||
expect(queryStub.getCall(0).args[0]).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("queryAllPages()", () => {
|
||||
const queryResultWithNoContinuation: ViewModels.QueryResults = {
|
||||
documents: [{ a: "123" }],
|
||||
activityId: "123",
|
||||
requestCharge: 1,
|
||||
hasMoreResults: false,
|
||||
firstItemIndex: 1,
|
||||
lastItemIndex: 1,
|
||||
itemCount: 1,
|
||||
};
|
||||
const queryResultWithContinuation: ViewModels.QueryResults = {
|
||||
documents: [{ b: "123" }],
|
||||
activityId: "123",
|
||||
requestCharge: 1,
|
||||
hasMoreResults: true,
|
||||
firstItemIndex: 0,
|
||||
lastItemIndex: 0,
|
||||
itemCount: 1,
|
||||
};
|
||||
|
||||
it("should follow continuation token to fetch all pages", (done) => {
|
||||
const queryStub = sinon
|
||||
.stub()
|
||||
.onFirstCall()
|
||||
.returns(Q.resolve(queryResultWithContinuation))
|
||||
.returns(Q.resolve(queryResultWithNoContinuation));
|
||||
QueryUtils.queryAllPages(queryStub).then(
|
||||
(results: ViewModels.QueryResults) => {
|
||||
expect(queryStub.callCount).toBe(2);
|
||||
expect(queryStub.getCall(0).args[0]).toBe(0);
|
||||
expect(queryStub.getCall(1).args[0]).toBe(1);
|
||||
expect(results.itemCount).toBe(
|
||||
queryResultWithContinuation.documents.length + queryResultWithNoContinuation.documents.length
|
||||
);
|
||||
expect(results.requestCharge).toBe(
|
||||
queryResultWithContinuation.requestCharge + queryResultWithNoContinuation.requestCharge
|
||||
);
|
||||
expect(results.documents).toEqual(
|
||||
queryResultWithContinuation.documents.concat(queryResultWithNoContinuation.documents)
|
||||
);
|
||||
done();
|
||||
},
|
||||
(error: any) => {
|
||||
fail(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("should not perform subsequent fetches when result has no continuation", (done) => {
|
||||
const queryStub = sinon.stub().returns(Q.resolve(queryResultWithNoContinuation));
|
||||
QueryUtils.queryAllPages(queryStub).then(
|
||||
(results: ViewModels.QueryResults) => {
|
||||
expect(queryStub.callCount).toBe(1);
|
||||
expect(queryStub.getCall(0).args[0]).toBe(0);
|
||||
expect(results.itemCount).toBe(queryResultWithNoContinuation.documents.length);
|
||||
expect(results.requestCharge).toBe(queryResultWithNoContinuation.requestCharge);
|
||||
expect(results.documents).toEqual(queryResultWithNoContinuation.documents);
|
||||
done();
|
||||
},
|
||||
(error: any) => {
|
||||
fail(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("should not proceed with subsequent fetches if the first one errors out", (done) => {
|
||||
const queryStub = sinon.stub().returns(Q.reject("Error injected for testing purposes"));
|
||||
QueryUtils.queryAllPages(queryStub).finally(() => {
|
||||
expect(queryStub.callCount).toBe(1);
|
||||
expect(queryStub.getCall(0).args[0]).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,118 +1,118 @@
|
||||
import Q from "q";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
|
||||
export class QueryUtils {
|
||||
public static buildDocumentsQuery(
|
||||
filter: string,
|
||||
partitionKeyProperty: string,
|
||||
partitionKey: DataModels.PartitionKey
|
||||
): string {
|
||||
let query: string = partitionKeyProperty
|
||||
? `select c.id, c._self, c._rid, c._ts, ${QueryUtils.buildDocumentsQueryPartitionProjections(
|
||||
"c",
|
||||
partitionKey
|
||||
)} as _partitionKeyValue from c`
|
||||
: `select c.id, c._self, c._rid, c._ts from c`;
|
||||
|
||||
if (filter) {
|
||||
query += " " + filter;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public static buildDocumentsQueryPartitionProjections(
|
||||
collectionAlias: string,
|
||||
partitionKey: DataModels.PartitionKey
|
||||
): string {
|
||||
if (!partitionKey) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// e.g., path /order/id will be projected as c["order"]["id"],
|
||||
// to escape any property names that match a keyword
|
||||
let projections = [];
|
||||
for (let index in partitionKey.paths) {
|
||||
// TODO: Handle "/" in partition key definitions
|
||||
const projectedProperties: string[] = partitionKey.paths[index].split("/").slice(1);
|
||||
let projectedProperty: string = "";
|
||||
|
||||
projectedProperties.forEach((property: string) => {
|
||||
const projection = property.trim();
|
||||
if (projection.length > 0 && projection.charAt(0) != "'" && projection.charAt(0) != '"') {
|
||||
projectedProperty = projectedProperty + `["${projection}"]`;
|
||||
} else if (projection.length > 0 && projection.charAt(0) == "'") {
|
||||
// trim single quotes and escape double quotes
|
||||
const projectionSlice = projection.slice(1, projection.length - 1);
|
||||
projectedProperty =
|
||||
projectedProperty + `["${projectionSlice.replace(/\\"/g, '"').replace(/"/g, '\\\\\\"')}"]`;
|
||||
} else {
|
||||
projectedProperty = projectedProperty + `[${projection}]`;
|
||||
}
|
||||
});
|
||||
|
||||
projections.push(`${collectionAlias}${projectedProperty}`);
|
||||
}
|
||||
|
||||
return projections.join(",");
|
||||
}
|
||||
|
||||
public static async queryPagesUntilContentPresent(
|
||||
firstItemIndex: number,
|
||||
queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
|
||||
): Promise<ViewModels.QueryResults> {
|
||||
let roundTrips: number = 0;
|
||||
let netRequestCharge: number = 0;
|
||||
const doRequest = async (itemIndex: number): Promise<ViewModels.QueryResults> => {
|
||||
const results: ViewModels.QueryResults = await queryItems(itemIndex);
|
||||
roundTrips = roundTrips + 1;
|
||||
results.roundTrips = roundTrips;
|
||||
results.requestCharge = Number(results.requestCharge) + netRequestCharge;
|
||||
netRequestCharge = Number(results.requestCharge);
|
||||
const resultsMetadata: ViewModels.QueryResultsMetadata = {
|
||||
hasMoreResults: results.hasMoreResults,
|
||||
itemCount: results.itemCount,
|
||||
firstItemIndex: results.firstItemIndex,
|
||||
lastItemIndex: results.lastItemIndex
|
||||
};
|
||||
if (resultsMetadata.itemCount === 0 && resultsMetadata.hasMoreResults) {
|
||||
return await doRequest(resultsMetadata.lastItemIndex);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
return await doRequest(firstItemIndex);
|
||||
}
|
||||
|
||||
public static async queryAllPages(
|
||||
queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
|
||||
): Promise<ViewModels.QueryResults> {
|
||||
const queryResults: ViewModels.QueryResults = {
|
||||
documents: [],
|
||||
activityId: undefined,
|
||||
hasMoreResults: false,
|
||||
itemCount: 0,
|
||||
firstItemIndex: 0,
|
||||
lastItemIndex: 0,
|
||||
requestCharge: 0,
|
||||
roundTrips: 0
|
||||
};
|
||||
const doRequest = async (itemIndex: number): Promise<ViewModels.QueryResults> => {
|
||||
const results: ViewModels.QueryResults = await queryItems(itemIndex);
|
||||
const { requestCharge, hasMoreResults, itemCount, lastItemIndex, documents } = results;
|
||||
queryResults.roundTrips = queryResults.roundTrips + 1;
|
||||
queryResults.requestCharge = Number(queryResults.requestCharge) + Number(requestCharge);
|
||||
queryResults.hasMoreResults = hasMoreResults;
|
||||
queryResults.itemCount = queryResults.itemCount + itemCount;
|
||||
queryResults.lastItemIndex = lastItemIndex;
|
||||
queryResults.documents = queryResults.documents.concat(documents);
|
||||
if (queryResults.hasMoreResults) {
|
||||
return doRequest(queryResults.lastItemIndex + 1);
|
||||
}
|
||||
return queryResults;
|
||||
};
|
||||
|
||||
return doRequest(0);
|
||||
}
|
||||
}
|
||||
import Q from "q";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
|
||||
export class QueryUtils {
|
||||
public static buildDocumentsQuery(
|
||||
filter: string,
|
||||
partitionKeyProperty: string,
|
||||
partitionKey: DataModels.PartitionKey
|
||||
): string {
|
||||
let query: string = partitionKeyProperty
|
||||
? `select c.id, c._self, c._rid, c._ts, ${QueryUtils.buildDocumentsQueryPartitionProjections(
|
||||
"c",
|
||||
partitionKey
|
||||
)} as _partitionKeyValue from c`
|
||||
: `select c.id, c._self, c._rid, c._ts from c`;
|
||||
|
||||
if (filter) {
|
||||
query += " " + filter;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public static buildDocumentsQueryPartitionProjections(
|
||||
collectionAlias: string,
|
||||
partitionKey: DataModels.PartitionKey
|
||||
): string {
|
||||
if (!partitionKey) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// e.g., path /order/id will be projected as c["order"]["id"],
|
||||
// to escape any property names that match a keyword
|
||||
let projections = [];
|
||||
for (let index in partitionKey.paths) {
|
||||
// TODO: Handle "/" in partition key definitions
|
||||
const projectedProperties: string[] = partitionKey.paths[index].split("/").slice(1);
|
||||
let projectedProperty: string = "";
|
||||
|
||||
projectedProperties.forEach((property: string) => {
|
||||
const projection = property.trim();
|
||||
if (projection.length > 0 && projection.charAt(0) != "'" && projection.charAt(0) != '"') {
|
||||
projectedProperty = projectedProperty + `["${projection}"]`;
|
||||
} else if (projection.length > 0 && projection.charAt(0) == "'") {
|
||||
// trim single quotes and escape double quotes
|
||||
const projectionSlice = projection.slice(1, projection.length - 1);
|
||||
projectedProperty =
|
||||
projectedProperty + `["${projectionSlice.replace(/\\"/g, '"').replace(/"/g, '\\\\\\"')}"]`;
|
||||
} else {
|
||||
projectedProperty = projectedProperty + `[${projection}]`;
|
||||
}
|
||||
});
|
||||
|
||||
projections.push(`${collectionAlias}${projectedProperty}`);
|
||||
}
|
||||
|
||||
return projections.join(",");
|
||||
}
|
||||
|
||||
public static async queryPagesUntilContentPresent(
|
||||
firstItemIndex: number,
|
||||
queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
|
||||
): Promise<ViewModels.QueryResults> {
|
||||
let roundTrips: number = 0;
|
||||
let netRequestCharge: number = 0;
|
||||
const doRequest = async (itemIndex: number): Promise<ViewModels.QueryResults> => {
|
||||
const results: ViewModels.QueryResults = await queryItems(itemIndex);
|
||||
roundTrips = roundTrips + 1;
|
||||
results.roundTrips = roundTrips;
|
||||
results.requestCharge = Number(results.requestCharge) + netRequestCharge;
|
||||
netRequestCharge = Number(results.requestCharge);
|
||||
const resultsMetadata: ViewModels.QueryResultsMetadata = {
|
||||
hasMoreResults: results.hasMoreResults,
|
||||
itemCount: results.itemCount,
|
||||
firstItemIndex: results.firstItemIndex,
|
||||
lastItemIndex: results.lastItemIndex,
|
||||
};
|
||||
if (resultsMetadata.itemCount === 0 && resultsMetadata.hasMoreResults) {
|
||||
return await doRequest(resultsMetadata.lastItemIndex);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
return await doRequest(firstItemIndex);
|
||||
}
|
||||
|
||||
public static async queryAllPages(
|
||||
queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
|
||||
): Promise<ViewModels.QueryResults> {
|
||||
const queryResults: ViewModels.QueryResults = {
|
||||
documents: [],
|
||||
activityId: undefined,
|
||||
hasMoreResults: false,
|
||||
itemCount: 0,
|
||||
firstItemIndex: 0,
|
||||
lastItemIndex: 0,
|
||||
requestCharge: 0,
|
||||
roundTrips: 0,
|
||||
};
|
||||
const doRequest = async (itemIndex: number): Promise<ViewModels.QueryResults> => {
|
||||
const results: ViewModels.QueryResults = await queryItems(itemIndex);
|
||||
const { requestCharge, hasMoreResults, itemCount, lastItemIndex, documents } = results;
|
||||
queryResults.roundTrips = queryResults.roundTrips + 1;
|
||||
queryResults.requestCharge = Number(queryResults.requestCharge) + Number(requestCharge);
|
||||
queryResults.hasMoreResults = hasMoreResults;
|
||||
queryResults.itemCount = queryResults.itemCount + itemCount;
|
||||
queryResults.lastItemIndex = lastItemIndex;
|
||||
queryResults.documents = queryResults.documents.concat(documents);
|
||||
if (queryResults.hasMoreResults) {
|
||||
return doRequest(queryResults.lastItemIndex + 1);
|
||||
}
|
||||
return queryResults;
|
||||
};
|
||||
|
||||
return doRequest(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { StringUtils } from "./StringUtils";
|
||||
|
||||
describe("StringUtils", () => {
|
||||
describe("stripSpacesFromString()", () => {
|
||||
it("should strip all spaces from input string", () => {
|
||||
const transformedString: string = StringUtils.stripSpacesFromString("a b c");
|
||||
expect(transformedString).toBe("abc");
|
||||
});
|
||||
|
||||
it("should return original string if input string has no spaces", () => {
|
||||
const transformedString: string = StringUtils.stripSpacesFromString("abc");
|
||||
expect(transformedString).toBe("abc");
|
||||
});
|
||||
|
||||
it("should return null if input is null", () => {
|
||||
const transformedString: string = StringUtils.stripSpacesFromString(null);
|
||||
expect(transformedString).toBeNull();
|
||||
});
|
||||
|
||||
it("should return undefined if input is undefiend", () => {
|
||||
const transformedString: string = StringUtils.stripSpacesFromString(undefined);
|
||||
expect(transformedString).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should return empty string if input is an empty string", () => {
|
||||
const transformedString: string = StringUtils.stripSpacesFromString("");
|
||||
expect(transformedString).toBe("");
|
||||
});
|
||||
});
|
||||
});
|
||||
import { StringUtils } from "./StringUtils";
|
||||
|
||||
describe("StringUtils", () => {
|
||||
describe("stripSpacesFromString()", () => {
|
||||
it("should strip all spaces from input string", () => {
|
||||
const transformedString: string = StringUtils.stripSpacesFromString("a b c");
|
||||
expect(transformedString).toBe("abc");
|
||||
});
|
||||
|
||||
it("should return original string if input string has no spaces", () => {
|
||||
const transformedString: string = StringUtils.stripSpacesFromString("abc");
|
||||
expect(transformedString).toBe("abc");
|
||||
});
|
||||
|
||||
it("should return null if input is null", () => {
|
||||
const transformedString: string = StringUtils.stripSpacesFromString(null);
|
||||
expect(transformedString).toBeNull();
|
||||
});
|
||||
|
||||
it("should return undefined if input is undefiend", () => {
|
||||
const transformedString: string = StringUtils.stripSpacesFromString(undefined);
|
||||
expect(transformedString).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should return empty string if input is an empty string", () => {
|
||||
const transformedString: string = StringUtils.stripSpacesFromString("");
|
||||
expect(transformedString).toBe("");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
export class StringUtils {
|
||||
public static stripSpacesFromString(inputString: string): string {
|
||||
if (inputString == null || typeof inputString !== "string") {
|
||||
return inputString;
|
||||
}
|
||||
return inputString.replace(/ /g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of endsWith which works for IE
|
||||
* @param stringToTest
|
||||
* @param suffix
|
||||
*/
|
||||
public static endsWith(stringToTest: string, suffix: string): boolean {
|
||||
return stringToTest.indexOf(suffix, stringToTest.length - suffix.length) !== -1;
|
||||
}
|
||||
|
||||
public static startsWith(stringToTest: string, prefix: string): boolean {
|
||||
return stringToTest.indexOf(prefix) === 0;
|
||||
}
|
||||
}
|
||||
export class StringUtils {
|
||||
public static stripSpacesFromString(inputString: string): string {
|
||||
if (inputString == null || typeof inputString !== "string") {
|
||||
return inputString;
|
||||
}
|
||||
return inputString.replace(/ /g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of endsWith which works for IE
|
||||
* @param stringToTest
|
||||
* @param suffix
|
||||
*/
|
||||
public static endsWith(stringToTest: string, suffix: string): boolean {
|
||||
return stringToTest.indexOf(suffix, stringToTest.length - suffix.length) !== -1;
|
||||
}
|
||||
|
||||
public static startsWith(stringToTest: string, prefix: string): boolean {
|
||||
return stringToTest.indexOf(prefix) === 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { decryptJWTToken } from "./AuthorizationUtils";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export function getFullName(): string {
|
||||
const authToken = userContext.authorizationToken;
|
||||
const props = decryptJWTToken(authToken);
|
||||
return props.name;
|
||||
}
|
||||
import { decryptJWTToken } from "./AuthorizationUtils";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export function getFullName(): string {
|
||||
const authToken = userContext.authorizationToken;
|
||||
const props = decryptJWTToken(authToken);
|
||||
return props.name;
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import { getDataExplorerWindow } from "./WindowUtils";
|
||||
|
||||
interface MockWindow {
|
||||
parent?: MockWindow;
|
||||
top?: MockWindow;
|
||||
}
|
||||
|
||||
describe("WindowUtils", () => {
|
||||
describe("getDataExplorerWindow", () => {
|
||||
it("should return undefined if current window is at the top", () => {
|
||||
const mockWindow: MockWindow = {};
|
||||
mockWindow.parent = mockWindow;
|
||||
|
||||
expect(getDataExplorerWindow(mockWindow as Window)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("should return current window if parent is top", () => {
|
||||
const dataExplorerWindow: MockWindow = {};
|
||||
const portalWindow: MockWindow = {};
|
||||
dataExplorerWindow.parent = portalWindow;
|
||||
dataExplorerWindow.top = portalWindow;
|
||||
|
||||
expect(getDataExplorerWindow(dataExplorerWindow as Window)).toEqual(dataExplorerWindow);
|
||||
});
|
||||
|
||||
it("should return closest window to top if in nested windows", () => {
|
||||
const terminalWindow: MockWindow = {};
|
||||
const dataExplorerWindow: MockWindow = {};
|
||||
const portalWindow: MockWindow = {};
|
||||
dataExplorerWindow.top = portalWindow;
|
||||
dataExplorerWindow.parent = portalWindow;
|
||||
terminalWindow.top = portalWindow;
|
||||
terminalWindow.parent = dataExplorerWindow;
|
||||
|
||||
expect(getDataExplorerWindow(terminalWindow as Window)).toEqual(dataExplorerWindow);
|
||||
expect(getDataExplorerWindow(dataExplorerWindow as Window)).toEqual(dataExplorerWindow);
|
||||
});
|
||||
});
|
||||
});
|
||||
import { getDataExplorerWindow } from "./WindowUtils";
|
||||
|
||||
interface MockWindow {
|
||||
parent?: MockWindow;
|
||||
top?: MockWindow;
|
||||
}
|
||||
|
||||
describe("WindowUtils", () => {
|
||||
describe("getDataExplorerWindow", () => {
|
||||
it("should return undefined if current window is at the top", () => {
|
||||
const mockWindow: MockWindow = {};
|
||||
mockWindow.parent = mockWindow;
|
||||
|
||||
expect(getDataExplorerWindow(mockWindow as Window)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("should return current window if parent is top", () => {
|
||||
const dataExplorerWindow: MockWindow = {};
|
||||
const portalWindow: MockWindow = {};
|
||||
dataExplorerWindow.parent = portalWindow;
|
||||
dataExplorerWindow.top = portalWindow;
|
||||
|
||||
expect(getDataExplorerWindow(dataExplorerWindow as Window)).toEqual(dataExplorerWindow);
|
||||
});
|
||||
|
||||
it("should return closest window to top if in nested windows", () => {
|
||||
const terminalWindow: MockWindow = {};
|
||||
const dataExplorerWindow: MockWindow = {};
|
||||
const portalWindow: MockWindow = {};
|
||||
dataExplorerWindow.top = portalWindow;
|
||||
dataExplorerWindow.parent = portalWindow;
|
||||
terminalWindow.top = portalWindow;
|
||||
terminalWindow.parent = dataExplorerWindow;
|
||||
|
||||
expect(getDataExplorerWindow(terminalWindow as Window)).toEqual(dataExplorerWindow);
|
||||
expect(getDataExplorerWindow(dataExplorerWindow as Window)).toEqual(dataExplorerWindow);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
|
||||
// Data explorer is always loaded in an iframe, so traverse the parents until we hit the top and return the first child window.
|
||||
try {
|
||||
while (currentWindow) {
|
||||
if (currentWindow.parent === currentWindow) {
|
||||
return undefined;
|
||||
}
|
||||
if (currentWindow.parent === currentWindow.top) {
|
||||
return currentWindow;
|
||||
}
|
||||
currentWindow = currentWindow.parent;
|
||||
}
|
||||
} catch (error) {
|
||||
// Hitting a cross domain error means we are in the portal and the current window is data explorer
|
||||
return currentWindow;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
|
||||
// Data explorer is always loaded in an iframe, so traverse the parents until we hit the top and return the first child window.
|
||||
try {
|
||||
while (currentWindow) {
|
||||
if (currentWindow.parent === currentWindow) {
|
||||
return undefined;
|
||||
}
|
||||
if (currentWindow.parent === currentWindow.top) {
|
||||
return currentWindow;
|
||||
}
|
||||
currentWindow = currentWindow.parent;
|
||||
}
|
||||
} catch (error) {
|
||||
// Hitting a cross domain error means we are in the portal and the current window is data explorer
|
||||
return currentWindow;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ interface Global {
|
||||
describe("ARM request", () => {
|
||||
window.authType = AuthType.AAD;
|
||||
updateUserContext({
|
||||
authorizationToken: "some-token"
|
||||
authorizationToken: "some-token",
|
||||
});
|
||||
|
||||
it("should call window.fetch", async () => {
|
||||
@@ -20,7 +20,7 @@ describe("ARM request", () => {
|
||||
ok: true,
|
||||
json: async () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
await armRequest({ apiVersion: "2001-01-01", host: "https://foo.com", path: "foo", method: "GET" });
|
||||
expect(window.fetch).toHaveBeenCalled();
|
||||
@@ -33,7 +33,7 @@ describe("ARM request", () => {
|
||||
ok: true,
|
||||
headers,
|
||||
status: 200,
|
||||
json: async () => ({})
|
||||
json: async () => ({}),
|
||||
});
|
||||
await armRequest({ apiVersion: "2001-01-01", host: "https://foo.com", path: "foo", method: "GET" });
|
||||
expect(window.fetch).toHaveBeenCalledTimes(2);
|
||||
@@ -48,7 +48,7 @@ describe("ARM request", () => {
|
||||
status: 200,
|
||||
json: async () => {
|
||||
return { status: "Failed" };
|
||||
}
|
||||
},
|
||||
});
|
||||
await expect(() =>
|
||||
armRequest({ apiVersion: "2001-01-01", host: "https://foo.com", path: "foo", method: "GET" })
|
||||
@@ -59,7 +59,7 @@ describe("ARM request", () => {
|
||||
it("should throw token error", async () => {
|
||||
window.authType = AuthType.AAD;
|
||||
updateUserContext({
|
||||
authorizationToken: undefined
|
||||
authorizationToken: undefined,
|
||||
});
|
||||
const headers = new Headers();
|
||||
headers.set("location", "https://foo.com/operationStatus");
|
||||
@@ -69,7 +69,7 @@ describe("ARM request", () => {
|
||||
status: 200,
|
||||
json: async () => {
|
||||
return { status: "Failed" };
|
||||
}
|
||||
},
|
||||
});
|
||||
await expect(() =>
|
||||
armRequest({ apiVersion: "2001-01-01", host: "https://foo.com", path: "foo", method: "GET" })
|
||||
|
||||
@@ -54,7 +54,7 @@ export async function armRequest<T>({
|
||||
apiVersion,
|
||||
method,
|
||||
body: requestBody,
|
||||
queryParams
|
||||
queryParams,
|
||||
}: Options): Promise<T> {
|
||||
const url = new URL(path, host);
|
||||
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
|
||||
@@ -70,9 +70,9 @@ export async function armRequest<T>({
|
||||
const response = await window.fetch(url.href, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: userContext.authorizationToken
|
||||
Authorization: userContext.authorizationToken,
|
||||
},
|
||||
body: requestBody ? JSON.stringify(requestBody) : undefined
|
||||
body: requestBody ? JSON.stringify(requestBody) : undefined,
|
||||
});
|
||||
if (!response.ok) {
|
||||
let error: ARMError;
|
||||
@@ -108,8 +108,8 @@ async function getOperationStatus(operationStatusUrl: string) {
|
||||
|
||||
const response = await window.fetch(operationStatusUrl, {
|
||||
headers: {
|
||||
Authorization: userContext.authorizationToken
|
||||
}
|
||||
Authorization: userContext.authorizationToken,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
Reference in New Issue
Block a user