Merge branch 'master' into users/srnara/pupeteerForNotebooks

This commit is contained in:
Srinath Narayanan
2020-11-17 11:07:16 -08:00
169 changed files with 2554 additions and 6136 deletions

View File

@@ -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<ArcadiaMenuPickerProps, A
selectedSparkPool: item.text
});
} catch (error) {
Logger.logError(error, "ArcadiaMenuPicker/_onSparkPoolClicked");
Logger.logError(getErrorMessage(error), "ArcadiaMenuPicker/_onSparkPoolClicked");
throw error;
}
};

View File

@@ -4,12 +4,10 @@
import * as React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import * as Logger from "../../../Common/Logger";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { StringUtils } from "../../../Utils/StringUtils";
import { userContext } from "../../../UserContext";
import { TerminalQueryParams } from "../../../Common/Constants";
import { handleError } from "../../../Common/ErrorHandlingUtils";
export interface NotebookTerminalComponentProps {
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
@@ -71,9 +69,10 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
params: Map<string, string>
): 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 "";
}

View File

@@ -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<CodeOfConductCompone
this.props.onAcceptCodeOfConduct(response.data);
} catch (error) {
const message = `Failed to accept code of conduct: ${error}`;
Logger.logError(message, "CodeOfConductComponent/acceptCodeOfConduct");
logConsoleError(message);
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct");
}
}

View File

@@ -16,11 +16,8 @@ import {
Text
} from "office-ui-fabric-react";
import * as React from "react";
import * as Logger from "../../../Common/Logger";
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
import "./GalleryViewerComponent.less";
@@ -28,6 +25,7 @@ import { HttpStatusCodes } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { CodeOfConductComponent } from "./CodeOfConductComponent";
import { InfoComponent } from "./InfoComponent/InfoComponent";
import { handleError } from "../../../Common/ErrorHandlingUtils";
export interface GalleryViewerComponentProps {
container?: Explorer;
@@ -354,9 +352,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
this.sampleNotebooks = response.data;
} catch (error) {
const message = `Failed to load sample notebooks: ${error}`;
Logger.logError(message, "GalleryViewerComponent/loadSampleNotebooks");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
handleError(error, "GalleryViewerComponent/loadSampleNotebooks", "Failed to load sample notebooks");
}
}
@@ -382,9 +378,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
}
} catch (error) {
const message = `Failed to load public notebooks: ${error}`;
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
handleError(error, "GalleryViewerComponent/loadPublicNotebooks", "Failed to load public notebooks");
}
}
@@ -404,9 +398,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
this.favoriteNotebooks = response.data;
} catch (error) {
const message = `Failed to load favorite notebooks: ${error}`;
Logger.logError(message, "GalleryViewerComponent/loadFavoriteNotebooks");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
handleError(error, "GalleryViewerComponent/loadFavoriteNotebooks", "Failed to load favorite notebooks");
}
}
@@ -432,9 +424,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
this.publishedNotebooks = response.data;
} catch (error) {
const message = `Failed to load published notebooks: ${error}`;
Logger.logError(message, "GalleryViewerComponent/loadPublishedNotebooks");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
handleError(error, "GalleryViewerComponent/loadPublishedNotebooks", "Failed to load published notebooks");
}
}

View File

@@ -21,6 +21,7 @@ import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
import { DialogHost } from "../../../Utils/GalleryUtils";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
export interface NotebookViewerComponentProps {
container?: Explorer;
@@ -100,9 +101,7 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
}
} catch (error) {
this.setState({ showProgressBar: false });
const message = `Failed to load notebook content: ${error}`;
Logger.logError(message, "NotebookViewerComponent/loadNotebookContent");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
handleError(error, "NotebookViewerComponent/loadNotebookContent", "Failed to load notebook content");
}
}

View File

@@ -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<QueriesGridComponentPr
databaseAccountName: container && container.databaseAccount().name,
defaultExperience: container && container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: container && container.browseQueriesPane.title()
paneTitle: container && container.browseQueriesPane.title(),
error: getErrorMessage(error),
errorStack: getErrorStack(error)
},
startKey
);

View File

@@ -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", () => ({

View File

@@ -46,10 +46,9 @@ 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, getErrorStack } from "../../../Common/ErrorHandlingUtils";
interface SettingsV2TabInfo {
tab: SettingsV2TabTypes;
@@ -211,6 +210,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
componentDidMount(): void {
this.refreshIndexTransformationProgress();
this.loadMongoIndexes();
this.setAutoPilotStates();
this.setBaseline();
@@ -232,8 +232,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.container.isEnableMongoCapabilityPresent() &&
this.container.databaseAccount()
) {
await this.refreshIndexTransformationProgress();
this.mongoDBCollectionResource = await readMongoDBCollectionThroughRP(
this.collection.databaseId,
this.collection.id()
@@ -248,10 +246,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
};
public refreshIndexTransformationProgress = async (): Promise<void> => {
const currentProgress = await getMongoDBCollectionIndexTransformationProgress(
this.collection.databaseId,
this.collection.id()
);
const currentProgress = await getIndexTransformationProgress(this.collection.databaseId, this.collection.id());
this.setState({ indexTransformationProgress: currentProgress });
};
@@ -351,6 +346,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
break;
}
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
newCollection.defaultTtl = defaultTtl;
newCollection.indexingPolicy = this.state.indexingPolicyContent;
@@ -386,6 +382,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
if (wasIndexingPolicyModified) {
await this.refreshIndexTransformationProgress();
}
this.setState({
isSubSettingsSaveable: false,
isSubSettingsDiscardable: false,
@@ -437,7 +438,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
error: error.message
error: getErrorMessage(error),
errorStack: getErrorStack(error)
},
startKey
);
@@ -559,10 +561,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
},
startKey
);
} catch (reason) {
} catch (error) {
this.container.isRefreshingExplorer(false);
this.props.settingsTab.isExecutionError(true);
console.error(reason);
console.error(error);
traceFailure(
Action.SettingsV2Updated,
{
@@ -572,7 +574,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
error: reason.message
error: getErrorMessage(error),
errorStack: getErrorStack(error)
},
startKey
);
@@ -945,6 +948,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
indexingPolicyContentBaseline: this.state.indexingPolicyContentBaseline,
onIndexingPolicyContentChange: this.onIndexingPolicyContentChange,
logIndexingPolicySuccessMessage: this.logIndexingPolicySuccessMessage,
indexTransformationProgress: this.state.indexTransformationProgress,
refreshIndexTransformationProgress: this.refreshIndexTransformationProgress,
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange
};

View File

@@ -6,7 +6,7 @@ import {
getEstimatedAutoscaleSpendElement,
manualToAutoscaleDisclaimerElement,
ttlWarning,
indexingPolicyTTLWarningMessage,
indexingPolicynUnsavedWarningMessage,
updateThroughputBeyondLimitWarningMessage,
updateThroughputDelayedApplyWarningMessage,
getThroughputApplyDelayedMessage,
@@ -37,7 +37,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
{manualToAutoscaleDisclaimerElement}
{ttlWarning}
{indexingPolicyTTLWarningMessage}
{indexingPolicynUnsavedWarningMessage}
{updateThroughputBeyondLimitWarningMessage}
{updateThroughputDelayedApplyWarningMessage}

View File

@@ -36,7 +36,7 @@ import {
} from "office-ui-fabric-react";
import { isDirtyTypes, isDirty } from "./SettingsUtils";
const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
label: {
@@ -245,15 +245,9 @@ export const ttlWarning: JSX.Element = (
</Text>
);
export const indexingPolicyTTLWarningMessage: JSX.Element = (
export const indexingPolicynUnsavedWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
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,{" "}
<Link target="_blank" href="https://aka.ms/cosmosdb/modify-index-policy">
Modifying Indexing Policies
</Link>
.
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
</Text>
);
@@ -410,8 +404,8 @@ export const mongoIndexingPolicyAADError: JSX.Element = (
export const mongoIndexTransformationRefreshingMessage: JSX.Element = (
<Stack horizontal {...mongoWarningStackProps}>
<Text>Refreshing index transformation progress</Text>
<Spinner size={SpinnerSize.medium} />
<Text styles={infoAndToolTipTextStyle}>Refreshing index transformation progress</Text>
<Spinner size={SpinnerSize.small} />
</Stack>
);
@@ -421,14 +415,14 @@ export const renderMongoIndexTransformationRefreshMessage = (
): JSX.Element => {
if (progress === 0) {
return (
<Text>
<Text styles={infoAndToolTipTextStyle}>
{"You can make more indexing changes once the current index transformation is complete. "}
<Link onClick={performRefresh}>{"Refresh to check if it has completed."}</Link>
</Text>
);
} else {
return (
<Text>
<Text styles={infoAndToolTipTextStyle}>
{`You can make more indexing changes once the current index transformation has completed. It is ${progress}% complete. `}
<Link onClick={performRefresh}>{"Refresh to check the progress."}</Link>
</Text>

View File

@@ -25,7 +25,9 @@ describe("IndexingPolicyComponent", () => {
},
onIndexingPolicyDirtyChange: () => {
return;
}
},
indexTransformationProgress: undefined,
refreshIndexTransformationProgress: () => new Promise(jest.fn())
};
it("renders", () => {

View File

@@ -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<void>;
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 (
<Stack {...titleAndInputStackProps}>
<IndexingPolicyRefreshComponent
indexTransformationProgress={this.props.indexTransformationProgress}
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
/>
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicyTTLWarningMessage}</MessageBar>
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar>
)}
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
</Stack>

View File

@@ -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(<IndexingPolicyRefreshComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -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<void>;
}
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, "RefreshIndexTransformationProgress", "Refreshing index transformation progress failed");
} finally {
this.setState({ isRefreshing: false });
}
};
public render(): JSX.Element {
return this.renderIndexTransformationWarning() ? (
<MessageBar messageBarType={MessageBarType.warning}>{this.renderIndexTransformationWarning()}</MessageBar>
) : (
<></>
);
}
}

View File

@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`IndexingPolicyRefreshComponent renders 1`] = `
<StyledMessageBarBase
messageBarType={5}
>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
<StyledLinkBase
onClick={[Function]}
>
Refresh to check the progress.
</StyledLinkBase>
</Text>
</StyledMessageBarBase>
`;

View File

@@ -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(<MongoIndexingPolicyComponent {...baseProps} />);
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(<MongoIndexingPolicyComponent {...baseProps} />);
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();
}
}
);
});

View File

@@ -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<MongoIndexingPolicyComponentProps> {
private shouldCheckComponentIsDirty = true;
private addMongoIndexComponentRefs: React.RefObject<AddMongoIndexComponent>[] = [];
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 <Text styles={infoAndToolTipTextStyle}>{warningMessage}</Text>;
}
return undefined;
};
private onRenderRow = (props: IDetailsRowProps): JSX.Element => {
@@ -159,7 +151,7 @@ export class MongoIndexingPolicyComponent extends React.Component<
<IconButton
ariaLabel="Delete index Button"
iconProps={{ iconName: "Delete" }}
disabled={this.isIndexingTransforming()}
disabled={isIndexTransforming(this.props.indexTransformationProgress)}
onClick={() => {
this.props.onIndexDrop(arrayPosition);
}}
@@ -230,7 +222,7 @@ export class MongoIndexingPolicyComponent extends React.Component<
<AddMongoIndexComponent
ref={this.addMongoIndexComponentRefs[indexesToAddLength]}
disabled={this.isIndexingTransforming()}
disabled={isIndexTransforming(this.props.indexTransformationProgress)}
position={indexesToAddLength}
key={indexesToAddLength}
description={undefined}
@@ -298,55 +290,21 @@ export class MongoIndexingPolicyComponent extends React.Component<
);
};
private refreshIndexTransformationProgress = async () => {
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() && (
<MessageBar messageBarType={MessageBarType.warning}>{this.renderIndexTransformationWarning()}</MessageBar>
)}
{warningMessage && (
<MessageBar messageBarType={MessageBarType.warning}>
<Text>{warningMessage}</Text>
</MessageBar>
)}
<IndexingPolicyRefreshComponent
indexTransformationProgress={this.props.indexTransformationProgress}
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
/>
{warningMessage && <MessageBar messageBarType={MessageBarType.warning}>{warningMessage}</MessageBar>}
</>
);
};

View File

@@ -8,6 +8,9 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
}
}
>
<IndexingPolicyRefreshComponent
refreshIndexTransformationProgress={[Function]}
/>
<Text>
For queries that filter on multiple properties, create multiple single field indexes instead of a compound index.
<StyledLinkBase

View File

@@ -200,6 +200,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
onScaleSaveableChange={this.props.onScaleSaveableChange}
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
getThroughputWarningMessage={this.getThroughputWarningMessage}
usageSizeInKB={this.props.collection.quotaInfo().usageSizeInKB}
/>
);

View File

@@ -17,6 +17,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
minimum: 10000,
maximum: 400,
step: 100,
usageSizeInKB: 10000,
isEnabled: true,
isEmulator: false,
spendAckChecked: false,

View File

@@ -30,6 +30,10 @@ import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../S
import * as SharedConstants from "../../../../../Shared/Constants";
import * as DataModels from "../../../../../Contracts/DataModels";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import { userContext } from "../../../../../UserContext";
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
import { usageInGB } from "../../../../../Utils/PricingUtils";
import { Features } from "../../../../../Common/Constants";
export interface ThroughputInputAutoPilotV3Props {
databaseAccount: DataModels.DatabaseAccount;
@@ -60,6 +64,7 @@ export interface ThroughputInputAutoPilotV3Props {
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
getThroughputWarningMessage: () => JSX.Element;
usageSizeInKB: number;
}
interface ThroughputInputAutoPilotV3State {
@@ -224,6 +229,29 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
option?: IChoiceGroupOption
): void => this.props.onAutoPilotSelected(option.key === "true");
private minRUperGBSurvey = (): JSX.Element => {
const href = `https://ncv.microsoft.com/vRBTO37jmO?ctx={"AzureSubscriptionId":"${userContext.subscriptionId}","CosmosDBAccountName":"${userContext.databaseAccount?.name}"}`;
const oneTBinKB = 1000000000;
const minRUperGB = 10;
const featureFlagEnabled = window.dataExplorer?.isFeatureEnabled(Features.showMinRUSurvey);
const collectionIsEligible =
userContext.subscriptionType !== SubscriptionType.Internal &&
this.props.usageSizeInKB > oneTBinKB &&
this.props.minimum >= usageInGB(this.props.usageSizeInKB) * minRUperGB;
if (featureFlagEnabled || collectionIsEligible) {
return (
<Text>
Need to scale below {this.props.minimum} RU/s? Reach out by filling{" "}
<a target="_blank" rel="noreferrer" href={href}>
this questionnaire
</a>
.
</Text>
);
}
return undefined;
};
private renderThroughputModeChoices = (): JSX.Element => {
const labelId = "settingsV2RadioButtonLabelId";
return (
@@ -275,6 +303,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onChange={this.onAutoPilotThroughputChange}
/>
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
{this.minRUperGBSurvey()}
{this.props.spendAckVisible && (
<Checkbox
id="spendAckCheckBox"
@@ -305,15 +334,13 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
}
onChange={this.onThroughputChange}
/>
{this.props.getThroughputWarningMessage() && (
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
{this.props.getThroughputWarningMessage()}
</MessageBar>
)}
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
{this.minRUperGBSurvey()}
{this.props.spendAckVisible && (
<Checkbox
id="spendAckCheckBox"
@@ -323,7 +350,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onChange={this.onSpendAckChecked}
/>
)}
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
</Stack>
);

View File

@@ -8,6 +8,9 @@ exports[`IndexingPolicyComponent renders 1`] = `
}
}
>
<IndexingPolicyRefreshComponent
refreshIndexTransformationProgress={[Function]}
/>
<div
className="settingsV2IndexingPolicyEditor"
tabIndex={0}

View File

@@ -15,7 +15,8 @@ import {
MongoWildcardPlaceHolder,
getMongoIndexTypeText,
SingleFieldText,
WildcardText
WildcardText,
isIndexTransforming
} from "./SettingsUtils";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
@@ -139,3 +140,10 @@ describe("SettingsUtils", () => {
expect(notification.type).toEqual(MongoNotificationType.Error);
});
});
it("isIndexingTransforming", () => {
expect(isIndexTransforming(undefined)).toBeFalsy();
expect(isIndexTransforming(0)).toBeTruthy();
expect(isIndexTransforming(90)).toBeTruthy();
expect(isIndexTransforming(100)).toBeFalsy();
});

View File

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

View File

@@ -971,6 +971,7 @@ exports[`SettingsComponent renders 1`] = `
"isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSettingsV2Enabled": [Function],
"isSparkEnabled": [Function],
@@ -2251,6 +2252,7 @@ exports[`SettingsComponent renders 1`] = `
"isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSettingsV2Enabled": [Function],
"isSparkEnabled": [Function],
@@ -3544,6 +3546,7 @@ exports[`SettingsComponent renders 1`] = `
"isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSettingsV2Enabled": [Function],
"isSparkEnabled": [Function],
@@ -4824,6 +4827,7 @@ exports[`SettingsComponent renders 1`] = `
"isRefreshingExplorer": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
"isSchemaEnabled": [Function],
"isServerlessEnabled": [Function],
"isSettingsV2Enabled": [Function],
"isSparkEnabled": [Function],
@@ -5189,6 +5193,7 @@ exports[`SettingsComponent renders 1`] = `
logIndexingPolicySuccessMessage={[Function]}
onIndexingPolicyContentChange={[Function]}
onIndexingPolicyDirtyChange={[Function]}
refreshIndexTransformationProgress={[Function]}
resetShouldDiscardIndexingPolicy={[Function]}
shouldDiscardIndexingPolicy={false}
/>

View File

@@ -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,
<StyledLinkBase
href="https://aka.ms/cosmosdb/modify-index-policy"
target="_blank"
>
Modifying Indexing Policies
</StyledLinkBase>
.
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
</Text>
<Text
id="updateThroughputBeyondLimitWarningMessage"
@@ -341,14 +333,30 @@ exports[`SettingsUtils functions render 1`] = `
}
}
>
<Text>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
Refreshing index transformation progress
</Text>
<StyledSpinnerBase
size={2}
size={1}
/>
</Stack>
<Text>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
You can make more indexing changes once the current index transformation is complete.
<StyledLinkBase
onClick={[Function]}
@@ -356,7 +364,15 @@ exports[`SettingsUtils functions render 1`] = `
Refresh to check if it has completed.
</StyledLinkBase>
</Text>
<Text>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
<StyledLinkBase
onClick={[Function]}

View File

@@ -86,6 +86,8 @@ import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandBut
import { updateUserContext, userContext } from "../UserContext";
import { stringToBlob } from "../Utils/BlobUtils";
import { IChoiceGroupProps } from "office-ui-fabric-react";
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../Contracts/SubscriptionType";
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -118,7 +120,7 @@ export default class Explorer {
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
public subscriptionType: ko.Observable<ViewModels.SubscriptionType>;
public subscriptionType: ko.Observable<SubscriptionType>;
public quotaId: ko.Observable<string>;
public defaultExperience: ko.Observable<string>;
public isPreferredApiDocumentDB: ko.Computed<boolean>;
@@ -224,6 +226,7 @@ export default class Explorer {
public shareTokenCopyHelperText: ko.Observable<string>;
public shouldShowDataAccessExpiryDialog: ko.Observable<boolean>;
public shouldShowContextSwitchPrompt: ko.Observable<boolean>;
public isSchemaEnabled: ko.Computed<boolean>;
// Notebooks
public isNotebookEnabled: ko.Observable<boolean>;
@@ -277,9 +280,7 @@ export default class Explorer {
this.refreshTreeTitle = ko.observable<string>("Refresh collections");
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
this.subscriptionType = ko.observable<ViewModels.SubscriptionType>(
SharedConstants.CollectionCreation.DefaultSubscriptionType
);
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
this.quotaId = ko.observable<string>("");
let firstInitialization = true;
this.isRefreshingExplorer = ko.observable<boolean>(true);
@@ -421,6 +422,7 @@ export default class Explorer {
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
);
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
this.databases = ko.observableArray<ViewModels.Database>();
@@ -1039,11 +1041,11 @@ export default class Explorer {
);
TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, startTime);
this.databaseAccount(databaseAccount);
} catch (e) {
} catch (error) {
NotificationConsoleUtils.clearInProgressMessageWithId(logId);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Enabling Azure Synapse Link for this account failed. ${e.message || JSON.stringify(e)}`
`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`
);
TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, startTime);
} finally {
@@ -1112,7 +1114,7 @@ export default class Explorer {
);
this.renewExplorerShareAccess(this, this.tokenForRenewal())
.fail((error: any) => {
const stringifiedError: string = error.message;
const stringifiedError: string = getErrorMessage(error);
this.renewTokenError("Invalid connection string specified");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
@@ -1141,7 +1143,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 +1168,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 +1445,21 @@ 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,
errorStack: getErrorStack(error)
},
startKey
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while refreshing databases: ${error.message}`
`Error while refreshing databases: ${errorMessage}`
);
}
);
@@ -1471,7 +1478,7 @@ export default class Explorer {
);
}
},
reason => {
error => {
if (resourceTreeStartKey != null) {
TelemetryProcessor.traceFailure(
Action.LoadResourceTree,
@@ -1479,7 +1486,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
);
@@ -1528,7 +1536,7 @@ export default class Explorer {
resolve(token);
},
(error: any) => {
Logger.logError(error, "Explorer/getArcadiaToken");
Logger.logError(getErrorMessage(error), "Explorer/getArcadiaToken");
resolve(undefined);
}
);
@@ -1546,7 +1554,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);
@@ -1554,8 +1562,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, "Explorer/this._arcadiaManager.listWorkspacesAsync", "Get Arcadia workspaces failed");
return Promise.resolve([]);
}
}
@@ -1590,10 +1597,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,
"initNotebooks/getNotebookConnectionInfoAsync",
`Failed to get notebook workspace connection info: ${getErrorMessage(error)}`
);
throw error;
} finally {
@@ -1616,9 +1623,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 = {
@@ -1643,7 +1651,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;
}
}
@@ -1669,8 +1677,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, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace");
} finally {
clearMessage && clearMessage();
}
@@ -1685,7 +1692,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);
@@ -1882,7 +1892,8 @@ export default class Explorer {
masterKey,
databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId
subscriptionId: inputs.subscriptionId,
subscriptionType: inputs.subscriptionType
});
TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount,
@@ -2053,7 +2064,8 @@ export default class Explorer {
databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree,
trace: error.message
error: getErrorMessage(error),
errorStack: getErrorStack(error)
},
startKey
);
@@ -2219,8 +2231,7 @@ export default class Explorer {
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
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);
}
@@ -2401,8 +2412,7 @@ export default class Explorer {
public renameNotebook(notebookFile: NotebookContentItem): Q.Promise<NotebookContentItem> {
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);
}
@@ -2450,8 +2460,7 @@ export default class Explorer {
public onCreateDirectory(parent: NotebookContentItem): Q.Promise<NotebookContentItem> {
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);
}
@@ -2472,8 +2481,7 @@ export default class Explorer {
public readFile(notebookFile: NotebookContentItem): Promise<string> {
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);
}
@@ -2483,8 +2491,7 @@ export default class Explorer {
public downloadFile(notebookFile: NotebookContentItem): Promise<void> {
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);
}
@@ -2515,7 +2522,7 @@ export default class Explorer {
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Could not download notebook ${error.message}`
`Could not download notebook ${getErrorMessage(error)}`
);
clearMessage();
@@ -2565,7 +2572,7 @@ export default class Explorer {
);
this.isNotebooksEnabledForAccount(isAccountInAllowedLocation);
} catch (error) {
Logger.logError(error, "Explorer/isNotebooksEnabledForAccount");
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
this.isNotebooksEnabledForAccount(false);
}
}
@@ -2594,7 +2601,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);
}
};
@@ -2619,7 +2626,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;
}
};
@@ -2638,8 +2645,7 @@ export default class Explorer {
public deleteNotebookFile(item: NotebookContentItem): Promise<void> {
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);
}
@@ -2688,8 +2694,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);
}
@@ -2722,16 +2727,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
);
@@ -2774,8 +2780,7 @@ export default class Explorer {
public refreshContentItem(item: NotebookContentItem): Promise<void> {
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));
}
@@ -2961,7 +2966,7 @@ export default class Explorer {
}
return tokenRefreshInterval;
} catch (error) {
Logger.logError(error, "Explorer/getTokenRefreshInterval");
Logger.logError(getErrorMessage(error), "Explorer/getTokenRefreshInterval");
return tokenRefreshInterval;
}
}

View File

@@ -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<GraphExplorerProps, GraphExpl
backendPromise.then(
(result: UserQueryResult) => (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<GraphExplorerProps, GraphExpl
promise
.then((result: GremlinClient.GremlinRequestResult) => 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

View File

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

View File

@@ -7,7 +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, handleError } from "../../../Common/ErrorHandlingUtils";
export interface GremlinClientParameters {
endpoint: string;
@@ -58,26 +58,23 @@ 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 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) {
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) => {
@@ -132,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;
}
@@ -158,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;
@@ -179,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;
@@ -190,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);
}
}

View File

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

View File

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

View File

@@ -288,7 +288,7 @@
range of values and is likely to have evenly distributed access patterns.</span>
</span>
</p>
<input type="text" data-test="addCollection-partitionKeyValue" aria-required="true" size="40"
<input type="text" id="partitionKeyValue" data-test="addCollection-partitionKeyValue" aria-required="true" size="40"
class="textfontclr collid" data-bind="textInput: partitionKey,
attr: {
placeholder: partitionKeyPlaceholder,

View File

@@ -3,11 +3,11 @@ 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";
import * as ViewModels from "../../Contracts/ViewModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import editable from "../../Common/EditableUtility";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
@@ -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, getErrorStack } from "../../Common/ErrorHandlingUtils";
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
isPreferredApiTable: ko.Computed<boolean>;
@@ -648,10 +649,8 @@ export default class AddCollectionPane extends ContextualPaneBase {
}
public getSharedThroughputDefault(): boolean {
const subscriptionType: ViewModels.SubscriptionType =
this.container.subscriptionType && this.container.subscriptionType();
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
const subscriptionType = this.container.subscriptionType && this.container.subscriptionType();
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
return false;
}
@@ -690,7 +689,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
databaseId: this.databaseId(),
rupm: this.rupm()
}),
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
@@ -793,7 +792,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
}),
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
@@ -868,7 +867,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
}),
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
@@ -881,10 +880,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 = {
@@ -904,7 +902,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
},
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
@@ -912,7 +910,8 @@ export default class AddCollectionPane extends ContextualPaneBase {
flight: this.container.flight()
},
dataExplorerArea: Constants.Areas.ContextualPane,
error: reason
error: errorMessage,
errorStack: getErrorStack(error)
};
TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey);
}

View File

@@ -1,5 +1,5 @@
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import Explorer from "../Explorer";
import AddDatabasePane from "./AddDatabasePane";
import { DatabaseAccount } from "../../Contracts/DataModels";
@@ -44,31 +44,31 @@ describe("Add Database Pane", () => {
});
it("should be true if subscription type is Benefits", () => {
explorer.subscriptionType(ViewModels.SubscriptionType.Benefits);
explorer.subscriptionType(SubscriptionType.Benefits);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be false if subscription type is EA", () => {
explorer.subscriptionType(ViewModels.SubscriptionType.EA);
explorer.subscriptionType(SubscriptionType.EA);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(false);
});
it("should be true if subscription type is Free", () => {
explorer.subscriptionType(ViewModels.SubscriptionType.Free);
explorer.subscriptionType(SubscriptionType.Free);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be true if subscription type is Internal", () => {
explorer.subscriptionType(ViewModels.SubscriptionType.Internal);
explorer.subscriptionType(SubscriptionType.Internal);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should be true if subscription type is PAYG", () => {
explorer.subscriptionType(ViewModels.SubscriptionType.PAYG);
explorer.subscriptionType(SubscriptionType.PAYG);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});

View File

@@ -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,8 @@ 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";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>;
@@ -256,7 +257,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
const addDatabasePaneOpenMessage = {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
throughput: this.throughput(),
@@ -284,7 +285,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
shared: this.databaseCreateNewShared()
}),
offerThroughput,
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
flight: this.container.flight()
@@ -296,18 +297,22 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.isExecuting(true);
const createDatabaseParams: DataModels.CreateDatabaseParams = {
autoPilotMaxThroughput: this.maxAutoPilotThroughputSet(),
databaseId: addDatabasePaneStartMessage.database.id,
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared,
offerThroughput: addDatabasePaneStartMessage.offerThroughput
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared
};
if (this.isAutoPilotSelected()) {
createDatabaseParams.autoPilotMaxThroughput = this.maxAutoPilotThroughputSet();
} else {
createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.offerThroughput;
}
createDatabase(createDatabaseParams).then(
(database: DataModels.Database) => {
this._onCreateDatabaseSuccess(offerThroughput, startKey);
},
(reason: any) => {
this._onCreateDatabaseFailure(reason, offerThroughput, reason);
(error: any) => {
this._onCreateDatabaseFailure(error, offerThroughput, startKey);
}
);
}
@@ -323,10 +328,9 @@ export default class AddDatabasePane extends ContextualPaneBase {
}
public getSharedThroughputDefault(): boolean {
const subscriptionType: ViewModels.SubscriptionType =
this.container.subscriptionType && this.container.subscriptionType();
const subscriptionType = this.container.subscriptionType && this.container.subscriptionType();
if (subscriptionType === ViewModels.SubscriptionType.EA || this.container.isServerlessEnabled()) {
if (subscriptionType === SubscriptionType.EA || this.container.isServerlessEnabled()) {
return false;
}
@@ -345,7 +349,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
shared: this.databaseCreateNewShared()
}),
offerThroughput: offerThroughput,
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
flight: this.container.flight()
@@ -356,11 +360,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(),
@@ -369,13 +373,14 @@ export default class AddDatabasePane extends ContextualPaneBase {
shared: this.databaseCreateNewShared()
}),
offerThroughput: offerThroughput,
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
flight: this.container.flight()
},
dataExplorerArea: Constants.Areas.ContextualPane,
error: reason
error: errorMessage,
errorStack: getErrorStack(error)
};
TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey);
}

View File

@@ -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, getErrorStack } from "../../Common/ErrorHandlingUtils";
export class BrowseQueriesPane extends ContextualPaneBase {
public queriesGridComponentAdapter: QueriesGridComponentAdapter;
@@ -60,17 +61,20 @@ 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,
errorStack: getErrorStack(error)
},
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);
}

View File

@@ -13,6 +13,8 @@ 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";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
export default class CassandraAddCollectionPane extends ContextualPaneBase {
public createTableQuery: ko.Observable<string>;
@@ -313,7 +315,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
databaseId: this.keyspaceId(),
rupm: false
}),
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: "u",
@@ -368,7 +370,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
hasDedicatedThroughput: this.dedicateTableThroughput()
}),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: "u",
@@ -415,7 +417,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
hasDedicatedThroughput: this.dedicateTableThroughput()
}),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: "u",
@@ -429,8 +431,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,
@@ -445,7 +448,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
hasDedicatedThroughput: this.dedicateTableThroughput()
},
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: ViewModels.SubscriptionType[this.container.subscriptionType()],
subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: {
storage: "u",
@@ -456,7 +459,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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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, getErrorStack } from "../../Common/ErrorHandlingUtils";
export class SaveQueryPane extends ContextualPaneBase {
public queryName: ko.Observable<string>;
@@ -87,18 +88,18 @@ 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,
errorStack: getErrorStack(error)
},
startKey
);
@@ -132,18 +133,21 @@ 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,
errorStack: getErrorStack(error)
},
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);
}

View File

@@ -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, getErrorStack } from "../../Common/ErrorHandlingUtils";
export class SetupNotebooksPane extends ContextualPaneBase {
private description: ko.Observable<string>;
@@ -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,
{
@@ -93,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
);

View File

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

View File

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

View File

@@ -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 <ViewModels.QueryError>{
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
);

View File

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

View File

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

View File

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

View File

@@ -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,6 +31,7 @@ import {
createDocument
} from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class DocumentsTab extends TabsBase {
public selectedDocumentId: ko.Observable<DocumentId>;
@@ -392,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));
});
}
@@ -474,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
);
@@ -541,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
);
@@ -642,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
);
@@ -696,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
);
@@ -774,10 +780,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 +792,8 @@ export default class DocumentsTab extends TabsBase {
defaultExperience: this.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: error
error: errorMessage,
errorStack: getErrorStack(error)
},
this.onLoadStartKey
);

View File

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

View File

@@ -47,7 +47,7 @@
</script>
<!-- Query Errors Tab - Start-->
<div class="active queryErrorsHeaderContainer" data-bind="visible: errors().length > 0">
<div class="active queryErrorsHeaderContainer" data-bind="visible: !!error()">
<span class="queryErrors" data-toggle="tab" data-bind="attr: { href: '#queryerrors' + tabId }">Errors</span>
</div>
<!-- Query Errors Tab - End -->
@@ -56,16 +56,16 @@
<div class="queryResultErrorContentContainer">
<div
class="queryEditorWatermark"
data-bind="visible: allResultsMetadata().length === 0 && errors().length === 0 && !queryResults() && !isExecuting()"
data-bind="visible: allResultsMetadata().length === 0 && !error() && !queryResults() && !isExecuting()"
>
<p><img src="/RunQuery.png" alt="Execute Query Watermark" /></p>
<p class="queryEditorWatermarkText">Execute a query to see the results</p>
</div>
<div
class="queryResultsErrorsContent"
data-bind="visible: allResultsMetadata().length > 0 || errors().length > 0 || queryResults()"
data-bind="visible: allResultsMetadata().length > 0 || !!error() || queryResults()"
>
<div class="togglesWithMetadata" data-bind="visible: errors().length === 0">
<div class="togglesWithMetadata" data-bind="visible: !error()">
<div
class="toggles"
aria-label="Successful execution"
@@ -103,12 +103,12 @@
</div>
<json-editor
params="{ content: queryResults, isReadOnly: true, ariaLabel: 'Query results' }"
data-bind="visible: queryResults().length > 0 && isResultToggled() && allResultsMetadata().length > 0 && errors().length === 0"
data-bind="visible: queryResults().length > 0 && isResultToggled() && allResultsMetadata().length > 0 && !error()"
>
</json-editor>
<div
class="queryMetricsSummaryContainer"
data-bind="visible: isMetricsToggled() && allResultsMetadata().length > 0 && errors().length === 0"
data-bind="visible: isMetricsToggled() && allResultsMetadata().length > 0 && !error()"
>
<table class="queryMetricsSummary">
<caption>
@@ -308,11 +308,10 @@
id: {
href: 'queryerrors' + tabId
},
visible: errors().length > 0"
visible: !!error()"
>
<!-- ko foreach: errors -->
<div class="errorContent">
<span class="errorMessage" data-bind="text: $data.message"></span>
<span class="errorMessage" data-bind="text: error"></span>
<span class="errorDetailsLink">
<a
data-bind="click: $parent.onErrorDetailsClick, event: { keypress: $parent.onErrorDetailsKeyPress }"
@@ -323,7 +322,6 @@
>
</span>
</div>
<!-- /ko -->
</div>
<!-- Query Errors Content - End-->
</div>

View File

@@ -4,7 +4,6 @@ import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import TabsBase from "./TabsBase";
import { HashMap } from "../../Common/HashMap";
import * as HeadersUtility from "../../Common/HeadersUtility";
@@ -18,6 +17,7 @@ import SaveQueryIcon from "../../../images/save-cosmos.svg";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import { queryDocuments, queryDocumentsPage } from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
enum ToggleState {
Result,
@@ -34,7 +34,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
public selectedContent: ko.Observable<string>;
public sqlStatementToExecute: ko.Observable<string>;
public queryResults: ko.Observable<string>;
public errors: ko.ObservableArray<ViewModels.QueryError>;
public error: ko.Observable<string>;
public statusMessge: ko.Observable<string>;
public statusIcon: ko.Observable<string>;
public allResultsMetadata: ko.ObservableArray<ViewModels.QueryResultsMetadata>;
@@ -82,7 +82,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
this.statusMessge = ko.observable<string>();
this.statusIcon = ko.observable<string>();
this.allResultsMetadata = ko.observableArray<ViewModels.QueryResultsMetadata>([]);
this.errors = ko.observableArray<ViewModels.QueryError>([]);
this.error = ko.observable<string>();
this._partitionKey = options.partitionKey;
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
this.splitterId = this.tabId + "_splitter";
@@ -266,7 +266,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
};
private _executeQueryDocumentsPage(firstItemIndex: number): Q.Promise<any> {
this.errors([]);
this.error("");
this.roundTrips(undefined);
if (this._iterator == null) {
const queryIteratorPromise = this._initIterator();
@@ -350,27 +350,19 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
startKey
);
},
(reason: any) => {
(error: any) => {
this.isExecutionError(true);
const parsedErrors = ErrorParserUtility.parse(reason);
var errors = parsedErrors.map((error: DataModels.ErrorDataModel) => {
return <ViewModels.QueryError>{
message: error.message,
start: error.location ? error.location.start : undefined,
end: error.location ? error.location.end : undefined,
code: error.code,
severity: error.severity
};
});
this.errors(errors);
const errorMessage = getErrorMessage(error);
this.error(errorMessage);
TelemetryProcessor.traceFailure(
Action.ExecuteQuery,
{
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
);

View File

@@ -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, getErrorStack } 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,8 @@ 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),
errorStack: getErrorStack(error)
},
startKey
);

View File

@@ -9,6 +9,7 @@ import ko from "knockout";
import * as Constants from "../../Common/Constants";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class SettingsTabV2 extends TabsBase {
public settingsComponentAdapter: SettingsComponentAdapter;
@@ -55,6 +56,7 @@ export default class SettingsTabV2 extends TabsBase {
this.isExecuting(false);
},
error => {
const errorMessage = getErrorMessage(error);
this.notification = undefined;
this.notificationRead(true);
this.isExecuting(false);
@@ -67,14 +69,13 @@ export default class SettingsTabV2 extends TabsBase {
defaultExperience: this.options.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle,
error: error
error: errorMessage,
errorStack: getErrorStack(error)
},
this.options.onLoadStartKey
);
logConsoleError(
`Error while fetching container settings for container ${this.options.collection.id()}: ${JSON.stringify(
error
)}`
`Error while fetching container settings for container ${this.options.collection.id()}: ${errorMessage}`
);
throw error;
}

View File

@@ -13,6 +13,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import StoredProcedure from "../Tree/StoredProcedure";
import ScriptTabBase from "./ScriptTabBase";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
enum ToggleState {
Result = "result",
@@ -104,7 +105,7 @@ export default class StoredProcedureTab extends ScriptTabBase {
startKey
);
},
(updateError: any) => {
(error: any) => {
this.isExecutionError(true);
TelemetryProcessor.traceFailure(
Action.UpdateStoredProcedure,
@@ -112,7 +113,9 @@ export default class StoredProcedureTab extends ScriptTabBase {
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
);
@@ -274,7 +277,9 @@ export default class StoredProcedureTab extends ScriptTabBase {
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(createError),
errorStack: getErrorStack(createError)
},
startKey
);

View File

@@ -8,6 +8,7 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Trigger from "../Tree/Trigger";
import ScriptTabBase from "./ScriptTabBase";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class TriggerTab extends ScriptTabBase {
public collection: ViewModels.Collection;
@@ -86,7 +87,9 @@ export default class TriggerTab extends ScriptTabBase {
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(createError),
errorStack: getErrorStack(createError)
},
startKey
);
@@ -168,7 +171,9 @@ export default class TriggerTab extends ScriptTabBase {
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(createError),
errorStack: getErrorStack(createError)
},
startKey
);

View File

@@ -7,6 +7,7 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import UserDefinedFunction from "../Tree/UserDefinedFunction";
import ScriptTabBase from "./ScriptTabBase";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class UserDefinedFunctionTab extends ScriptTabBase {
public collection: ViewModels.Collection;
@@ -68,7 +69,9 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
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(createError),
errorStack: getErrorStack(createError)
},
startKey
);
@@ -143,7 +146,9 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
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(createError),
errorStack: getErrorStack(createError)
},
startKey
);

View File

@@ -40,6 +40,7 @@ import Explorer from "../Explorer";
import { userContext } from "../../UserContext";
import TabsBase from "../Tabs/TabsBase";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class Collection implements ViewModels.Collection {
public nodeKind: string;
@@ -62,6 +63,8 @@ export default class Collection implements ViewModels.Collection {
public throughput: ko.Computed<number>;
public rawDataModel: DataModels.Collection;
public analyticalStorageTtl: ko.Observable<number>;
public schema: DataModels.ISchema;
public requestSchema: () => void;
public geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
// TODO move this to API customization class
@@ -116,6 +119,8 @@ export default class Collection implements ViewModels.Collection {
this.conflictResolutionPolicy = ko.observable(data.conflictResolutionPolicy);
this.changeFeedPolicy = ko.observable<DataModels.ChangeFeedPolicy>(data.changeFeedPolicy);
this.analyticalStorageTtl = ko.observable(data.analyticalStorageTtl);
this.schema = data.schema;
this.requestSchema = data.requestSchema;
this.geospatialConfig = ko.observable(data.geospatialConfig);
// TODO fix this to only replace non-excaped single quotes
@@ -610,6 +615,7 @@ export default class Collection implements ViewModels.Collection {
settingsTab.pendingNotification(pendingNotification);
},
(error: any) => {
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure(
Action.Tab,
{
@@ -619,13 +625,14 @@ export default class Collection implements ViewModels.Collection {
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: settingsTabOptions.title,
error: error
error: errorMessage,
errorStack: getErrorStack(error)
},
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 +876,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 +935,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 +995,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 +1192,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 +1245,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()
@@ -1366,7 +1373,9 @@ export default class Collection implements ViewModels.Collection {
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.databaseId,
collectionName: this.id(),
defaultExperience: this.container.defaultExperience()
defaultExperience: this.container.defaultExperience(),
error: getErrorMessage(error),
errorStack: getErrorStack(error)
},
startKey
);

View File

@@ -0,0 +1,82 @@
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout";
import Database from "./Database";
import Explorer from "../Explorer";
import { HttpStatusCodes } from "../../Common/Constants";
import { JunoClient } from "../../Juno/JunoClient";
import { userContext, updateUserContext } from "../../UserContext";
const createMockContainer = (): Explorer => {
const mockContainer = new Explorer();
return mockContainer;
};
updateUserContext({
subscriptionId: "fakeSubscriptionId",
resourceGroup: "fakeResourceGroup",
databaseAccount: {
id: "id",
name: "fakeName",
location: "fakeLocation",
type: "fakeType",
tags: undefined,
kind: "fakeKind",
properties: {
documentEndpoint: "fakeEndpoint",
tableEndpoint: "fakeEndpoint",
gremlinEndpoint: "fakeEndpoint",
cassandraEndpoint: "fakeEndpoint"
}
}
});
describe("Add Schema", () => {
it("should not call requestSchema or getSchema if analyticalStorageTtl is undefined", () => {
const collection: DataModels.Collection = {} as DataModels.Collection;
collection.analyticalStorageTtl = undefined;
const database = new Database(createMockContainer(), { id: "fakeId" });
database.container = createMockContainer();
database.container.isSchemaEnabled = ko.computed<boolean>(() => false);
database.junoClient = new JunoClient();
database.junoClient.requestSchema = jest.fn();
database.junoClient.getSchema = jest.fn();
database.addSchema(collection);
expect(database.junoClient.requestSchema).toBeCalledTimes(0);
});
it("should call requestSchema or getSchema if analyticalStorageTtl is not undefined", () => {
const collection: DataModels.Collection = { id: "fakeId" } as DataModels.Collection;
collection.analyticalStorageTtl = 0;
const database = new Database(createMockContainer(), {});
database.container = createMockContainer();
database.container.isSchemaEnabled = ko.computed<boolean>(() => true);
database.junoClient = new JunoClient();
database.junoClient.requestSchema = jest.fn();
database.junoClient.getSchema = jest.fn().mockResolvedValue({ status: HttpStatusCodes.OK, data: {} });
jest.useFakeTimers();
const interval = 5000;
const checkForSchema: NodeJS.Timeout = database.addSchema(collection, interval);
jest.advanceTimersByTime(interval + 1000);
expect(database.junoClient.requestSchema).toBeCalledWith({
id: undefined,
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
accountName: userContext.databaseAccount.name,
resource: `dbs/${database.id}/colls/${collection.id}`,
status: "new"
});
expect(checkForSchema).not.toBeNull();
expect(database.junoClient.getSchema).toBeCalledWith(
userContext.databaseAccount.name,
database.id(),
collection.id
);
});
});

View File

@@ -13,9 +13,12 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol
import * as Logger from "../../Common/Logger";
import Explorer from "../Explorer";
import { readCollections } from "../../Common/dataAccess/readCollections";
import { JunoClient, IJunoResponse } from "../../Juno/JunoClient";
import { userContext } from "../../UserContext";
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class Database implements ViewModels.Database {
public nodeKind: string;
@@ -28,6 +31,7 @@ export default class Database implements ViewModels.Database {
public isDatabaseExpanded: ko.Observable<boolean>;
public isDatabaseShared: ko.Computed<boolean>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
public junoClient: JunoClient;
constructor(container: Explorer, data: any) {
this.nodeKind = "Database";
@@ -42,6 +46,7 @@ export default class Database implements ViewModels.Database {
this.isDatabaseShared = ko.pureComputed(() => {
return this.offer && !!this.offer();
});
this.junoClient = new JunoClient();
}
public onSettingsClick = () => {
@@ -88,6 +93,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 +103,14 @@ export default class Database implements ViewModels.Database {
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Scale",
error: error
error: errorMessage,
errorStack: getErrorStack(error)
},
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;
}
@@ -181,6 +188,10 @@ export default class Database implements ViewModels.Database {
const collections: DataModels.Collection[] = await readCollections(this.id());
const deltaCollections = this.getDeltaCollections(collections);
collections.forEach((collection: DataModels.Collection) => {
this.addSchema(collection);
});
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
collectionVMs.push(collectionVM);
@@ -239,7 +250,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()
@@ -305,4 +316,42 @@ export default class Database implements ViewModels.Database {
this.collections(collectionsToKeep);
}
public addSchema(collection: DataModels.Collection, interval?: number): NodeJS.Timeout {
let checkForSchema: NodeJS.Timeout = null;
interval = interval || 5000;
if (collection.analyticalStorageTtl !== undefined && this.container.isSchemaEnabled()) {
collection.requestSchema = () => {
this.junoClient.requestSchema({
id: undefined,
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
accountName: userContext.databaseAccount.name,
resource: `dbs/${this.id}/colls/${collection.id}`,
status: "new"
});
checkForSchema = setInterval(async () => {
const response: IJunoResponse<DataModels.ISchema> = await this.junoClient.getSchema(
userContext.databaseAccount.name,
this.id(),
collection.id
);
if (response.status >= 404) {
clearInterval(checkForSchema);
}
if (response.data !== null) {
clearInterval(checkForSchema);
collection.schema = response.data;
}
}, interval);
};
collection.requestSchema();
}
return checkForSchema;
}
}

View File

@@ -0,0 +1,253 @@
import * as ko from "knockout";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import React from "react";
import { ResourceTreeAdapter } from "./ResourceTreeAdapter";
import { shallow } from "enzyme";
import { TreeComponent, TreeNode, TreeComponentProps } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer";
import Collection from "./Collection";
const schema: DataModels.ISchema = {
id: "fakeSchemaId",
accountName: "fakeAccountName",
resource: "dbs/FakeDbName/colls/FakeCollectionName",
fields: [
{
dataType: {
code: 15,
name: "String"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "_rid",
path: "_rid",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 11,
name: "Int64"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "_ts",
path: "_ts",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 15,
name: "String"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "id",
path: "id",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 15,
name: "String"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "pk",
path: "pk",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 15,
name: "String"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "other",
path: "other",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 15,
name: "String"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "name",
path: "nested.name",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 11,
name: "Int64"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "someNumber",
path: "nested.someNumber",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 17,
name: "Double"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "anotherNumber",
path: "nested.anotherNumber",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
},
{
dataType: {
code: 15,
name: "String"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "name",
path: "items.list.items.name",
maxRepetitionLevel: 1,
maxDefinitionLevel: 3
},
{
dataType: {
code: 11,
name: "Int64"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "someNumber",
path: "items.list.items.someNumber",
maxRepetitionLevel: 1,
maxDefinitionLevel: 3
},
{
dataType: {
code: 17,
name: "Double"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "anotherNumber",
path: "items.list.items.anotherNumber",
maxRepetitionLevel: 1,
maxDefinitionLevel: 3
},
{
dataType: {
code: 15,
name: "String"
},
hasNulls: true,
isArray: false,
schemaType: {
code: 0,
name: "Data"
},
name: "_etag",
path: "_etag",
maxRepetitionLevel: 0,
maxDefinitionLevel: 1
}
]
};
const createMockContainer = (): Explorer => {
const mockContainer = new Explorer();
mockContainer.selectedNode = ko.observable<ViewModels.TreeNode>();
mockContainer.onUpdateTabsButtons = () => {
return;
};
return mockContainer;
};
const createMockCollection = (): ViewModels.Collection => {
const mockCollection = {} as DataModels.Collection;
mockCollection._rid = "fakeRid";
mockCollection._self = "fakeSelf";
mockCollection.id = "fakeId";
mockCollection.analyticalStorageTtl = 0;
mockCollection.schema = schema;
const mockCollectionVM: ViewModels.Collection = new Collection(
createMockContainer(),
"fakeDatabaseId",
mockCollection,
undefined,
undefined
);
return mockCollectionVM;
};
describe("Resource tree for schema", () => {
const mockContainer: Explorer = createMockContainer();
const resourceTree = new ResourceTreeAdapter(mockContainer);
it("should render", () => {
const rootNode: TreeNode = resourceTree.buildSchemaNode(createMockCollection());
const props: TreeComponentProps = {
rootNode,
className: "dataResourceTree"
};
const wrapper = shallow(<TreeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -2,7 +2,7 @@ import * as ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import { TreeComponent, TreeNode, TreeNodeMenuItem, TreeNodeComponent } from "../Controls/TreeComponent/TreeComponent";
import * as ViewModels from "../../Contracts/ViewModels";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
@@ -32,6 +32,7 @@ import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger";
import TabsBase from "../Tabs/TabsBase";
import { userContext } from "../../UserContext";
import * as DataModels from "../../Contracts/DataModels";
export class ResourceTreeAdapter implements ReactAdapter {
public static readonly MyNotebooksTitle = "My Notebooks";
@@ -280,12 +281,19 @@ 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])
});
}
const schemaNode: TreeNode = this.buildSchemaNode(collection);
if (schemaNode) {
children.push(schemaNode);
}
if (ResourceTreeAdapter.showScriptNodes(this.container)) {
children.push(this.buildStoredProcedureNode(collection));
@@ -403,6 +411,75 @@ export class ResourceTreeAdapter implements ReactAdapter {
};
}
public buildSchemaNode(collection: ViewModels.Collection): TreeNode {
if (collection.analyticalStorageTtl() == undefined) {
return undefined;
}
if (!collection.schema || !collection.schema.fields) {
return undefined;
}
return {
label: "Schema",
children: this.getSchemaNodes(collection.schema.fields),
onClick: () => {
collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Schema);
this.container.tabsManager.refreshActiveTab(
(tab: TabsBase) => tab.collection && tab.collection.rid === collection.rid
);
}
};
}
private getSchemaNodes(fields: DataModels.IDataField[]): TreeNode[] {
const schema: any = {};
//unflatten
fields.forEach((field: DataModels.IDataField, fieldIndex: number) => {
const path: string[] = field.path.split(".");
const fieldProperties = [field.dataType.name, `HasNulls: ${field.hasNulls}`];
let current: any = {};
path.forEach((name: string, pathIndex: number) => {
if (pathIndex === 0) {
if (schema[name] === undefined) {
if (pathIndex === path.length - 1) {
schema[name] = fieldProperties;
} else {
schema[name] = {};
}
}
current = schema[name];
} else {
if (current[name] === undefined) {
if (pathIndex === path.length - 1) {
current[name] = fieldProperties;
} else {
current[name] = {};
}
}
current = current[name];
}
});
});
const traverse = (obj: any): TreeNode[] => {
const children: TreeNode[] = [];
if (obj !== null && !Array.isArray(obj) && typeof obj === "object") {
Object.entries(obj).forEach(([key, value]) => {
children.push({ label: key, children: traverse(value) });
});
} else if (Array.isArray(obj)) {
return [{ label: obj[0] }, { label: obj[1] }];
}
return children;
};
return traverse(schema);
}
private buildNotebooksTrees(): TreeNode {
let notebooksTree: TreeNode = {
label: undefined,

View File

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

View File

@@ -0,0 +1,172 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Resource tree for schema should render 1`] = `
<div
className="treeComponent dataResourceTree"
>
<TreeNodeComponent
generation={0}
node={
Object {
"children": Array [
Object {
"children": Array [
Object {
"label": "String",
},
Object {
"label": "HasNulls: true",
},
],
"label": "_rid",
},
Object {
"children": Array [
Object {
"label": "Int64",
},
Object {
"label": "HasNulls: true",
},
],
"label": "_ts",
},
Object {
"children": Array [
Object {
"label": "String",
},
Object {
"label": "HasNulls: true",
},
],
"label": "id",
},
Object {
"children": Array [
Object {
"label": "String",
},
Object {
"label": "HasNulls: true",
},
],
"label": "pk",
},
Object {
"children": Array [
Object {
"label": "String",
},
Object {
"label": "HasNulls: true",
},
],
"label": "other",
},
Object {
"children": Array [
Object {
"children": Array [
Object {
"label": "String",
},
Object {
"label": "HasNulls: true",
},
],
"label": "name",
},
Object {
"children": Array [
Object {
"label": "Int64",
},
Object {
"label": "HasNulls: true",
},
],
"label": "someNumber",
},
Object {
"children": Array [
Object {
"label": "Double",
},
Object {
"label": "HasNulls: true",
},
],
"label": "anotherNumber",
},
],
"label": "nested",
},
Object {
"children": Array [
Object {
"children": Array [
Object {
"children": Array [
Object {
"children": Array [
Object {
"label": "String",
},
Object {
"label": "HasNulls: true",
},
],
"label": "name",
},
Object {
"children": Array [
Object {
"label": "Int64",
},
Object {
"label": "HasNulls: true",
},
],
"label": "someNumber",
},
Object {
"children": Array [
Object {
"label": "Double",
},
Object {
"label": "HasNulls: true",
},
],
"label": "anotherNumber",
},
],
"label": "items",
},
],
"label": "list",
},
],
"label": "items",
},
Object {
"children": Array [
Object {
"label": "String",
},
Object {
"label": "HasNulls: true",
},
],
"label": "_etag",
},
],
"label": "Schema",
"onClick": [Function],
}
}
paddingLeft={0}
/>
</div>
`;