From 5741802c25cf3efc903eae299443e9c47724b068 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Fri, 30 Oct 2020 15:09:24 -0700 Subject: [PATCH 01/13] refactor error handling part 1 (#307) - created `getErrorMessage` function which takes in an error string or any type of error object and returns the correct error message - replaced `error.message` with `getErrorMessage` since `error` could be a string in some cases - merged sendNotificationForError.ts with ErrorHandlingUtils.ts - some minor refactoring In part 2, I will make the following changes: - Make `Logger.logError` function take an error message string instead of an error object. This will reduce some redundancy where the `getErrorMessage` function is being called twice (the error object passed by the caller is already an error message). - Update every `TelemetryProcessor.traceFailure` call to make sure we pass in an error message instead of an error object since we stringify the data we send. --- src/Common/CosmosClient.ts | 3 +- src/Common/ErrorHandlingUtils.ts | 55 ++++++++++++++++--- src/Common/ErrorParserUtility.ts | 14 ----- src/Common/Logger.ts | 12 ++-- src/Common/QueriesClient.ts | 37 +++---------- src/Common/dataAccess/createTrigger.ts | 9 +-- src/Common/dataAccess/readOffers.ts | 4 +- .../dataAccess/sendNotificationForError.ts | 20 ------- .../updateOfferThroughputBeyondLimit.ts | 12 +++- .../Controls/Settings/SettingsComponent.tsx | 3 +- src/Explorer/Explorer.ts | 33 ++++++----- .../GraphExplorerComponent/GraphExplorer.tsx | 5 +- .../GraphExplorerComponent/GremlinClient.ts | 12 ++-- src/Explorer/Panes/AddCollectionPane.ts | 8 +-- src/Explorer/Panes/BrowseQueriesPane.ts | 7 ++- src/Explorer/Panes/RenewAdHocAccessPane.ts | 3 +- src/Explorer/Panes/SaveQueryPane.ts | 16 +++--- src/Explorer/Panes/SetupNotebooksPane.ts | 3 +- src/Explorer/Tabs/DocumentsTab.ts | 9 ++- src/Explorer/Tabs/SettingsTab.ts | 3 +- src/Explorer/Tree/Collection.ts | 16 +++--- src/Explorer/Tree/Database.ts | 8 ++- src/Explorer/Tree/StoredProcedure.ts | 3 +- src/GitHub/GitHubContentProvider.ts | 3 +- src/HostedExplorer.ts | 15 +++-- src/Platform/Hosted/Main.ts | 3 +- src/workers/upload/index.ts | 3 +- tsconfig.strict.json | 2 - 28 files changed, 162 insertions(+), 159 deletions(-) delete mode 100644 src/Common/dataAccess/sendNotificationForError.ts diff --git a/src/Common/CosmosClient.ts b/src/Common/CosmosClient.ts index de71c11b1..f54e8a073 100644 --- a/src/Common/CosmosClient.ts +++ b/src/Common/CosmosClient.ts @@ -1,6 +1,7 @@ import * as Cosmos from "@azure/cosmos"; import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos"; import { configContext, Platform } from "../ConfigContext"; +import { getErrorMessage } from "./ErrorHandlingUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { EmulatorMasterKey, HttpHeaders } from "./Constants"; import { userContext } from "../UserContext"; @@ -69,7 +70,7 @@ export async function getTokenFromAuthService(verb: string, resourceType: string const result = JSON.parse(await response.json()); return result; } catch (error) { - logConsoleError(`Failed to get authorization headers for ${resourceType}: ${error.message}`); + logConsoleError(`Failed to get authorization headers for ${resourceType}: ${getErrorMessage(error)}`); return Promise.reject(error); } } diff --git a/src/Common/ErrorHandlingUtils.ts b/src/Common/ErrorHandlingUtils.ts index f100e0b07..e5acc6b49 100644 --- a/src/Common/ErrorHandlingUtils.ts +++ b/src/Common/ErrorHandlingUtils.ts @@ -1,11 +1,52 @@ -import { CosmosError, sendNotificationForError } from "./dataAccess/sendNotificationForError"; +import { HttpStatusCodes } from "./Constants"; +import { MessageTypes } from "../Contracts/ExplorerContracts"; +import { SubscriptionType } from "../Contracts/ViewModels"; import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logError } from "./Logger"; -import { replaceKnownError } from "./ErrorParserUtility"; +import { sendMessage } from "./MessageHandler"; -export const handleError = (error: CosmosError, consoleErrorPrefix: string, area: string): void => { - const sanitizedErrorMsg = replaceKnownError(error.message); - logConsoleError(`${consoleErrorPrefix}:\n ${sanitizedErrorMsg}`); - logError(sanitizedErrorMsg, area, error.code); - sendNotificationForError(error); +export interface CosmosError { + code: number; + message?: string; +} + +export const handleError = (error: string | CosmosError, consoleErrorPrefix: string, area: string): void => { + const errorMessage = getErrorMessage(error); + const errorCode = typeof error === "string" ? undefined : error.code; + // logs error to data explorer console + logConsoleError(`${consoleErrorPrefix}:\n ${errorMessage}`); + // logs error to both app insight and kusto + logError(errorMessage, area, errorCode); + // checks for errors caused by firewall and sends them to portal to handle + sendNotificationForError(errorMessage, errorCode); +}; + +export const getErrorMessage = (error: string | CosmosError | Error): string => { + const errorMessage = typeof error === "string" ? error : error.message; + return replaceKnownError(errorMessage); +}; + +const sendNotificationForError = (errorMessage: string, errorCode: number): void => { + if (errorCode === HttpStatusCodes.Forbidden) { + if (errorMessage?.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) { + return; + } + sendMessage({ + type: MessageTypes.ForbiddenError, + reason: errorMessage + }); + } +}; + +const replaceKnownError = (errorMessage: string): string => { + if ( + window.dataExplorer?.subscriptionType() === SubscriptionType.Internal && + errorMessage.indexOf("SharedOffer is Disabled for your account") >= 0 + ) { + return "Database throughput is not supported for internal subscriptions."; + } else if (errorMessage.indexOf("Partition key paths must contain only valid") >= 0) { + return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character."; + } + + return errorMessage; }; diff --git a/src/Common/ErrorParserUtility.ts b/src/Common/ErrorParserUtility.ts index 95d1ace34..968737f5c 100644 --- a/src/Common/ErrorParserUtility.ts +++ b/src/Common/ErrorParserUtility.ts @@ -1,18 +1,4 @@ import * as DataModels from "../Contracts/DataModels"; -import * as ViewModels from "../Contracts/ViewModels"; - -export function replaceKnownError(err: string): string { - if ( - window.dataExplorer.subscriptionType() === ViewModels.SubscriptionType.Internal && - err.indexOf("SharedOffer is Disabled for your account") >= 0 - ) { - return "Database throughput is not supported for internal subscriptions."; - } else if (err.indexOf("Partition key paths must contain only valid") >= 0) { - return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character."; - } - - return err; -} export function parse(err: any): DataModels.ErrorDataModel[] { try { diff --git a/src/Common/Logger.ts b/src/Common/Logger.ts index 73d50a15e..39b3196c5 100644 --- a/src/Common/Logger.ts +++ b/src/Common/Logger.ts @@ -1,3 +1,4 @@ +import { CosmosError, getErrorMessage } from "./ErrorHandlingUtils"; import { sendMessage } from "./MessageHandler"; import { Diagnostics, MessageTypes } from "../Contracts/ExplorerContracts"; import { appInsights } from "../Shared/appInsights"; @@ -21,14 +22,9 @@ export function logWarning(message: string, area: string, code?: number): void { return _logEntry(entry); } -export function logError(message: string | Error, area: string, code?: number): void { - let logMessage: string; - if (typeof message === "string") { - logMessage = message; - } else { - logMessage = JSON.stringify(message, Object.getOwnPropertyNames(message)); - } - const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Error, logMessage, area, code); +export function logError(error: string | CosmosError | Error, area: string, code?: number): void { + const errorMessage: string = getErrorMessage(error); + const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Error, errorMessage, area, code); return _logEntry(entry); } diff --git a/src/Common/QueriesClient.ts b/src/Common/QueriesClient.ts index c54d167fc..7a3106890 100644 --- a/src/Common/QueriesClient.ts +++ b/src/Common/QueriesClient.ts @@ -12,6 +12,7 @@ import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { userContext } from "../UserContext"; import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase"; import { createCollection } from "./dataAccess/createCollection"; +import { handleError } from "./ErrorHandlingUtils"; import * as ErrorParserUtility from "./ErrorParserUtility"; import * as Logger from "./Logger"; @@ -53,13 +54,8 @@ export class QueriesClient { return Promise.resolve(collection); }, (error: any) => { - const stringifiedError: string = error.message; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to set up account for saving queries: ${stringifiedError}` - ); - Logger.logError(stringifiedError, "setupQueriesCollection"); - return Promise.reject(stringifiedError); + handleError(error, "Failed to set up account for saving queries", "setupQueriesCollection"); + return Promise.reject(error); } ) .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); @@ -163,25 +159,15 @@ export class QueriesClient { return Promise.resolve(queries); }, (error: any) => { - const stringifiedError: string = error.message; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to fetch saved queries: ${stringifiedError}` - ); - Logger.logError(stringifiedError, "getSavedQueries"); - return Promise.reject(stringifiedError); + handleError(error, "Failed to fetch saved queries", "getSavedQueries"); + return Promise.reject(error); } ); }, (error: any) => { // should never get into this state but we handle this regardless - const stringifiedError: string = error.message; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to fetch saved queries: ${stringifiedError}` - ); - Logger.logError(stringifiedError, "getSavedQueries"); - return Promise.reject(stringifiedError); + handleError(error, "Failed to fetch saved queries", "getSavedQueries"); + return Promise.reject(error); } ) .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); @@ -232,13 +218,8 @@ export class QueriesClient { return Promise.resolve(); }, (error: any) => { - const stringifiedError: string = error.message; - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to delete query ${query.queryName}: ${stringifiedError}` - ); - Logger.logError(stringifiedError, "deleteQuery"); - return Promise.reject(stringifiedError); + handleError(error, `Failed to delete query ${query.queryName}`, "deleteQuery"); + return Promise.reject(error); } ) .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); diff --git a/src/Common/dataAccess/createTrigger.ts b/src/Common/dataAccess/createTrigger.ts index 341dd46e7..179d0103e 100644 --- a/src/Common/dataAccess/createTrigger.ts +++ b/src/Common/dataAccess/createTrigger.ts @@ -7,9 +7,8 @@ import { } from "../../Utils/arm/generatedClients/2020-04-01/types"; import { client } from "../CosmosClient"; import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; -import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; -import { logError } from "../Logger"; -import { sendNotificationForError } from "./sendNotificationForError"; +import { handleError } from "../ErrorHandlingUtils"; +import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { userContext } from "../../UserContext"; export async function createTrigger( @@ -66,9 +65,7 @@ export async function createTrigger( .scripts.triggers.create(trigger); return response.resource; } catch (error) { - logConsoleError(`Error while creating trigger ${trigger.id}:\n ${error.message}`); - logError(error.message, "CreateTrigger", error.code); - sendNotificationForError(error); + handleError(error, `Error while creating trigger ${trigger.id}`, "CreateTrigger"); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/readOffers.ts b/src/Common/dataAccess/readOffers.ts index c3717c5b3..8fc743c9c 100644 --- a/src/Common/dataAccess/readOffers.ts +++ b/src/Common/dataAccess/readOffers.ts @@ -1,7 +1,7 @@ import { Offer } from "../../Contracts/DataModels"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { client } from "../CosmosClient"; -import { handleError } from "../ErrorHandlingUtils"; +import { handleError, getErrorMessage } from "../ErrorHandlingUtils"; export const readOffers = async (): Promise => { const clearMessage = logConsoleProgress(`Querying offers`); @@ -13,7 +13,7 @@ export const readOffers = async (): Promise => { return response?.resources; } catch (error) { // This should be removed when we can correctly identify if an account is serverless when connected using connection string too. - if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) { + if (getErrorMessage(error)?.includes("Reading or replacing offers is not supported for serverless accounts")) { return []; } diff --git a/src/Common/dataAccess/sendNotificationForError.ts b/src/Common/dataAccess/sendNotificationForError.ts deleted file mode 100644 index 8c7666991..000000000 --- a/src/Common/dataAccess/sendNotificationForError.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as Constants from "../Constants"; -import { sendMessage } from "../MessageHandler"; -import { MessageTypes } from "../../Contracts/ExplorerContracts"; - -export interface CosmosError { - code: number; - message?: string; -} - -export function sendNotificationForError(error: CosmosError): void { - if (error && error.code === Constants.HttpStatusCodes.Forbidden) { - if (error.message && error.message.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) { - return; - } - sendMessage({ - type: MessageTypes.ForbiddenError, - reason: error && error.message ? error.message : error - }); - } -} diff --git a/src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts b/src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts index b25ed571a..1b713de85 100644 --- a/src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts +++ b/src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts @@ -1,8 +1,9 @@ import { Platform, configContext } from "../../ConfigContext"; import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; import { AutoPilotOfferSettings } from "../../Contracts/DataModels"; -import { logConsoleProgress, logConsoleInfo, logConsoleError } from "../../Utils/NotificationConsoleUtils"; +import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { HttpHeaders } from "../Constants"; +import { handleError } from "../ErrorHandlingUtils"; interface UpdateOfferThroughputRequest { subscriptionId: string; @@ -44,8 +45,13 @@ export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThrou clearMessage(); return undefined; } + const error = await response.json(); - logConsoleError(`Failed to request an increase in throughput for ${request.throughput}: ${error.message}`); + handleError( + error, + `Failed to request an increase in throughput for ${request.throughput}`, + "updateOfferThroughputBeyondLimit" + ); clearMessage(); - throw new Error(error.message); + throw error; } diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index a3c14f5f8..2966e1cf8 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -50,6 +50,7 @@ import { getMongoDBCollectionIndexTransformationProgress, readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection"; +import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; interface SettingsV2TabInfo { tab: SettingsV2TabTypes; @@ -437,7 +438,7 @@ export class SettingsComponent extends React.Component { - const stringifiedError: string = error.message; + const stringifiedError: string = getErrorMessage(error); this.renewTokenError("Invalid connection string specified"); NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, @@ -1141,7 +1142,7 @@ export default class Explorer { NotificationConsoleUtils.clearInProgressMessageWithId(id); NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, - `Failed to generate share url: ${error.message}` + `Failed to generate share url: ${getErrorMessage(error)}` ); console.error(error); } @@ -1166,7 +1167,10 @@ export default class Explorer { deferred.resolve(); }, (error: any) => { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to connect: ${error.message}`); + NotificationConsoleUtils.logConsoleMessage( + ConsoleDataType.Error, + `Failed to connect: ${getErrorMessage(error)}` + ); deferred.reject(error); } ) @@ -1440,19 +1444,20 @@ export default class Explorer { this._setLoadingStatusText("Failed to fetch databases."); this.isRefreshingExplorer(false); deferred.reject(error); + const errorMessage = getErrorMessage(error); TelemetryProcessor.traceFailure( Action.LoadDatabases, { databaseAccountName: this.databaseAccount().name, defaultExperience: this.defaultExperience(), dataExplorerArea: Constants.Areas.ResourceTree, - error: error.message + error: errorMessage }, startKey ); NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, - `Error while refreshing databases: ${error.message}` + `Error while refreshing databases: ${errorMessage}` ); } ); @@ -1554,8 +1559,7 @@ export default class Explorer { return Promise.all(sparkPromises).then(() => workspaceItems); } catch (error) { - Logger.logError(error, "Explorer/this._arcadiaManager.listWorkspacesAsync"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error.message); + handleError(error, "Get Arcadia workspaces failed", "Explorer/this._arcadiaManager.listWorkspacesAsync"); return Promise.resolve([]); } } @@ -1590,10 +1594,10 @@ export default class Explorer { ); } catch (error) { this._isInitializingNotebooks = false; - Logger.logError(error, "initNotebooks/getNotebookConnectionInfoAsync"); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to get notebook workspace connection info: ${error.message}` + handleError( + error, + `Failed to get notebook workspace connection info: ${getErrorMessage(error)}`, + "initNotebooks/getNotebookConnectionInfoAsync" ); throw error; } finally { @@ -1669,8 +1673,7 @@ export default class Explorer { await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default"); } } catch (error) { - Logger.logError(error, "Explorer/ensureNotebookWorkspaceRunning"); - NotificationConsoleUtils.logConsoleError(`Failed to initialize notebook workspace: ${error.message}`); + handleError(error, "Failed to initialize notebook workspace", "Explorer/ensureNotebookWorkspaceRunning"); } finally { clearMessage && clearMessage(); } @@ -2052,7 +2055,7 @@ export default class Explorer { databaseAccountName: this.databaseAccount() && this.databaseAccount().name, defaultExperience: this.defaultExperience && this.defaultExperience(), dataExplorerArea: Constants.Areas.ResourceTree, - trace: error.message + trace: getErrorMessage(error) }, startKey ); @@ -2514,7 +2517,7 @@ export default class Explorer { (error: any) => { NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, - `Could not download notebook ${error.message}` + `Could not download notebook ${getErrorMessage(error)}` ); clearMessage(); diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx index bbe2982b7..974ec3a8f 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx @@ -29,6 +29,7 @@ import { InputProperty } from "../../../Contracts/ViewModels"; import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif"; import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase"; +import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; export interface GraphAccessor { applyFilter: () => void; @@ -892,7 +893,7 @@ export class GraphExplorer extends React.Component (this.queryTotalRequestCharge = result.requestCharge), (error: any) => { - const errorMsg = `Failure in submitting query: ${query}: ${error.message}`; + const errorMsg = `Failure in submitting query: ${query}: ${getErrorMessage(error)}`; GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); this.setState({ filterQueryError: errorMsg @@ -1826,7 +1827,7 @@ export class GraphExplorer extends React.Component this.processGremlinQueryResults(result)) .catch((error: any) => { - const errorMsg = `Failed to process query result: ${error.message}`; + const errorMsg = `Failed to process query result: ${getErrorMessage(error)}`; GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); this.setState({ filterQueryError: errorMsg diff --git a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts index 05627e2bf..7350f71d5 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts @@ -8,6 +8,7 @@ import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUti import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { HashMap } from "../../../Common/HashMap"; import * as Logger from "../../../Common/Logger"; +import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; export interface GremlinClientParameters { endpoint: string; @@ -58,14 +59,11 @@ export class GremlinClient { } }, failureCallback: (result: Result, error: any) => { - if (typeof error !== "string") { - error = error.message; - } - + const errorMessage = getErrorMessage(error); const requestId = result.requestId; if (!requestId || !this.pendingResults.has(requestId)) { - const msg = `Error: ${error}, unknown requestId:${requestId} ${GremlinClient.getRequestChargeString( + const msg = `Error: ${errorMessage}, unknown requestId:${requestId} ${GremlinClient.getRequestChargeString( result.requestCharge )}`; GremlinClient.reportError(msg); @@ -73,11 +71,11 @@ export class GremlinClient { // Fail all pending requests if no request id (fatal) if (!requestId) { this.pendingResults.keys().forEach((reqId: string) => { - this.abortPendingRequest(reqId, error, null); + this.abortPendingRequest(reqId, errorMessage, null); }); } } else { - this.abortPendingRequest(requestId, error, result.requestCharge); + this.abortPendingRequest(requestId, errorMessage, result.requestCharge); } }, infoCallback: (msg: string) => { diff --git a/src/Explorer/Panes/AddCollectionPane.ts b/src/Explorer/Panes/AddCollectionPane.ts index 08141f678..8c38a7809 100644 --- a/src/Explorer/Panes/AddCollectionPane.ts +++ b/src/Explorer/Panes/AddCollectionPane.ts @@ -15,6 +15,7 @@ import { configContext, Platform } from "../../ConfigContext"; import { ContextualPaneBase } from "./ContextualPaneBase"; import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent"; import { createCollection } from "../../Common/dataAccess/createCollection"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; export interface AddCollectionPaneOptions extends ViewModels.PaneOptions { isPreferredApiTable: ko.Computed; @@ -881,10 +882,9 @@ export default class AddCollectionPane extends ContextualPaneBase { this.resetData(); this.container.refreshAllDatabases(); }, - (reason: any) => { + (error: any) => { this.isExecuting(false); - const message = ErrorParserUtility.parse(reason); - const errorMessage = ErrorParserUtility.replaceKnownError(message[0].message); + const errorMessage: string = getErrorMessage(error); this.formErrors(errorMessage); this.formErrorsDetails(errorMessage); const addCollectionPaneFailedMessage = { @@ -912,7 +912,7 @@ export default class AddCollectionPane extends ContextualPaneBase { flight: this.container.flight() }, dataExplorerArea: Constants.Areas.ContextualPane, - error: reason + error: errorMessage }; TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey); } diff --git a/src/Explorer/Panes/BrowseQueriesPane.ts b/src/Explorer/Panes/BrowseQueriesPane.ts index 5c194c970..af3ef0a15 100644 --- a/src/Explorer/Panes/BrowseQueriesPane.ts +++ b/src/Explorer/Panes/BrowseQueriesPane.ts @@ -7,6 +7,7 @@ import * as Logger from "../../Common/Logger"; import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import QueryTab from "../Tabs/QueryTab"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; export class BrowseQueriesPane extends ContextualPaneBase { public queriesGridComponentAdapter: QueriesGridComponentAdapter; @@ -60,17 +61,19 @@ export class BrowseQueriesPane extends ContextualPaneBase { startKey ); } catch (error) { + const errorMessage = getErrorMessage(error); TelemetryProcessor.traceFailure( Action.SetupSavedQueries, { databaseAccountName: this.container && this.container.databaseAccount().name, defaultExperience: this.container && this.container.defaultExperience(), dataExplorerArea: Areas.ContextualPane, - paneTitle: this.title() + paneTitle: this.title(), + error: errorMessage }, startKey ); - this.formErrors(`Failed to setup a collection for saved queries: ${error.message}`); + this.formErrors(`Failed to setup a collection for saved queries: ${errorMessage}`); } finally { this.isExecuting(false); } diff --git a/src/Explorer/Panes/RenewAdHocAccessPane.ts b/src/Explorer/Panes/RenewAdHocAccessPane.ts index 440df54ee..f9476c341 100644 --- a/src/Explorer/Panes/RenewAdHocAccessPane.ts +++ b/src/Explorer/Panes/RenewAdHocAccessPane.ts @@ -7,6 +7,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; export class RenewAdHocAccessPane extends ContextualPaneBase { public accessKey: ko.Observable; @@ -82,7 +83,7 @@ export class RenewAdHocAccessPane extends ContextualPaneBase { this.container .renewShareAccess(this.accessKey()) .fail((error: any) => { - const errorMessage: string = error.message; + const errorMessage: string = getErrorMessage(error); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to connect: ${errorMessage}`); this.formErrors(errorMessage); this.formErrorsDetails(errorMessage); diff --git a/src/Explorer/Panes/SaveQueryPane.ts b/src/Explorer/Panes/SaveQueryPane.ts index 4d249d6a9..b379f6d3a 100644 --- a/src/Explorer/Panes/SaveQueryPane.ts +++ b/src/Explorer/Panes/SaveQueryPane.ts @@ -8,6 +8,7 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import QueryTab from "../Tabs/QueryTab"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; export class SaveQueryPane extends ContextualPaneBase { public queryName: ko.Observable; @@ -87,18 +88,17 @@ export class SaveQueryPane extends ContextualPaneBase { }, (error: any) => { this.isExecuting(false); - if (typeof error != "string") { - error = error.message; - } + const errorMessage = getErrorMessage(error); this.formErrors("Failed to save query"); - this.formErrorsDetails(`Failed to save query: ${error}`); + this.formErrorsDetails(`Failed to save query: ${errorMessage}`); TelemetryProcessor.traceFailure( Action.SaveQuery, { databaseAccountName: this.container.databaseAccount().name, defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: this.title() + paneTitle: this.title(), + error: errorMessage }, startKey ); @@ -132,18 +132,20 @@ export class SaveQueryPane extends ContextualPaneBase { startKey ); } catch (error) { + const errorMessage = getErrorMessage(error); TelemetryProcessor.traceFailure( Action.SetupSavedQueries, { databaseAccountName: this.container && this.container.databaseAccount().name, defaultExperience: this.container && this.container.defaultExperience(), dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: this.title() + paneTitle: this.title(), + error: errorMessage }, startKey ); this.formErrors("Failed to setup a container for saved queries"); - this.formErrors(`Failed to setup a container for saved queries: ${error.message}`); + this.formErrorsDetails(`Failed to setup a container for saved queries: ${errorMessage}`); } finally { this.isExecuting(false); } diff --git a/src/Explorer/Panes/SetupNotebooksPane.ts b/src/Explorer/Panes/SetupNotebooksPane.ts index 6a8e56dc9..a95bcb667 100644 --- a/src/Explorer/Panes/SetupNotebooksPane.ts +++ b/src/Explorer/Panes/SetupNotebooksPane.ts @@ -6,6 +6,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as ko from "knockout"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; export class SetupNotebooksPane extends ContextualPaneBase { private description: ko.Observable; @@ -85,7 +86,7 @@ export class SetupNotebooksPane extends ContextualPaneBase { "Successfully created a default notebook workspace for the account" ); } catch (error) { - const errorMessage = typeof error == "string" ? error : error.message; + const errorMessage = getErrorMessage(error); TelemetryProcessor.traceFailure( Action.CreateNotebookWorkspace, { diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 1779e10a0..368c85ada 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -32,6 +32,7 @@ import { createDocument } from "../../Common/DocumentClientUtilityBase"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; export default class DocumentsTab extends TabsBase { public selectedDocumentId: ko.Observable; @@ -774,10 +775,8 @@ export default class DocumentsTab extends TabsBase { }, error => { this.isExecutionError(true); - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - typeof error === "string" ? error : error.message - ); + const errorMessage = getErrorMessage(error); + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage); if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) { TelemetryProcessor.traceFailure( Action.Tab, @@ -788,7 +787,7 @@ export default class DocumentsTab extends TabsBase { defaultExperience: this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), - error: error + error: errorMessage }, this.onLoadStartKey ); diff --git a/src/Explorer/Tabs/SettingsTab.ts b/src/Explorer/Tabs/SettingsTab.ts index 4ae2785fe..29dfbe9fc 100644 --- a/src/Explorer/Tabs/SettingsTab.ts +++ b/src/Explorer/Tabs/SettingsTab.ts @@ -22,6 +22,7 @@ import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandBu import { userContext } from "../../UserContext"; import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit"; import { configContext, Platform } from "../../ConfigContext"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; const ttlWarning: string = ` The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete operation explicitly issued by a client application. @@ -1174,7 +1175,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), - error: error.message + error: getErrorMessage(error) }, startKey ); diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 14192ae2b..a1921cda7 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -40,6 +40,7 @@ import Explorer from "../Explorer"; import { userContext } from "../../UserContext"; import TabsBase from "../Tabs/TabsBase"; import { fetchPortalNotifications } from "../../Common/PortalNotifications"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; export default class Collection implements ViewModels.Collection { public nodeKind: string; @@ -610,6 +611,7 @@ export default class Collection implements ViewModels.Collection { settingsTab.pendingNotification(pendingNotification); }, (error: any) => { + const errorMessage = getErrorMessage(error); TelemetryProcessor.traceFailure( Action.Tab, { @@ -619,13 +621,13 @@ export default class Collection implements ViewModels.Collection { defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, tabTitle: settingsTabOptions.title, - error: error + error: errorMessage }, startKey ); NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, - `Error while fetching container settings for container ${this.id()}: ${error.message}` + `Error while fetching container settings for container ${this.id()}: ${errorMessage}` ); throw error; } @@ -869,7 +871,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.ResourceTree, - error: typeof error === "string" ? error : error.message + error: getErrorMessage(error) }); } ); @@ -928,7 +930,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.ResourceTree, - error: typeof error === "string" ? error : error.message + error: getErrorMessage(error) }); } ); @@ -988,7 +990,7 @@ export default class Collection implements ViewModels.Collection { collectionName: this.id(), defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.ResourceTree, - error: typeof error === "string" ? error : error.message + error: getErrorMessage(error) }); } ); @@ -1185,7 +1187,7 @@ export default class Collection implements ViewModels.Collection { }, error => { record.numFailed++; - record.errors = [...record.errors, error.message]; + record.errors = [...record.errors, getErrorMessage(error)]; return Q.resolve(); } ); @@ -1238,7 +1240,7 @@ export default class Collection implements ViewModels.Collection { (error: any) => { Logger.logError( JSON.stringify({ - error: error.message, + error: getErrorMessage(error), accountName: this.container && this.container.databaseAccount(), databaseName: this.databaseId, collectionName: this.id() diff --git a/src/Explorer/Tree/Database.ts b/src/Explorer/Tree/Database.ts index faa33d406..83bb3c361 100644 --- a/src/Explorer/Tree/Database.ts +++ b/src/Explorer/Tree/Database.ts @@ -16,6 +16,7 @@ import { readCollections } from "../../Common/dataAccess/readCollections"; import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { fetchPortalNotifications } from "../../Common/PortalNotifications"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; export default class Database implements ViewModels.Database { public nodeKind: string; @@ -88,6 +89,7 @@ export default class Database implements ViewModels.Database { this.container.tabsManager.activateNewTab(settingsTab); }, (error: any) => { + const errorMessage = getErrorMessage(error); TelemetryProcessor.traceFailure( Action.Tab, { @@ -97,13 +99,13 @@ export default class Database implements ViewModels.Database { defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, tabTitle: "Scale", - error: error + error: errorMessage }, startKey ); NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.Error, - `Error while fetching database settings for database ${this.id()}: ${error.message}` + `Error while fetching database settings for database ${this.id()}: ${errorMessage}` ); throw error; } @@ -239,7 +241,7 @@ export default class Database implements ViewModels.Database { (error: any) => { Logger.logError( JSON.stringify({ - error: error.message, + error: getErrorMessage(error), accountName: this.container && this.container.databaseAccount(), databaseName: this.id(), collectionName: this.id() diff --git a/src/Explorer/Tree/StoredProcedure.ts b/src/Explorer/Tree/StoredProcedure.ts index f2db0b577..1c35dfa8f 100644 --- a/src/Explorer/Tree/StoredProcedure.ts +++ b/src/Explorer/Tree/StoredProcedure.ts @@ -9,6 +9,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import Explorer from "../Explorer"; import StoredProcedureTab from "../Tabs/StoredProcedureTab"; import TabsBase from "../Tabs/TabsBase"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; const sampleStoredProcedureBody: string = `// SAMPLE STORED PROCEDURE function sample(prefix) { @@ -158,7 +159,7 @@ export default class StoredProcedure { sprocTab.onExecuteSprocsResult(result, result.scriptLogs); }, (error: any) => { - sprocTab.onExecuteSprocsError(error.message); + sprocTab.onExecuteSprocsError(getErrorMessage(error)); } ) .finally(() => { diff --git a/src/GitHub/GitHubContentProvider.ts b/src/GitHub/GitHubContentProvider.ts index 9d6dcc82b..6142b7a6f 100644 --- a/src/GitHub/GitHubContentProvider.ts +++ b/src/GitHub/GitHubContentProvider.ts @@ -9,6 +9,7 @@ import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil"; import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient"; import * as GitHubUtils from "../Utils/GitHubUtils"; import UrlUtility from "../Common/UrlUtility"; +import { getErrorMessage } from "../Common/ErrorHandlingUtils"; export interface GitHubContentProviderParams { gitHubClient: GitHubClient; @@ -423,7 +424,7 @@ export class GitHubContentProvider implements IContentProvider { request: {}, status: error.errno, response: error, - responseText: error.message, + responseText: getErrorMessage(error), responseType: "json" }; } diff --git a/src/HostedExplorer.ts b/src/HostedExplorer.ts index 77024a87c..ac24eeaea 100644 --- a/src/HostedExplorer.ts +++ b/src/HostedExplorer.ts @@ -21,6 +21,7 @@ import { import { DialogComponentAdapter } from "./Explorer/Controls/DialogReactComponent/DialogComponentAdapter"; import { DialogProps } from "./Explorer/Controls/DialogReactComponent/DialogComponent"; import { DirectoryListProps } from "./Explorer/Controls/Directory/DirectoryListComponent"; +import { getErrorMessage } from "./Common/ErrorHandlingUtils"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; import { LocalStorageUtility, StorageKey, SessionStorageUtility } from "./Shared/StorageUtility"; import * as Logger from "./Common/Logger"; @@ -509,12 +510,13 @@ class HostedExplorer { } }); } catch (error) { - Logger.logError(error, "HostedExplorer/_getArcadiaToken"); + const errorMessage = getErrorMessage(error); + Logger.logError(errorMessage, "HostedExplorer/_getArcadiaToken"); this._sendMessageToExplorerFrame({ actionType: ActionType.TransmitCachedData, message: { id: message && message.id, - error: error.message + error: errorMessage } }); } @@ -559,12 +561,9 @@ class HostedExplorer { }); }, error => { - if (typeof error !== "string") { - error = JSON.stringify(error, Object.getOwnPropertyNames(error)); - } this._sendMessageToExplorerFrame({ type: MessageTypes.GetAccessAadResponse, - error + error: getErrorMessage(error) }); } ); @@ -1008,7 +1007,7 @@ class HostedExplorer { return accounts; } catch (error) { - this._logConsoleMessage(ConsoleDataType.Error, `Failed to fetch accounts: ${error.message}`); + this._logConsoleMessage(ConsoleDataType.Error, `Failed to fetch accounts: ${getErrorMessage(error)}`); this._clearInProgressMessageWithId(id); throw error; @@ -1047,7 +1046,7 @@ class HostedExplorer { displayText: "Error loading account" }); this._updateLoadingStatusText(`Failed to load selected account: ${newAccount.name}`); - this._logConsoleMessage(ConsoleDataType.Error, `Failed to connect: ${error.message}`); + this._logConsoleMessage(ConsoleDataType.Error, `Failed to connect: ${getErrorMessage(error)}`); this._clearInProgressMessageWithId(id); throw error; } diff --git a/src/Platform/Hosted/Main.ts b/src/Platform/Hosted/Main.ts index d424704f4..e218a136e 100644 --- a/src/Platform/Hosted/Main.ts +++ b/src/Platform/Hosted/Main.ts @@ -23,6 +23,7 @@ import "../../Explorer/Tables/DataTable/DataTableBindingManager"; import Explorer from "../../Explorer/Explorer"; import { updateUserContext } from "../../UserContext"; import { configContext } from "../../ConfigContext"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; export default class Main { private static _databaseAccountId: string; @@ -245,7 +246,7 @@ export default class Main { ); }, (error: any) => { - deferred.reject(`Failed to generate encrypted token: ${error.message}`); + deferred.reject(`Failed to generate encrypted token: ${getErrorMessage(error)}`); } ); diff --git a/src/workers/upload/index.ts b/src/workers/upload/index.ts index 7f5172db3..5e38b8d79 100644 --- a/src/workers/upload/index.ts +++ b/src/workers/upload/index.ts @@ -3,6 +3,7 @@ import { DocumentClientParams, UploadDetailsRecord, UploadDetails } from "./defi import { client } from "../../Common/CosmosClient"; import { configContext, updateConfigContext } from "../../ConfigContext"; import { updateUserContext } from "../../UserContext"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; let numUploadsSuccessful = 0; let numUploadsFailed = 0; @@ -93,7 +94,7 @@ function createDocumentsFromFile(fileName: string, documentContent: string): voi }) .catch(error => { console.error(error); - recordUploadDetailErrorForFile(fileName, error.message); + recordUploadDetailErrorForFile(fileName, getErrorMessage(error)); numUploadsFailed++; }) .finally(() => { diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 94bd2d8fa..89cfd09a2 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -15,13 +15,11 @@ "./src/Common/DeleteFeedback.ts", "./src/Common/HashMap.ts", "./src/Common/HeadersUtility.ts", - "./src/Common/Logger.ts", "./src/Common/MessageHandler.ts", "./src/Common/MongoUtility.ts", "./src/Common/ObjectCache.ts", "./src/Common/ThemeUtility.ts", "./src/Common/UrlUtility.ts", - "./src/Common/dataAccess/sendNotificationForError.ts", "./src/ConfigContext.ts", "./src/Contracts/ActionContracts.ts", "./src/Contracts/DataModels.ts", From 473f722dcc163f950b39a00d1215bc703fba5e37 Mon Sep 17 00:00:00 2001 From: Zachary Foster Date: Mon, 2 Nov 2020 14:33:14 -0500 Subject: [PATCH 02/13] E2E Test Rewrite (#300) * Adds tables test * Include .env var * Adds asElement on again * Add further loading states * Format * Hope to not lose focus * Adds ID to shared key and modifies value of input directly * Fix tables test * Format * Try uploading screenshots * indent * Fixes connection string * Try wildcard upload path * Rebuilds test structure, assertions, dependencies * Wait longer for container create * format --- .github/workflows/ci.yml | 5 + src/Explorer/Panes/AddCollectionPane.html | 2 +- test/cassandra/container.spec.ts | 117 +++++++++++++++----- test/mongo/container.spec.ts | 128 ++++++++++++++++------ test/sql/container.spec.ts | 124 +++++++++++++++------ test/tables/container.spec.ts | 111 +++++++++++++++++++ test/utils/shared.ts | 4 +- 7 files changed, 388 insertions(+), 103 deletions(-) create mode 100644 test/tables/container.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78a043e29..53a39150a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,6 +150,11 @@ jobs: PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }} MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }} CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }} + TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }} + - uses: actions/upload-artifact@v2 + with: + name: screenshots + path: failed-* nuget: name: Publish Nuget if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') diff --git a/src/Explorer/Panes/AddCollectionPane.html b/src/Explorer/Panes/AddCollectionPane.html index 775790fce..9167bcf93 100644 --- a/src/Explorer/Panes/AddCollectionPane.html +++ b/src/Explorer/Panes/AddCollectionPane.html @@ -288,7 +288,7 @@ range of values and is likely to have evenly distributed access patterns.

- { it("creates a collection", async () => { try { - const keyspaceId = generateUniqueName("keyspaceid"); - const tableId = generateUniqueName("tableid"); + const keyspaceId = generateUniqueName("key"); + const tableId = generateUniqueName("tab"); const frame = await login(process.env.CASSANDRA_CONNECTION_STRING); // create new table @@ -31,38 +34,69 @@ describe("Collection Add and Delete Cassandra spec", () => { // open database menu await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); - - await frame.waitFor(`div[data-test="${keyspaceId}"]`, { visible: true }); - await frame.waitFor(LOADING_STATE_DELAY); - await frame.waitFor(`div[data-test="${keyspaceId}"]`, { visible: true }); - await frame.click(`div[data-test="${keyspaceId}"]`); - await frame.waitFor(`span[title="${tableId}"]`, { visible: true }); - - // delete container - - // click context menu for container - await frame.waitFor(`div[data-test="${tableId}"] > div > button`, { visible: true }); - await frame.click(`div[data-test="${tableId}"] > div > button`); - - // click delete container - await frame.waitForSelector("body > div.ms-Layer.ms-Layer--fixed"); - await frame.waitFor(RENDER_DELAY); - const elements = await frame.$$('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); - await elements[0].click(); - - // confirm delete container - await frame.type('input[data-test="confirmCollectionId"]', tableId.trim()); - - // click delete - await frame.click('input[data-test="deleteCollection"]'); - await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitFor(LOADING_STATE_DELAY); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); - await expect(page).not.toMatchElement(`div[data-test="${tableId}"]`); + const databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`); + const selectedDbId = await frame.evaluate(element => { + return element.attributes["data-test"].textContent; + }, databases[0]); + + await frame.waitFor(`div[data-test="${selectedDbId}"]`), { visible: true }; + await frame.waitFor(CREATE_DELAY); + await frame.waitFor("div[class='rowData'] > span[class='message']"); + + const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", elements => { + return elements.some(el => el.textContent.includes("Successfully created")); + }); + + expect(didCreateContainer).toBe(true); + + await frame.waitFor(`div[data-test="${selectedDbId}"]`), { visible: true }; + await frame.waitFor(LOADING_STATE_DELAY); + + await clickDBMenu(selectedDbId, frame); + + const collections = await frame.$$( + `div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]` + ); + + if (collections.length) { + await frame.waitFor(`div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]`, { + visible: true + }); + + const textId = await frame.evaluate(element => { + return element.attributes["data-test"].textContent; + }, collections[0]); + await frame.waitFor(`div[data-test="${textId}"]`, { visible: true }); + // delete container + + // click context menu for container + await frame.waitFor(`div[data-test="${textId}"] > div > button`, { visible: true }); + await frame.click(`div[data-test="${textId}"] > div > button`); + + // click delete container + await frame.waitFor(RENDER_DELAY); + await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); + await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); + + // confirm delete container + await frame.waitFor('input[data-test="confirmCollectionId"]', { visible: true }); + await frame.type('input[data-test="confirmCollectionId"]', textId); + + // click delete + await frame.waitFor('input[data-test="deleteCollection"]', { visible: true }); + await frame.click('input[data-test="deleteCollection"]'); + await frame.waitFor(LOADING_STATE_DELAY); + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + + await expect(page).not.toMatchElement(`div[data-test="${textId}"]`); + } // click context menu for database await frame.waitFor(`div[data-test="${keyspaceId}"] > div > button`); + await frame.waitFor(RENDER_DELAY); const button = await frame.$(`div[data-test="${keyspaceId}"] > div > button`); await button.focus(); await button.asElement().click(); @@ -80,12 +114,35 @@ describe("Collection Add and Delete Cassandra spec", () => { // click delete await frame.click('input[data-test="deleteDatabase"]'); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + await frame.waitFor(LOADING_STATE_DELAY); + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await expect(page).not.toMatchElement(`div[data-test="${keyspaceId}"]`); } catch (error) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const testName = (expect as any).getState().currentTestName; - await page.screenshot({ path: `Test Failed ${testName}.png` }); + await page.screenshot({ path: `failed-${testName}.jpg` }); throw error; } }); }); + +async function clickDBMenu(dbId: string, frame: Frame, retries = 0) { + const button = await frame.$(`div[data-test="${dbId}"]`); + await button.focus(); + const handler = await button.asElement(); + await handler.click(); + await ensureMenuIsOpen(dbId, frame, retries); + return button; +} + +async function ensureMenuIsOpen(dbId: string, frame: Frame, retries: number) { + await frame.waitFor(RETRY_DELAY); + const button = await frame.$(`div[data-test="${dbId}"]`); + const classList = await frame.evaluate(button => { + return button.parentElement.classList; + }, button); + if (!Object.values(classList).includes("selected") && retries < 5) { + retries = retries + 1; + await clickDBMenu(dbId, frame, retries); + } +} diff --git a/test/mongo/container.spec.ts b/test/mongo/container.spec.ts index 54febbcf1..b5e0599de 100644 --- a/test/mongo/container.spec.ts +++ b/test/mongo/container.spec.ts @@ -1,17 +1,19 @@ import "expect-puppeteer"; +import { Frame } from "puppeteer"; import { generateUniqueName, login } from "../utils/shared"; jest.setTimeout(300000); - const LOADING_STATE_DELAY = 2500; +const RETRY_DELAY = 5000; +const CREATE_DELAY = 10000; const RENDER_DELAY = 1000; describe("Collection Add and Delete Mongo spec", () => { - it("creates and deletes a collection", async () => { + it("creates a collection", async () => { try { - const dbId = generateUniqueName("TestDatabase"); - const collectionId = generateUniqueName("TestCollection"); - const sharedKey = generateUniqueName("SharedKey"); + const dbId = generateUniqueName("db"); + const collectionId = generateUniqueName("col"); + const sharedKey = `${generateUniqueName()}`; const frame = await login(process.env.MONGO_CONNECTION_STRING); // create new collection @@ -52,39 +54,70 @@ describe("Collection Add and Delete Mongo spec", () => { // validate created // open database menu await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); - - await frame.waitFor(`div[data-test="${dbId}"]`), { visible: true }; - await frame.waitFor(LOADING_STATE_DELAY); - await frame.waitFor(`div[data-test="${dbId}"]`), { visible: true }; - await frame.click(`div[data-test="${dbId}"]`); - await frame.waitFor(`div[data-test="${collectionId}"]`, { visible: true }); - - // delete container - - // click context menu for container - await frame.waitFor(`div[data-test="${collectionId}"] > div > button`, { visible: true }); - await frame.click(`div[data-test="${collectionId}"] > div > button`); - - // click delete container - await frame.waitFor(RENDER_DELAY); - await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]', { visible: true }); - await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); - - // confirm delete container - await frame.waitFor('input[data-test="confirmCollectionId"]', { visible: true }); - await frame.type('input[data-test="confirmCollectionId"]', collectionId.trim()); - - // click delete - await frame.waitFor('input[data-test="deleteCollection"]', { visible: true }); - await frame.click('input[data-test="deleteCollection"]'); await frame.waitFor(LOADING_STATE_DELAY); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); - await expect(page).not.toMatchElement(`div[data-test="${collectionId}"]`); + const databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`); + const selectedDbId = await frame.evaluate(element => { + return element.attributes["data-test"].textContent; + }, databases[0]); + + await frame.waitFor(`div[data-test="${selectedDbId}"]`), { visible: true }; + await frame.waitFor(CREATE_DELAY); + await frame.waitFor("div[class='rowData'] > span[class='message']"); + + const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", elements => { + return elements.some(el => el.textContent.includes("Successfully created")); + }); + + expect(didCreateContainer).toBe(true); + + await frame.waitFor(`div[data-test="${selectedDbId}"]`), { visible: true }; + await frame.waitFor(LOADING_STATE_DELAY); + + await clickDBMenu(selectedDbId, frame); + + const collections = await frame.$$( + `div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]` + ); + + if (collections.length) { + await frame.waitFor(`div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]`, { + visible: true + }); + + const textId = await frame.evaluate(element => { + return element.attributes["data-test"].textContent; + }, collections[0]); + await frame.waitFor(`div[data-test="${textId}"]`, { visible: true }); + // delete container + + // click context menu for container + await frame.waitFor(`div[data-test="${textId}"] > div > button`, { visible: true }); + await frame.click(`div[data-test="${textId}"] > div > button`); + + // click delete container + await frame.waitFor(RENDER_DELAY); + await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); + await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); + + // confirm delete container + await frame.waitFor('input[data-test="confirmCollectionId"]', { visible: true }); + await frame.type('input[data-test="confirmCollectionId"]', textId); + + // click delete + await frame.waitFor('input[data-test="deleteCollection"]', { visible: true }); + await frame.click('input[data-test="deleteCollection"]'); + await frame.waitFor(LOADING_STATE_DELAY); + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + + await expect(page).not.toMatchElement(`div[data-test="${textId}"]`); + } // click context menu for database - await frame.waitFor(`div[data-test="${dbId}"] > div > button`); - const button = await frame.$(`div[data-test="${dbId}"] > div > button`); + await frame.waitFor(`div[data-test="${selectedDbId}"] > div > button`); + await frame.waitFor(RENDER_DELAY); + const button = await frame.$(`div[data-test="${selectedDbId}"] > div > button`); await button.focus(); await button.asElement().click(); @@ -96,17 +129,40 @@ describe("Collection Add and Delete Mongo spec", () => { // confirm delete database await frame.waitForSelector('input[data-test="confirmDatabaseId"]', { visible: true }); await frame.waitFor(RENDER_DELAY); - await frame.type('input[data-test="confirmDatabaseId"]', dbId.trim()); + await frame.type('input[data-test="confirmDatabaseId"]', selectedDbId); // click delete await frame.click('input[data-test="deleteDatabase"]'); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); - await expect(page).not.toMatchElement(`div[data-test="${dbId}"]`); + await frame.waitFor(LOADING_STATE_DELAY); + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + await expect(page).not.toMatchElement(`div[data-test="${selectedDbId}"]`); } catch (error) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const testName = (expect as any).getState().currentTestName; - await page.screenshot({ path: `Test Failed ${testName}.png` }); + await page.screenshot({ path: `failed-${testName}.jpg` }); throw error; } }); }); + +async function clickDBMenu(dbId: string, frame: Frame, retries = 0) { + const button = await frame.$(`div[data-test="${dbId}"]`); + await button.focus(); + const handler = await button.asElement(); + await handler.click(); + await ensureMenuIsOpen(dbId, frame, retries); + return button; +} + +async function ensureMenuIsOpen(dbId: string, frame: Frame, retries: number) { + await frame.waitFor(RETRY_DELAY); + const button = await frame.$(`div[data-test="${dbId}"]`); + const classList = await frame.evaluate(button => { + return button.parentElement.classList; + }, button); + if (!Object.values(classList).includes("selected") && retries < 5) { + retries = retries + 1; + await clickDBMenu(dbId, frame, retries); + } +} diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index 6b801c817..ad5d7f251 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -1,16 +1,19 @@ import "expect-puppeteer"; +import { Frame } from "puppeteer"; import { generateUniqueName, login } from "../utils/shared"; jest.setTimeout(300000); const LOADING_STATE_DELAY = 2500; +const RETRY_DELAY = 5000; +const CREATE_DELAY = 10000; const RENDER_DELAY = 1000; describe("Collection Add and Delete SQL spec", () => { it("creates a collection", async () => { try { - const dbId = generateUniqueName("TestDatabase"); - const collectionId = generateUniqueName("TestCollection"); - const sharedKey = generateUniqueName("SharedKey"); + const dbId = generateUniqueName("db"); + const collectionId = generateUniqueName("col"); + const sharedKey = `/skey${generateUniqueName()}`; const frame = await login(process.env.PORTAL_RUNNER_CONNECTION_STRING); // create new collection @@ -51,39 +54,69 @@ describe("Collection Add and Delete SQL spec", () => { // validate created // open database menu await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); - - await frame.waitFor(`div[data-test="${dbId}"]`), { visible: true }; - await frame.waitFor(LOADING_STATE_DELAY); - await frame.waitFor(`div[data-test="${dbId}"]`), { visible: true }; - await frame.click(`div[data-test="${dbId}"]`); - await frame.waitFor(`div[data-test="${collectionId}"]`, { visible: true }); - - // delete container - - // click context menu for container - await frame.waitFor(`div[data-test="${collectionId}"] > div > button`, { visible: true }); - await frame.click(`div[data-test="${collectionId}"] > div > button`); - - // click delete container - await frame.waitFor(RENDER_DELAY); - await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); - await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); - - // confirm delete container - await frame.waitFor('input[data-test="confirmCollectionId"]', { visible: true }); - await frame.type('input[data-test="confirmCollectionId"]', collectionId); - - // click delete - await frame.waitFor('input[data-test="deleteCollection"]', { visible: true }); - await frame.click('input[data-test="deleteCollection"]'); await frame.waitFor(LOADING_STATE_DELAY); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + const databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`); + const selectedDbId = await frame.evaluate(element => { + return element.attributes["data-test"].textContent; + }, databases[0]); - await expect(page).not.toMatchElement(`div[data-test="${collectionId}"]`); + await frame.waitFor(`div[data-test="${selectedDbId}"]`), { visible: true }; + await frame.waitFor(CREATE_DELAY); + await frame.waitFor("div[class='rowData'] > span[class='message']"); + + const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", elements => { + return elements.some(el => el.textContent.includes("Successfully created")); + }); + + expect(didCreateContainer).toBe(true); + + await frame.waitFor(`div[data-test="${selectedDbId}"]`), { visible: true }; + await frame.waitFor(LOADING_STATE_DELAY); + + await clickDBMenu(selectedDbId, frame); + + const collections = await frame.$$( + `div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]` + ); + + if (collections.length) { + await frame.waitFor(`div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]`, { + visible: true + }); + + const textId = await frame.evaluate(element => { + return element.attributes["data-test"].textContent; + }, collections[0]); + await frame.waitFor(`div[data-test="${textId}"]`, { visible: true }); + // delete container + + // click context menu for container + await frame.waitFor(`div[data-test="${textId}"] > div > button`, { visible: true }); + await frame.click(`div[data-test="${textId}"] > div > button`); + + // click delete container + await frame.waitFor(RENDER_DELAY); + await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); + await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); + + // confirm delete container + await frame.waitFor('input[data-test="confirmCollectionId"]', { visible: true }); + await frame.type('input[data-test="confirmCollectionId"]', textId); + + // click delete + await frame.waitFor('input[data-test="deleteCollection"]', { visible: true }); + await frame.click('input[data-test="deleteCollection"]'); + await frame.waitFor(LOADING_STATE_DELAY); + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + + await expect(page).not.toMatchElement(`div[data-test="${textId}"]`); + } // click context menu for database - await frame.waitFor(`div[data-test="${dbId}"] > div > button`); - const button = await frame.$(`div[data-test="${dbId}"] > div > button`); + await frame.waitFor(`div[data-test="${selectedDbId}"] > div > button`); + await frame.waitFor(RENDER_DELAY); + const button = await frame.$(`div[data-test="${selectedDbId}"] > div > button`); await button.focus(); await button.asElement().click(); @@ -95,17 +128,40 @@ describe("Collection Add and Delete SQL spec", () => { // confirm delete database await frame.waitForSelector('input[data-test="confirmDatabaseId"]', { visible: true }); await frame.waitFor(RENDER_DELAY); - await frame.type('input[data-test="confirmDatabaseId"]', dbId.trim()); + await frame.type('input[data-test="confirmDatabaseId"]', selectedDbId); // click delete await frame.click('input[data-test="deleteDatabase"]'); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); - await expect(page).not.toMatchElement(`div[data-test="${dbId}"]`); + await frame.waitFor(LOADING_STATE_DELAY); + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + await expect(page).not.toMatchElement(`div[data-test="${selectedDbId}"]`); } catch (error) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const testName = (expect as any).getState().currentTestName; - await page.screenshot({ path: `Test Failed ${testName}.jpg` }); + await page.screenshot({ path: `failed-${testName}.jpg` }); throw error; } }); }); + +async function clickDBMenu(dbId: string, frame: Frame, retries = 0) { + const button = await frame.$(`div[data-test="${dbId}"]`); + await button.focus(); + const handler = await button.asElement(); + await handler.click(); + await ensureMenuIsOpen(dbId, frame, retries); + return button; +} + +async function ensureMenuIsOpen(dbId: string, frame: Frame, retries: number) { + await frame.waitFor(RETRY_DELAY); + const button = await frame.$(`div[data-test="${dbId}"]`); + const classList = await frame.evaluate(button => { + return button.parentElement.classList; + }, button); + if (!Object.values(classList).includes("selected") && retries < 5) { + retries = retries + 1; + await clickDBMenu(dbId, frame, retries); + } +} diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts new file mode 100644 index 000000000..0f1c07eef --- /dev/null +++ b/test/tables/container.spec.ts @@ -0,0 +1,111 @@ +import "expect-puppeteer"; +import { Frame } from "puppeteer"; +import { generateUniqueName, login } from "../utils/shared"; + +jest.setTimeout(300000); +const RETRY_DELAY = 5000; +const LOADING_STATE_DELAY = 2500; +const RENDER_DELAY = 1000; + +describe("Collection Add and Delete Tables spec", () => { + it("creates a collection", async () => { + try { + const tableId = generateUniqueName("tab"); + const frame = await login(process.env.TABLES_CONNECTION_STRING); + + // create new collection + await frame.waitFor('button[data-test="New Table"]', { visible: true }); + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + await frame.click('button[data-test="New Table"]'); + + // type database id + await frame.waitFor('input[data-test="addCollection-newDatabaseId"]'); + const dbInput = await frame.$('input[data-test="addCollection-newDatabaseId"]'); + await dbInput.press("Backspace"); + await dbInput.type(tableId); + + // click submit + await frame.waitFor("#submitBtnAddCollection"); + await frame.click("#submitBtnAddCollection"); + + // validate created + // open database menu + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + await frame.waitFor(LOADING_STATE_DELAY); + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + + await frame.waitFor(`div[data-test="TablesDB"]`), { visible: true }; + await frame.waitFor(LOADING_STATE_DELAY); + + const didCreateContainer = await frame.$$eval("div[class='rowData'] > span[class='message']", elements => { + return elements.some(el => el.textContent.includes("Successfully created")); + }); + + expect(didCreateContainer).toBe(true); + + await frame.waitFor(`div[data-test="TablesDB"]`), { visible: true }; + await frame.waitFor(LOADING_STATE_DELAY); + + await clickTablesMenu(frame); + + const collections = await frame.$$( + `div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]` + ); + const textId = await frame.evaluate(element => { + return element.attributes["data-test"].textContent; + }, collections[0]); + await frame.waitFor(`div[data-test="${textId}"]`, { visible: true }); + + // delete container + + // click context menu for container + await frame.waitFor(`div[data-test="${textId}"] > div > button`, { visible: true }); + await frame.click(`div[data-test="${textId}"] > div > button`); + + // click delete container + await frame.waitFor(RENDER_DELAY); + await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); + await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]'); + + // confirm delete container + await frame.waitFor('input[data-test="confirmCollectionId"]', { visible: true }); + await frame.type('input[data-test="confirmCollectionId"]', textId); + + // click delete + await frame.waitFor('input[data-test="deleteCollection"]', { visible: true }); + await frame.click('input[data-test="deleteCollection"]'); + await frame.waitFor(LOADING_STATE_DELAY); + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + await frame.waitFor(LOADING_STATE_DELAY); + await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); + + await expect(page).not.toMatchElement(`div[data-test="${textId}"]`); + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const testName = (expect as any).getState().currentTestName; + await page.screenshot({ path: `failed-${testName}.jpg` }); + throw error; + } + }); +}); + +async function clickTablesMenu(frame: Frame, retries = 0) { + const button = await frame.$(`div[data-test="TablesDB"]`); + await button.focus(); + const handler = await button.asElement(); + await handler.click(); + await ensureMenuIsOpen(frame, retries); + return button; +} + +async function ensureMenuIsOpen(frame: Frame, retries: number) { + await frame.waitFor(RETRY_DELAY); + const button = await frame.$(`div[data-test="TablesDB"]`); + const classList = await frame.evaluate(button => { + return button.parentElement.classList; + }, button); + if (!Object.values(classList).includes("selected") && retries < 5) { + retries = retries + 1; + await clickTablesMenu(frame, retries); + } +} diff --git a/test/utils/shared.ts b/test/utils/shared.ts index 458899700..f7c1f3faf 100644 --- a/test/utils/shared.ts +++ b/test/utils/shared.ts @@ -3,7 +3,7 @@ import { Frame } from "puppeteer"; export async function login(connectionString: string): Promise { const prodUrl = "https://localhost:1234/hostedExplorer.html"; - page.goto(prodUrl); + page.goto(prodUrl, { waitUntil: "networkidle2" }); // log in with connection string const handle = await page.waitForSelector("iframe"); @@ -16,6 +16,6 @@ export async function login(connectionString: string): Promise { return frame; } -export function generateUniqueName(baseName: string, length = 8): string { +export function generateUniqueName(baseName = "", length = 4): string { return `${baseName}${crypto.randomBytes(length).toString("hex")}`; } From e6ca1d25c942811d10cf86f6620b6decf9e5a43a Mon Sep 17 00:00:00 2001 From: Srinath Narayanan Date: Mon, 2 Nov 2020 13:19:45 -0800 Subject: [PATCH 03/13] Added index refresh to SQL API indexing policy editor (#306) * Index refresh component introduced - Made all notifications in Mongo Index editor have 12 font size - Added indexing policy refresh to sql indexing policy editor - Added "you have unsaved changes" message, replace old message for lazy indexing policy changes * formatting changes * addressed PR comments --- .../getIndexTransformationProgress.ts | 28 ++++++ .../dataAccess/readMongoDBCollection.tsx | 28 ------ .../Settings/SettingsComponent.test.tsx | 4 +- .../Controls/Settings/SettingsComponent.tsx | 22 ++--- .../Settings/SettingsRenderUtils.test.tsx | 4 +- .../Controls/Settings/SettingsRenderUtils.tsx | 20 ++--- .../IndexingPolicyComponent.test.tsx | 4 +- .../IndexingPolicyComponent.tsx | 18 +++- .../IndexingPolicyRefreshComponent.test.tsx | 15 ++++ .../IndexingPolicyRefreshComponent.tsx | 62 +++++++++++++ ...dexingPolicyRefreshComponent.test.tsx.snap | 24 ++++++ .../MongoIndexingPolicyComponent.test.tsx | 27 ++---- .../MongoIndexingPolicyComponent.tsx | 86 +++++-------------- ...MongoIndexingPolicyComponent.test.tsx.snap | 3 + .../IndexingPolicyComponent.test.tsx.snap | 3 + .../Controls/Settings/SettingsUtils.test.tsx | 10 ++- .../Controls/Settings/SettingsUtils.tsx | 4 + .../SettingsComponent.test.tsx.snap | 1 + .../SettingsRenderUtils.test.tsx.snap | 42 ++++++--- 19 files changed, 249 insertions(+), 156 deletions(-) create mode 100644 src/Common/dataAccess/getIndexTransformationProgress.ts create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.test.tsx create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.tsx create mode 100644 src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/__snapshots__/IndexingPolicyRefreshComponent.test.tsx.snap diff --git a/src/Common/dataAccess/getIndexTransformationProgress.ts b/src/Common/dataAccess/getIndexTransformationProgress.ts new file mode 100644 index 000000000..1c08fe847 --- /dev/null +++ b/src/Common/dataAccess/getIndexTransformationProgress.ts @@ -0,0 +1,28 @@ +import { client } from "../CosmosClient"; +import { handleError } from "../ErrorHandlingUtils"; +import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; +import * as Constants from "../Constants"; +import { AuthType } from "../../AuthType"; + +export async function getIndexTransformationProgress(databaseId: string, collectionId: string): Promise { + if (window.authType !== AuthType.AAD) { + return undefined; + } + let indexTransformationPercentage: number; + const clearMessage = logConsoleProgress(`Reading container ${collectionId}`); + try { + const response = await client() + .database(databaseId) + .container(collectionId) + .read({ populateQuotaInfo: true }); + + indexTransformationPercentage = parseInt( + response.headers[Constants.HttpHeaders.collectionIndexTransformationProgress] as string + ); + } catch (error) { + handleError(error, `Error while reading container ${collectionId}`, "ReadMongoDBCollection"); + throw error; + } + clearMessage(); + return indexTransformationPercentage; +} diff --git a/src/Common/dataAccess/readMongoDBCollection.tsx b/src/Common/dataAccess/readMongoDBCollection.tsx index 9d7123bda..80013cf94 100644 --- a/src/Common/dataAccess/readMongoDBCollection.tsx +++ b/src/Common/dataAccess/readMongoDBCollection.tsx @@ -2,8 +2,6 @@ import { userContext } from "../../UserContext"; import { getMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; import { MongoDBCollectionResource } from "../../Utils/arm/generatedClients/2020-04-01/types"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; -import * as Constants from "../Constants"; -import { client } from "../CosmosClient"; import { handleError } from "../ErrorHandlingUtils"; import { AuthType } from "../../AuthType"; @@ -30,29 +28,3 @@ export async function readMongoDBCollectionThroughRP( clearMessage(); return collection; } - -export async function getMongoDBCollectionIndexTransformationProgress( - databaseId: string, - collectionId: string -): Promise { - if (window.authType !== AuthType.AAD) { - return undefined; - } - let indexTransformationPercentage: number; - const clearMessage = logConsoleProgress(`Reading container ${collectionId}`); - try { - const response = await client() - .database(databaseId) - .container(collectionId) - .read({ populateQuotaInfo: true }); - - indexTransformationPercentage = parseInt( - response.headers[Constants.HttpHeaders.collectionIndexTransformationProgress] as string - ); - } catch (error) { - handleError(error, `Error while reading container ${collectionId}`, "ReadMongoDBCollection"); - throw error; - } - clearMessage(); - return indexTransformationPercentage; -} diff --git a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx index 9d862864c..d4a3b1ec1 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx @@ -8,8 +8,8 @@ import * as DataModels from "../../../Contracts/DataModels"; import ko from "knockout"; import { TtlType, isDirty } from "./SettingsUtils"; import Explorer from "../../Explorer"; -jest.mock("../../../Common/dataAccess/readMongoDBCollection", () => ({ - getMongoDBCollectionIndexTransformationProgress: jest.fn().mockReturnValue(undefined) +jest.mock("../../../Common/dataAccess/getIndexTransformationProgress", () => ({ + getIndexTransformationProgress: jest.fn().mockReturnValue(undefined) })); import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection"; jest.mock("../../../Common/dataAccess/updateCollection", () => ({ diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 2966e1cf8..878804b22 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -46,10 +46,8 @@ import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric import "./SettingsComponent.less"; import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent"; import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types"; -import { - getMongoDBCollectionIndexTransformationProgress, - readMongoDBCollectionThroughRP -} from "../../../Common/dataAccess/readMongoDBCollection"; +import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection"; +import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress"; import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; interface SettingsV2TabInfo { @@ -212,6 +210,7 @@ export class SettingsComponent extends React.Component => { - const currentProgress = await getMongoDBCollectionIndexTransformationProgress( - this.collection.databaseId, - this.collection.id() - ); + const currentProgress = await getIndexTransformationProgress(this.collection.databaseId, this.collection.id()); this.setState({ indexTransformationProgress: currentProgress }); }; @@ -352,6 +346,7 @@ export class SettingsComponent extends React.Component ); -export const indexingPolicyTTLWarningMessage: JSX.Element = ( +export const indexingPolicynUnsavedWarningMessage: JSX.Element = ( - Changing the Indexing Policy impacts query results while the index transformation occurs. When a change is made and - the indexing mode is set to consistent or lazy, queries return eventual results until the operation completes. For - more information see,{" "} - - Modifying Indexing Policies - - . + You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes. ); @@ -410,8 +404,8 @@ export const mongoIndexingPolicyAADError: JSX.Element = ( export const mongoIndexTransformationRefreshingMessage: JSX.Element = ( - Refreshing index transformation progress - + Refreshing index transformation progress + ); @@ -421,14 +415,14 @@ export const renderMongoIndexTransformationRefreshMessage = ( ): JSX.Element => { if (progress === 0) { return ( - + {"You can make more indexing changes once the current index transformation is complete. "} {"Refresh to check if it has completed."} ); } else { return ( - + {`You can make more indexing changes once the current index transformation has completed. It is ${progress}% complete. `} {"Refresh to check the progress."} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.test.tsx index 38d192f6c..a232c6ee4 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.test.tsx @@ -25,7 +25,9 @@ describe("IndexingPolicyComponent", () => { }, onIndexingPolicyDirtyChange: () => { return; - } + }, + indexTransformationProgress: undefined, + refreshIndexTransformationProgress: () => new Promise(jest.fn()) }; it("renders", () => { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx index f1dede13a..992a92b47 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx @@ -1,9 +1,10 @@ import * as React from "react"; import * as DataModels from "../../../../Contracts/DataModels"; import * as monaco from "monaco-editor"; -import { isDirty } from "../SettingsUtils"; +import { isDirty, isIndexTransforming } from "../SettingsUtils"; import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react"; -import { indexingPolicyTTLWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils"; +import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils"; +import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent"; export interface IndexingPolicyComponentProps { shouldDiscardIndexingPolicy: boolean; @@ -12,6 +13,8 @@ export interface IndexingPolicyComponentProps { indexingPolicyContentBaseline: DataModels.IndexingPolicy; onIndexingPolicyContentChange: (newIndexingPolicy: DataModels.IndexingPolicy) => void; logIndexingPolicySuccessMessage: () => void; + indexTransformationProgress: number; + refreshIndexTransformationProgress: () => Promise; onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void; } @@ -51,6 +54,9 @@ export class IndexingPolicyComponent extends React.Component< if (!this.indexingPolicyEditor) { this.createIndexingPolicyEditor(); } else { + this.indexingPolicyEditor.updateOptions({ + readOnly: isIndexTransforming(this.props.indexTransformationProgress) + }); const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel(); const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4); indexingPolicyEditorModel.setValue(value); @@ -84,7 +90,7 @@ export class IndexingPolicyComponent extends React.Component< this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, { value: value, language: "json", - readOnly: false, + readOnly: isIndexTransforming(this.props.indexTransformationProgress), ariaLabel: "Indexing Policy" }); if (this.indexingPolicyEditor) { @@ -108,8 +114,12 @@ export class IndexingPolicyComponent extends React.Component< public render(): JSX.Element { return ( + {isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && ( - {indexingPolicyTTLWarningMessage} + {indexingPolicynUnsavedWarningMessage} )}
diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.test.tsx new file mode 100644 index 000000000..2e0da86de --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.test.tsx @@ -0,0 +1,15 @@ +import { shallow } from "enzyme"; +import React from "react"; +import { IndexingPolicyRefreshComponentProps, IndexingPolicyRefreshComponent } from "./IndexingPolicyRefreshComponent"; + +describe("IndexingPolicyRefreshComponent", () => { + it("renders", () => { + const props: IndexingPolicyRefreshComponentProps = { + indexTransformationProgress: 90, + refreshIndexTransformationProgress: () => new Promise(jest.fn()) + }; + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.tsx new file mode 100644 index 000000000..0f497fc22 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/IndexingPolicyRefreshComponent.tsx @@ -0,0 +1,62 @@ +import * as React from "react"; +import { MessageBar, MessageBarType } from "office-ui-fabric-react"; +import { + mongoIndexTransformationRefreshingMessage, + renderMongoIndexTransformationRefreshMessage +} from "../../SettingsRenderUtils"; +import { handleError } from "../../../../../Common/ErrorHandlingUtils"; +import { isIndexTransforming } from "../../SettingsUtils"; + +export interface IndexingPolicyRefreshComponentProps { + indexTransformationProgress: number; + refreshIndexTransformationProgress: () => Promise; +} + +interface IndexingPolicyRefreshComponentState { + isRefreshing: boolean; +} + +export class IndexingPolicyRefreshComponent extends React.Component< + IndexingPolicyRefreshComponentProps, + IndexingPolicyRefreshComponentState +> { + constructor(props: IndexingPolicyRefreshComponentProps) { + super(props); + this.state = { + isRefreshing: false + }; + } + + private onClickRefreshIndexingTransformationLink = async () => await this.refreshIndexTransformationProgress(); + + private renderIndexTransformationWarning = (): JSX.Element => { + if (this.state.isRefreshing) { + return mongoIndexTransformationRefreshingMessage; + } else if (isIndexTransforming(this.props.indexTransformationProgress)) { + return renderMongoIndexTransformationRefreshMessage( + this.props.indexTransformationProgress, + this.onClickRefreshIndexingTransformationLink + ); + } + return undefined; + }; + + private refreshIndexTransformationProgress = async () => { + this.setState({ isRefreshing: true }); + try { + await this.props.refreshIndexTransformationProgress(); + } catch (error) { + handleError(error, "Refreshing index transformation progress failed.", "RefreshIndexTransformationProgress"); + } finally { + this.setState({ isRefreshing: false }); + } + }; + + public render(): JSX.Element { + return this.renderIndexTransformationWarning() ? ( + {this.renderIndexTransformationWarning()} + ) : ( + <> + ); + } +} diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/__snapshots__/IndexingPolicyRefreshComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/__snapshots__/IndexingPolicyRefreshComponent.test.tsx.snap new file mode 100644 index 000000000..1c44dd435 --- /dev/null +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyRefresh/__snapshots__/IndexingPolicyRefreshComponent.test.tsx.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IndexingPolicyRefreshComponent renders 1`] = ` + + + You can make more indexing changes once the current index transformation has completed. It is 90% complete. + + Refresh to check the progress. + + + +`; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.test.tsx index 246bc8186..95ca5eef1 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.test.tsx @@ -2,6 +2,7 @@ import { shallow } from "enzyme"; import React from "react"; import { MongoIndexTypes, MongoNotificationMessage, MongoNotificationType } from "../../SettingsUtils"; import { MongoIndexingPolicyComponent, MongoIndexingPolicyComponentProps } from "./MongoIndexingPolicyComponent"; +import { renderToString } from "react-dom/server"; describe("MongoIndexingPolicyComponent", () => { const baseProps: MongoIndexingPolicyComponentProps = { @@ -21,10 +22,7 @@ describe("MongoIndexingPolicyComponent", () => { return; }, indexTransformationProgress: undefined, - refreshIndexTransformationProgress: () => - new Promise(() => { - return; - }), + refreshIndexTransformationProgress: () => new Promise(jest.fn()), onMongoIndexingPolicySaveableChange: () => { return; }, @@ -38,16 +36,6 @@ describe("MongoIndexingPolicyComponent", () => { expect(wrapper).toMatchSnapshot(); }); - it("isIndexingTransforming", () => { - const wrapper = shallow(); - const mongoIndexingPolicyComponent = wrapper.instance() as MongoIndexingPolicyComponent; - expect(mongoIndexingPolicyComponent.isIndexingTransforming()).toEqual(false); - wrapper.setProps({ indexTransformationProgress: 50 }); - expect(mongoIndexingPolicyComponent.isIndexingTransforming()).toEqual(true); - wrapper.setProps({ indexTransformationProgress: 100 }); - expect(mongoIndexingPolicyComponent.isIndexingTransforming()).toEqual(false); - }); - describe("AddMongoIndexProps test", () => { const wrapper = shallow(); const mongoIndexingPolicyComponent = wrapper.instance() as MongoIndexingPolicyComponent; @@ -55,7 +43,7 @@ describe("MongoIndexingPolicyComponent", () => { it("defaults", () => { expect(mongoIndexingPolicyComponent.isMongoIndexingPolicySaveable()).toEqual(false); expect(mongoIndexingPolicyComponent.isMongoIndexingPolicyDiscardable()).toEqual(false); - expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toEqual(undefined); + expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toBeUndefined(); }); const sampleWarning = "sampleWarning"; @@ -113,9 +101,12 @@ describe("MongoIndexingPolicyComponent", () => { expect(mongoIndexingPolicyComponent.isMongoIndexingPolicyDiscardable()).toEqual( isMongoIndexingPolicyDiscardable ); - expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toEqual( - mongoWarningNotificationMessage - ); + if (mongoWarningNotificationMessage) { + const elementAsString = renderToString(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()); + expect(elementAsString).toContain(mongoWarningNotificationMessage); + } else { + expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toBeUndefined(); + } } ); }); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx index cc9c25794..435cec265 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent.tsx @@ -25,8 +25,8 @@ import { createAndAddMongoIndexStackProps, separatorStyles, mongoIndexingPolicyAADError, - mongoIndexTransformationRefreshingMessage, - renderMongoIndexTransformationRefreshMessage + indexingPolicynUnsavedWarningMessage, + infoAndToolTipTextStyle } from "../../SettingsRenderUtils"; import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types"; import { @@ -35,12 +35,13 @@ import { MongoIndexIdField, MongoNotificationType, getMongoIndexType, - getMongoIndexTypeText + getMongoIndexTypeText, + isIndexTransforming } from "../../SettingsUtils"; import { AddMongoIndexComponent } from "./AddMongoIndexComponent"; import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent"; -import { handleError } from "../../../../../Common/ErrorHandlingUtils"; import { AuthType } from "../../../../../AuthType"; +import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent"; export interface MongoIndexingPolicyComponentProps { mongoIndexes: MongoIndex[]; @@ -56,20 +57,13 @@ export interface MongoIndexingPolicyComponentProps { onMongoIndexingPolicyDiscardableChange: (isMongoIndexingPolicyDiscardable: boolean) => void; } -interface MongoIndexingPolicyComponentState { - isRefreshingIndexTransformationProgress: boolean; -} - interface MongoIndexDisplayProps { definition: JSX.Element; type: JSX.Element; actionButton: JSX.Element; } -export class MongoIndexingPolicyComponent extends React.Component< - MongoIndexingPolicyComponentProps, - MongoIndexingPolicyComponentState -> { +export class MongoIndexingPolicyComponent extends React.Component { private shouldCheckComponentIsDirty = true; private addMongoIndexComponentRefs: React.RefObject[] = []; private initialIndexesColumns: IColumn[] = [ @@ -98,13 +92,6 @@ export class MongoIndexingPolicyComponent extends React.Component< } ]; - constructor(props: MongoIndexingPolicyComponentProps) { - super(props); - this.state = { - isRefreshingIndexTransformationProgress: false - }; - } - componentDidUpdate(prevProps: MongoIndexingPolicyComponentProps): void { if (this.props.indexesToAdd.length > prevProps.indexesToAdd.length) { this.addMongoIndexComponentRefs[prevProps.indexesToAdd.length]?.current?.focus(); @@ -144,10 +131,15 @@ export class MongoIndexingPolicyComponent extends React.Component< return this.props.indexesToAdd.length > 0 || this.props.indexesToDrop.length > 0; }; - public getMongoWarningNotificationMessage = (): string => { - return this.props.indexesToAdd.find( + public getMongoWarningNotificationMessage = (): JSX.Element => { + const warningMessage = this.props.indexesToAdd.find( addMongoIndexProps => addMongoIndexProps.notification?.type === MongoNotificationType.Warning )?.notification.message; + + if (warningMessage) { + return {warningMessage}; + } + return undefined; }; private onRenderRow = (props: IDetailsRowProps): JSX.Element => { @@ -159,7 +151,7 @@ export class MongoIndexingPolicyComponent extends React.Component< { this.props.onIndexDrop(arrayPosition); }} @@ -230,7 +222,7 @@ export class MongoIndexingPolicyComponent extends React.Component< { - this.setState({ isRefreshingIndexTransformationProgress: true }); - try { - await this.props.refreshIndexTransformationProgress(); - } catch (error) { - handleError(error, "Refreshing index transformation progress failed.", "RefreshIndexTransformationProgress"); - } finally { - this.setState({ isRefreshingIndexTransformationProgress: false }); - } - }; - - public isIndexingTransforming = (): boolean => - // index transformation progress can be 0 - this.props.indexTransformationProgress !== undefined && this.props.indexTransformationProgress !== 100; - - private onClickRefreshIndexingTransformationLink = async () => await this.refreshIndexTransformationProgress(); - - private renderIndexTransformationWarning = (): JSX.Element => { - if (this.state.isRefreshingIndexTransformationProgress) { - return mongoIndexTransformationRefreshingMessage; - } else if (this.isIndexingTransforming()) { - return renderMongoIndexTransformationRefreshMessage( - this.props.indexTransformationProgress, - this.onClickRefreshIndexingTransformationLink - ); - } - return undefined; - }; - private renderWarningMessage = (): JSX.Element => { - let warningMessage: string; + let warningMessage: JSX.Element; if (this.getMongoWarningNotificationMessage()) { warningMessage = this.getMongoWarningNotificationMessage(); } else if (this.isMongoIndexingPolicySaveable()) { - warningMessage = - "You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes."; + warningMessage = indexingPolicynUnsavedWarningMessage; } return ( <> - {this.renderIndexTransformationWarning() && ( - {this.renderIndexTransformationWarning()} - )} - - {warningMessage && ( - - {warningMessage} - - )} + + {warningMessage && {warningMessage}} ); }; diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/__snapshots__/MongoIndexingPolicyComponent.test.tsx.snap b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/__snapshots__/MongoIndexingPolicyComponent.test.tsx.snap index ab19cbb45..577fa2862 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/__snapshots__/MongoIndexingPolicyComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/MongoIndexingPolicy/__snapshots__/MongoIndexingPolicyComponent.test.tsx.snap @@ -8,6 +8,9 @@ exports[`MongoIndexingPolicyComponent renders 1`] = ` } } > + For queries that filter on multiple properties, create multiple single field indexes instead of a compound index. +
{ expect(notification.type).toEqual(MongoNotificationType.Error); }); }); + +it("isIndexingTransforming", () => { + expect(isIndexTransforming(undefined)).toBeFalsy(); + expect(isIndexTransforming(0)).toBeTruthy(); + expect(isIndexTransforming(90)).toBeTruthy(); + expect(isIndexTransforming(100)).toBeFalsy(); +}); diff --git a/src/Explorer/Controls/Settings/SettingsUtils.tsx b/src/Explorer/Controls/Settings/SettingsUtils.tsx index 556df274f..9e29b73fe 100644 --- a/src/Explorer/Controls/Settings/SettingsUtils.tsx +++ b/src/Explorer/Controls/Settings/SettingsUtils.tsx @@ -250,3 +250,7 @@ export const getMongoIndexTypeText = (index: MongoIndexTypes): string => { } return WildcardText; }; + +export const isIndexTransforming = (indexTransformationProgress: number): boolean => + // index transformation progress can be 0 + indexTransformationProgress !== undefined && indexTransformationProgress !== 100; diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 1a0a9747b..db28fa5a8 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -5189,6 +5189,7 @@ exports[`SettingsComponent renders 1`] = ` logIndexingPolicySuccessMessage={[Function]} onIndexingPolicyContentChange={[Function]} onIndexingPolicyDirtyChange={[Function]} + refreshIndexTransformationProgress={[Function]} resetShouldDiscardIndexingPolicy={[Function]} shouldDiscardIndexingPolicy={false} /> diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap index 77b4fc444..90e5c8af8 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsRenderUtils.test.tsx.snap @@ -166,15 +166,7 @@ exports[`SettingsUtils functions render 1`] = ` } } > - Changing the Indexing Policy impacts query results while the index transformation occurs. When a change is made and the indexing mode is set to consistent or lazy, queries return eventual results until the operation completes. For more information see, - - - Modifying Indexing Policies - - . + You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes. - + Refreshing index transformation progress - + You can make more indexing changes once the current index transformation is complete. - + You can make more indexing changes once the current index transformation has completed. It is 90% complete. Date: Mon, 2 Nov 2020 16:59:08 -0800 Subject: [PATCH 04/13] Poll on Location header for operation status (#309) --- src/Utils/arm/request.test.ts | 10 +++++----- src/Utils/arm/request.ts | 36 ++++++++--------------------------- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/src/Utils/arm/request.test.ts b/src/Utils/arm/request.test.ts index 69f0be223..89b6d488a 100644 --- a/src/Utils/arm/request.test.ts +++ b/src/Utils/arm/request.test.ts @@ -21,13 +21,12 @@ describe("ARM request", () => { it("should poll for async operations", async () => { const headers = new Headers(); - headers.set("azure-asyncoperation", "https://foo.com/operationStatus"); + headers.set("location", "https://foo.com/operationStatus"); window.fetch = jest.fn().mockResolvedValue({ ok: true, headers, - json: async () => { - return { status: "Succeeded" }; - } + status: 200, + json: async () => ({}) }); await armRequest({ apiVersion: "2001-01-01", host: "https://foo.com", path: "foo", method: "GET" }); expect(window.fetch).toHaveBeenCalledTimes(2); @@ -35,10 +34,11 @@ describe("ARM request", () => { it("should throw for failed async operations", async () => { const headers = new Headers(); - headers.set("azure-asyncoperation", "https://foo.com/operationStatus"); + headers.set("location", "https://foo.com/operationStatus"); window.fetch = jest.fn().mockResolvedValue({ ok: true, headers, + status: 200, json: async () => { return { status: "Failed" }; } diff --git a/src/Utils/arm/request.ts b/src/Utils/arm/request.ts index d41803e37..e40e38b8f 100644 --- a/src/Utils/arm/request.ts +++ b/src/Utils/arm/request.ts @@ -70,55 +70,35 @@ export async function armRequest({ host, path, apiVersion, method, body: requ throw error; } - const operationStatusUrl = response.headers && response.headers.get("azure-asyncoperation"); + const operationStatusUrl = response.headers && response.headers.get("location"); if (operationStatusUrl) { - await promiseRetry(() => getOperationStatus(operationStatusUrl)); - // TODO: ARM is supposed to return a resourceLocation property, but it does not https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#target-resource-location - // When Cosmos RP adds resourceLocation, we should use it instead - // For now manually execute a GET if the operation was a mutation and not a deletion - if (method === "POST" || method === "PATCH" || method === "PUT") { - return armRequest({ - host, - path, - apiVersion, - method: "GET" - }); - } + return await promiseRetry(() => getOperationStatus(operationStatusUrl)); } const responseBody = (await response.json()) as T; return responseBody; } -const SUCCEEDED = "Succeeded" as const; -const FAILED = "Failed" as const; -const CANCELED = "Canceled" as const; - -type Status = typeof SUCCEEDED | typeof FAILED | typeof CANCELED; - -interface OperationResponse { - status: Status; - error: unknown; -} - async function getOperationStatus(operationStatusUrl: string) { const response = await window.fetch(operationStatusUrl, { headers: { Authorization: userContext.authorizationToken } }); + if (!response.ok) { const errorResponse = (await response.json()) as ErrorResponse; const error = new Error(errorResponse.message) as ARMError; error.code = errorResponse.code; throw new AbortError(error); } - const body = (await response.json()) as OperationResponse; + + const body = await response.json(); const status = body.status; - if (status === SUCCEEDED) { - return; + if (!status && response.status === 200) { + return body; } - if (status === CANCELED || status === FAILED) { + if (status === "Canceled" || status === "Failed") { const errorMessage = body.error ? JSON.stringify(body.error) : "Operation could not be completed"; const error = new Error(errorMessage); throw new AbortError(error); From a009a8ba5f1e0a4b96e3a2f0b102c0b91667968f Mon Sep 17 00:00:00 2001 From: Zachary Foster Date: Tue, 3 Nov 2020 14:05:54 -0500 Subject: [PATCH 05/13] Adds e2e readme and new endpoint env var (#314) --- .env.example | 7 +++++-- .github/workflows/ci.yml | 1 + README.md | 15 +++++++++------ test/utils/shared.ts | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index b37aeff49..fdf920188 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,10 @@ -# These options are only needed when if running end to end tests locally PORTAL_RUNNER_USERNAME= PORTAL_RUNNER_PASSWORD= PORTAL_RUNNER_SUBSCRIPTION= PORTAL_RUNNER_RESOURCE_GROUP= PORTAL_RUNNER_DATABASE_ACCOUNT= -PORTAL_RUNNER_CONNECTION_STRING= \ No newline at end of file +PORTAL_RUNNER_CONNECTION_STRING= +CASSANDRA_CONNECTION_STRING= +MONGO_CONNECTION_STRING= +TABLES_CONNECTION_STRING= +DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53a39150a..b04588330 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,6 +151,7 @@ jobs: MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }} CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }} TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }} + DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html" - uses: actions/upload-artifact@v2 with: name: screenshots diff --git a/README.md b/README.md index 791985365..f9a05488f 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ To run pure hosted mode, in `webpack.config.js` change index HtmlWebpackPlugin t ### Emulator Development -In a window environment, running `npm run build` will automatically copy the built files from `/dist` over to the default emulator install paths. In a non-windows enironment you can specify an alternate endpoint using `EMULATOR_ENDPOINT` and webpack dev server will proxy requests for you. +In a window environment, running `npm run build` will automatically copy the built files from `/dist` over to the default emulator install paths. In a non-windows environment you can specify an alternate endpoint using `EMULATOR_ENDPOINT` and webpack dev server will proxy requests for you. `PLATFORM=Emulator EMULATOR_ENDPOINT=https://my-vm.azure.com:8081 npm run watch` @@ -60,7 +60,7 @@ The Cosmos Portal that consumes this repo is not currently open source. If you h You can however load a local running instance of data explorer in the production portal. 1. Turn off browser SSL validation for localhost: chrome://flags/#allow-insecure-localhost OR Install valid SSL certs for localhost (on IE, follow these [instructions](https://www.technipages.com/ie-bypass-problem-with-this-websites-security-certificate) to install the localhost certificate in the right place) -2. Whitelist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal +2. Allowlist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal 3. Start the project in portal mode: `PLATFORM=Portal npm run watch` 4. Load the portal using the following link: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html @@ -84,16 +84,19 @@ Unit tests are located adjacent to the code under test and run with [Jest](https 4. Install dependencies: `npm install` 5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`) -#### End to End Production Runners +#### End to End Production Tests Jest and Puppeteer are used for end to end production runners and are contained in `test/`. To run these tests locally: -1. Copy .env.example to .env and fill in all variables -2. Run `npm run test:e2e` +1. Copy .env.example to .env +2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values) +3. Make sure all packages are installed `npm install` +4. Run the server `npm run start` and wait for it to start +5. Run `npm run test:e2e` ### Releasing -We generally adhear to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details. +We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details. # Contributing diff --git a/test/utils/shared.ts b/test/utils/shared.ts index f7c1f3faf..352d14a6c 100644 --- a/test/utils/shared.ts +++ b/test/utils/shared.ts @@ -2,7 +2,7 @@ import crypto from "crypto"; import { Frame } from "puppeteer"; export async function login(connectionString: string): Promise { - const prodUrl = "https://localhost:1234/hostedExplorer.html"; + const prodUrl = process.env.DATA_EXPLORER_ENDPOINT; page.goto(prodUrl, { waitUntil: "networkidle2" }); // log in with connection string From 5f1f7a826654ca2d7081e4d52c5e954eb1f7e29a Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Tue, 3 Nov 2020 13:40:44 -0800 Subject: [PATCH 06/13] Refactor error handling part 2 (#313) --- src/Common/DocumentClientUtilityBase.ts | 16 +-- src/Common/ErrorHandlingUtils.ts | 20 +-- src/Common/Logger.ts | 6 +- src/Common/QueriesClient.ts | 15 +-- src/Common/dataAccess/createCollection.ts | 2 +- src/Common/dataAccess/createDatabase.ts | 2 +- .../dataAccess/createStoredProcedure.ts | 2 +- src/Common/dataAccess/createTrigger.ts | 2 +- .../dataAccess/createUserDefinedFunction.ts | 4 +- src/Common/dataAccess/deleteCollection.ts | 2 +- src/Common/dataAccess/deleteDatabase.ts | 2 +- .../dataAccess/deleteStoredProcedure.ts | 2 +- src/Common/dataAccess/deleteTrigger.ts | 2 +- .../dataAccess/deleteUserDefinedFunction.ts | 2 +- .../getIndexTransformationProgress.ts | 2 +- src/Common/dataAccess/readCollection.ts | 2 +- src/Common/dataAccess/readCollectionOffer.ts | 2 +- .../dataAccess/readCollectionQuotaInfo.ts | 2 +- src/Common/dataAccess/readCollections.ts | 2 +- src/Common/dataAccess/readDatabaseOffer.ts | 2 +- src/Common/dataAccess/readDatabases.ts | 2 +- .../dataAccess/readMongoDBCollection.tsx | 2 +- src/Common/dataAccess/readOffers.ts | 2 +- src/Common/dataAccess/readStoredProcedures.ts | 2 +- src/Common/dataAccess/readTriggers.ts | 2 +- .../dataAccess/readUserDefinedFunctions.ts | 4 +- src/Common/dataAccess/updateCollection.ts | 2 +- src/Common/dataAccess/updateOffer.ts | 2 +- .../updateOfferThroughputBeyondLimit.ts | 4 +- .../dataAccess/updateStoredProcedure.ts | 2 +- src/Common/dataAccess/updateTrigger.ts | 2 +- .../dataAccess/updateUserDefinedFunction.ts | 4 +- src/Contracts/Diagnostics.ts | 2 +- .../Controls/Arcadia/ArcadiaMenuPicker.tsx | 9 +- .../Notebook/NotebookTerminalComponent.tsx | 11 +- .../CodeOfConductComponent.tsx | 7 +- .../GalleryViewerComponent.tsx | 20 +-- .../NotebookViewerComponent.tsx | 5 +- .../IndexingPolicyRefreshComponent.tsx | 2 +- src/Explorer/Explorer.ts | 53 ++++---- .../GremlinClient.test.ts | 6 +- .../GraphExplorerComponent/GremlinClient.ts | 32 ++--- .../Notebook/NotebookContainerClient.ts | 7 +- src/Explorer/Notebook/NotebookManager.ts | 3 +- src/Explorer/Panes/CopyNotebookPane.tsx | 14 +-- src/Explorer/Panes/GitHubReposPane.ts | 24 +--- .../Panes/PublishNotebookPaneAdapter.tsx | 24 ++-- src/Explorer/Tables/TableDataClient.ts | 114 +++++------------- src/GitHub/GitHubClient.ts | 11 +- src/GitHub/GitHubContentProvider.ts | 18 +-- src/GitHub/GitHubOAuthService.ts | 6 +- src/HostedExplorer.ts | 8 +- .../NotebookWorkspaceManager.ts | 13 +- src/Platform/Hosted/ArmResourceUtils.ts | 11 +- .../ArcadiaResourceManager.ts | 10 +- src/Utils/AuthorizationUtils.ts | 3 +- src/Utils/GalleryUtils.ts | 26 ++-- src/Utils/NotebookConfigurationUtils.ts | 5 +- 58 files changed, 229 insertions(+), 336 deletions(-) diff --git a/src/Common/DocumentClientUtilityBase.ts b/src/Common/DocumentClientUtilityBase.ts index bc5e3ed33..ded90cf05 100644 --- a/src/Common/DocumentClientUtilityBase.ts +++ b/src/Common/DocumentClientUtilityBase.ts @@ -58,8 +58,8 @@ export function executeStoredProcedure( (error: any) => { handleError( error, - `Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`, - "ExecuteStoredProcedure" + "ExecuteStoredProcedure", + `Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}` ); deferred.reject(error); } @@ -88,7 +88,7 @@ export function queryDocumentsPage( deferred.resolve(result); }, (error: any) => { - handleError(error, `Failed to query ${entityName} for container ${resourceName}`, "QueryDocumentsPage"); + handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`); deferred.reject(error); } ) @@ -109,7 +109,7 @@ export function readDocument(collection: ViewModels.CollectionBase, documentId: deferred.resolve(document); }, (error: any) => { - handleError(error, `Failed to read ${entityName} ${documentId.id()}`, "ReadDocument"); + handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`); deferred.reject(error); } ) @@ -135,7 +135,7 @@ export function updateDocument( deferred.resolve(updatedDocument); }, (error: any) => { - handleError(error, `Failed to update ${entityName} ${documentId.id()}`, "UpdateDocument"); + handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`); deferred.reject(error); } ) @@ -157,7 +157,7 @@ export function createDocument(collection: ViewModels.CollectionBase, newDocumen deferred.resolve(savedDocument); }, (error: any) => { - handleError(error, `Error while creating new ${entityName} for container ${collection.id()}`, "CreateDocument"); + handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`); deferred.reject(error); } ) @@ -179,7 +179,7 @@ export function deleteDocument(collection: ViewModels.CollectionBase, documentId deferred.resolve(response); }, (error: any) => { - handleError(error, `Error while deleting ${entityName} ${documentId.id()}`, "DeleteDocument"); + handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`); deferred.reject(error); } ) @@ -205,7 +205,7 @@ export function deleteConflict( deferred.resolve(response); }, (error: any) => { - handleError(error, `Error while deleting conflict ${conflictId.id()}`, "DeleteConflict"); + handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`); deferred.reject(error); } ) diff --git a/src/Common/ErrorHandlingUtils.ts b/src/Common/ErrorHandlingUtils.ts index e5acc6b49..2e2f51365 100644 --- a/src/Common/ErrorHandlingUtils.ts +++ b/src/Common/ErrorHandlingUtils.ts @@ -1,3 +1,4 @@ +import { ARMError } from "../Utils/arm/request"; import { HttpStatusCodes } from "./Constants"; import { MessageTypes } from "../Contracts/ExplorerContracts"; import { SubscriptionType } from "../Contracts/ViewModels"; @@ -5,28 +6,27 @@ import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logError } from "./Logger"; import { sendMessage } from "./MessageHandler"; -export interface CosmosError { - code: number; - message?: string; -} - -export const handleError = (error: string | CosmosError, consoleErrorPrefix: string, area: string): void => { +export const handleError = (error: string | ARMError | Error, area: string, consoleErrorPrefix?: string): void => { const errorMessage = getErrorMessage(error); - const errorCode = typeof error === "string" ? undefined : error.code; + const errorCode = error instanceof ARMError ? error.code : undefined; + // logs error to data explorer console - logConsoleError(`${consoleErrorPrefix}:\n ${errorMessage}`); + const consoleErrorMessage = consoleErrorPrefix ? `${consoleErrorPrefix}:\n ${errorMessage}` : errorMessage; + logConsoleError(consoleErrorMessage); + // logs error to both app insight and kusto logError(errorMessage, area, errorCode); + // checks for errors caused by firewall and sends them to portal to handle sendNotificationForError(errorMessage, errorCode); }; -export const getErrorMessage = (error: string | CosmosError | Error): string => { +export const getErrorMessage = (error: string | Error): string => { const errorMessage = typeof error === "string" ? error : error.message; return replaceKnownError(errorMessage); }; -const sendNotificationForError = (errorMessage: string, errorCode: number): void => { +const sendNotificationForError = (errorMessage: string, errorCode: number | string): void => { if (errorCode === HttpStatusCodes.Forbidden) { if (errorMessage?.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) { return; diff --git a/src/Common/Logger.ts b/src/Common/Logger.ts index 39b3196c5..58186d21e 100644 --- a/src/Common/Logger.ts +++ b/src/Common/Logger.ts @@ -1,4 +1,3 @@ -import { CosmosError, getErrorMessage } from "./ErrorHandlingUtils"; import { sendMessage } from "./MessageHandler"; import { Diagnostics, MessageTypes } from "../Contracts/ExplorerContracts"; import { appInsights } from "../Shared/appInsights"; @@ -22,8 +21,7 @@ export function logWarning(message: string, area: string, code?: number): void { return _logEntry(entry); } -export function logError(error: string | CosmosError | Error, area: string, code?: number): void { - const errorMessage: string = getErrorMessage(error); +export function logError(errorMessage: string, area: string, code?: number | string): void { const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Error, errorMessage, area, code); return _logEntry(entry); } @@ -55,7 +53,7 @@ function _generateLogEntry( level: Diagnostics.LogEntryLevel, message: string, area: string, - code?: number + code?: number | string ): Diagnostics.LogEntry { return { timestamp: new Date().getUTCSeconds(), diff --git a/src/Common/QueriesClient.ts b/src/Common/QueriesClient.ts index 7a3106890..fb81d1c65 100644 --- a/src/Common/QueriesClient.ts +++ b/src/Common/QueriesClient.ts @@ -14,7 +14,6 @@ import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } fr import { createCollection } from "./dataAccess/createCollection"; import { handleError } from "./ErrorHandlingUtils"; import * as ErrorParserUtility from "./ErrorParserUtility"; -import * as Logger from "./Logger"; export class QueriesClient { private static readonly PartitionKey: DataModels.PartitionKey = { @@ -54,7 +53,7 @@ export class QueriesClient { return Promise.resolve(collection); }, (error: any) => { - handleError(error, "Failed to set up account for saving queries", "setupQueriesCollection"); + handleError(error, "setupQueriesCollection", "Failed to set up account for saving queries"); return Promise.reject(error); } ) @@ -105,11 +104,7 @@ export class QueriesClient { } else { errorMessage = parsedError.message; } - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to save query ${query.queryName}: ${errorMessage}` - ); - Logger.logError(JSON.stringify(parsedError), "saveQuery"); + handleError(errorMessage, "saveQuery", `Failed to save query ${query.queryName}`); return Promise.reject(errorMessage); } ) @@ -159,14 +154,14 @@ export class QueriesClient { return Promise.resolve(queries); }, (error: any) => { - handleError(error, "Failed to fetch saved queries", "getSavedQueries"); + handleError(error, "getSavedQueries", "Failed to fetch saved queries"); return Promise.reject(error); } ); }, (error: any) => { // should never get into this state but we handle this regardless - handleError(error, "Failed to fetch saved queries", "getSavedQueries"); + handleError(error, "getSavedQueries", "Failed to fetch saved queries"); return Promise.reject(error); } ) @@ -218,7 +213,7 @@ export class QueriesClient { return Promise.resolve(); }, (error: any) => { - handleError(error, `Failed to delete query ${query.queryName}`, "deleteQuery"); + handleError(error, "deleteQuery", `Failed to delete query ${query.queryName}`); return Promise.reject(error); } ) diff --git a/src/Common/dataAccess/createCollection.ts b/src/Common/dataAccess/createCollection.ts index 6209a4b86..97a0c2a62 100644 --- a/src/Common/dataAccess/createCollection.ts +++ b/src/Common/dataAccess/createCollection.ts @@ -55,7 +55,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams logConsoleInfo(`Successfully created container ${params.collectionId}`); return collection; } catch (error) { - handleError(error, `Error while creating container ${params.collectionId}`, "CreateCollection"); + handleError(error, "CreateCollection", `Error while creating container ${params.collectionId}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/createDatabase.ts b/src/Common/dataAccess/createDatabase.ts index 22162e071..6f555b324 100644 --- a/src/Common/dataAccess/createDatabase.ts +++ b/src/Common/dataAccess/createDatabase.ts @@ -41,7 +41,7 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P logConsoleInfo(`Successfully created database ${params.databaseId}`); return database; } catch (error) { - handleError(error, `Error while creating database ${params.databaseId}`, "CreateDatabase"); + handleError(error, "CreateDatabase", `Error while creating database ${params.databaseId}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/createStoredProcedure.ts b/src/Common/dataAccess/createStoredProcedure.ts index a2281c213..d8f344ea3 100644 --- a/src/Common/dataAccess/createStoredProcedure.ts +++ b/src/Common/dataAccess/createStoredProcedure.ts @@ -70,7 +70,7 @@ export async function createStoredProcedure( .scripts.storedProcedures.create(storedProcedure); return response?.resource; } catch (error) { - handleError(error, `Error while creating stored procedure ${storedProcedure.id}`, "CreateStoredProcedure"); + handleError(error, "CreateStoredProcedure", `Error while creating stored procedure ${storedProcedure.id}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/createTrigger.ts b/src/Common/dataAccess/createTrigger.ts index 179d0103e..a8c3f448f 100644 --- a/src/Common/dataAccess/createTrigger.ts +++ b/src/Common/dataAccess/createTrigger.ts @@ -65,7 +65,7 @@ export async function createTrigger( .scripts.triggers.create(trigger); return response.resource; } catch (error) { - handleError(error, `Error while creating trigger ${trigger.id}`, "CreateTrigger"); + handleError(error, "CreateTrigger", `Error while creating trigger ${trigger.id}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/createUserDefinedFunction.ts b/src/Common/dataAccess/createUserDefinedFunction.ts index 30ff8be3a..bda4f0654 100644 --- a/src/Common/dataAccess/createUserDefinedFunction.ts +++ b/src/Common/dataAccess/createUserDefinedFunction.ts @@ -72,8 +72,8 @@ export async function createUserDefinedFunction( } catch (error) { handleError( error, - `Error while creating user defined function ${userDefinedFunction.id}`, - "CreateUserupdateUserDefinedFunction" + "CreateUserupdateUserDefinedFunction", + `Error while creating user defined function ${userDefinedFunction.id}` ); throw error; } finally { diff --git a/src/Common/dataAccess/deleteCollection.ts b/src/Common/dataAccess/deleteCollection.ts index cbe91b70f..5a5c4fd7d 100644 --- a/src/Common/dataAccess/deleteCollection.ts +++ b/src/Common/dataAccess/deleteCollection.ts @@ -23,7 +23,7 @@ export async function deleteCollection(databaseId: string, collectionId: string) } logConsoleInfo(`Successfully deleted container ${collectionId}`); } catch (error) { - handleError(error, `Error while deleting container ${collectionId}`, "DeleteCollection"); + handleError(error, "DeleteCollection", `Error while deleting container ${collectionId}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/deleteDatabase.ts b/src/Common/dataAccess/deleteDatabase.ts index e48f4ceb4..40029c0d2 100644 --- a/src/Common/dataAccess/deleteDatabase.ts +++ b/src/Common/dataAccess/deleteDatabase.ts @@ -25,7 +25,7 @@ export async function deleteDatabase(databaseId: string): Promise { } logConsoleInfo(`Successfully deleted database ${databaseId}`); } catch (error) { - handleError(error, `Error while deleting database ${databaseId}`, "DeleteDatabase"); + handleError(error, "DeleteDatabase", `Error while deleting database ${databaseId}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/deleteStoredProcedure.ts b/src/Common/dataAccess/deleteStoredProcedure.ts index 317fb3167..fac47de33 100644 --- a/src/Common/dataAccess/deleteStoredProcedure.ts +++ b/src/Common/dataAccess/deleteStoredProcedure.ts @@ -34,7 +34,7 @@ export async function deleteStoredProcedure( .delete(); } } catch (error) { - handleError(error, `Error while deleting stored procedure ${storedProcedureId}`, "DeleteStoredProcedure"); + handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/deleteTrigger.ts b/src/Common/dataAccess/deleteTrigger.ts index 0ec6ef4e1..f8a5713db 100644 --- a/src/Common/dataAccess/deleteTrigger.ts +++ b/src/Common/dataAccess/deleteTrigger.ts @@ -30,7 +30,7 @@ export async function deleteTrigger(databaseId: string, collectionId: string, tr .delete(); } } catch (error) { - handleError(error, `Error while deleting trigger ${triggerId}`, "DeleteTrigger"); + handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/deleteUserDefinedFunction.ts b/src/Common/dataAccess/deleteUserDefinedFunction.ts index 83630169e..6160ac52c 100644 --- a/src/Common/dataAccess/deleteUserDefinedFunction.ts +++ b/src/Common/dataAccess/deleteUserDefinedFunction.ts @@ -30,7 +30,7 @@ export async function deleteUserDefinedFunction(databaseId: string, collectionId .delete(); } } catch (error) { - handleError(error, `Error while deleting user defined function ${id}`, "DeleteUserDefinedFunction"); + handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/getIndexTransformationProgress.ts b/src/Common/dataAccess/getIndexTransformationProgress.ts index 1c08fe847..fa0298fbc 100644 --- a/src/Common/dataAccess/getIndexTransformationProgress.ts +++ b/src/Common/dataAccess/getIndexTransformationProgress.ts @@ -20,7 +20,7 @@ export async function getIndexTransformationProgress(databaseId: string, collect response.headers[Constants.HttpHeaders.collectionIndexTransformationProgress] as string ); } catch (error) { - handleError(error, `Error while reading container ${collectionId}`, "ReadMongoDBCollection"); + handleError(error, "ReadMongoDBCollection", `Error while reading container ${collectionId}`); throw error; } clearMessage(); diff --git a/src/Common/dataAccess/readCollection.ts b/src/Common/dataAccess/readCollection.ts index 0b434ba03..ce886328d 100644 --- a/src/Common/dataAccess/readCollection.ts +++ b/src/Common/dataAccess/readCollection.ts @@ -13,7 +13,7 @@ export async function readCollection(databaseId: string, collectionId: string): .read(); collection = response.resource as DataModels.Collection; } catch (error) { - handleError(error, `Error while querying container ${collectionId}`, "ReadCollection"); + handleError(error, "ReadCollection", `Error while querying container ${collectionId}`); throw error; } clearMessage(); diff --git a/src/Common/dataAccess/readCollectionOffer.ts b/src/Common/dataAccess/readCollectionOffer.ts index ad4c8eef5..b98da68c6 100644 --- a/src/Common/dataAccess/readCollectionOffer.ts +++ b/src/Common/dataAccess/readCollectionOffer.ts @@ -56,7 +56,7 @@ export const readCollectionOffer = async ( } ); } catch (error) { - handleError(error, `Error while querying offer for collection ${params.collectionId}`, "ReadCollectionOffer"); + handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/readCollectionQuotaInfo.ts b/src/Common/dataAccess/readCollectionQuotaInfo.ts index 574913f06..81565f821 100644 --- a/src/Common/dataAccess/readCollectionQuotaInfo.ts +++ b/src/Common/dataAccess/readCollectionQuotaInfo.ts @@ -37,7 +37,7 @@ export const readCollectionQuotaInfo = async ( return quota; } catch (error) { - handleError(error, `Error while querying quota info for container ${collection.id}`, "ReadCollectionQuotaInfo"); + handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/readCollections.ts b/src/Common/dataAccess/readCollections.ts index 405232f49..a6b741d91 100644 --- a/src/Common/dataAccess/readCollections.ts +++ b/src/Common/dataAccess/readCollections.ts @@ -29,7 +29,7 @@ export async function readCollections(databaseId: string): Promise { databases = sdkResponse.resources as DataModels.Database[]; } } catch (error) { - handleError(error, `Error while querying databases`, "ReadDatabases"); + handleError(error, "ReadDatabases", `Error while querying databases`); throw error; } clearMessage(); diff --git a/src/Common/dataAccess/readMongoDBCollection.tsx b/src/Common/dataAccess/readMongoDBCollection.tsx index 80013cf94..b66f8dc9c 100644 --- a/src/Common/dataAccess/readMongoDBCollection.tsx +++ b/src/Common/dataAccess/readMongoDBCollection.tsx @@ -22,7 +22,7 @@ export async function readMongoDBCollectionThroughRP( const response = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId); collection = response.properties.resource; } catch (error) { - handleError(error, `Error while reading container ${collectionId}`, "ReadMongoDBCollection"); + handleError(error, "ReadMongoDBCollection", `Error while reading container ${collectionId}`); throw error; } clearMessage(); diff --git a/src/Common/dataAccess/readOffers.ts b/src/Common/dataAccess/readOffers.ts index 8fc743c9c..4afc452aa 100644 --- a/src/Common/dataAccess/readOffers.ts +++ b/src/Common/dataAccess/readOffers.ts @@ -17,7 +17,7 @@ export const readOffers = async (): Promise => { return []; } - handleError(error, `Error while querying offers`, "ReadOffers"); + handleError(error, "ReadOffers", `Error while querying offers`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/readStoredProcedures.ts b/src/Common/dataAccess/readStoredProcedures.ts index cb64654e6..75fb3111b 100644 --- a/src/Common/dataAccess/readStoredProcedures.ts +++ b/src/Common/dataAccess/readStoredProcedures.ts @@ -35,7 +35,7 @@ export async function readStoredProcedures( .fetchAll(); return response?.resources; } catch (error) { - handleError(error, `Failed to query stored procedures for container ${collectionId}`, "ReadStoredProcedures"); + handleError(error, "ReadStoredProcedures", `Failed to query stored procedures for container ${collectionId}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/readTriggers.ts b/src/Common/dataAccess/readTriggers.ts index d8b6f6469..85a96331f 100644 --- a/src/Common/dataAccess/readTriggers.ts +++ b/src/Common/dataAccess/readTriggers.ts @@ -35,7 +35,7 @@ export async function readTriggers( .fetchAll(); return response?.resources; } catch (error) { - handleError(error, `Failed to query triggers for container ${collectionId}`, "ReadTriggers"); + handleError(error, "ReadTriggers", `Failed to query triggers for container ${collectionId}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/readUserDefinedFunctions.ts b/src/Common/dataAccess/readUserDefinedFunctions.ts index 93d26f73a..f6990db7c 100644 --- a/src/Common/dataAccess/readUserDefinedFunctions.ts +++ b/src/Common/dataAccess/readUserDefinedFunctions.ts @@ -37,8 +37,8 @@ export async function readUserDefinedFunctions( } catch (error) { handleError( error, - `Failed to query user defined functions for container ${collectionId}`, - "ReadUserDefinedFunctions" + "ReadUserDefinedFunctions", + `Failed to query user defined functions for container ${collectionId}` ); throw error; } finally { diff --git a/src/Common/dataAccess/updateCollection.ts b/src/Common/dataAccess/updateCollection.ts index 3f317c069..3a45af694 100644 --- a/src/Common/dataAccess/updateCollection.ts +++ b/src/Common/dataAccess/updateCollection.ts @@ -59,7 +59,7 @@ export async function updateCollection( logConsoleInfo(`Successfully updated container ${collectionId}`); return collection; } catch (error) { - handleError(error, `Failed to update container ${collectionId}`, "UpdateCollection"); + handleError(error, "UpdateCollection", `Failed to update container ${collectionId}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/updateOffer.ts b/src/Common/dataAccess/updateOffer.ts index f7568abd1..24cd150c8 100644 --- a/src/Common/dataAccess/updateOffer.ts +++ b/src/Common/dataAccess/updateOffer.ts @@ -72,7 +72,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise => logConsoleInfo(`Successfully updated offer for ${offerResourceText}`); return updatedOffer; } catch (error) { - handleError(error, `Error updating offer for ${offerResourceText}`, "UpdateCollection"); + handleError(error, "UpdateCollection", `Error updating offer for ${offerResourceText}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts b/src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts index 1b713de85..93a8f8737 100644 --- a/src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts +++ b/src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts @@ -49,8 +49,8 @@ export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThrou const error = await response.json(); handleError( error, - `Failed to request an increase in throughput for ${request.throughput}`, - "updateOfferThroughputBeyondLimit" + "updateOfferThroughputBeyondLimit", + `Failed to request an increase in throughput for ${request.throughput}` ); clearMessage(); throw error; diff --git a/src/Common/dataAccess/updateStoredProcedure.ts b/src/Common/dataAccess/updateStoredProcedure.ts index 17c498d66..4ca900905 100644 --- a/src/Common/dataAccess/updateStoredProcedure.ts +++ b/src/Common/dataAccess/updateStoredProcedure.ts @@ -64,7 +64,7 @@ export async function updateStoredProcedure( .replace(storedProcedure); return response?.resource; } catch (error) { - handleError(error, `Error while updating stored procedure ${storedProcedure.id}`, "UpdateStoredProcedure"); + handleError(error, "UpdateStoredProcedure", `Error while updating stored procedure ${storedProcedure.id}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/updateTrigger.ts b/src/Common/dataAccess/updateTrigger.ts index 845e19274..49759bb2f 100644 --- a/src/Common/dataAccess/updateTrigger.ts +++ b/src/Common/dataAccess/updateTrigger.ts @@ -61,7 +61,7 @@ export async function updateTrigger( .replace(trigger); return response?.resource; } catch (error) { - handleError(error, `Error while updating trigger ${trigger.id}`, "UpdateTrigger"); + handleError(error, "UpdateTrigger", `Error while updating trigger ${trigger.id}`); throw error; } finally { clearMessage(); diff --git a/src/Common/dataAccess/updateUserDefinedFunction.ts b/src/Common/dataAccess/updateUserDefinedFunction.ts index 373cb1dcd..3e2ebab1b 100644 --- a/src/Common/dataAccess/updateUserDefinedFunction.ts +++ b/src/Common/dataAccess/updateUserDefinedFunction.ts @@ -66,8 +66,8 @@ export async function updateUserDefinedFunction( } catch (error) { handleError( error, - `Error while updating user defined function ${userDefinedFunction.id}`, - "UpdateUserupdateUserDefinedFunction" + "UpdateUserupdateUserDefinedFunction", + `Error while updating user defined function ${userDefinedFunction.id}` ); throw error; } finally { diff --git a/src/Contracts/Diagnostics.ts b/src/Contracts/Diagnostics.ts index 6a523bfd5..d6a54740a 100644 --- a/src/Contracts/Diagnostics.ts +++ b/src/Contracts/Diagnostics.ts @@ -46,7 +46,7 @@ export interface LogEntry { /** * The message code. */ - code?: number; + code?: number | string; /** * Any additional data to be logged. */ diff --git a/src/Explorer/Controls/Arcadia/ArcadiaMenuPicker.tsx b/src/Explorer/Controls/Arcadia/ArcadiaMenuPicker.tsx index c46dbebab..e4165421f 100644 --- a/src/Explorer/Controls/Arcadia/ArcadiaMenuPicker.tsx +++ b/src/Explorer/Controls/Arcadia/ArcadiaMenuPicker.tsx @@ -1,12 +1,9 @@ import * as React from "react"; import { ArcadiaWorkspace, SparkPool } from "../../../Contracts/DataModels"; import { DefaultButton, IButtonStyles } from "office-ui-fabric-react/lib/Button"; -import { - IContextualMenuItem, - IContextualMenuProps, - ContextualMenuItemType -} from "office-ui-fabric-react/lib/ContextualMenu"; +import { IContextualMenuItem, IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu"; import * as Logger from "../../../Common/Logger"; +import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; export interface ArcadiaMenuPickerProps { selectText?: string; @@ -47,7 +44,7 @@ export class ArcadiaMenuPicker extends React.Component ): string { if (!serverInfo.notebookServerEndpoint) { - const error = "Notebook server endpoint not defined. Terminal will fail to connect to jupyter server."; - Logger.logError(error, "NotebookTerminalComponent/createNotebookAppSrc"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); + handleError( + "Notebook server endpoint not defined. Terminal will fail to connect to jupyter server.", + "NotebookTerminalComponent/createNotebookAppSrc" + ); return ""; } diff --git a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx index 02dc407e0..90ddf7b09 100644 --- a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx +++ b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx @@ -1,9 +1,8 @@ import * as React from "react"; import { JunoClient } from "../../../Juno/JunoClient"; import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants"; -import * as Logger from "../../../Common/Logger"; -import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react"; +import { handleError } from "../../../Common/ErrorHandlingUtils"; export interface CodeOfConductComponentProps { junoClient: JunoClient; @@ -45,9 +44,7 @@ export class CodeOfConductComponent extends React.Component { - Logger.logError(error, "Explorer/getArcadiaToken"); + Logger.logError(getErrorMessage(error), "Explorer/getArcadiaToken"); resolve(undefined); } ); @@ -1551,7 +1551,7 @@ export default class Explorer { workspaceItems[i] = { ...workspace, sparkPools: sparkpools }; }, error => { - Logger.logError(error, "Explorer/this._arcadiaManager.listSparkPoolsAsync"); + Logger.logError(getErrorMessage(error), "Explorer/this._arcadiaManager.listSparkPoolsAsync"); } ); sparkPromises.push(promise); @@ -1559,7 +1559,7 @@ export default class Explorer { return Promise.all(sparkPromises).then(() => workspaceItems); } catch (error) { - handleError(error, "Get Arcadia workspaces failed", "Explorer/this._arcadiaManager.listWorkspacesAsync"); + handleError(error, "Explorer/this._arcadiaManager.listWorkspacesAsync", "Get Arcadia workspaces failed"); return Promise.resolve([]); } } @@ -1596,8 +1596,8 @@ export default class Explorer { this._isInitializingNotebooks = false; handleError( error, - `Failed to get notebook workspace connection info: ${getErrorMessage(error)}`, - "initNotebooks/getNotebookConnectionInfoAsync" + "initNotebooks/getNotebookConnectionInfoAsync", + `Failed to get notebook workspace connection info: ${getErrorMessage(error)}` ); throw error; } finally { @@ -1620,9 +1620,10 @@ export default class Explorer { public resetNotebookWorkspace() { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookClient) { - const error = "Attempt to reset notebook workspace, but notebook is not enabled"; - Logger.logError(error, "Explorer/resetNotebookWorkspace"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); + handleError( + "Attempt to reset notebook workspace, but notebook is not enabled", + "Explorer/resetNotebookWorkspace" + ); return; } const resetConfirmationDialogProps: DialogProps = { @@ -1647,7 +1648,7 @@ export default class Explorer { const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount?.id); return workspaces && workspaces.length > 0 && workspaces.some(workspace => workspace.name === "default"); } catch (error) { - Logger.logError(error, "Explorer/_containsDefaultNotebookWorkspace"); + Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace"); return false; } } @@ -1673,7 +1674,7 @@ export default class Explorer { await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default"); } } catch (error) { - handleError(error, "Failed to initialize notebook workspace", "Explorer/ensureNotebookWorkspaceRunning"); + handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace"); } finally { clearMessage && clearMessage(); } @@ -2221,8 +2222,7 @@ export default class Explorer { public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { const error = "Attempt to upload notebook, but notebook is not enabled"; - Logger.logError(error, "Explorer/uploadFile"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); + handleError(error, "Explorer/uploadFile"); throw new Error(error); } @@ -2403,8 +2403,7 @@ export default class Explorer { public renameNotebook(notebookFile: NotebookContentItem): Q.Promise { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { const error = "Attempt to rename notebook, but notebook is not enabled"; - Logger.logError(error, "Explorer/renameNotebook"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); + handleError(error, "Explorer/renameNotebook"); throw new Error(error); } @@ -2452,8 +2451,7 @@ export default class Explorer { public onCreateDirectory(parent: NotebookContentItem): Q.Promise { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { const error = "Attempt to create notebook directory, but notebook is not enabled"; - Logger.logError(error, "Explorer/onCreateDirectory"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); + handleError(error, "Explorer/onCreateDirectory"); throw new Error(error); } @@ -2474,8 +2472,7 @@ export default class Explorer { public readFile(notebookFile: NotebookContentItem): Promise { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { const error = "Attempt to read file, but notebook is not enabled"; - Logger.logError(error, "Explorer/downloadFile"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); + handleError(error, "Explorer/downloadFile"); throw new Error(error); } @@ -2485,8 +2482,7 @@ export default class Explorer { public downloadFile(notebookFile: NotebookContentItem): Promise { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { const error = "Attempt to download file, but notebook is not enabled"; - Logger.logError(error, "Explorer/downloadFile"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); + handleError(error, "Explorer/downloadFile"); throw new Error(error); } @@ -2567,7 +2563,7 @@ export default class Explorer { ); this.isNotebooksEnabledForAccount(isAccountInAllowedLocation); } catch (error) { - Logger.logError(error, "Explorer/isNotebooksEnabledForAccount"); + Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount"); this.isNotebooksEnabledForAccount(false); } } @@ -2596,7 +2592,7 @@ export default class Explorer { false; this.isSparkEnabledForAccount(isEnabled); } catch (error) { - Logger.logError(error, "Explorer/isSparkEnabledForAccount"); + Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount"); this.isSparkEnabledForAccount(false); } }; @@ -2621,7 +2617,7 @@ export default class Explorer { (featureStatus && featureStatus.properties && featureStatus.properties.state === "Registered") || false; return isEnabled; } catch (error) { - Logger.logError(error, "Explorer/isSparkEnabledForAccount"); + Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount"); return false; } }; @@ -2640,8 +2636,7 @@ export default class Explorer { public deleteNotebookFile(item: NotebookContentItem): Promise { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { const error = "Attempt to delete notebook file, but notebook is not enabled"; - Logger.logError(error, "Explorer/deleteNotebookFile"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); + handleError(error, "Explorer/deleteNotebookFile"); throw new Error(error); } @@ -2690,8 +2685,7 @@ export default class Explorer { public onNewNotebookClicked(parent?: NotebookContentItem): void { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { const error = "Attempt to create new notebook, but notebook is not enabled"; - Logger.logError(error, "Explorer/onNewNotebookClicked"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); + handleError(error, "Explorer/onNewNotebookClicked"); throw new Error(error); } @@ -2776,8 +2770,7 @@ export default class Explorer { public refreshContentItem(item: NotebookContentItem): Promise { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { const error = "Attempt to refresh notebook list, but notebook is not enabled"; - Logger.logError(error, "Explorer/refreshContentItem"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); + handleError(error, "Explorer/refreshContentItem"); return Promise.reject(new Error(error)); } @@ -2963,7 +2956,7 @@ export default class Explorer { } return tokenRefreshInterval; } catch (error) { - Logger.logError(error, "Explorer/getTokenRefreshInterval"); + Logger.logError(getErrorMessage(error), "Explorer/getTokenRefreshInterval"); return tokenRefreshInterval; } } diff --git a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts index f5256b9d5..5947c5439 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts @@ -94,7 +94,7 @@ describe("Gremlin Client", () => { it("should log and display error out on unknown requestId", () => { const gremlinClient = new GremlinClient(); - const logConsoleSpy = sinon.spy(NotificationConsoleUtils, "logConsoleMessage"); + const logConsoleSpy = sinon.spy(NotificationConsoleUtils, "logConsoleError"); const logErrorSpy = sinon.spy(Logger, "logError"); gremlinClient.initialize(emptyParams); @@ -122,7 +122,7 @@ describe("Gremlin Client", () => { }); it("should not aggregate RU if not a number and reset totalRequestCharge to undefined", done => { - const logConsoleSpy = sinon.spy(NotificationConsoleUtils, "logConsoleMessage"); + const logConsoleSpy = sinon.spy(NotificationConsoleUtils, "logConsoleError"); const logErrorSpy = sinon.spy(Logger, "logError"); const gremlinClient = new GremlinClient(); @@ -165,7 +165,7 @@ describe("Gremlin Client", () => { }); it("should not aggregate RU if undefined and reset totalRequestCharge to undefined", done => { - const logConsoleSpy = sinon.spy(NotificationConsoleUtils, "logConsoleMessage"); + const logConsoleSpy = sinon.spy(NotificationConsoleUtils, "logConsoleError"); const logErrorSpy = sinon.spy(Logger, "logError"); const gremlinClient = new GremlinClient(); diff --git a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts index 7350f71d5..e66ae5cf2 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts @@ -7,8 +7,7 @@ import { GremlinSimpleClient, Result } from "./GremlinSimpleClient"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { HashMap } from "../../../Common/HashMap"; -import * as Logger from "../../../Common/Logger"; -import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; +import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils"; export interface GremlinClientParameters { endpoint: string; @@ -63,10 +62,10 @@ export class GremlinClient { const requestId = result.requestId; if (!requestId || !this.pendingResults.has(requestId)) { - const msg = `Error: ${errorMessage}, unknown requestId:${requestId} ${GremlinClient.getRequestChargeString( + const errorMsg = `Error: ${errorMessage}, unknown requestId:${requestId} ${GremlinClient.getRequestChargeString( result.requestCharge )}`; - GremlinClient.reportError(msg); + handleError(errorMsg, GremlinClient.LOG_AREA); // Fail all pending requests if no request id (fatal) if (!requestId) { @@ -130,15 +129,16 @@ export class GremlinClient { deferred.reject(error); this.pendingResults.delete(requestId); - GremlinClient.reportError( - `Aborting pending request ${requestId}. Error:${error} ${GremlinClient.getRequestChargeString(requestCharge)}` - ); + const errorMsg = `Aborting pending request ${requestId}. Error:${error} ${GremlinClient.getRequestChargeString( + requestCharge + )}`; + handleError(errorMsg, GremlinClient.LOG_AREA); } private flushResult(requestId: string) { if (!this.pendingResults.has(requestId)) { - const msg = `Unknown requestId:${requestId}`; - GremlinClient.reportError(msg); + const errorMsg = `Unknown requestId:${requestId}`; + handleError(errorMsg, GremlinClient.LOG_AREA); return; } @@ -156,8 +156,8 @@ export class GremlinClient { */ private storePendingResult(result: Result): boolean { if (!this.pendingResults.has(result.requestId)) { - const msg = `Dropping result for unknown requestId:${result.requestId}`; - GremlinClient.reportError(msg); + const errorMsg = `Dropping result for unknown requestId:${result.requestId}`; + handleError(errorMsg, GremlinClient.LOG_AREA); return false; } const pendingResults = this.pendingResults.get(result.requestId).result; @@ -177,9 +177,8 @@ export class GremlinClient { if (result.requestCharge === undefined || typeof result.requestCharge !== "number") { // Clear totalRequestCharge, even if it was a valid number as the total might be incomplete therefore incorrect pendingResults.totalRequestCharge = undefined; - GremlinClient.reportError( - `Unable to perform RU aggregation calculation with non numbers. Result request charge: ${result.requestCharge}. RequestId: ${result.requestId}` - ); + const errorMsg = `Unable to perform RU aggregation calculation with non numbers. Result request charge: ${result.requestCharge}. RequestId: ${result.requestId}`; + handleError(errorMsg, GremlinClient.LOG_AREA); } else { if (pendingResults.totalRequestCharge === undefined) { pendingResults.totalRequestCharge = 0; @@ -188,9 +187,4 @@ export class GremlinClient { } return pendingResults.isIncomplete; } - - private static reportError(msg: string): void { - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg); - Logger.logError(msg, GremlinClient.LOG_AREA); - } } diff --git a/src/Explorer/Notebook/NotebookContainerClient.ts b/src/Explorer/Notebook/NotebookContainerClient.ts index 78cfa0fa3..90f8a9a74 100644 --- a/src/Explorer/Notebook/NotebookContainerClient.ts +++ b/src/Explorer/Notebook/NotebookContainerClient.ts @@ -6,6 +6,7 @@ import * as Constants from "../../Common/Constants"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as Logger from "../../Common/Logger"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; export class NotebookContainerClient { private reconnectingNotificationId: string; @@ -74,7 +75,7 @@ export class NotebookContainerClient { } return undefined; } catch (error) { - Logger.logError(error, "NotebookContainerClient/getMemoryUsage"); + Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage"); if (!this.reconnectingNotificationId) { this.reconnectingNotificationId = NotificationConsoleUtils.logConsoleMessage( ConsoleDataType.InProgress, @@ -110,7 +111,7 @@ export class NotebookContainerClient { headers: { Authorization: authToken } }); } catch (error) { - Logger.logError(error, "NotebookContainerClient/resetWorkspace"); + Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace"); await this.recreateNotebookWorkspaceAsync(); } } @@ -140,7 +141,7 @@ export class NotebookContainerClient { await notebookWorkspaceManager.deleteNotebookWorkspaceAsync(explorer.databaseAccount().id, "default"); await notebookWorkspaceManager.createNotebookWorkspaceAsync(explorer.databaseAccount().id, "default"); } catch (error) { - Logger.logError(error, "NotebookContainerClient/recreateNotebookWorkspaceAsync"); + Logger.logError(getErrorMessage(error), "NotebookContainerClient/recreateNotebookWorkspaceAsync"); return Promise.reject(error); } } diff --git a/src/Explorer/Notebook/NotebookManager.ts b/src/Explorer/Notebook/NotebookManager.ts index 2fe8748d0..932d80e13 100644 --- a/src/Explorer/Notebook/NotebookManager.ts +++ b/src/Explorer/Notebook/NotebookManager.ts @@ -26,6 +26,7 @@ import { ImmutableNotebook } from "@nteract/commutable"; import Explorer from "../Explorer"; import { ContextualPaneBase } from "../Panes/ContextualPaneBase"; import { CopyNotebookPaneAdapter } from "../Panes/CopyNotebookPane"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; export interface NotebookManagerOptions { container: Explorer; @@ -147,7 +148,7 @@ export default class NotebookManager { // Octokit's error handler uses any // eslint-disable-next-line @typescript-eslint/no-explicit-any private onGitHubClientError = (error: any): void => { - Logger.logError(error, "NotebookManager/onGitHubClientError"); + Logger.logError(getErrorMessage(error), "NotebookManager/onGitHubClientError"); if (error.status === HttpStatusCodes.Unauthorized) { this.gitHubOAuthService.resetToken(); diff --git a/src/Explorer/Panes/CopyNotebookPane.tsx b/src/Explorer/Panes/CopyNotebookPane.tsx index 586af6318..1ef61eb3a 100644 --- a/src/Explorer/Panes/CopyNotebookPane.tsx +++ b/src/Explorer/Panes/CopyNotebookPane.tsx @@ -1,7 +1,6 @@ import ko from "knockout"; import * as React from "react"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; -import * as Logger from "../../Common/Logger"; import { JunoClient, IPinnedRepo } from "../../Juno/JunoClient"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import Explorer from "../Explorer"; @@ -13,6 +12,7 @@ import { HttpStatusCodes } from "../../Common/Constants"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; +import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils"; interface Location { type: "MyNotebooks" | "GitHub"; @@ -90,9 +90,7 @@ export class CopyNotebookPaneAdapter implements ReactAdapter { if (this.gitHubOAuthService.isLoggedIn()) { const response = await this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope); if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { - const message = `Received HTTP ${response.status} when fetching pinned repos`; - Logger.logError(message, "CopyNotebookPaneAdapter/submit"); - NotificationConsoleUtils.logConsoleError(message); + handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit"); } if (response.data?.length > 0) { @@ -134,12 +132,10 @@ export class CopyNotebookPaneAdapter implements ReactAdapter { NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${this.name} to ${destination}`); } catch (error) { + const errorMessage = getErrorMessage(error); this.formError = `Failed to copy ${this.name} to ${destination}`; - this.formErrorDetail = `${error}`; - - const message = `${this.formError}: ${this.formErrorDetail}`; - Logger.logError(message, "CopyNotebookPaneAdapter/submit"); - NotificationConsoleUtils.logConsoleError(message); + this.formErrorDetail = `${errorMessage}`; + handleError(errorMessage, "CopyNotebookPaneAdapter/submit", this.formError); return; } finally { clearMessage && clearMessage(); diff --git a/src/Explorer/Panes/GitHubReposPane.ts b/src/Explorer/Panes/GitHubReposPane.ts index a18f7d867..403abfd75 100644 --- a/src/Explorer/Panes/GitHubReposPane.ts +++ b/src/Explorer/Panes/GitHubReposPane.ts @@ -1,6 +1,5 @@ import _ from "underscore"; import { Areas, HttpStatusCodes } from "../../Common/Constants"; -import * as Logger from "../../Common/Logger"; import * as ViewModels from "../../Contracts/ViewModels"; import { GitHubClient, IGitHubPageInfo, IGitHubRepo } from "../../GitHub/GitHubClient"; import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient"; @@ -8,13 +7,12 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as GitHubUtils from "../../Utils/GitHubUtils"; import { JunoUtils } from "../../Utils/JunoUtils"; -import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent"; import { GitHubReposComponent, GitHubReposComponentProps, RepoListItem } from "../Controls/GitHub/GitHubReposComponent"; import { GitHubReposComponentAdapter } from "../Controls/GitHub/GitHubReposComponentAdapter"; import { BranchesProps, PinnedReposProps, UnpinnedReposProps } from "../Controls/GitHub/ReposListComponent"; -import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ContextualPaneBase } from "./ContextualPaneBase"; +import { handleError } from "../../Common/ErrorHandlingUtils"; interface GitHubReposPaneOptions extends ViewModels.PaneOptions { gitHubClient: GitHubClient; @@ -105,9 +103,7 @@ export class GitHubReposPane extends ContextualPaneBase { throw new Error(`Received HTTP ${response.status} when saving pinned repos`); } } catch (error) { - const message = `Failed to save pinned repos: ${error}`; - Logger.logError(message, "GitHubReposPane/submit"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); + handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos"); } } } @@ -206,9 +202,7 @@ export class GitHubReposPane extends ContextualPaneBase { branchesProps.lastPageInfo = response.pageInfo; } } catch (error) { - const message = `Failed to fetch branches: ${error}`; - Logger.logError(message, "GitHubReposPane/loadMoreBranches"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); + handleError(error, "GitHubReposPane/loadMoreBranches", "Failed to fetch branches"); } branchesProps.isLoading = false; @@ -236,9 +230,7 @@ export class GitHubReposPane extends ContextualPaneBase { this.unpinnedReposProps.repos = this.calculateUnpinnedRepos(); } } catch (error) { - const message = `Failed to fetch unpinned repos: ${error}`; - Logger.logError(message, "GitHubReposPane/loadMoreUnpinnedRepos"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); + handleError(error, "GitHubReposPane/loadMoreUnpinnedRepos", "Failed to fetch unpinned repos"); } this.unpinnedReposProps.isLoading = false; @@ -255,9 +247,7 @@ export class GitHubReposPane extends ContextualPaneBase { return response.data; } catch (error) { - const message = `Failed to fetch repo: ${error}`; - Logger.logError(message, "GitHubReposPane/getRepo"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); + handleError(error, "GitHubReposPane/getRepo", "Failed to fetch repo"); return Promise.resolve(undefined); } } @@ -320,9 +310,7 @@ export class GitHubReposPane extends ContextualPaneBase { this.triggerRender(); } } catch (error) { - const message = `Failed to fetch pinned repos: ${error}`; - Logger.logError(message, "GitHubReposPane/refreshPinnedReposListItems"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); + handleError(error, "GitHubReposPane/refreshPinnedReposListItems", "Failed to fetch pinned repos"); } } diff --git a/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx b/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx index c8f94f7d2..967355251 100644 --- a/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx +++ b/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx @@ -1,17 +1,16 @@ import ko from "knockout"; import * as React from "react"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; -import * as Logger from "../../Common/Logger"; import Explorer from "../Explorer"; import { JunoClient } from "../../Juno/JunoClient"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; -import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent"; import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent"; import { ImmutableNotebook } from "@nteract/commutable/src"; import { toJS } from "@nteract/commutable"; import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent"; import { HttpStatusCodes } from "../../Common/Constants"; +import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils"; export class PublishNotebookPaneAdapter implements ReactAdapter { parameters: ko.Observable; @@ -111,9 +110,11 @@ export class PublishNotebookPaneAdapter implements ReactAdapter { this.isCodeOfConductAccepted = response.data; } catch (error) { - const message = `Failed to check if code of conduct was accepted: ${error}`; - Logger.logError(message, "PublishNotebookPaneAdapter/isCodeOfConductAccepted"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); + handleError( + error, + "PublishNotebookPaneAdapter/isCodeOfConductAccepted", + "Failed to check if code of conduct was accepted" + ); } } else { this.isCodeOfConductAccepted = true; @@ -170,12 +171,10 @@ export class PublishNotebookPaneAdapter implements ReactAdapter { } } } catch (error) { + const errorMessage = getErrorMessage(error); this.formError = `Failed to publish ${this.name} to gallery`; - this.formErrorDetail = `${error}`; - - const message = `${this.formError}: ${this.formErrorDetail}`; - Logger.logError(message, "PublishNotebookPaneAdapter/submit"); - NotificationConsoleUtils.logConsoleError(message); + this.formErrorDetail = `${errorMessage}`; + handleError(errorMessage, "PublishNotebookPaneAdapter/submit", this.formError); return; } finally { clearPublishingMessage(); @@ -189,10 +188,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter { private createFormErrorForLargeImageSelection = (formError: string, formErrorDetail: string, area: string): void => { this.formError = formError; this.formErrorDetail = formErrorDetail; - - const message = `${this.formError}: ${this.formErrorDetail}`; - Logger.logError(message, area); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); + handleError(formErrorDetail, area, formError); this.triggerRender(); }; diff --git a/src/Explorer/Tables/TableDataClient.ts b/src/Explorer/Tables/TableDataClient.ts index 1dda5b9b4..ba6ae246d 100644 --- a/src/Explorer/Tables/TableDataClient.ts +++ b/src/Explorer/Tables/TableDataClient.ts @@ -7,16 +7,14 @@ import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/Notifi import * as Constants from "../../Common/Constants"; import * as Entities from "./Entities"; import * as HeadersUtility from "../../Common/HeadersUtility"; -import * as Logger from "../../Common/Logger"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as TableConstants from "./Constants"; import * as TableEntityProcessor from "./TableEntityProcessor"; import * as ViewModels from "../../Contracts/ViewModels"; -import { MessageTypes } from "../../Contracts/ExplorerContracts"; -import { sendMessage } from "../../Common/MessageHandler"; import Explorer from "../Explorer"; import { queryDocuments, deleteDocument, updateDocument, createDocument } from "../../Common/DocumentClientUtilityBase"; import { configContext } from "../../ConfigContext"; +import { handleError } from "../../Common/ErrorHandlingUtils"; export interface CassandraTableKeys { partitionKeys: CassandraTableKey[]; @@ -188,14 +186,9 @@ export class CassandraAPIDataClient extends TableDataClient { ); deferred.resolve(entity); }, - reason => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error while adding new row to table ${collection.id()}:\n ${JSON.stringify(reason)}` - ); - Logger.logError(JSON.stringify(reason), "AddRowCassandra", reason.code); - this._checkForbiddenError(reason); - deferred.reject(reason); + error => { + handleError(error, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`); + deferred.reject(error); } ) .finally(() => { @@ -267,14 +260,9 @@ export class CassandraAPIDataClient extends TableDataClient { ); deferred.resolve(newEntity); }, - reason => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to update row ${newEntity.RowKey._}: ${JSON.stringify(reason)}` - ); - Logger.logError(JSON.stringify(reason), "UpdateRowCassandra", reason.code); - this._checkForbiddenError(reason); - deferred.reject(reason); + error => { + handleError(error, "UpdateRowCassandra", `Failed to update row ${newEntity.RowKey._}`); + deferred.reject(error); } ) .finally(() => { @@ -332,16 +320,11 @@ export class CassandraAPIDataClient extends TableDataClient { ContinuationToken: data.paginationToken }); }, - reason => { + (error: any) => { if (shouldNotify) { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Failed to query rows for table ${collection.id()}: ${JSON.stringify(reason)}` - ); - Logger.logError(JSON.stringify(reason), "QueryDocumentsCassandra", reason.status); - this._checkForbiddenError(reason); + handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`); } - deferred.reject(reason); + deferred.reject(error); } ) .done(() => { @@ -379,13 +362,8 @@ export class CassandraAPIDataClient extends TableDataClient { `Successfully deleted row ${currEntityToDelete.RowKey._}` ); }, - reason => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error while deleting row ${currEntityToDelete.RowKey._}:\n ${JSON.stringify(reason)}` - ); - Logger.logError(JSON.stringify(reason), "DeleteRowCassandra", reason.code); - this._checkForbiddenError(reason); + error => { + handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`); } ) .finally(() => { @@ -420,14 +398,13 @@ export class CassandraAPIDataClient extends TableDataClient { ); deferred.resolve(); }, - reason => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error while creating a keyspace with query ${createKeyspaceQuery}:\n ${JSON.stringify(reason)}` + error => { + handleError( + error, + "CreateKeyspaceCassandra", + `Error while creating a keyspace with query ${createKeyspaceQuery}` ); - Logger.logError(JSON.stringify(reason), "CreateKeyspaceCassandra", reason.code); - this._checkForbiddenError(reason); - deferred.reject(reason); + deferred.reject(error); } ) .finally(() => { @@ -467,14 +444,9 @@ export class CassandraAPIDataClient extends TableDataClient { ); deferred.resolve(); }, - reason => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error while creating a table with query ${createTableQuery}:\n ${JSON.stringify(reason)}` - ); - Logger.logError(JSON.stringify(reason), "CreateTableCassandra", reason.code); - this._checkForbiddenError(reason); - deferred.reject(reason); + error => { + handleError(error, "CreateTableCassandra", `Error while creating a table with query ${createTableQuery}`); + deferred.reject(error); } ) .finally(() => { @@ -508,14 +480,13 @@ export class CassandraAPIDataClient extends TableDataClient { ); deferred.resolve(); }, - reason => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error while deleting resource with query ${deleteQuery}:\n ${JSON.stringify(reason)}` + error => { + handleError( + error, + "DeleteKeyspaceOrTableCassandra", + `Error while deleting resource with query ${deleteQuery}` ); - Logger.logError(JSON.stringify(reason), "DeleteKeyspaceOrTableCassandra", reason.code); - this._checkForbiddenError(reason); - deferred.reject(reason); + deferred.reject(error); } ) .finally(() => { @@ -563,14 +534,9 @@ export class CassandraAPIDataClient extends TableDataClient { ); deferred.resolve(data); }, - reason => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error fetching keys for table ${collection.id()}:\n ${JSON.stringify(reason)}` - ); - Logger.logError(JSON.stringify(reason), "FetchKeysCassandra", reason.status); - this._checkForbiddenError(reason); - deferred.reject(reason); + (error: any) => { + handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); + deferred.reject(error); } ) .done(() => { @@ -618,14 +584,9 @@ export class CassandraAPIDataClient extends TableDataClient { ); deferred.resolve(data.columns); }, - reason => { - NotificationConsoleUtils.logConsoleMessage( - ConsoleDataType.Error, - `Error fetching schema for table ${collection.id()}:\n ${JSON.stringify(reason)}` - ); - Logger.logError(JSON.stringify(reason), "FetchSchemaCassandra", reason.status); - this._checkForbiddenError(reason); - deferred.reject(reason); + (error: any) => { + handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); + deferred.reject(error); } ) .done(() => { @@ -712,13 +673,4 @@ export class CassandraAPIDataClient extends TableDataClient { displayTokenRenewalPromptForStatus(xhrObj.status); }; - - private _checkForbiddenError(reason: any) { - if (reason && reason.code === Constants.HttpStatusCodes.Forbidden) { - sendMessage({ - type: MessageTypes.ForbiddenError, - reason: typeof reason === "string" ? "reason" : JSON.stringify(reason) - }); - } - } } diff --git a/src/GitHub/GitHubClient.ts b/src/GitHub/GitHubClient.ts index 64ed9e2d1..6125836cf 100644 --- a/src/GitHub/GitHubClient.ts +++ b/src/GitHub/GitHubClient.ts @@ -3,6 +3,7 @@ import { HttpStatusCodes } from "../Common/Constants"; import * as Logger from "../Common/Logger"; import UrlUtility from "../Common/UrlUtility"; import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil"; +import { getErrorMessage } from "../Common/ErrorHandlingUtils"; export interface IGitHubPageInfo { endCursor: string; @@ -244,7 +245,7 @@ export class GitHubClient { data: GitHubClient.toGitHubRepo(response.repository) }; } catch (error) { - GitHubClient.log(Logger.logError, `GitHubClient.getRepoAsync failed: ${error}`); + Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getRepoAsync failed"); return { status: GitHubClient.SelfErrorCode, data: undefined @@ -265,7 +266,7 @@ export class GitHubClient { pageInfo: GitHubClient.toGitHubPageInfo(response.viewer.repositories.pageInfo) }; } catch (error) { - GitHubClient.log(Logger.logError, `GitHubClient.getReposAsync failed: ${error}`); + Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getRepoAsync failed"); return { status: GitHubClient.SelfErrorCode, data: undefined @@ -294,7 +295,7 @@ export class GitHubClient { pageInfo: GitHubClient.toGitHubPageInfo(response.repository.refs.pageInfo) }; } catch (error) { - GitHubClient.log(Logger.logError, `GitHubClient.getBranchesAsync failed: ${error}`); + Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getBranchesAsync failed"); return { status: GitHubClient.SelfErrorCode, data: undefined @@ -359,7 +360,7 @@ export class GitHubClient { data }; } catch (error) { - GitHubClient.log(Logger.logError, `GitHubClient.getContentsAsync failed: ${error}`); + Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getContentsAsync failed"); return { status: GitHubClient.SelfErrorCode, data: undefined @@ -503,7 +504,7 @@ export class GitHubClient { debug: () => {}, info: (message?: any) => GitHubClient.log(Logger.logInfo, message), warn: (message?: any) => GitHubClient.log(Logger.logWarning, message), - error: (message?: any) => GitHubClient.log(Logger.logError, message) + error: (error?: any) => Logger.logError(getErrorMessage(error), "GitHubClient.Octokit") } }); diff --git a/src/GitHub/GitHubContentProvider.ts b/src/GitHub/GitHubContentProvider.ts index 6142b7a6f..c406be440 100644 --- a/src/GitHub/GitHubContentProvider.ts +++ b/src/GitHub/GitHubContentProvider.ts @@ -41,7 +41,7 @@ export class GitHubContentProvider implements IContentProvider { return this.createSuccessAjaxResponse(HttpStatusCodes.NoContent, undefined); } catch (error) { - Logger.logError(error, "GitHubContentProvider/remove", error.errno); + Logger.logError(getErrorMessage(error), "GitHubContentProvider/remove", error.errno); return this.createErrorAjaxResponse(error); } }) @@ -65,7 +65,7 @@ export class GitHubContentProvider implements IContentProvider { return this.createSuccessAjaxResponse(HttpStatusCodes.OK, this.createContentModel(uri, content.data, params)); } catch (error) { - Logger.logError(error, "GitHubContentProvider/get", error.errno); + Logger.logError(getErrorMessage(error), "GitHubContentProvider/get", error.errno); return this.createErrorAjaxResponse(error); } }) @@ -105,7 +105,7 @@ export class GitHubContentProvider implements IContentProvider { this.createContentModel(newUri, gitHubFile, { content: 0 }) ); } catch (error) { - Logger.logError(error, "GitHubContentProvider/update", error.errno); + Logger.logError(getErrorMessage(error), "GitHubContentProvider/update", error.errno); return this.createErrorAjaxResponse(error); } }) @@ -182,7 +182,7 @@ export class GitHubContentProvider implements IContentProvider { this.createContentModel(newUri, newGitHubFile, { content: 0 }) ); } catch (error) { - Logger.logError(error, "GitHubContentProvider/create", error.errno); + Logger.logError(getErrorMessage(error), "GitHubContentProvider/create", error.errno); return this.createErrorAjaxResponse(error); } }) @@ -260,7 +260,7 @@ export class GitHubContentProvider implements IContentProvider { this.createContentModel(uri, gitHubFile, { content: 0 }) ); } catch (error) { - Logger.logError(error, "GitHubContentProvider/update", error.errno); + Logger.logError(getErrorMessage(error), "GitHubContentProvider/update", error.errno); return this.createErrorAjaxResponse(error); } }) @@ -269,25 +269,25 @@ export class GitHubContentProvider implements IContentProvider { public listCheckpoints(_: ServerConfig, path: string): Observable { const error = new GitHubContentProviderError("Not implemented"); - Logger.logError(error, "GitHubContentProvider/listCheckpoints", error.errno); + Logger.logError(error.message, "GitHubContentProvider/listCheckpoints", error.errno); return of(this.createErrorAjaxResponse(error)); } public createCheckpoint(_: ServerConfig, path: string): Observable { const error = new GitHubContentProviderError("Not implemented"); - Logger.logError(error, "GitHubContentProvider/createCheckpoint", error.errno); + Logger.logError(error.message, "GitHubContentProvider/createCheckpoint", error.errno); return of(this.createErrorAjaxResponse(error)); } public deleteCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable { const error = new GitHubContentProviderError("Not implemented"); - Logger.logError(error, "GitHubContentProvider/deleteCheckpoint", error.errno); + Logger.logError(error.message, "GitHubContentProvider/deleteCheckpoint", error.errno); return of(this.createErrorAjaxResponse(error)); } public restoreFromCheckpoint(_: ServerConfig, path: string, checkpointID: string): Observable { const error = new GitHubContentProviderError("Not implemented"); - Logger.logError(error, "GitHubContentProvider/restoreFromCheckpoint", error.errno); + Logger.logError(error.message, "GitHubContentProvider/restoreFromCheckpoint", error.errno); return of(this.createErrorAjaxResponse(error)); } diff --git a/src/GitHub/GitHubOAuthService.ts b/src/GitHub/GitHubOAuthService.ts index 746e79152..80a6d2c42 100644 --- a/src/GitHub/GitHubOAuthService.ts +++ b/src/GitHub/GitHubOAuthService.ts @@ -1,6 +1,5 @@ import ko from "knockout"; import { HttpStatusCodes } from "../Common/Constants"; -import * as Logger from "../Common/Logger"; import { configContext } from "../ConfigContext"; import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent"; import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; @@ -8,6 +7,7 @@ import { JunoClient } from "../Juno/JunoClient"; import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { GitHubConnectorMsgType, IGitHubConnectorParams } from "./GitHubConnector"; +import { handleError } from "../Common/ErrorHandlingUtils"; window.addEventListener("message", (event: MessageEvent) => { if (isInvalidParentFrameOrigin(event)) { @@ -99,9 +99,7 @@ export class GitHubOAuthService { this.resetToken(); return true; } catch (error) { - const message = `Failed to delete app authorization: ${error}`; - Logger.logError(message, "GitHubOAuthService/logout"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); + handleError(error, "GitHubOAuthService/logout", "Failed to delete app authorization"); return false; } } diff --git a/src/HostedExplorer.ts b/src/HostedExplorer.ts index ac24eeaea..30e97cf74 100644 --- a/src/HostedExplorer.ts +++ b/src/HostedExplorer.ts @@ -611,7 +611,7 @@ class HostedExplorer { return loadAccountResult; } catch (error) { LocalStorageUtility.removeEntry(StorageKey.DatabaseAccountId); - Logger.logError(error, "HostedExplorer/_getAccessCached"); + Logger.logError(getErrorMessage(error), "HostedExplorer/_getAccessCached"); throw error; } } @@ -637,7 +637,7 @@ class HostedExplorer { const accountResponse = this._getAccessAfterTenantSelection(defaultTenant.tenantId); return accountResponse; } catch (error) { - Logger.logError(error, "HostedExplorer/_getAccessNew"); + Logger.logError(getErrorMessage(error), "HostedExplorer/_getAccessNew"); throw error; } } @@ -658,7 +658,7 @@ class HostedExplorer { const keys = await this._getAccountKeysHelper(defaultAccount, true); return [defaultAccount, keys, authToken]; } catch (error) { - Logger.logError(error, "HostedExplorer/_getAccessAfterTenantSelection"); + Logger.logError(getErrorMessage(error), "HostedExplorer/_getAccessAfterTenantSelection"); throw error; } } @@ -1131,7 +1131,7 @@ class HostedExplorer { }); }, error => { - Logger.logError(error, "HostedExplorer/_onNewDirectorySelected"); + Logger.logError(getErrorMessage(error), "HostedExplorer/_onNewDirectorySelected"); } ); TelemetryProcessor.trace(Action.TenantSwitch); diff --git a/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts b/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts index 6e5a5d0a9..563645f25 100644 --- a/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts +++ b/src/NotebookWorkspaceManager/NotebookWorkspaceManager.ts @@ -7,6 +7,7 @@ import { NotebookWorkspaceFeedResponse } from "../Contracts/DataModels"; import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; +import { getErrorMessage } from "../Common/ErrorHandlingUtils"; export class NotebookWorkspaceManager { private resourceProviderClientFactory: IResourceProviderClientFactory; @@ -24,7 +25,7 @@ export class NotebookWorkspaceManager { )) as NotebookWorkspaceFeedResponse; return response && response.value; } catch (error) { - Logger.logError(error, "NotebookWorkspaceManager/getNotebookWorkspacesAsync"); + Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/getNotebookWorkspacesAsync"); throw error; } } @@ -37,7 +38,7 @@ export class NotebookWorkspaceManager { try { return (await this.rpClient(uri).getAsync(uri, ArmApiVersions.documentDB)) as NotebookWorkspace; } catch (error) { - Logger.logError(error, "NotebookWorkspaceManager/getNotebookWorkspaceAsync"); + Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/getNotebookWorkspaceAsync"); throw error; } } @@ -47,7 +48,7 @@ export class NotebookWorkspaceManager { try { await this.rpClient(uri).putAsync(uri, ArmApiVersions.documentDB, { name: notebookWorkspaceId }); } catch (error) { - Logger.logError(error, "NotebookWorkspaceManager/createNotebookWorkspaceAsync"); + Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/createNotebookWorkspaceAsync"); throw error; } } @@ -57,7 +58,7 @@ export class NotebookWorkspaceManager { try { await this.rpClient(uri).deleteAsync(uri, ArmApiVersions.documentDB); } catch (error) { - Logger.logError(error, "NotebookWorkspaceManager/deleteNotebookWorkspaceAsync"); + Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/deleteNotebookWorkspaceAsync"); throw error; } } @@ -74,7 +75,7 @@ export class NotebookWorkspaceManager { undefined ); } catch (error) { - Logger.logError(error, "NotebookWorkspaceManager/getNotebookConnectionInfoAsync"); + Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/getNotebookConnectionInfoAsync"); throw error; } } @@ -86,7 +87,7 @@ export class NotebookWorkspaceManager { skipResourceValidation: true }); } catch (error) { - Logger.logError(error, "NotebookWorkspaceManager/startNotebookWorkspaceAsync"); + Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/startNotebookWorkspaceAsync"); throw error; } } diff --git a/src/Platform/Hosted/ArmResourceUtils.ts b/src/Platform/Hosted/ArmResourceUtils.ts index 998fbe868..dbe9a5f64 100644 --- a/src/Platform/Hosted/ArmResourceUtils.ts +++ b/src/Platform/Hosted/ArmResourceUtils.ts @@ -3,6 +3,7 @@ import * as Constants from "../../Common/Constants"; import * as Logger from "../../Common/Logger"; import { Tenant, Subscription, DatabaseAccount, AccountKeys } from "../../Contracts/DataModels"; import { configContext } from "../../ConfigContext"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; // TODO: 421864 - add a fetch wrapper export abstract class ArmResourceUtils { @@ -30,7 +31,7 @@ export abstract class ArmResourceUtils { } return tenants; } catch (error) { - Logger.logError(error, "ArmResourceUtils/listTenants"); + Logger.logError(getErrorMessage(error), "ArmResourceUtils/listTenants"); throw error; } } @@ -58,7 +59,7 @@ export abstract class ArmResourceUtils { } return subscriptions; } catch (error) { - Logger.logError(error, "ArmResourceUtils/listSubscriptions"); + Logger.logError(getErrorMessage(error), "ArmResourceUtils/listSubscriptions"); throw error; } } @@ -92,7 +93,7 @@ export abstract class ArmResourceUtils { } return accounts; } catch (error) { - Logger.logError(error, "ArmResourceUtils/listAccounts"); + Logger.logError(getErrorMessage(error), "ArmResourceUtils/listAccounts"); throw error; } } @@ -140,7 +141,7 @@ export abstract class ArmResourceUtils { } return result; } catch (error) { - Logger.logError(error, "ArmResourceUtils/getAccountKeys"); + Logger.logError(getErrorMessage(error), "ArmResourceUtils/getAccountKeys"); throw error; } } @@ -150,7 +151,7 @@ export abstract class ArmResourceUtils { const token = await AuthHeadersUtil.getAccessToken(ArmResourceUtils._armAuthArea, tenantId); return token; } catch (error) { - Logger.logError(error, "ArmResourceUtils/getAuthToken"); + Logger.logError(getErrorMessage(error), "ArmResourceUtils/getAuthToken"); throw error; } } diff --git a/src/SparkClusterManager/ArcadiaResourceManager.ts b/src/SparkClusterManager/ArcadiaResourceManager.ts index 28a699f64..68fa2318b 100644 --- a/src/SparkClusterManager/ArcadiaResourceManager.ts +++ b/src/SparkClusterManager/ArcadiaResourceManager.ts @@ -1,4 +1,3 @@ -import * as ViewModels from "../Contracts/ViewModels"; import { ArcadiaWorkspace, ArcadiaWorkspaceFeedResponse, @@ -10,6 +9,7 @@ import { IResourceProviderClient, IResourceProviderClientFactory } from "../Reso import * as Logger from "../Common/Logger"; import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; import { configContext } from "../ConfigContext"; +import { getErrorMessage } from "../Common/ErrorHandlingUtils"; export class ArcadiaResourceManager { private resourceProviderClientFactory: IResourceProviderClientFactory; @@ -27,7 +27,7 @@ export class ArcadiaResourceManager { )) as ArcadiaWorkspaceFeedResponse; return response && response.value; } catch (error) { - Logger.logError(error, "ArcadiaResourceManager/getWorkspaceAsync"); + Logger.logError(getErrorMessage(error), "ArcadiaResourceManager/getWorkspaceAsync"); throw error; } } @@ -37,7 +37,7 @@ export class ArcadiaResourceManager { try { return (await this._rpClient(uri).getAsync(uri, ArmApiVersions.arcadia)) as ArcadiaWorkspace; } catch (error) { - Logger.logError(error, "ArcadiaResourceManager/getWorkspaceAsync"); + Logger.logError(getErrorMessage(error), "ArcadiaResourceManager/getWorkspaceAsync"); throw error; } } @@ -56,7 +56,7 @@ export class ArcadiaResourceManager { )) as ArcadiaWorkspaceFeedResponse; return response && response.value; } catch (error) { - Logger.logError(error, "ArcadiaManager/listWorkspacesAsync"); + Logger.logError(getErrorMessage(error), "ArcadiaManager/listWorkspacesAsync"); throw error; } } @@ -68,7 +68,7 @@ export class ArcadiaResourceManager { const response = (await this._rpClient(uri).getAsync(uri, ArmApiVersions.arcadia)) as SparkPoolFeedResponse; return response && response.value; } catch (error) { - Logger.logError(error, "ArcadiaManager/listSparkPoolsAsync"); + Logger.logError(getErrorMessage(error), "ArcadiaManager/listSparkPoolsAsync"); throw error; } } diff --git a/src/Utils/AuthorizationUtils.ts b/src/Utils/AuthorizationUtils.ts index 73c66d0a3..48f023dc9 100644 --- a/src/Utils/AuthorizationUtils.ts +++ b/src/Utils/AuthorizationUtils.ts @@ -5,6 +5,7 @@ import { AuthType } from "../AuthType"; import * as Logger from "../Common/Logger"; import { configContext, Platform } from "../ConfigContext"; import { userContext } from "../UserContext"; +import { getErrorMessage } from "../Common/ErrorHandlingUtils"; export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata { if (window.authType === AuthType.EncryptedToken) { @@ -28,7 +29,7 @@ export async function getArcadiaAuthToken( const token = await AuthHeadersUtil.getAccessToken(arcadiaEndpoint, tenantId); return token; } catch (error) { - Logger.logError(error, "AuthorizationUtils/getArcadiaAuthToken"); + Logger.logError(getErrorMessage(error), "AuthorizationUtils/getArcadiaAuthToken"); throw error; } } diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index 723b04144..99dee0f07 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -1,7 +1,6 @@ import { IGalleryItem, JunoClient } from "../Juno/JunoClient"; import * as NotificationConsoleUtils from "./NotificationConsoleUtils"; import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; -import * as Logger from "../Common/Logger"; import { GalleryTab, SortBy, @@ -10,6 +9,7 @@ import { 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"; const defaultSelectedAbuseCategory = "Other"; const abuseCategories: IChoiceGroupOption[] = [ @@ -122,9 +122,11 @@ export function reportAbuse( ); onComplete(response.data); } catch (error) { - const message = `Failed to submit report on ${data.name} violating code of conduct: ${error}`; - Logger.logError(message, "GalleryUtils/reportAbuse"); - NotificationConsoleUtils.logConsoleInfo(message); + handleError( + error, + "GalleryUtils/reportAbuse", + `Failed to submit report on ${data.name} violating code of conduct` + ); } clearSubmitReportNotification(); @@ -185,9 +187,7 @@ export function downloadItem( onComplete(increaseDownloadResponse.data); } } catch (error) { - const message = `Failed to download ${data.name}: ${error}`; - Logger.logError(message, "GalleryUtils/downloadItem"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); + handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`); } NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); @@ -212,9 +212,7 @@ export async function favoriteItem( onComplete(response.data); } catch (error) { - const message = `Failed to favorite ${data.name}: ${error}`; - Logger.logError(message, "GalleryUtils/favoriteItem"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); + handleError(error, "GalleryUtils/favoriteItem", `Failed to favorite ${data.name}`); } } } @@ -234,9 +232,7 @@ export async function unfavoriteItem( onComplete(response.data); } catch (error) { - const message = `Failed to unfavorite ${data.name}: ${error}`; - Logger.logError(message, "GalleryUtils/unfavoriteItem"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); + handleError(error, "GalleryUtils/unfavoriteItem", `Failed to unfavorite ${data.name}`); } } } @@ -268,9 +264,7 @@ export function deleteItem( NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully removed ${name} from gallery`); onComplete(response.data); } catch (error) { - const message = `Failed to remove ${name} from gallery: ${error}`; - Logger.logError(message, "GalleryUtils/deleteItem"); - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); + handleError(error, "GalleryUtils/deleteItem", `Failed to remove ${name} from gallery`); } NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); diff --git a/src/Utils/NotebookConfigurationUtils.ts b/src/Utils/NotebookConfigurationUtils.ts index 815fa435e..20a10fc76 100644 --- a/src/Utils/NotebookConfigurationUtils.ts +++ b/src/Utils/NotebookConfigurationUtils.ts @@ -1,5 +1,6 @@ import * as DataModels from "../Contracts/DataModels"; import * as Logger from "../Common/Logger"; +import { getErrorMessage } from "../Common/ErrorHandlingUtils"; interface KernelConnectionMetadata { name: string; @@ -78,13 +79,13 @@ export class NotebookConfigurationUtils { if (!response.ok) { const responseMessage = await response.json(); Logger.logError( - JSON.stringify(responseMessage), + getErrorMessage(responseMessage), "NotebookConfigurationUtils/configureServiceEndpoints", response.status ); } } catch (error) { - Logger.logError(error, "NotebookConfigurationUtils/configureServiceEndpoints"); + Logger.logError(getErrorMessage(error), "NotebookConfigurationUtils/configureServiceEndpoints"); } } } From 53a8cea95e5ded17525895cc5ca1aa0c1d1555d3 Mon Sep 17 00:00:00 2001 From: Tanuj Mittal Date: Wed, 4 Nov 2020 16:11:36 -0800 Subject: [PATCH 07/13] Hide Settings for Cassandra Serverless accounts (#311) In case of Serverless Cassandra accounts we don't have any Settings to tweak in the Settings Tab. So this change hides the option to open Settings tab in those cases. --- src/Explorer/Tree/ResourceTreeAdapter.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 6ef9f5623..b5bf40448 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -280,12 +280,14 @@ export class ResourceTreeAdapter implements ReactAdapter { contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection) }); - children.push({ - label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings", - onClick: collection.onSettingsClick.bind(collection), - isSelected: () => - this.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Settings]) - }); + if (!this.container.isPreferredApiCassandra() || !this.container.isServerlessEnabled()) { + children.push({ + label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings", + onClick: collection.onSettingsClick.bind(collection), + isSelected: () => + this.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.Settings]) + }); + } if (ResourceTreeAdapter.showScriptNodes(this.container)) { children.push(this.buildStoredProcedureNode(collection)); From 79dec6a8a8ac9da17ff80bf50cd95f61579c952c Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Thu, 5 Nov 2020 20:02:57 -0800 Subject: [PATCH 08/13] Refactor error handling in data explorer Part 3 (#315) - Make sure we pass the error message string instead of an error object when we call `TelemetryProcessor.traceFailure` since TelemetryProcessor will call `JSON.stringify` on the error object which would result in an empty object - Removed ErrorParserUtility since it only works on specific error types. We can just log the full error message and manually derive information we need from the message. - Added option to include stack trace in `getErrorMessage`. This is useful for figuring out where the client side script errors are coming from. - Some minor refactors --- .eslintignore | 2 - src/Common/ErrorHandlingUtils.ts | 4 ++ src/Common/ErrorParserUtility.test.ts | 24 -------- src/Common/ErrorParserUtility.ts | 55 ------------------- src/Common/QueriesClient.ts | 13 ++--- src/Contracts/DataModels.ts | 12 ---- .../QueriesGridComponent.tsx | 5 +- .../Controls/Settings/SettingsComponent.tsx | 12 ++-- src/Explorer/Explorer.ts | 31 +++++++---- src/Explorer/Panes/AddCollectionPane.ts | 6 +- src/Explorer/Panes/AddDatabasePane.ts | 17 +++--- src/Explorer/Panes/BrowseQueriesPane.ts | 5 +- .../Panes/CassandraAddCollectionPane.ts | 9 ++- .../Panes/DeleteCollectionConfirmationPane.ts | 14 +++-- .../Panes/DeleteDatabaseConfirmationPane.ts | 15 ++--- src/Explorer/Panes/SaveQueryPane.ts | 8 ++- src/Explorer/Panes/SetupNotebooksPane.ts | 5 +- src/Explorer/Panes/UploadItemsPane.ts | 8 +-- src/Explorer/Panes/UploadItemsPaneAdapter.tsx | 8 +-- .../DataTable/TableEntityListViewModel.ts | 18 ++---- src/Explorer/Tabs/ConflictsTab.ts | 33 ++++++----- src/Explorer/Tabs/DatabaseSettingsTab.ts | 8 ++- src/Explorer/Tabs/DocumentsTab.ts | 42 ++++++++------ src/Explorer/Tabs/MongoDocumentsTab.ts | 28 ++++++---- src/Explorer/Tabs/QueryTab.html | 18 +++--- src/Explorer/Tabs/QueryTab.ts | 28 ++++------ src/Explorer/Tabs/SettingsTab.ts | 5 +- src/Explorer/Tabs/SettingsTabV2.tsx | 9 +-- src/Explorer/Tabs/StoredProcedureTab.ts | 11 +++- src/Explorer/Tabs/TriggerTab.ts | 9 ++- src/Explorer/Tabs/UserDefinedFunctionTab.ts | 9 ++- src/Explorer/Tree/Collection.ts | 9 ++- src/Explorer/Tree/Database.ts | 5 +- 33 files changed, 218 insertions(+), 267 deletions(-) delete mode 100644 src/Common/ErrorParserUtility.test.ts delete mode 100644 src/Common/ErrorParserUtility.ts diff --git a/.eslintignore b/.eslintignore index 65d47bd0e..5d9d08948 100644 --- a/.eslintignore +++ b/.eslintignore @@ -15,8 +15,6 @@ src/Common/DeleteFeedback.ts src/Common/DocumentClientUtilityBase.ts src/Common/EditableUtility.ts src/Common/EnvironmentUtility.ts -src/Common/ErrorParserUtility.test.ts -src/Common/ErrorParserUtility.ts src/Common/HashMap.test.ts src/Common/HashMap.ts src/Common/HeadersUtility.test.ts diff --git a/src/Common/ErrorHandlingUtils.ts b/src/Common/ErrorHandlingUtils.ts index 2e2f51365..f384f6b73 100644 --- a/src/Common/ErrorHandlingUtils.ts +++ b/src/Common/ErrorHandlingUtils.ts @@ -26,6 +26,10 @@ export const getErrorMessage = (error: string | Error): string => { return replaceKnownError(errorMessage); }; +export const getErrorStack = (error: string | Error): string => { + return typeof error === "string" ? undefined : error.stack; +}; + const sendNotificationForError = (errorMessage: string, errorCode: number | string): void => { if (errorCode === HttpStatusCodes.Forbidden) { if (errorMessage?.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) { diff --git a/src/Common/ErrorParserUtility.test.ts b/src/Common/ErrorParserUtility.test.ts deleted file mode 100644 index a08c5f575..000000000 --- a/src/Common/ErrorParserUtility.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as ErrorParserUtility from "./ErrorParserUtility"; - -describe("Error Parser Utility", () => { - describe("shouldEnableCrossPartitionKeyForResourceWithPartitionKey()", () => { - it("should parse a backend error correctly", () => { - // A fake error matching what is thrown by the SDK on a bad collection create request - const innerMessage = - "The partition key component definition path '/asdwqr31 @#$#$WRadf' could not be accepted, failed near position '10'. Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character."; - const message = `Message: {\"Errors\":[\"${innerMessage}\"]}\r\nActivityId: 97b2e684-7505-4921-85f6-2513b9b28220, Request URI: /apps/89fdcf25-2a0b-4d2a-aab6-e161e565b26f/services/54911149-7bb1-4e7d-a1fa-22c8b36a4bb9/partitions/cc2a7a04-5f5a-4709-bcf7-8509b264963f/replicas/132304018743619218p, RequestStats: , SDK: Microsoft.Azure.Documents.Common/2.10.0`; - const err = new Error(message) as any; - err.code = 400; - err.body = { - code: "BadRequest", - message - }; - err.headers = {}; - err.activityId = "97b2e684-7505-4921-85f6-2513b9b28220"; - - const parsedError = ErrorParserUtility.parse(err); - expect(parsedError.length).toBe(1); - expect(parsedError[0].message).toBe(innerMessage); - }); - }); -}); diff --git a/src/Common/ErrorParserUtility.ts b/src/Common/ErrorParserUtility.ts deleted file mode 100644 index 968737f5c..000000000 --- a/src/Common/ErrorParserUtility.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as DataModels from "../Contracts/DataModels"; - -export function parse(err: any): DataModels.ErrorDataModel[] { - try { - return _parse(err); - } catch (e) { - return [{ message: JSON.stringify(err) }]; - } -} - -function _parse(err: any): DataModels.ErrorDataModel[] { - var normalizedErrors: DataModels.ErrorDataModel[] = []; - if (err.message && !err.code) { - normalizedErrors.push(err); - } else { - const innerErrors: any[] = _getInnerErrors(err.message); - normalizedErrors = innerErrors.map(innerError => - typeof innerError === "string" ? { message: innerError } : innerError - ); - } - - return normalizedErrors; -} - -function _getInnerErrors(message: string): any[] { - /* - The backend error message has an inner-message which is a stringified object. - - For SQL errors, the "errors" property is an array of SqlErrorDataModel. - Example: - "Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p" - For non-SQL errors the "Errors" propery is an array of string. - Example: - "Message: {"errors":[{"severity":"Error","location":{"start":7,"end":8},"code":"SC1001","message":"Syntax error, incorrect syntax near '.'."}]}\r\nActivityId: d3300016d4084e310a, Request URI: /apps/12401f9e1df77/services/dc100232b1f44545/partitions/f86f3bc0001a2f78/replicas/13085003638s" - */ - - let innerMessage: any = null; - - const singleLineMessage = message.replace(/[\r\n]|\r|\n/g, ""); - try { - // Multi-Partition error flavor - const regExp = /^(.*)ActivityId: (.*)/g; - const regString = regExp.exec(singleLineMessage); - const innerMessageString = regString[1]; - innerMessage = JSON.parse(innerMessageString); - } catch (e) { - // Single-partition error flavor - const regExp = /^Message: (.*)ActivityId: (.*), Request URI: (.*)/g; - const regString = regExp.exec(singleLineMessage); - const innerMessageString = regString[1]; - innerMessage = JSON.parse(innerMessageString); - } - - return innerMessage.errors ? innerMessage.errors : innerMessage.Errors; -} diff --git a/src/Common/QueriesClient.ts b/src/Common/QueriesClient.ts index fb81d1c65..a7f28f7b7 100644 --- a/src/Common/QueriesClient.ts +++ b/src/Common/QueriesClient.ts @@ -13,7 +13,6 @@ import { userContext } from "../UserContext"; import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase"; import { createCollection } from "./dataAccess/createCollection"; import { handleError } from "./ErrorHandlingUtils"; -import * as ErrorParserUtility from "./ErrorParserUtility"; export class QueriesClient { private static readonly PartitionKey: DataModels.PartitionKey = { @@ -97,15 +96,11 @@ export class QueriesClient { return Promise.resolve(); }, (error: any) => { - let errorMessage: string; - const parsedError: DataModels.ErrorDataModel = ErrorParserUtility.parse(error)[0]; - if (parsedError.code === HttpStatusCodes.Conflict.toString()) { - errorMessage = `Query ${query.queryName} already exists`; - } else { - errorMessage = parsedError.message; + if (error.code === HttpStatusCodes.Conflict.toString()) { + error = `Query ${query.queryName} already exists`; } - handleError(errorMessage, "saveQuery", `Failed to save query ${query.queryName}`); - return Promise.reject(errorMessage); + handleError(error, "saveQuery", `Failed to save query ${query.queryName}`); + return Promise.reject(error); } ) .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index b1a467b63..3aadf14cd 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -216,18 +216,6 @@ export interface UniqueKey { paths: string[]; } -// Returned by DocumentDb client proxy -// Inner errors in BackendErrorDataModel when error is in SQL syntax -export interface ErrorDataModel { - message: string; - severity?: string; - location?: { - start: string; - end: string; - }; - code?: string; -} - export interface CreateDatabaseAndCollectionRequest { databaseId: string; collectionId: string; diff --git a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx index 636736577..2b732ce89 100644 --- a/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx +++ b/src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx @@ -28,6 +28,7 @@ import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcesso import SaveQueryBannerIcon from "../../../../images/save_query_banner.png"; import { QueriesClient } from "../../../Common/QueriesClient"; +import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; export interface QueriesGridComponentProps { queriesClient: QueriesClient; @@ -244,7 +245,9 @@ export class QueriesGridComponent extends React.Component { + error => { if (resourceTreeStartKey != null) { TelemetryProcessor.traceFailure( Action.LoadResourceTree, @@ -1484,7 +1485,8 @@ export default class Explorer { databaseAccountName: this.databaseAccount() && this.databaseAccount().name, defaultExperience: this.defaultExperience && this.defaultExperience(), dataExplorerArea: Constants.Areas.ResourceTree, - error: reason + error: getErrorMessage(error), + errorStack: getErrorStack(error) }, resourceTreeStartKey ); @@ -1689,7 +1691,10 @@ export default class Explorer { TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace); } catch (error) { NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to reset notebook workspace: ${error}`); - TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, error); + TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, { + error: getErrorMessage(error), + errorStack: getErrorStack(error) + }); throw error; } finally { NotificationConsoleUtils.clearInProgressMessageWithId(id); @@ -2056,7 +2061,8 @@ export default class Explorer { databaseAccountName: this.databaseAccount() && this.databaseAccount().name, defaultExperience: this.defaultExperience && this.defaultExperience(), dataExplorerArea: Constants.Areas.ResourceTree, - trace: getErrorMessage(error) + error: getErrorMessage(error), + errorStack: getErrorStack(error) }, startKey ); @@ -2718,16 +2724,17 @@ export default class Explorer { return this.openNotebook(newFile); }) .then(() => this.resourceTree.triggerRender()) - .catch((reason: any) => { - const error = `Failed to create a new notebook: ${reason}`; - NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); + .catch((error: any) => { + const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`; + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage); TelemetryProcessor.traceFailure( Action.CreateNewNotebook, { databaseAccountName: this.databaseAccount().name, defaultExperience: this.defaultExperience(), dataExplorerArea: Constants.Areas.Notebook, - error + error: errorMessage, + errorStack: getErrorStack(error) }, startKey ); diff --git a/src/Explorer/Panes/AddCollectionPane.ts b/src/Explorer/Panes/AddCollectionPane.ts index 8c38a7809..204435904 100644 --- a/src/Explorer/Panes/AddCollectionPane.ts +++ b/src/Explorer/Panes/AddCollectionPane.ts @@ -3,7 +3,6 @@ import * as AddCollectionUtility from "../../Shared/AddCollectionUtility"; import * as AutoPilotUtils from "../../Utils/AutoPilotUtils"; import * as Constants from "../../Common/Constants"; import * as DataModels from "../../Contracts/DataModels"; -import * as ErrorParserUtility from "../../Common/ErrorParserUtility"; import * as ko from "knockout"; import * as PricingUtils from "../../Utils/PricingUtils"; import * as SharedConstants from "../../Shared/Constants"; @@ -15,7 +14,7 @@ import { configContext, Platform } from "../../ConfigContext"; import { ContextualPaneBase } from "./ContextualPaneBase"; import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent"; import { createCollection } from "../../Common/dataAccess/createCollection"; -import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; export interface AddCollectionPaneOptions extends ViewModels.PaneOptions { isPreferredApiTable: ko.Computed; @@ -912,7 +911,8 @@ export default class AddCollectionPane extends ContextualPaneBase { flight: this.container.flight() }, dataExplorerArea: Constants.Areas.ContextualPane, - error: errorMessage + error: errorMessage, + errorStack: getErrorStack(error) }; TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey); } diff --git a/src/Explorer/Panes/AddDatabasePane.ts b/src/Explorer/Panes/AddDatabasePane.ts index b91496161..2010a1c60 100644 --- a/src/Explorer/Panes/AddDatabasePane.ts +++ b/src/Explorer/Panes/AddDatabasePane.ts @@ -1,7 +1,6 @@ import * as AutoPilotUtils from "../../Utils/AutoPilotUtils"; import * as Constants from "../../Common/Constants"; import * as DataModels from "../../Contracts/DataModels"; -import * as ErrorParserUtility from "../../Common/ErrorParserUtility"; import * as ko from "knockout"; import * as PricingUtils from "../../Utils/PricingUtils"; import * as SharedConstants from "../../Shared/Constants"; @@ -12,6 +11,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan import { ContextualPaneBase } from "./ContextualPaneBase"; import { createDatabase } from "../../Common/dataAccess/createDatabase"; import { configContext, Platform } from "../../ConfigContext"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; export default class AddDatabasePane extends ContextualPaneBase { public defaultExperience: ko.Computed; @@ -306,8 +306,8 @@ export default class AddDatabasePane extends ContextualPaneBase { (database: DataModels.Database) => { this._onCreateDatabaseSuccess(offerThroughput, startKey); }, - (reason: any) => { - this._onCreateDatabaseFailure(reason, offerThroughput, reason); + (error: any) => { + this._onCreateDatabaseFailure(error, offerThroughput, startKey); } ); } @@ -356,11 +356,11 @@ export default class AddDatabasePane extends ContextualPaneBase { this.resetData(); } - private _onCreateDatabaseFailure(reason: any, offerThroughput: number, startKey: number): void { + private _onCreateDatabaseFailure(error: any, offerThroughput: number, startKey: number): void { this.isExecuting(false); - const message = ErrorParserUtility.parse(reason); - this.formErrors(message[0].message); - this.formErrorsDetails(message[0].message); + const errorMessage = getErrorMessage(error); + this.formErrors(errorMessage); + this.formErrorsDetails(errorMessage); const addDatabasePaneFailedMessage = { databaseAccountName: this.container.databaseAccount().name, defaultExperience: this.container.defaultExperience(), @@ -375,7 +375,8 @@ export default class AddDatabasePane extends ContextualPaneBase { flight: this.container.flight() }, dataExplorerArea: Constants.Areas.ContextualPane, - error: reason + error: errorMessage, + errorStack: getErrorStack(error) }; TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey); } diff --git a/src/Explorer/Panes/BrowseQueriesPane.ts b/src/Explorer/Panes/BrowseQueriesPane.ts index af3ef0a15..06ad64eef 100644 --- a/src/Explorer/Panes/BrowseQueriesPane.ts +++ b/src/Explorer/Panes/BrowseQueriesPane.ts @@ -7,7 +7,7 @@ import * as Logger from "../../Common/Logger"; import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import QueryTab from "../Tabs/QueryTab"; -import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; export class BrowseQueriesPane extends ContextualPaneBase { public queriesGridComponentAdapter: QueriesGridComponentAdapter; @@ -69,7 +69,8 @@ export class BrowseQueriesPane extends ContextualPaneBase { defaultExperience: this.container && this.container.defaultExperience(), dataExplorerArea: Areas.ContextualPane, paneTitle: this.title(), - error: errorMessage + error: errorMessage, + errorStack: getErrorStack(error) }, startKey ); diff --git a/src/Explorer/Panes/CassandraAddCollectionPane.ts b/src/Explorer/Panes/CassandraAddCollectionPane.ts index 5ba82c627..1fa823a75 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane.ts +++ b/src/Explorer/Panes/CassandraAddCollectionPane.ts @@ -13,6 +13,7 @@ import { CassandraAPIDataClient } from "../Tables/TableDataClient"; import { ContextualPaneBase } from "./ContextualPaneBase"; import { HashMap } from "../../Common/HashMap"; import { configContext, Platform } from "../../ConfigContext"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; export default class CassandraAddCollectionPane extends ContextualPaneBase { public createTableQuery: ko.Observable; @@ -429,8 +430,9 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { }; TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneSuccessMessage, startKey); }, - reason => { - this.formErrors(reason.exceptionMessage); + error => { + const errorMessage = getErrorMessage(error); + this.formErrors(errorMessage); this.isExecuting(false); const addCollectionPaneFailedMessage = { databaseAccountName: this.container.databaseAccount().name, @@ -456,7 +458,8 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { toCreateKeyspace: toCreateKeyspace, createKeyspaceQuery: createKeyspaceQuery, createTableQuery: createTableQuery, - error: reason + error: errorMessage, + errorStack: getErrorStack(error) }; TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey); } diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane.ts b/src/Explorer/Panes/DeleteCollectionConfirmationPane.ts index 5af4cd9d4..2c5e67e1e 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane.ts +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane.ts @@ -3,7 +3,6 @@ import Q from "q"; import * as ViewModels from "../../Contracts/ViewModels"; import * as Constants from "../../Common/Constants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; -import * as ErrorParserUtility from "../../Common/ErrorParserUtility"; import { CassandraAPIDataClient } from "../Tables/TableDataClient"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ContextualPaneBase } from "./ContextualPaneBase"; @@ -12,6 +11,7 @@ import DeleteFeedback from "../../Common/DeleteFeedback"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { deleteCollection } from "../../Common/dataAccess/deleteCollection"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; export default class DeleteCollectionConfirmationPane extends ContextualPaneBase { public collectionIdConfirmationText: ko.Observable; @@ -99,11 +99,11 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase this.containerDeleteFeedback(""); } }, - (reason: any) => { + (error: any) => { this.isExecuting(false); - const message = ErrorParserUtility.parse(reason); - this.formErrors(message[0].message); - this.formErrorsDetails(message[0].message); + const errorMessage = getErrorMessage(error); + this.formErrors(errorMessage); + this.formErrorsDetails(errorMessage); TelemetryProcessor.traceFailure( Action.DeleteCollection, { @@ -111,7 +111,9 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase defaultExperience: this.container.defaultExperience(), collectionId: selectedCollection.id(), dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: this.title() + paneTitle: this.title(), + error: errorMessage, + errorStack: getErrorStack(error) }, startKey ); diff --git a/src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts b/src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts index 980d41ee8..4ec595601 100644 --- a/src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts +++ b/src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts @@ -3,7 +3,6 @@ import Q from "q"; import * as Constants from "../../Common/Constants"; import * as ViewModels from "../../Contracts/ViewModels"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; -import * as ErrorParserUtility from "../../Common/ErrorParserUtility"; import { CassandraAPIDataClient } from "../Tables/TableDataClient"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ContextualPaneBase } from "./ContextualPaneBase"; @@ -14,6 +13,7 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils" import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase"; import { ARMError } from "../../Utils/arm/request"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase { public databaseIdConfirmationText: ko.Observable; @@ -108,12 +108,11 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase { this.databaseDeleteFeedback(""); } }, - (reason: unknown) => { + (error: any) => { this.isExecuting(false); - - const message = reason instanceof ARMError ? reason.message : ErrorParserUtility.parse(reason)[0].message; - this.formErrors(message); - this.formErrorsDetails(message); + const errorMessage = getErrorMessage(error); + this.formErrors(errorMessage); + this.formErrorsDetails(errorMessage); TelemetryProcessor.traceFailure( Action.DeleteDatabase, { @@ -121,7 +120,9 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase { defaultExperience: this.container.defaultExperience(), databaseId: selectedDatabase.id(), dataExplorerArea: Constants.Areas.ContextualPane, - paneTitle: this.title() + paneTitle: this.title(), + error: errorMessage, + errorStack: getErrorStack(error) }, startKey ); diff --git a/src/Explorer/Panes/SaveQueryPane.ts b/src/Explorer/Panes/SaveQueryPane.ts index b379f6d3a..3ddc6756d 100644 --- a/src/Explorer/Panes/SaveQueryPane.ts +++ b/src/Explorer/Panes/SaveQueryPane.ts @@ -8,7 +8,7 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import QueryTab from "../Tabs/QueryTab"; -import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; export class SaveQueryPane extends ContextualPaneBase { public queryName: ko.Observable; @@ -98,7 +98,8 @@ export class SaveQueryPane extends ContextualPaneBase { defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.ContextualPane, paneTitle: this.title(), - error: errorMessage + error: errorMessage, + errorStack: getErrorStack(error) }, startKey ); @@ -140,7 +141,8 @@ export class SaveQueryPane extends ContextualPaneBase { defaultExperience: this.container && this.container.defaultExperience(), dataExplorerArea: Constants.Areas.ContextualPane, paneTitle: this.title(), - error: errorMessage + error: errorMessage, + errorStack: getErrorStack(error) }, startKey ); diff --git a/src/Explorer/Panes/SetupNotebooksPane.ts b/src/Explorer/Panes/SetupNotebooksPane.ts index a95bcb667..8f00572ea 100644 --- a/src/Explorer/Panes/SetupNotebooksPane.ts +++ b/src/Explorer/Panes/SetupNotebooksPane.ts @@ -6,7 +6,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as ko from "knockout"; -import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; export class SetupNotebooksPane extends ContextualPaneBase { private description: ko.Observable; @@ -94,7 +94,8 @@ export class SetupNotebooksPane extends ContextualPaneBase { defaultExperience: this.container && this.container.defaultExperience(), dataExplorerArea: Areas.ContextualPane, paneTitle: this.title(), - error: errorMessage + error: errorMessage, + errorStack: getErrorStack(error) }, startKey ); diff --git a/src/Explorer/Panes/UploadItemsPane.ts b/src/Explorer/Panes/UploadItemsPane.ts index 7b1f227a9..a1d4255eb 100644 --- a/src/Explorer/Panes/UploadItemsPane.ts +++ b/src/Explorer/Panes/UploadItemsPane.ts @@ -4,8 +4,8 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { ContextualPaneBase } from "./ContextualPaneBase"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; -import * as ErrorParserUtility from "../../Common/ErrorParserUtility"; import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; const UPLOAD_FILE_SIZE_LIMIT = 2097152; @@ -61,9 +61,9 @@ export class UploadItemsPane extends ContextualPaneBase { this._resetFileInput(); }, (error: any) => { - const message = ErrorParserUtility.parse(error); - this.formErrors(message[0].message); - this.formErrorsDetails(message[0].message); + const errorMessage = getErrorMessage(error); + this.formErrors(errorMessage); + this.formErrorsDetails(errorMessage); } ) .finally(() => { diff --git a/src/Explorer/Panes/UploadItemsPaneAdapter.tsx b/src/Explorer/Panes/UploadItemsPaneAdapter.tsx index 4c78501cc..37e514c61 100644 --- a/src/Explorer/Panes/UploadItemsPaneAdapter.tsx +++ b/src/Explorer/Panes/UploadItemsPaneAdapter.tsx @@ -1,4 +1,3 @@ -import * as ErrorParserUtility from "../../Common/ErrorParserUtility"; import * as ko from "knockout"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as React from "react"; @@ -9,6 +8,7 @@ import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions"; import { UploadItemsPaneComponent, UploadItemsPaneProps } from "./UploadItemsPaneComponent"; import Explorer from "../Explorer"; +import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; const UPLOAD_FILE_SIZE_LIMIT = 2097152; @@ -107,9 +107,9 @@ export class UploadItemsPaneAdapter implements ReactAdapter { this.selectedFilesTitle = ""; }, error => { - const message = ErrorParserUtility.parse(error); - this.formError = message[0].message; - this.formErrorDetail = message[0].message; + const errorMessage = getErrorMessage(error); + this.formError = errorMessage; + this.formErrorDetail = errorMessage; } ) .finally(() => { diff --git a/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts b/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts index 0ae172404..1991d7012 100644 --- a/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts +++ b/src/Explorer/Tables/DataTable/TableEntityListViewModel.ts @@ -16,9 +16,9 @@ import * as Entities from "../Entities"; import QueryTablesTab from "../../Tabs/QueryTablesTab"; import * as TableEntityProcessor from "../TableEntityProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; -import * as ErrorParserUtility from "../../../Common/ErrorParserUtility"; import * as DataModels from "../../../Contracts/DataModels"; import * as ViewModels from "../../../Contracts/ViewModels"; +import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; interface IListTableEntitiesSegmentedResult extends Entities.IListTableEntitiesResult { ExceedMaximumRetries?: boolean; @@ -387,17 +387,8 @@ export default class TableEntityListViewModel extends DataTableViewModel { } }) .catch((error: any) => { - const parsedErrors = ErrorParserUtility.parse(error); - var errors = parsedErrors.map((error: DataModels.ErrorDataModel) => { - return { - message: error.message, - start: error.location ? error.location.start : undefined, - end: error.location ? error.location.end : undefined, - code: error.code, - severity: error.severity - }; - }); - this.queryErrorMessage(errors[0].message); + const errorMessage = getErrorMessage(error); + this.queryErrorMessage(errorMessage); if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) { TelemetryProcessor.traceFailure( Action.Tab, @@ -408,7 +399,8 @@ export default class TableEntityListViewModel extends DataTableViewModel { defaultExperience: this.queryTablesTab.collection.container.defaultExperience(), dataExplorerArea: Areas.Tab, tabTitle: this.queryTablesTab.tabTitle(), - error: error + error: errorMessage, + errorStack: getErrorStack(error) }, this.queryTablesTab.onLoadStartKey ); diff --git a/src/Explorer/Tabs/ConflictsTab.ts b/src/Explorer/Tabs/ConflictsTab.ts index a5c188805..bbe5370c0 100644 --- a/src/Explorer/Tabs/ConflictsTab.ts +++ b/src/Explorer/Tabs/ConflictsTab.ts @@ -6,7 +6,6 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; import { KeyCodes } from "../../Common/Constants"; -import * as ErrorParserUtility from "../../Common/ErrorParserUtility"; import ConflictId from "../Tree/ConflictId"; import editable from "../../Common/EditableUtility"; import * as HeadersUtility from "../../Common/HeadersUtility"; @@ -28,6 +27,7 @@ import { updateDocument } from "../../Common/DocumentClientUtilityBase"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; export default class ConflictsTab extends TabsBase { public selectedConflictId: ko.Observable; @@ -241,9 +241,8 @@ export default class ConflictsTab extends TabsBase { return this.loadNextPage(); } ) - .catch(reason => { - const message = ErrorParserUtility.parse(reason)[0].message; - window.alert(message); + .catch(error => { + window.alert(getErrorMessage(error)); }); } @@ -336,10 +335,10 @@ export default class ConflictsTab extends TabsBase { ); }); }, - reason => { + error => { this.isExecutionError(true); - const message = ErrorParserUtility.parse(reason)[0].message; - window.alert(message); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); TelemetryProcessor.traceFailure( Action.ResolveConflict, { @@ -349,7 +348,9 @@ export default class ConflictsTab extends TabsBase { tabTitle: this.tabTitle(), conflictResourceType: selectedConflict.resourceType, conflictOperationType: selectedConflict.operationType, - conflictResourceId: selectedConflict.resourceId + conflictResourceId: selectedConflict.resourceId, + error: errorMessage, + errorStack: getErrorStack(error) }, startKey ); @@ -396,10 +397,10 @@ export default class ConflictsTab extends TabsBase { startKey ); }, - reason => { + error => { this.isExecutionError(true); - const message = ErrorParserUtility.parse(reason)[0].message; - window.alert(message); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); TelemetryProcessor.traceFailure( Action.DeleteConflict, { @@ -409,7 +410,9 @@ export default class ConflictsTab extends TabsBase { tabTitle: this.tabTitle(), conflictResourceType: selectedConflict.resourceType, conflictOperationType: selectedConflict.operationType, - conflictResourceId: selectedConflict.resourceId + conflictResourceId: selectedConflict.resourceId, + error: errorMessage, + errorStack: getErrorStack(error) }, startKey ); @@ -470,7 +473,8 @@ export default class ConflictsTab extends TabsBase { defaultExperience: this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), - error: error + error: getErrorMessage(error), + errorStack: getErrorStack(error) }, this.onLoadStartKey ); @@ -545,7 +549,8 @@ export default class ConflictsTab extends TabsBase { defaultExperience: this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), - error: error + error: getErrorMessage(error), + errorStack: getErrorStack(error) }, this.onLoadStartKey ); diff --git a/src/Explorer/Tabs/DatabaseSettingsTab.ts b/src/Explorer/Tabs/DatabaseSettingsTab.ts index cd32dc301..085d7d3be 100644 --- a/src/Explorer/Tabs/DatabaseSettingsTab.ts +++ b/src/Explorer/Tabs/DatabaseSettingsTab.ts @@ -1,7 +1,6 @@ import * as AutoPilotUtils from "../../Utils/AutoPilotUtils"; import * as Constants from "../../Common/Constants"; import * as DataModels from "../../Contracts/DataModels"; -import * as ErrorParserUtility from "../../Common/ErrorParserUtility"; import * as ko from "knockout"; import * as PricingUtils from "../../Utils/PricingUtils"; import * as SharedConstants from "../../Shared/Constants"; @@ -20,6 +19,7 @@ import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandBu import { userContext } from "../../UserContext"; import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit"; import { configContext, Platform } from "../../ConfigContext"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; const updateThroughputBeyondLimitWarningMessage: string = ` You are about to request an increase in throughput beyond the pre-allocated capacity. @@ -490,7 +490,8 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. this.container.isRefreshingExplorer(false); this.isExecutionError(true); console.error(error); - this.displayedError(ErrorParserUtility.parse(error)[0].message); + const errorMessage = getErrorMessage(error); + this.displayedError(errorMessage); TelemetryProcessor.traceFailure( Action.UpdateSettings, { @@ -499,7 +500,8 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels. defaultExperience: this.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), - error: error + error: errorMessage, + errorStack: getErrorStack(error) }, startKey ); diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 368c85ada..03fd9777c 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -6,7 +6,6 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList"; import { KeyCodes } from "../../Common/Constants"; -import * as ErrorParserUtility from "../../Common/ErrorParserUtility"; import DocumentId from "../Tree/DocumentId"; import editable from "../../Common/EditableUtility"; import * as HeadersUtility from "../../Common/HeadersUtility"; @@ -32,7 +31,7 @@ import { createDocument } from "../../Common/DocumentClientUtilityBase"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; -import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; export default class DocumentsTab extends TabsBase { public selectedDocumentId: ko.Observable; @@ -393,9 +392,8 @@ export default class DocumentsTab extends TabsBase { const focusElement = document.getElementById("errorStatusIcon"); focusElement && focusElement.focus(); }) - .catch(reason => { - const message = ErrorParserUtility.parse(reason)[0].message; - window.alert(message); + .catch(error => { + window.alert(getErrorMessage(error)); }); } @@ -475,17 +473,19 @@ export default class DocumentsTab extends TabsBase { startKey ); }, - reason => { + error => { this.isExecutionError(true); - const message = ErrorParserUtility.parse(reason)[0].message; - window.alert(message); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); TelemetryProcessor.traceFailure( Action.CreateDocument, { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), + error: errorMessage, + errorStack: getErrorStack(error) }, startKey ); @@ -542,17 +542,19 @@ export default class DocumentsTab extends TabsBase { startKey ); }, - reason => { + error => { this.isExecutionError(true); - const message = ErrorParserUtility.parse(reason)[0].message; - window.alert(message); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); TelemetryProcessor.traceFailure( Action.UpdateDocument, { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), + error: errorMessage, + errorStack: getErrorStack(error) }, startKey ); @@ -643,7 +645,8 @@ export default class DocumentsTab extends TabsBase { defaultExperience: this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), - error: error + error: getErrorMessage(error), + errorStack: getErrorStack(error) }, this.onLoadStartKey ); @@ -697,16 +700,18 @@ export default class DocumentsTab extends TabsBase { startKey ); }, - reason => { + error => { this.isExecutionError(true); - console.error(reason); + console.error(error); TelemetryProcessor.traceFailure( Action.DeleteDocument, { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), + error: getErrorMessage(error), + errorStack: getErrorStack(error) }, startKey ); @@ -787,7 +792,8 @@ export default class DocumentsTab extends TabsBase { defaultExperience: this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), - error: errorMessage + error: errorMessage, + errorStack: getErrorStack(error) }, this.onLoadStartKey ); diff --git a/src/Explorer/Tabs/MongoDocumentsTab.ts b/src/Explorer/Tabs/MongoDocumentsTab.ts index 491f3adf0..e685d5b9c 100644 --- a/src/Explorer/Tabs/MongoDocumentsTab.ts +++ b/src/Explorer/Tabs/MongoDocumentsTab.ts @@ -4,7 +4,6 @@ import * as ko from "knockout"; import * as ViewModels from "../../Contracts/ViewModels"; import DocumentId from "../Tree/DocumentId"; import DocumentsTab from "./DocumentsTab"; -import * as ErrorParserUtility from "../../Common/ErrorParserUtility"; import MongoUtility from "../../Common/MongoUtility"; import ObjectId from "../Tree/ObjectId"; import Q from "q"; @@ -20,6 +19,7 @@ import { import { extractPartitionKey } from "@azure/cosmos"; import * as Logger from "../../Common/Logger"; import { PartitionKeyDefinition } from "@azure/cosmos"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; export default class MongoDocumentsTab extends DocumentsTab { public collection: ViewModels.Collection; @@ -72,7 +72,8 @@ export default class MongoDocumentsTab extends DocumentsTab { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), + error: message }, startKey ); @@ -114,17 +115,19 @@ export default class MongoDocumentsTab extends DocumentsTab { startKey ); }, - reason => { + error => { this.isExecutionError(true); - const message = ErrorParserUtility.parse(reason)[0].message; - window.alert(message); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); TelemetryProcessor.traceFailure( Action.CreateDocument, { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), + error: errorMessage, + errorStack: getErrorStack(error) }, startKey ); @@ -176,17 +179,19 @@ export default class MongoDocumentsTab extends DocumentsTab { startKey ); }, - reason => { + error => { this.isExecutionError(true); - const message = ErrorParserUtility.parse(reason)[0].message; - window.alert(message); + const errorMessage = getErrorMessage(error); + window.alert(errorMessage); TelemetryProcessor.traceFailure( Action.UpdateDocument, { databaseAccountName: this.collection && this.collection.container.databaseAccount().name, defaultExperience: this.collection && this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle() + tabTitle: this.tabTitle(), + error: errorMessage, + errorStack: getErrorStack(error) }, startKey ); @@ -267,7 +272,8 @@ export default class MongoDocumentsTab extends DocumentsTab { defaultExperience: this.collection.container.defaultExperience(), dataExplorerArea: Constants.Areas.Tab, tabTitle: this.tabTitle(), - error: error + error: getErrorMessage(error), + errorStack: getErrorStack(error) }, this.onLoadStartKey ); diff --git a/src/Explorer/Tabs/QueryTab.html b/src/Explorer/Tabs/QueryTab.html index 2f0483d03..0d16152da 100644 --- a/src/Explorer/Tabs/QueryTab.html +++ b/src/Explorer/Tabs/QueryTab.html @@ -47,7 +47,7 @@ -
+
Errors
@@ -56,16 +56,16 @@

Execute Query Watermark

Execute a query to see the results

-