Prettier 2.0 (#393)

This commit is contained in:
Steve Faulkner
2021-01-20 09:15:01 -06:00
committed by GitHub
parent c1937ca464
commit 4be53284b5
500 changed files with 41927 additions and 41838 deletions

View File

@@ -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();
});
});
});

View File

@@ -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();
}

View File

@@ -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=");
});
});
});

View File

@@ -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));
})
);
};

View File

@@ -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 });
};

View File

@@ -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");
});
});

View File

@@ -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 };
}

View File

@@ -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,
});
});
});

View File

@@ -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}`;
}

View File

@@ -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);
});
});

View File

@@ -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,
};
}
}

View File

@@ -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");
}
}
}
}

View File

@@ -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 });
}
}

View File

@@ -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);
});

View File

@@ -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;

View File

@@ -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();
});
});
});
});

View File

@@ -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);
}
}

View File

@@ -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("");
});
});
});

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
});
});
});

View File

@@ -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;
};

View File

@@ -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" })

View File

@@ -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) {