Merge branch 'master' into users/srnara/selfserve

This commit is contained in:
Srinath Narayanan 2021-01-13 06:21:29 -08:00
commit 41f37055ef
35 changed files with 329 additions and 151 deletions

View File

@ -69,6 +69,10 @@ Jest and Puppeteer are used for end to end browser based tests and are contained
We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details. We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details.
### Architechture
[![](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggTFJcbiAgaG9zdGVkKGh0dHBzOi8vY29zbW9zLmF6dXJlLmNvbSlcbiAgcG9ydGFsKFBvcnRhbClcbiAgZW11bGF0b3IoRW11bGF0b3IpXG4gIGFhZFtBQURdXG4gIHJlc291cmNlVG9rZW5bUmVzb3VyY2UgVG9rZW5dXG4gIGNvbm5lY3Rpb25TdHJpbmdbQ29ubmVjdGlvbiBTdHJpbmddXG4gIHBvcnRhbFRva2VuW0VuY3J5cHRlZCBQb3J0YWwgVG9rZW5dXG4gIG1hc3RlcktleVtNYXN0ZXIgS2V5XVxuICBhcm1bQVJNIFJlc291cmNlIFByb3ZpZGVyXVxuICBkYXRhcGxhbmVbRGF0YSBQbGFuZV1cbiAgcHJveHlbUG9ydGFsIEFQSSBQcm94eV1cbiAgc3FsW1NRTF1cbiAgbW9uZ29bTW9uZ29dXG4gIHRhYmxlc1tUYWJsZXNdXG4gIGNhc3NhbmRyYVtDYXNzYW5kcmFdXG4gIGdyYWZbR3JhcGhdXG5cblxuICBlbXVsYXRvciAtLT4gbWFzdGVyS2V5IC0tLS0-IGRhdGFwbGFuZVxuICBwb3J0YWwgLS0-IGFhZFxuICBob3N0ZWQgLS0-IHBvcnRhbFRva2VuICYgcmVzb3VyY2VUb2tlbiAmIGNvbm5lY3Rpb25TdHJpbmcgJiBhYWRcbiAgYWFkIC0tLT4gYXJtXG4gIGFhZCAtLS0-IGRhdGFwbGFuZVxuICBhYWQgLS0tPiBwcm94eVxuICByZXNvdXJjZVRva2VuIC0tLT4gc3FsIC0tPiBkYXRhcGxhbmVcbiAgcG9ydGFsVG9rZW4gLS0tPiBwcm94eVxuICBwcm94eSAtLT4gZGF0YXBsYW5lXG4gIGNvbm5lY3Rpb25TdHJpbmcgLS0-IHNxbCAmIG1vbmdvICYgY2Fzc2FuZHJhICYgZ3JhZiAmIHRhYmxlc1xuICBzcWwgLS0-IGRhdGFwbGFuZVxuICB0YWJsZXMgLS0-IGRhdGFwbGFuZVxuICBtb25nbyAtLT4gcHJveHlcbiAgY2Fzc2FuZHJhIC0tPiBwcm94eVxuICBncmFmIC0tPiBwcm94eVxuXG5cdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)
# Contributing # Contributing
Please read the [contribution guidelines](./CONTRIBUTING.md). Please read the [contribution guidelines](./CONTRIBUTING.md).

View File

@ -13,6 +13,11 @@
@NavMediumSpace: 10px; @NavMediumSpace: 10px;
@NavLargeSpace: 15px; @NavLargeSpace: 15px;
.skip-link {
position: fixed;
top: -200px;
}
html { html {
font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif; font-family: wf_segoe-ui_normal, "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif;
padding: 0px; padding: 0px;

View File

@ -1,20 +1,12 @@
@import "./Common/Constants"; @import "./Common/Constants";
.main {
width: 100%;
float: left;
transition: all .0s ease-in-out;
-ms-transition: all 0s ease-in-out;
-webkit-transition: all 0s ease-in-out;
-moz-transition: all .0s ease-in-out;
height: 100%;
background-color: white;
border-left: 0px solid white;
}
.resourceTree { .resourceTree {
height: 100%; height: 100%;
flex: 0 0 auto; flex: 0 0 auto;
.main {
height: 100%;
}
} }
.resourceTreeScroll { .resourceTreeScroll {

View File

@ -133,6 +133,7 @@ export class Features {
export class Flights { export class Flights {
public static readonly SettingsV2 = "settingsv2"; public static readonly SettingsV2 = "settingsv2";
public static readonly MongoIndexEditor = "mongoindexeditor"; public static readonly MongoIndexEditor = "mongoindexeditor";
public static readonly MongoIndexing = "mongoindexing";
} }
export class AfecFeatures { export class AfecFeatures {

View File

@ -21,7 +21,7 @@ export const handleError = (error: string | ARMError | Error, area: string, cons
sendNotificationForError(errorMessage, errorCode); sendNotificationForError(errorMessage, errorCode);
}; };
export const getErrorMessage = (error: string | Error): string => { export const getErrorMessage = (error: string | Error = ""): string => {
const errorMessage = typeof error === "string" ? error : error.message; const errorMessage = typeof error === "string" ? error : error.message;
return replaceKnownError(errorMessage); return replaceKnownError(errorMessage);
}; };
@ -45,10 +45,10 @@ const sendNotificationForError = (errorMessage: string, errorCode: number | stri
const replaceKnownError = (errorMessage: string): string => { const replaceKnownError = (errorMessage: string): string => {
if ( if (
window.dataExplorer?.subscriptionType() === SubscriptionType.Internal && window.dataExplorer?.subscriptionType() === SubscriptionType.Internal &&
errorMessage.indexOf("SharedOffer is Disabled for your account") >= 0 errorMessage?.indexOf("SharedOffer is Disabled for your account") >= 0
) { ) {
return "Database throughput is not supported for internal subscriptions."; return "Database throughput is not supported for internal subscriptions.";
} else if (errorMessage.indexOf("Partition key paths must contain only valid") >= 0) { } else if (errorMessage?.indexOf("Partition key paths must contain only valid") >= 0) {
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character."; return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
} }

View File

@ -2,8 +2,11 @@ import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
import { OfferResponse } from "@azure/cosmos"; import { OfferResponse } from "@azure/cosmos";
import { HttpHeaders } from "./Constants"; import { HttpHeaders } from "./Constants";
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => { export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer | undefined => {
const offerDefinition: SDKOfferDefinition = offerResponse?.resource; const offerDefinition: SDKOfferDefinition | undefined = offerResponse?.resource;
if (!offerDefinition) {
return undefined;
}
const offerContent = offerDefinition.content; const offerContent = offerDefinition.content;
if (!offerContent) { if (!offerContent) {
return undefined; return undefined;
@ -12,7 +15,7 @@ export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection; const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection;
const autopilotSettings = offerContent.offerAutopilotSettings; const autopilotSettings = offerContent.offerAutopilotSettings;
if (autopilotSettings) { if (autopilotSettings && autopilotSettings.maxThroughput && minimumThroughput) {
return { return {
id: offerDefinition.id, id: offerDefinition.id,
autoscaleMaxThroughput: autopilotSettings.maxThroughput, autoscaleMaxThroughput: autopilotSettings.maxThroughput,

View File

@ -23,10 +23,10 @@ export class Splitter {
public splitterId: string; public splitterId: string;
public leftSideId: string; public leftSideId: string;
public splitter: HTMLElement; public splitter!: HTMLElement;
public leftSide: HTMLElement; public leftSide!: HTMLElement;
public lastX: number; public lastX!: number;
public lastWidth: number; public lastWidth!: number;
private isCollapsed: ko.Observable<boolean>; private isCollapsed: ko.Observable<boolean>;
private bounds: SplitterBounds; private bounds: SplitterBounds;
@ -42,9 +42,10 @@ export class Splitter {
} }
public initialize() { public initialize() {
this.splitter = document.getElementById(this.splitterId); if (document.getElementById(this.splitterId) !== null && document.getElementById(this.leftSideId) != null) {
this.leftSide = document.getElementById(this.leftSideId); this.splitter = <HTMLElement>document.getElementById(this.splitterId);
this.leftSide = <HTMLElement>document.getElementById(this.leftSideId);
}
const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical; const isVerticalSplitter: boolean = this.direction === SplitterDirection.Vertical;
const splitterOptions: JQueryUI.ResizableOptions = { const splitterOptions: JQueryUI.ResizableOptions = {
animate: true, animate: true,

View File

@ -210,9 +210,9 @@ export interface QueryMetrics {
export interface Offer { export interface Offer {
id: string; id: string;
autoscaleMaxThroughput: number; autoscaleMaxThroughput: number | undefined;
manualThroughput: number; manualThroughput: number | undefined;
minimumThroughput: number; minimumThroughput: number | undefined;
offerDefinition?: SDKOfferDefinition; offerDefinition?: SDKOfferDefinition;
offerReplacePending: boolean; offerReplacePending: boolean;
} }

View File

@ -42,7 +42,7 @@ interface CollapsiblePanelParams {
* Use the optional "collapseToLeft" parameter to collapse to the left. * Use the optional "collapseToLeft" parameter to collapse to the left.
*/ */
class CollapsiblePanelViewModel { class CollapsiblePanelViewModel {
private params: CollapsiblePanelParams; public params: CollapsiblePanelParams;
private isCollapsed: ko.Observable<boolean>; private isCollapsed: ko.Observable<boolean>;
public constructor(params: CollapsiblePanelParams) { public constructor(params: CollapsiblePanelParams) {
@ -50,7 +50,7 @@ class CollapsiblePanelViewModel {
this.isCollapsed = params.isCollapsed || ko.observable(false); this.isCollapsed = params.isCollapsed || ko.observable(false);
} }
private toggleCollapse(): void { public toggleCollapse(): void {
this.isCollapsed(!this.isCollapsed()); this.isCollapsed(!this.isCollapsed());
} }
} }

View File

@ -71,7 +71,7 @@ interface InputTypeaheadParams {
/** /**
* This function gets called when pressing ENTER on the input box * This function gets called when pressing ENTER on the input box
*/ */
submitFct?: (inputValue: string, selection: Item) => void; submitFct?: (inputValue: string | null, selection: Item | null) => void;
/** /**
* Typehead comes with a Search button that we normally remove. * Typehead comes with a Search button that we normally remove.
@ -88,8 +88,8 @@ interface OnClickItem {
} }
interface Cache { interface Cache {
inputValue: string; inputValue: string | null;
selection: Item; selection: Item | null;
} }
class InputTypeaheadViewModel { class InputTypeaheadViewModel {
@ -98,15 +98,12 @@ class InputTypeaheadViewModel {
private params: InputTypeaheadParams; private params: InputTypeaheadParams;
private cache: Cache; private cache: Cache;
private inputValue: string;
private selection: Item;
public constructor(params: InputTypeaheadParams) { public constructor(params: InputTypeaheadParams) {
this.instanceNumber = InputTypeaheadViewModel.instanceCount++; this.instanceNumber = InputTypeaheadViewModel.instanceCount++;
this.params = params; this.params = params;
this.params.choices.subscribe(this.initializeTypeahead.bind(this)); this.params.choices.subscribe(this.initializeTypeahead.bind(this));
this.cache = { this.cache = {
inputValue: null, inputValue: null,
selection: null selection: null
@ -161,7 +158,7 @@ class InputTypeaheadViewModel {
} }
} }
$.typeahead(options); ($ as any).typeahead(options);
} }
/** /**
@ -177,11 +174,11 @@ class InputTypeaheadViewModel {
* Use ko's "template: afterRender" callback to do that without actually using any template. * Use ko's "template: afterRender" callback to do that without actually using any template.
* Another way is to call it within setTimeout() in constructor. * Another way is to call it within setTimeout() in constructor.
*/ */
private afterRender(): void { public afterRender(): void {
this.initializeTypeahead(); this.initializeTypeahead();
} }
private submit(): void { public submit(): void {
if (this.params.submitFct) { if (this.params.submitFct) {
this.params.submitFct(this.cache.inputValue, this.cache.selection); this.params.submitFct(this.cache.inputValue, this.cache.selection);
} }

View File

@ -59,11 +59,13 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
this.params = params; this.params = params;
this.params.content.subscribe((newValue: string) => { this.params.content.subscribe((newValue: string) => {
if (newValue) {
if (!!this.editor) { if (!!this.editor) {
this.editor.getModel().setValue(newValue); this.editor.getModel().setValue(newValue);
} else { } else {
this.createEditor(newValue, this.configureEditor.bind(this)); this.createEditor(newValue, this.configureEditor.bind(this));
} }
}
}); });
const onObserve: MutationCallback = (mutations: MutationRecord[], observer: MutationObserver): void => { const onObserve: MutationCallback = (mutations: MutationRecord[], observer: MutationObserver): void => {

View File

@ -231,7 +231,7 @@ describe("SettingsComponent", () => {
it("getUpdatedConflictResolutionPolicy", () => { it("getUpdatedConflictResolutionPolicy", () => {
const wrapper = shallow(<SettingsComponent {...baseProps} />); const wrapper = shallow(<SettingsComponent {...baseProps} />);
const conflictResolutionPolicyPath = "_ts"; const conflictResolutionPolicyPath = "/_ts";
const conflictResolutionPolicyProcedure = "sample_sproc"; const conflictResolutionPolicyProcedure = "sample_sproc";
const expectSprocPath = const expectSprocPath =
"/dbs/" + collection.databaseId + "/colls/" + collection.id() + "/sprocs/" + conflictResolutionPolicyProcedure; "/dbs/" + collection.databaseId + "/colls/" + collection.id() + "/sprocs/" + conflictResolutionPolicyProcedure;

View File

@ -138,8 +138,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
// Mongo container with system partition key still treat as "Fixed" // Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer = this.isFixedContainer =
!this.collection.partitionKey || this.container.isPreferredApiMongoDB() &&
(this.container.isPreferredApiMongoDB() && this.collection.partitionKey.systemKey); (!this.collection.partitionKey || this.collection.partitionKey.systemKey);
this.state = { this.state = {
throughput: undefined, throughput: undefined,
@ -684,7 +684,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if (policy.mode === DataModels.ConflictResolutionMode.LastWriterWins) { if (policy.mode === DataModels.ConflictResolutionMode.LastWriterWins) {
policy.conflictResolutionPath = this.state.conflictResolutionPolicyPath; policy.conflictResolutionPath = this.state.conflictResolutionPolicyPath;
if (policy.conflictResolutionPath?.startsWith("/")) { if (!policy.conflictResolutionPath?.startsWith("/")) {
policy.conflictResolutionPath = "/" + policy.conflictResolutionPath; policy.conflictResolutionPath = "/" + policy.conflictResolutionPath;
} }
} }

View File

@ -427,7 +427,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string newValue?: string
): void => { ): void => {
const newThroughput = getSanitizedInputValue(newValue, this.autoPilotInputMaxValue); const newThroughput = getSanitizedInputValue(newValue);
this.props.onMaxAutoPilotThroughputChange(newThroughput); this.props.onMaxAutoPilotThroughputChange(newThroughput);
}; };
@ -435,7 +435,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string newValue?: string
): void => { ): void => {
const newThroughput = getSanitizedInputValue(newValue, this.throughputInputMaxValue); const newThroughput = getSanitizedInputValue(newValue);
if (this.overrideWithAutoPilotSettings()) { if (this.overrideWithAutoPilotSettings()) {
this.props.onMaxAutoPilotThroughputChange(newThroughput); this.props.onMaxAutoPilotThroughputChange(newThroughput);
} else { } else {

View File

@ -101,13 +101,13 @@ export const parseConflictResolutionProcedure = (procedureFromBackEnd: string):
return procedureFromBackEnd; return procedureFromBackEnd;
}; };
export const getSanitizedInputValue = (newValueString: string, max: number): number => { export const getSanitizedInputValue = (newValueString: string, max?: number): number => {
const newValue = parseInt(newValueString); const newValue = parseInt(newValueString);
if (isNaN(newValue)) { if (isNaN(newValue)) {
return zeroValue; return zeroValue;
} }
// make sure new value does not exceed the maximum throughput // make sure new value does not exceed the maximum throughput
return Math.min(newValue, max); return max ? Math.min(newValue, max) : newValue;
}; };
export const isDirty = (current: isDirtyTypes, baseline: isDirtyTypes): boolean => { export const isDirty = (current: isDirtyTypes, baseline: isDirtyTypes): boolean => {

View File

@ -954,6 +954,7 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function], "isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function], "isLinkInjectionEnabled": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function], "isNotificationConsoleExpanded": [Function],
@ -1187,11 +1188,9 @@ exports[`SettingsComponent renders 1`] = `
}, },
"direction": "vertical", "direction": "vertical",
"isCollapsed": [Function], "isCollapsed": [Function],
"leftSide": null,
"leftSideId": "resourcetree", "leftSideId": "resourcetree",
"onResizeStart": [Function], "onResizeStart": [Function],
"onResizeStop": [Function], "onResizeStop": [Function],
"splitter": null,
"splitterId": "h_splitter1", "splitterId": "h_splitter1",
}, },
"stringInputPane": StringInputPane { "stringInputPane": StringInputPane {
@ -2237,6 +2236,7 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function], "isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function], "isLinkInjectionEnabled": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function], "isNotificationConsoleExpanded": [Function],
@ -2470,11 +2470,9 @@ exports[`SettingsComponent renders 1`] = `
}, },
"direction": "vertical", "direction": "vertical",
"isCollapsed": [Function], "isCollapsed": [Function],
"leftSide": null,
"leftSideId": "resourcetree", "leftSideId": "resourcetree",
"onResizeStart": [Function], "onResizeStart": [Function],
"onResizeStop": [Function], "onResizeStop": [Function],
"splitter": null,
"splitterId": "h_splitter1", "splitterId": "h_splitter1",
}, },
"stringInputPane": StringInputPane { "stringInputPane": StringInputPane {
@ -3533,6 +3531,7 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function], "isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function], "isLinkInjectionEnabled": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function], "isNotificationConsoleExpanded": [Function],
@ -3766,11 +3765,9 @@ exports[`SettingsComponent renders 1`] = `
}, },
"direction": "vertical", "direction": "vertical",
"isCollapsed": [Function], "isCollapsed": [Function],
"leftSide": null,
"leftSideId": "resourcetree", "leftSideId": "resourcetree",
"onResizeStart": [Function], "onResizeStart": [Function],
"onResizeStop": [Function], "onResizeStop": [Function],
"splitter": null,
"splitterId": "h_splitter1", "splitterId": "h_splitter1",
}, },
"stringInputPane": StringInputPane { "stringInputPane": StringInputPane {
@ -4816,6 +4813,7 @@ exports[`SettingsComponent renders 1`] = `
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function], "isLeftPaneExpanded": [Function],
"isLinkInjectionEnabled": [Function], "isLinkInjectionEnabled": [Function],
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
"isNotificationConsoleExpanded": [Function], "isNotificationConsoleExpanded": [Function],
@ -5049,11 +5047,9 @@ exports[`SettingsComponent renders 1`] = `
}, },
"direction": "vertical", "direction": "vertical",
"isCollapsed": [Function], "isCollapsed": [Function],
"leftSide": null,
"leftSideId": "resourcetree", "leftSideId": "resourcetree",
"onResizeStart": [Function], "onResizeStart": [Function],
"onResizeStop": [Function], "onResizeStop": [Function],
"splitter": null,
"splitterId": "h_splitter1", "splitterId": "h_splitter1",
}, },
"stringInputPane": StringInputPane { "stringInputPane": StringInputPane {

View File

@ -212,6 +212,7 @@ export default class Explorer {
public isCopyNotebookPaneEnabled: ko.Observable<boolean>; public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
public isHostedDataExplorerEnabled: ko.Computed<boolean>; public isHostedDataExplorerEnabled: ko.Computed<boolean>;
public isRightPanelV2Enabled: ko.Computed<boolean>; public isRightPanelV2Enabled: ko.Computed<boolean>;
public isMongoIndexingEnabled: ko.Observable<boolean>;
public canExceedMaximumValue: ko.Computed<boolean>; public canExceedMaximumValue: ko.Computed<boolean>;
public shouldShowShareDialogContents: ko.Observable<boolean>; public shouldShowShareDialogContents: ko.Observable<boolean>;
@ -409,6 +410,7 @@ export default class Explorer {
this.isFeatureEnabled(Constants.Features.enableLinkInjection) this.isFeatureEnabled(Constants.Features.enableLinkInjection)
); );
this.isGitHubPaneEnabled = ko.observable<boolean>(false); this.isGitHubPaneEnabled = ko.observable<boolean>(false);
this.isMongoIndexingEnabled = ko.observable<boolean>(false);
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false); this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false); this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
@ -1918,6 +1920,9 @@ export default class Explorer {
if (!flights) { if (!flights) {
return; return;
} }
if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) {
this.isMongoIndexingEnabled(true);
}
} }
public findSelectedCollection(): ViewModels.Collection { public findSelectedCollection(): ViewModels.Collection {

View File

@ -11,7 +11,9 @@ export class ArraysByKeyCache<T> {
public constructor(maxNbElements: number) { public constructor(maxNbElements: number) {
this.maxNbElements = maxNbElements; this.maxNbElements = maxNbElements;
this.clear(); this.keyQueue = [];
this.cache = {};
this.totalElements = 0;
} }
public clear(): void { public clear(): void {
@ -58,7 +60,7 @@ export class ArraysByKeyCache<T> {
* @param startIndex * @param startIndex
* @param pageSize * @param pageSize
*/ */
public retrieve(key: string, startIndex: number, pageSize: number): T[] { public retrieve(key: string, startIndex: number, pageSize: number): T[] | null {
if (!this.cache.hasOwnProperty(key)) { if (!this.cache.hasOwnProperty(key)) {
return null; return null;
} }
@ -77,9 +79,11 @@ export class ArraysByKeyCache<T> {
private reduceCacheSize(): void { private reduceCacheSize(): void {
// remove an key and its array // remove an key and its array
const oldKey = this.keyQueue.shift(); const oldKey = this.keyQueue.shift();
if (oldKey) {
this.totalElements -= this.cache[oldKey].length; this.totalElements -= this.cache[oldKey].length;
delete this.cache[oldKey]; delete this.cache[oldKey];
} }
}
/** /**
* Bubble up this key as new. * Bubble up this key as new.

View File

@ -413,13 +413,13 @@ export class GraphData<V extends GremlinVertex, E extends GremlinEdge> {
* @param node * @param node
* @param prop * @param prop
*/ */
public static getNodePropValue(node: D3Node, prop: string): string | number | boolean { public static getNodePropValue(node: D3Node, prop: string): undefined | string | number | boolean {
if (node.hasOwnProperty(prop)) { if (node.hasOwnProperty(prop)) {
return (node as any)[prop]; return (node as any)[prop];
} }
// This is DocDB specific // This is DocDB specific
if (node.hasOwnProperty("properties") && node.properties.hasOwnProperty(prop)) { if (node.properties && node.properties.hasOwnProperty(prop)) {
return node.properties[prop][0]["value"]; return node.properties[prop][0]["value"];
} }
@ -496,7 +496,7 @@ export class GraphData<V extends GremlinVertex, E extends GremlinEdge> {
* Get list of children ids of a given vertex * Get list of children ids of a given vertex
* @param vertex * @param vertex
*/ */
private static getChildrenId(vertex: GremlinVertex): string[] { public static getChildrenId(vertex: GremlinVertex): string[] {
const ids = <any>{}; // HashSet const ids = <any>{}; // HashSet
if (vertex.hasOwnProperty("outE")) { if (vertex.hasOwnProperty("outE")) {
let outE = vertex.outE; let outE = vertex.outE;

View File

@ -1,18 +1,17 @@
import { Observable, of } from "rxjs"; import { Observable, of } from "rxjs";
import { AjaxResponse } from "rxjs/ajax"; import { AjaxRequest, AjaxResponse } from "rxjs/ajax";
import { ServerConfig } from "rx-jupyter";
let fakeAjaxResponse: AjaxResponse = { let fakeAjaxResponse: AjaxResponse = {
originalEvent: undefined, originalEvent: <Event>(<unknown>undefined),
xhr: new XMLHttpRequest(), xhr: new XMLHttpRequest(),
request: null, request: <AjaxRequest>(<unknown>null),
status: 200, status: 200,
response: {}, response: {},
responseText: null, responseText: "",
responseType: "json" responseType: "json"
}; };
export const sessions = { export const sessions = {
create: (serverConfig: ServerConfig, body: object): Observable<AjaxResponse> => of(fakeAjaxResponse), create: (serverConfig: unknown, body: object): Observable<AjaxResponse> => of(fakeAjaxResponse),
__setResponse: (response: AjaxResponse) => { __setResponse: (response: AjaxResponse) => {
fakeAjaxResponse = response; fakeAjaxResponse = response;
}, },

View File

@ -11,7 +11,7 @@ import { stringifyNotebook } from "@nteract/commutable";
export class NotebookContentClient { export class NotebookContentClient {
constructor( constructor(
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>, private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
private notebookBasePath: ko.Observable<string>, public notebookBasePath: ko.Observable<string>,
private contentProvider: IContentProvider private contentProvider: IContentProvider
) {} ) {}
@ -117,9 +117,12 @@ export class NotebookContentClient {
private async checkIfFilepathExists(filepath: string): Promise<boolean> { private async checkIfFilepathExists(filepath: string): Promise<boolean> {
const parentDirPath = NotebookUtil.getParentPath(filepath); const parentDirPath = NotebookUtil.getParentPath(filepath);
if (parentDirPath) {
const items = await this.fetchNotebookFiles(parentDirPath); const items = await this.fetchNotebookFiles(parentDirPath);
return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath)); return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath));
} }
return false;
}
/** /**
* *
@ -189,7 +192,7 @@ export class NotebookContentClient {
const dir = xhr.response; const dir = xhr.response;
const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type); const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type);
item.parent = parent; item.parent = parent;
parent.children.push(item); parent.children!.push(item);
return item; return item;
}); });
} }
@ -225,7 +228,7 @@ export class NotebookContentClient {
* Convert rx-jupyter type to our type * Convert rx-jupyter type to our type
* @param type * @param type
*/ */
private static getType(type: FileType): NotebookContentItemType { public static getType(type: FileType): NotebookContentItemType {
switch (type) { switch (type) {
case "directory": case "directory":
return NotebookContentItemType.Directory; return NotebookContentItemType.Directory;

View File

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

View File

@ -654,7 +654,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
}); });
}); });
this.shouldCreateMongoWildcardIndex = ko.observable(false); this.shouldCreateMongoWildcardIndex = ko.observable(this.container.isMongoIndexingEnabled());
} }
public getSharedThroughputDefault(): boolean { public getSharedThroughputDefault(): boolean {
@ -679,6 +679,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
// TODO: Figure out if a database level partition split is about to happen once shared throughput read is available // TODO: Figure out if a database level partition split is about to happen once shared throughput read is available
this.formWarnings(""); this.formWarnings("");
this.databaseCreateNewShared(this.getSharedThroughputDefault()); this.databaseCreateNewShared(this.getSharedThroughputDefault());
this.shouldCreateMongoWildcardIndex(this.container.isMongoIndexingEnabled());
if (this.isPreferredApiTable() && !databaseId) { if (this.isPreferredApiTable() && !databaseId) {
databaseId = SharedConstants.CollectionCreation.TablesAPIDefaultDatabase; databaseId = SharedConstants.CollectionCreation.TablesAPIDefaultDatabase;
} }
@ -933,6 +934,8 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.shouldCreateMongoWildcardIndex = ko.observable(this.container.isMongoIndexingEnabled());
this.uniqueKeys([]); this.uniqueKeys([]);
this.useIndexingForSharedThroughput(true); this.useIndexingForSharedThroughput(true);

View File

@ -1,5 +1,5 @@
abstract class CacheBase<T> { abstract class CacheBase<T> {
public data: T[]; public data: T[] | null;
public sortOrder: any; public sortOrder: any;
public serverCallInProgress: boolean; public serverCallInProgress: boolean;

View File

@ -16,14 +16,75 @@ import * as Entities from "../Entities";
import QueryTablesTab from "../../Tabs/QueryTablesTab"; import QueryTablesTab from "../../Tabs/QueryTablesTab";
import * as TableEntityProcessor from "../TableEntityProcessor"; import * as TableEntityProcessor from "../TableEntityProcessor";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
interface IListTableEntitiesSegmentedResult extends Entities.IListTableEntitiesResult { interface IListTableEntitiesSegmentedResult extends Entities.IListTableEntitiesResult {
ExceedMaximumRetries?: boolean; ExceedMaximumRetries?: boolean;
} }
export interface ErrorDataModel {
message: string;
severity?: string;
location?: {
start: string;
end: string;
};
code?: string;
}
function parseError(err: any): ErrorDataModel[] {
try {
return _parse(err);
} catch (e) {
return [<ErrorDataModel>{ message: JSON.stringify(err) }];
}
}
function _parse(err: any): ErrorDataModel[] {
var normalizedErrors: ErrorDataModel[] = [];
if (err.message && !err.code) {
normalizedErrors.push(err);
} else {
const innerErrors: any[] = _getInnerErrors(err.message);
normalizedErrors = innerErrors.map(innerError =>
typeof innerError === "string" ? { message: innerError } : innerError
);
}
return normalizedErrors;
}
function _getInnerErrors(message: string): any[] {
/*
The backend error message has an inner-message which is a stringified object.
For SQL errors, the "errors" property is an array of SqlErrorDataModel.
Example:
"Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p"
For non-SQL errors the "Errors" propery is an array of string.
Example:
"Message: {"errors":[{"severity":"Error","location":{"start":7,"end":8},"code":"SC1001","message":"Syntax error, incorrect syntax near '.'."}]}\r\nActivityId: d3300016d4084e310a, Request URI: /apps/12401f9e1df77/services/dc100232b1f44545/partitions/f86f3bc0001a2f78/replicas/13085003638s"
*/
let innerMessage: any = null;
const singleLineMessage = message.replace(/[\r\n]|\r|\n/g, "");
try {
// Multi-Partition error flavor
const regExp = /^(.*)ActivityId: (.*)/g;
const regString = regExp.exec(singleLineMessage);
const innerMessageString = regString[1];
innerMessage = JSON.parse(innerMessageString);
} catch (e) {
// Single-partition error flavor
const regExp = /^Message: (.*)ActivityId: (.*), Request URI: (.*)/g;
const regString = regExp.exec(singleLineMessage);
const innerMessageString = regString[1];
innerMessage = JSON.parse(innerMessageString);
}
return innerMessage.errors ? innerMessage.errors : innerMessage.Errors;
}
/** /**
* Storage Table Entity List ViewModel * Storage Table Entity List ViewModel
*/ */
@ -387,8 +448,17 @@ export default class TableEntityListViewModel extends DataTableViewModel {
} }
}) })
.catch((error: any) => { .catch((error: any) => {
const errorMessage = getErrorMessage(error); const parsedErrors = parseError(error);
this.queryErrorMessage(errorMessage); var errors = parsedErrors.map(error => {
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);
if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) { if (this.queryTablesTab.onLoadStartKey != null && this.queryTablesTab.onLoadStartKey != undefined) {
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.Tab, Action.Tab,
@ -399,8 +469,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
defaultExperience: this.queryTablesTab.collection.container.defaultExperience(), defaultExperience: this.queryTablesTab.collection.container.defaultExperience(),
dataExplorerArea: Areas.Tab, dataExplorerArea: Areas.Tab,
tabTitle: this.queryTablesTab.tabTitle(), tabTitle: this.queryTablesTab.tabTitle(),
error: errorMessage, error: error
errorStack: getErrorStack(error)
}, },
this.queryTablesTab.onLoadStartKey this.queryTablesTab.onLoadStartKey
); );
@ -421,47 +490,53 @@ export default class TableEntityListViewModel extends DataTableViewModel {
* Note that this also means that we can get less entities than the requested download size in a successful call. * Note that this also means that we can get less entities than the requested download size in a successful call.
* See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx * See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx
*/ */
private async prefetchData( private prefetchData(
tableQuery: Entities.ITableQuery, tableQuery: Entities.ITableQuery,
downloadSize: number, downloadSize: number,
currentRetry: number = 0 currentRetry: number = 0
): Promise<IListTableEntitiesSegmentedResult> { ): Q.Promise<any> {
if (!this.cache.serverCallInProgress) { if (!this.cache.serverCallInProgress) {
this.cache.serverCallInProgress = true; this.cache.serverCallInProgress = true;
this.allDownloaded = false; this.allDownloaded = false;
this.lastPrefetchTime = new Date().getTime(); this.lastPrefetchTime = new Date().getTime();
const time = this.lastPrefetchTime; var time = this.lastPrefetchTime;
var promise: Q.Promise<IListTableEntitiesSegmentedResult>;
if (this._documentIterator && this.continuationToken) { if (this._documentIterator && this.continuationToken) {
// TODO handle Cassandra case // TODO handle Cassandra case
const response = await this._documentIterator.fetchNext();
const entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(response?.resources);
return { promise = Q(this._documentIterator.fetchNext().then(response => response.resources)).then(
(documents: any[]) => {
let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents);
let finalEntities: IListTableEntitiesSegmentedResult = <IListTableEntitiesSegmentedResult>{
Results: entities, Results: entities,
ContinuationToken: this._documentIterator.hasMoreResults() ContinuationToken: this._documentIterator.hasMoreResults()
}; };
return Q.resolve(finalEntities);
} }
);
try { } else if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) {
let documents: IListTableEntitiesSegmentedResult; promise = Q(
if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) { this.queryTablesTab.container.tableDataClient.queryDocuments(
documents = await this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection, this.queryTablesTab.collection,
this.cqlQuery(), this.cqlQuery(),
true, true,
this.continuationToken this.continuationToken
)
); );
} else { } else {
const query = this.queryTablesTab.container.isPreferredApiCassandra() ? this.cqlQuery() : this.sqlQuery(); let query = this.sqlQuery();
documents = await this.queryTablesTab.container.tableDataClient.queryDocuments( if (this.queryTablesTab.container.isPreferredApiCassandra()) {
this.queryTablesTab.collection, query = this.cqlQuery();
query, }
true promise = Q(
this.queryTablesTab.container.tableDataClient.queryDocuments(this.queryTablesTab.collection, query, true)
); );
}
return promise
.then((result: IListTableEntitiesSegmentedResult) => {
if (!this._documentIterator) { if (!this._documentIterator) {
this._documentIterator = documents.iterator; this._documentIterator = result.iterator;
} }
var actualDownloadSize: number = 0; var actualDownloadSize: number = 0;
@ -472,11 +547,11 @@ export default class TableEntityListViewModel extends DataTableViewModel {
return Q.resolve(null); return Q.resolve(null);
} }
var entities = documents.Results; var entities = result.Results;
actualDownloadSize = entities.length; actualDownloadSize = entities.length;
// Queries can fetch no results and still return a continuation header. See prefetchAndRender() method. // Queries can fetch no results and still return a continuation header. See prefetchAndRender() method.
this.continuationToken = this.isCancelled ? null : documents.ContinuationToken; this.continuationToken = this.isCancelled ? null : result.ContinuationToken;
if (!this.continuationToken) { if (!this.continuationToken) {
this.allDownloaded = true; this.allDownloaded = true;
@ -508,22 +583,20 @@ export default class TableEntityListViewModel extends DataTableViewModel {
// For #2.1, set prefetch exceeds maximum retry number and end prefetch. // For #2.1, set prefetch exceeds maximum retry number and end prefetch.
// For #2.2, go to next round prefetch. // For #2.2, go to next round prefetch.
if (this.allDownloaded || nextDownloadSize === 0) { if (this.allDownloaded || nextDownloadSize === 0) {
return documents; return Q.resolve(result);
} }
if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) { if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) {
documents.ExceedMaximumRetries = true; result.ExceedMaximumRetries = true;
return documents; return Q.resolve(result);
} }
return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
return await this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1); })
} .catch((error: Error) => {
} catch (error) {
this.cache.serverCallInProgress = false; this.cache.serverCallInProgress = false;
throw error; return Q.reject(error);
});
} }
} return null;
return undefined;
} }
} }

View File

@ -7,7 +7,7 @@ export interface ITableEntity {
export interface ITableEntityForTablesAPI extends ITableEntity { export interface ITableEntityForTablesAPI extends ITableEntity {
PartitionKey: ITableEntityAttribute; PartitionKey: ITableEntityAttribute;
RowKey: ITableEntityAttribute; RowKey: ITableEntityAttribute;
Timestamp?: ITableEntityAttribute; Timestamp: ITableEntityAttribute;
} }
export interface ITableEntityAttribute { export interface ITableEntityAttribute {

View File

@ -11,6 +11,17 @@
Start by writing a Mongo query, for example: <strong>{'id':'foo'}</strong> or <strong>{ }</strong> to get all the Start by writing a Mongo query, for example: <strong>{'id':'foo'}</strong> or <strong>{ }</strong> to get all the
documents. documents.
</div> </div>
<div class="warningErrorContainer" aria-live="assertive" data-bind="visible: maybeSubQuery">
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/info_color.svg" alt="Error"/></span>
<span class="warningErrorDetailsLinkContainer">
We have detected you may be using a subquery. Non-correlated subqueries are not currently supported.
<a href="https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery"
>Please see Cosmos sub query documentation for further information</a
>
</span>
</div>
</div>
<div class="queryEditorWithSplitter" data-bind="attr: { id: queryEditorId }"> <div class="queryEditorWithSplitter" data-bind="attr: { id: queryEditorId }">
<editor <editor
class="queryEditor" class="queryEditor"

View File

@ -1,5 +1,4 @@
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
@ -7,7 +6,6 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
import { HashMap } from "../../Common/HashMap"; import { HashMap } from "../../Common/HashMap";
import * as HeadersUtility from "../../Common/HeadersUtility"; import * as HeadersUtility from "../../Common/HeadersUtility";
import * as Logger from "../../Common/Logger";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
@ -31,6 +29,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
public fetchNextPageButton: ViewModels.Button; public fetchNextPageButton: ViewModels.Button;
public saveQueryButton: ViewModels.Button; public saveQueryButton: ViewModels.Button;
public initialEditorContent: ko.Observable<string>; public initialEditorContent: ko.Observable<string>;
public maybeSubQuery: ko.Computed<boolean>;
public sqlQueryEditorContent: ko.Observable<string>; public sqlQueryEditorContent: ko.Observable<string>;
public selectedContent: ko.Observable<string>; public selectedContent: ko.Observable<string>;
public sqlStatementToExecute: ko.Observable<string>; public sqlStatementToExecute: ko.Observable<string>;
@ -120,6 +119,11 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
return (container && (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph())) || false; return (container && (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph())) || false;
}); });
this.maybeSubQuery = ko.computed<boolean>(function() {
const sql = this.sqlQueryEditorContent();
return sql && /.*\(.*SELECT.*\)/i.test(sql);
}, this);
this.saveQueryButton = { this.saveQueryButton = {
enabled: this._isSaveQueriesEnabled, enabled: this._isSaveQueriesEnabled,
visible: this._isSaveQueriesEnabled visible: this._isSaveQueriesEnabled

View File

@ -8,7 +8,7 @@ enum ScrollPosition {
export class AccessibleVerticalList { export class AccessibleVerticalList {
private items: any[] = []; private items: any[] = [];
private onSelect: (item: any) => void; private onSelect?: (item: any) => void;
public currentItemIndex: ko.Observable<number>; public currentItemIndex: ko.Observable<number>;
public currentItem: ko.Computed<any>; public currentItem: ko.Computed<any>;
@ -42,7 +42,9 @@ export class AccessibleVerticalList {
const targetElement = targetContainer const targetElement = targetContainer
.getElementsByClassName("accessibleListElement") .getElementsByClassName("accessibleListElement")
.item(this.currentItemIndex()); .item(this.currentItemIndex());
if (targetElement) {
this.scrollElementIntoContainerViewIfNeeded(targetElement, targetContainer, ScrollPosition.Top); this.scrollElementIntoContainerViewIfNeeded(targetElement, targetContainer, ScrollPosition.Top);
}
return false; return false;
} }
if (event.keyCode === 40) { if (event.keyCode === 40) {
@ -52,7 +54,9 @@ export class AccessibleVerticalList {
const targetElement = targetContainer const targetElement = targetContainer
.getElementsByClassName("accessibleListElement") .getElementsByClassName("accessibleListElement")
.item(this.currentItemIndex()); .item(this.currentItemIndex());
this.scrollElementIntoContainerViewIfNeeded(targetElement, targetContainer, ScrollPosition.Bottom); if (targetElement) {
this.scrollElementIntoContainerViewIfNeeded(targetElement, targetContainer, ScrollPosition.Top);
}
return false; return false;
} }
return true; return true;

View File

@ -4,7 +4,7 @@ import * as DataModels from "../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
export class DefaultExperienceUtility { export class DefaultExperienceUtility {
public static getDefaultExperienceFromDatabaseAccount(databaseAccount: DataModels.DatabaseAccount): string { public static getDefaultExperienceFromDatabaseAccount(databaseAccount: DataModels.DatabaseAccount): string | null {
if (!databaseAccount) { if (!databaseAccount) {
return null; return null;
} }
@ -81,11 +81,9 @@ export class DefaultExperienceUtility {
private static _getDefaultExperience(kind: string, capabilities: DataModels.Capability[]): string { private static _getDefaultExperience(kind: string, capabilities: DataModels.Capability[]): string {
const defaultDefaultExperience: string = Constants.DefaultAccountExperience.DocumentDB; const defaultDefaultExperience: string = Constants.DefaultAccountExperience.DocumentDB;
const defaultExperienceFromKind: string = DefaultExperienceUtility._getDefaultExperienceFromAccountKind(kind); const defaultExperienceFromKind: string = DefaultExperienceUtility._getDefaultExperienceFromAccountKind(kind) || "";
const defaultExperienceFromCapabilities: string = DefaultExperienceUtility._getDefaultExperienceFromAccountCapabilities( const defaultExperienceFromCapabilities: string =
capabilities DefaultExperienceUtility._getDefaultExperienceFromAccountCapabilities(capabilities) || "";
);
if (!!defaultExperienceFromKind) { if (!!defaultExperienceFromKind) {
return defaultExperienceFromKind; return defaultExperienceFromKind;
} }
@ -97,7 +95,7 @@ export class DefaultExperienceUtility {
return defaultDefaultExperience; return defaultDefaultExperience;
} }
private static _getDefaultExperienceFromAccountKind(kind: string): string { private static _getDefaultExperienceFromAccountKind(kind: string): string | null {
if (!kind) { if (!kind) {
return null; return null;
} }
@ -113,7 +111,7 @@ export class DefaultExperienceUtility {
return null; return null;
} }
private static _getDefaultExperienceFromAccountCapabilities(capabilities: DataModels.Capability[]): string { private static _getDefaultExperienceFromAccountCapabilities(capabilities: DataModels.Capability[]): string | null {
if (!capabilities) { if (!capabilities) {
return null; return null;
} }

View File

@ -19,8 +19,8 @@ const getUrlVars = (): { [key: string]: string } => {
}; };
const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => { const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => {
let body: BodyInit; let body: BodyInit | undefined;
let headers: HeadersInit; let headers: HeadersInit | undefined;
if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) { if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) {
body = JSON.stringify({ body = JSON.stringify({
endpoint: urlVars[TerminalQueryParams.TerminalEndpoint] endpoint: urlVars[TerminalQueryParams.TerminalEndpoint]

View File

@ -1,5 +1,7 @@
import { armRequest } from "./request"; import { armRequest } from "./request";
import fetch from "node-fetch"; import fetch from "node-fetch";
import { updateUserContext } from "../../UserContext";
import { AuthType } from "../../AuthType";
interface Global { interface Global {
Headers: unknown; Headers: unknown;
@ -8,6 +10,11 @@ interface Global {
((global as unknown) as Global).Headers = ((fetch as unknown) as Global).Headers; ((global as unknown) as Global).Headers = ((fetch as unknown) as Global).Headers;
describe("ARM request", () => { describe("ARM request", () => {
window.authType = AuthType.AAD;
updateUserContext({
authorizationToken: "some-token"
});
it("should call window.fetch", async () => { it("should call window.fetch", async () => {
window.fetch = jest.fn().mockResolvedValue({ window.fetch = jest.fn().mockResolvedValue({
ok: true, ok: true,
@ -48,4 +55,24 @@ describe("ARM request", () => {
).rejects.toThrow(); ).rejects.toThrow();
expect(window.fetch).toHaveBeenCalledTimes(2); expect(window.fetch).toHaveBeenCalledTimes(2);
}); });
it("should throw token error", async () => {
window.authType = AuthType.AAD;
updateUserContext({
authorizationToken: undefined
});
const headers = new Headers();
headers.set("location", "https://foo.com/operationStatus");
window.fetch = jest.fn().mockResolvedValue({
ok: true,
headers,
status: 200,
json: async () => {
return { status: "Failed" };
}
});
await expect(() =>
armRequest({ apiVersion: "2001-01-01", host: "https://foo.com", path: "foo", method: "GET" })
).rejects.toThrow("No authority token provided");
});
}); });

View File

@ -30,7 +30,7 @@ export class ARMError extends Error {
Object.setPrototypeOf(this, ARMError.prototype); Object.setPrototypeOf(this, ARMError.prototype);
} }
public code: string | number; public code?: string | number;
} }
interface ARMQueryParams { interface ARMQueryParams {
@ -63,6 +63,10 @@ export async function armRequest<T>({
queryParams.metricNames && url.searchParams.append("metricnames", queryParams.metricNames); queryParams.metricNames && url.searchParams.append("metricnames", queryParams.metricNames);
} }
if (!userContext.authorizationToken) {
throw new Error("No authority token provided");
}
const response = await window.fetch(url.href, { const response = await window.fetch(url.href, {
method, method,
headers: { headers: {
@ -98,6 +102,10 @@ export async function armRequest<T>({
} }
async function getOperationStatus(operationStatusUrl: string) { async function getOperationStatus(operationStatusUrl: string) {
if (!userContext.authorizationToken) {
throw new Error("No authority token provided");
}
const response = await window.fetch(operationStatusUrl, { const response = await window.fetch(operationStatusUrl, {
headers: { headers: {
Authorization: userContext.authorizationToken Authorization: userContext.authorizationToken

View File

@ -7,6 +7,7 @@
</head> </head>
<body> <body>
<a class="skip-link" href="#data-explorer-content">Skip to content</a>
<header> <header>
<div class="items" role="menubar"> <div class="items" role="menubar">
<div class="cosmosDBTitle"> <div class="cosmosDBTitle">
@ -48,6 +49,7 @@
<switch-directory-pane params="{data: switchDirectoryPane}"></switch-directory-pane> <switch-directory-pane params="{data: switchDirectoryPane}"></switch-directory-pane>
<div id="data-explorer-content">
<!-- TODO generate version number dynamically --> <!-- TODO generate version number dynamically -->
<iframe <iframe
id="explorerMenu" id="explorerMenu"
@ -58,6 +60,7 @@
data-bind="visible: navigationSelection() === 'explorer'" data-bind="visible: navigationSelection() === 'explorer'"
> >
</iframe> </iframe>
</div>
<div data-bind="react: firewallWarningComponentAdapter"></div> <div data-bind="react: firewallWarningComponentAdapter"></div>
<div data-bind="react: dialogComponentAdapter"></div> <div data-bind="react: dialogComponentAdapter"></div>

View File

@ -13,6 +13,7 @@
"./src/Common/ArrayHashMap.ts", "./src/Common/ArrayHashMap.ts",
"./src/Common/Constants.ts", "./src/Common/Constants.ts",
"./src/Common/DeleteFeedback.ts", "./src/Common/DeleteFeedback.ts",
"./src/Common/DocumentUtility.ts",
"./src/Common/EnvironmentUtility.ts", "./src/Common/EnvironmentUtility.ts",
"./src/Common/HashMap.ts", "./src/Common/HashMap.ts",
"./src/Common/HeadersUtility.ts", "./src/Common/HeadersUtility.ts",
@ -20,8 +21,10 @@
"./src/Common/MessageHandler.ts", "./src/Common/MessageHandler.ts",
"./src/Common/MongoUtility.ts", "./src/Common/MongoUtility.ts",
"./src/Common/ObjectCache.ts", "./src/Common/ObjectCache.ts",
"./src/Common/OfferUtility.ts",
"./src/Common/ThemeUtility.ts", "./src/Common/ThemeUtility.ts",
"./src/Common/UrlUtility.ts", "./src/Common/UrlUtility.ts",
"./src/Common/Splitter.ts",
"./src/ConfigContext.ts", "./src/ConfigContext.ts",
"./src/Contracts/ActionContracts.ts", "./src/Contracts/ActionContracts.ts",
"./src/Contracts/DataModels.ts", "./src/Contracts/DataModels.ts",
@ -39,9 +42,15 @@
"./src/Definitions/plotly.js-cartesian-dist.d-min.ts", "./src/Definitions/plotly.js-cartesian-dist.d-min.ts",
"./src/Definitions/svg.d.ts", "./src/Definitions/svg.d.ts",
"./src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts", "./src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts",
"./src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts",
"./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts", "./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts",
"./src/Explorer/Controls/InputTypeahead/InputTypeahead.ts",
"./src/Explorer/Controls/SmartUi/InputUtils.ts", "./src/Explorer/Controls/SmartUi/InputUtils.ts",
"./src/Explorer/Graph/GraphExplorerComponent/__mocks__/GremlinClient.ts", "./src/Explorer/Graph/GraphExplorerComponent/__mocks__/GremlinClient.ts",
"./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts",
"./src/Explorer/Graph/GraphExplorerComponent/EdgeInfoCache.ts",
"./src/Explorer/Graph/GraphExplorerComponent/GraphData.ts",
"./src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts",
"./src/Explorer/Notebook/FileSystemUtil.ts", "./src/Explorer/Notebook/FileSystemUtil.ts",
"./src/Explorer/Notebook/NTeractUtil.ts", "./src/Explorer/Notebook/NTeractUtil.ts",
"./src/Explorer/Notebook/NotebookComponent/actions.ts", "./src/Explorer/Notebook/NotebookComponent/actions.ts",
@ -50,13 +59,18 @@
"./src/Explorer/Notebook/NotebookComponent/types.ts", "./src/Explorer/Notebook/NotebookComponent/types.ts",
"./src/Explorer/Notebook/NotebookContentItem.ts", "./src/Explorer/Notebook/NotebookContentItem.ts",
"./src/Explorer/Notebook/NotebookUtil.ts", "./src/Explorer/Notebook/NotebookUtil.ts",
"./src/Explorer/Tree/AccessibleVerticalList.ts",
"./src/Explorer/Panes/PaneComponents.ts", "./src/Explorer/Panes/PaneComponents.ts",
"./src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts", "./src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts",
"./src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts", "./src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts",
"./src/Explorer/Tables/Constants.ts", "./src/Explorer/Tables/Constants.ts",
"./src/Explorer/Tables/CqlUtilities.ts", "./src/Explorer/Tables/CqlUtilities.ts",
"./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts", "./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts",
"./src/Explorer/Tables/DataTable/CacheBase.ts",
"./src/Explorer/Tables/Entities.ts",
"./src/Explorer/Tabs/TabComponents.ts", "./src/Explorer/Tabs/TabComponents.ts",
"./src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts",
"./src/Explorer/Notebook/NotebookContentClient.ts",
"./src/GitHub/GitHubConnector.ts", "./src/GitHub/GitHubConnector.ts",
"./src/Index.ts", "./src/Index.ts",
"./src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts", "./src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts",
@ -70,6 +84,8 @@
"./src/Shared/Telemetry/TelemetryConstants.ts", "./src/Shared/Telemetry/TelemetryConstants.ts",
"./src/Shared/Telemetry/TelemetryProcessor.ts", "./src/Shared/Telemetry/TelemetryProcessor.ts",
"./src/Shared/appInsights.ts", "./src/Shared/appInsights.ts",
"./src/Shared/DefaultExperienceUtility.ts",
"./src/Terminal/index.ts",
"./src/Terminal/JupyterLabAppFactory.ts", "./src/Terminal/JupyterLabAppFactory.ts",
"./src/UserContext.ts", "./src/UserContext.ts",
"./src/Utils/Base64Utils.ts", "./src/Utils/Base64Utils.ts",
@ -79,6 +95,25 @@
"./src/Utils/StringUtils.ts", "./src/Utils/StringUtils.ts",
"./src/Utils/WindowUtils.ts", "./src/Utils/WindowUtils.ts",
"./src/Utils/arm/generatedClients/2020-04-01/types.ts", "./src/Utils/arm/generatedClients/2020-04-01/types.ts",
"./src/Utils/arm/generatedClients/2020-04-01/cassandraResources.ts",
"./src/Utils/arm/generatedClients/2020-04-01/collection.ts",
"./src/Utils/arm/generatedClients/2020-04-01/collectionPartition.ts",
"./src/Utils/arm/generatedClients/2020-04-01/collectionPartitionRegion.ts",
"./src/Utils/arm/generatedClients/2020-04-01/collectionRegion.ts",
"./src/Utils/arm/generatedClients/2020-04-01/database.ts",
"./src/Utils/arm/generatedClients/2020-04-01/databaseAccountRegion.ts",
"./src/Utils/arm/generatedClients/2020-04-01/databaseAccounts.ts",
"./src/Utils/arm/generatedClients/2020-04-01/gremlinResources.ts",
"./src/Utils/arm/generatedClients/2020-04-01/mongoDBResources.ts",
"./src/Utils/arm/generatedClients/2020-04-01/operations.ts",
"./src/Utils/arm/generatedClients/2020-04-01/partitionKeyRangeId.ts",
"./src/Utils/arm/generatedClients/2020-04-01/partitionKeyRangeIdRegion.ts",
"./src/Utils/arm/generatedClients/2020-04-01/percentile.ts",
"./src/Utils/arm/generatedClients/2020-04-01/percentileSourceTarget.ts",
"./src/Utils/arm/generatedClients/2020-04-01/percentileTarget.ts",
"./src/Utils/arm/generatedClients/2020-04-01/sqlResources.ts",
"./src/Utils/arm/generatedClients/2020-04-01/tableResources.ts",
"./src/Utils/arm/request.ts",
"./src/quickstart.ts", "./src/quickstart.ts",
"./src/setupTests.ts", "./src/setupTests.ts",
"./src/workers/upload/definitions.ts" "./src/workers/upload/definitions.ts"