Compare commits

...

30 Commits

Author SHA1 Message Date
Steve Faulkner
5cc291a04f Test 2020-09-21 15:39:46 -05:00
Tanuj Mittal
92c4440d38 Skip refreshing pinned repos if token not available (#219) 2020-09-21 11:41:48 -07:00
victor-meng
1ccffab911 [Accessibility] Add horizontal scroll bar if tooltip box goes outside of panel boundary (#215) 2020-09-18 17:01:12 -07:00
victor-meng
dc56f7e154 Lazy load database offer in data explorer (#208)
Co-authored-by: zfoster <notzachfoster@gmail.com>
2020-09-18 16:00:21 -07:00
Laurent Nguyen
e62184a1f2 Fix inaccessible link by keyboard nav in info bubble (#216)
* Fix inaccessible link by keyboard nav in info bubble

* Fix shift-tab on link to focus previous element
2020-09-18 16:26:04 +02:00
Laurent Nguyen
26c832437b Support showing only notebook output scenario (#177)
* Enable hiding prompt when hiding inputs (so that only output is showing)

* Fix format

* Rename variable

* Use nteract prompt instead of copying source
2020-09-18 16:25:09 +02:00
Tanuj Mittal
832f8d560d Reset focus to trigger element when pane is closed (#217) 2020-09-17 15:08:46 -07:00
Steve Faulkner
d85c96d408 Allow remote config to set valid origins (#205) 2020-09-17 16:13:22 -05:00
Tanuj Mittal
bad6a60d07 Fix aria-labels in AddCollectionPane and DeleteCollectionConfirmationPane (#213)
* Fix aria-labels in AddCollectionPane and DeleteCollectionConfirmationPane

* Fix broken delete
2020-09-17 12:20:22 -07:00
Zachary Foster
b690fe18e6 Focuses header control element on Add Row click in Cassandra (#212) 2020-09-17 13:00:15 -04:00
Garrett Ausfeldt
1bbe08378c Fix focus when adding and removing a unique key (#214)
* fix focus when adding and removing a unique key

* cleanup

Co-authored-by: REDMOND\gaausfel <gaausfel@microsoft.com>
2020-09-17 09:48:42 -07:00
Laurent Nguyen
9b021b29b9 Fix a11y bugs in query stats table: too many columns, header info not passed to column contents (#209) 2020-09-17 10:43:01 +02:00
Laurent Nguyen
562ac38ff1 Fix contrast issue (blue hyperlink over black background) (#210) 2020-09-17 10:40:24 +02:00
DanielSPham
949f9203b8 Analytical storage label (#211)
Co-authored-by: Daniel Si Pham <v-danpha@microsoft.com>
2020-09-16 17:18:00 -07:00
DanielSPham
de7761ba4b Fixed aria label for autopilot throughput (#206)
Co-authored-by: Daniel Si Pham <v-danpha@microsoft.com>
2020-09-16 17:17:41 -07:00
DanielSPham
40f4efab7c Added heading role to pane title (#203)
Co-authored-by: Daniel Si Pham <v-danpha@microsoft.com>
2020-09-16 17:17:23 -07:00
Tanuj Mittal
34c41e1557 Add telemetry for OpenTerminal (#200) 2020-09-16 17:04:34 -07:00
Steve Faulkner
03b19fc875 Split all script data access methods (#197)
* Split all script data access methods

* More cleanup

* Fix Typo
2020-09-15 13:31:30 -05:00
Tanuj Mittal
d6a4924710 Add actionModifier to appInsights telemetry (#202) 2020-09-15 11:21:18 -07:00
DanielSPham
5ccf26e403 Change load more role (#190)
Co-authored-by: Daniel Si Pham <v-danpha@microsoft.com>
2020-09-14 17:02:29 -07:00
DanielSPham
ef7da10b6e Added alert role to query tables tab error (#199)
Co-authored-by: Daniel Si Pham <v-danpha@microsoft.com>
2020-09-14 16:53:39 -07:00
DanielSPham
dfd18152ca Fixed placeholder (#201)
* Fixed placeholder

* Clean up

Co-authored-by: Daniel Si Pham <v-danpha@microsoft.com>
2020-09-14 16:34:56 -07:00
DanielSPham
e22675bc40 Updated TreeComponent alt text (#188)
Co-authored-by: Daniel Si Pham <v-danpha@microsoft.com>
2020-09-14 16:27:12 -07:00
Tanuj Mittal
c4257bf4a9 Revert "Add telemetry for OpenTerminal success/failure (#192)" (#198)
This reverts commit 83b13de685.
2020-09-11 23:12:28 -07:00
Tanuj Mittal
728eeefa17 Fix file downloads from notebooks container (#196)
* Fix file downloads from notebooks container

* Add downloading message
2020-09-11 19:37:00 -07:00
Tanuj Mittal
83b13de685 Add telemetry for OpenTerminal success/failure (#192)
* Add telemetry for OpenTerminal success/failure

* Address review comments
2020-09-11 15:42:55 -07:00
Steve Faulkner
c401f88aae Improve ARM error parsing and display (#189) 2020-09-11 12:54:30 -05:00
DanielSPham
af820c0fbf Fixed notification console screen reader issue (#193)
* Fixed notification console screen reader issue

* Added aria expanded

Co-authored-by: Daniel Si Pham <v-danpha@microsoft.com>
2020-09-11 10:36:53 -07:00
swviswan
a2845a0102 Notebooks: Ability to download file in notebooks. (#194)
Today, if we have non notebook file in the container and we try to download the file, it fails. That is because, the filetype is always defined as notebook in the call to content provider's get method. Fixing it to use the right type in this change.
2020-09-10 23:39:33 -07:00
Tanuj Mittal
ed9b443bf6 Disable fetch tracking in appInsights (#187) 2020-09-10 12:15:24 -07:00
94 changed files with 1400 additions and 1566 deletions

View File

@@ -1522,6 +1522,10 @@ p {
.tooltipVisible(); .tooltipVisible();
} }
.infoTooltip a {
color: @AccentHigh;
}
.nowrap { .nowrap {
white-space: nowrap; white-space: nowrap;
} }
@@ -1739,7 +1743,7 @@ input::-webkit-calendar-picker-indicator {
padding-right: 34px; padding-right: 34px;
color: @BaseDark; color: @BaseDark;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: auto;
margin: (2 * @MediumSpace) 0px; margin: (2 * @MediumSpace) 0px;
} }
@@ -2972,6 +2976,10 @@ settings-pane {
.enableAnalyticalStorageRadio:nth-child(n+2) { .enableAnalyticalStorageRadio:nth-child(n+2) {
margin-left: @LargeSpace; margin-left: @LargeSpace;
} }
.enableAnalyticalStorageRadioLabel {
padding: 0px
}
} }
.addCollectionLabel { .addCollectionLabel {

View File

@@ -1,28 +1,26 @@
import * as _ from "underscore";
import * as Constants from "./Constants";
import * as DataModels from "../Contracts/DataModels";
import * as HeadersUtility from "./HeadersUtility";
import * as ViewModels from "../Contracts/ViewModels";
import Q from "q";
import { import {
ConflictDefinition, ConflictDefinition,
FeedOptions, FeedOptions,
ItemDefinition, ItemDefinition,
OfferDefinition,
QueryIterator, QueryIterator,
Resource, Resource
TriggerDefinition,
OfferDefinition
} from "@azure/cosmos"; } from "@azure/cosmos";
import { client } from "./CosmosClient";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
import { sendCachedDataMessage } from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { OfferUtils } from "../Utils/OfferUtils";
import { RequestOptions } from "@azure/cosmos/dist-esm"; import { RequestOptions } from "@azure/cosmos/dist-esm";
import StoredProcedure from "../Explorer/Tree/StoredProcedure"; import Q from "q";
import { Platform, configContext } from "../ConfigContext"; import { configContext, Platform } from "../ConfigContext";
import DocumentId from "../Explorer/Tree/DocumentId"; import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import * as ViewModels from "../Contracts/ViewModels";
import ConflictId from "../Explorer/Tree/ConflictId"; import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
import { OfferUtils } from "../Utils/OfferUtils";
import * as Constants from "./Constants";
import { client } from "./CosmosClient";
import * as HeadersUtility from "./HeadersUtility";
import { sendCachedDataMessage } from "./MessageHandler";
export function getCommonQueryOptions(options: FeedOptions): any { export function getCommonQueryOptions(options: FeedOptions): any {
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage); const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
@@ -55,85 +53,55 @@ export function queryDocuments(
return Q(documentsIterator); return Q(documentsIterator);
} }
export function readStoredProcedures( export function getPartitionKeyHeaderForConflict(conflictId: ConflictId): Object {
collection: ViewModels.Collection, const partitionKeyDefinition: DataModels.PartitionKey = conflictId.partitionKey;
options?: any const partitionKeyValue: any = conflictId.partitionKeyValue;
): Q.Promise<DataModels.StoredProcedure[]> {
return getPartitionKeyHeader(partitionKeyDefinition, partitionKeyValue);
}
export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.PartitionKey, partitionKeyValue: any): Object {
if (!partitionKeyDefinition) {
return undefined;
}
if (partitionKeyValue === undefined) {
return [{}];
}
return [partitionKeyValue];
}
export function updateOffer(
offer: DataModels.Offer,
newOffer: DataModels.Offer,
options?: RequestOptions
): Q.Promise<DataModels.Offer> {
return Q( return Q(
client() client()
.database(collection.databaseId) .offer(offer.id)
.container(collection.id()) // TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
.scripts.storedProcedures.readAll(options) .replace((newOffer as unknown) as OfferDefinition, options)
.fetchAll() .then(response => {
.then(response => response.resources as DataModels.StoredProcedure[]) return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
})
); );
} }
export function readStoredProcedure( export function updateDocument(
collection: ViewModels.Collection, collection: ViewModels.CollectionBase,
requestedResource: DataModels.Resource, documentId: DocumentId,
options?: any newDocument: any
): Q.Promise<DataModels.StoredProcedure> { ): Q.Promise<any> {
return Q( const partitionKey = documentId.partitionKeyValue;
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedure(requestedResource.id)
.read(options)
.then(response => response.resource as DataModels.StoredProcedure)
);
}
export function readUserDefinedFunctions(
collection: ViewModels.Collection,
options: any
): Q.Promise<DataModels.UserDefinedFunction[]> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.userDefinedFunctions.readAll(options)
.fetchAll()
.then(response => response.resources as DataModels.UserDefinedFunction[])
);
}
export function readUserDefinedFunction(
collection: ViewModels.Collection,
requestedResource: DataModels.Resource,
options?: any
): Q.Promise<DataModels.UserDefinedFunction> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.userDefinedFunction(requestedResource.id)
.read(options)
.then(response => response.resource as DataModels.UserDefinedFunction)
);
}
export function readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> {
return Q( return Q(
client() client()
.database(collection.databaseId) .database(collection.databaseId)
.container(collection.id()) .container(collection.id())
.scripts.triggers.readAll(options) .item(documentId.id(), partitionKey)
.fetchAll() .replace(newDocument)
.then(response => response.resources as DataModels.Trigger[]) .then(response => response.resource)
);
}
export function readTrigger(
collection: ViewModels.Collection,
requestedResource: DataModels.Resource,
options?: any
): Q.Promise<DataModels.Trigger> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.trigger(requestedResource.id)
.read(options)
.then(response => response.resource as DataModels.Trigger)
); );
} }
@@ -165,6 +133,16 @@ export function executeStoredProcedure(
); );
} }
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.items.create(newDocument)
.then(response => response.resource)
);
}
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> { export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue; const partitionKey = documentId.partitionKeyValue;
@@ -178,155 +156,6 @@ export function readDocument(collection: ViewModels.CollectionBase, documentId:
); );
} }
export function getPartitionKeyHeaderForConflict(conflictId: ConflictId): Object {
const partitionKeyDefinition: DataModels.PartitionKey = conflictId.partitionKey;
const partitionKeyValue: any = conflictId.partitionKeyValue;
return getPartitionKeyHeader(partitionKeyDefinition, partitionKeyValue);
}
export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.PartitionKey, partitionKeyValue: any): Object {
if (!partitionKeyDefinition) {
return undefined;
}
if (partitionKeyValue === undefined) {
return [{}];
}
return [partitionKeyValue];
}
export function updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
newDocument: any
): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue;
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
.replace(newDocument)
.then(response => response.resource)
);
}
export function updateOffer(
offer: DataModels.Offer,
newOffer: DataModels.Offer,
options?: RequestOptions
): Q.Promise<DataModels.Offer> {
return Q(
client()
.offer(offer.id)
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
.replace((newOffer as unknown) as OfferDefinition, options)
.then(response => {
return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
})
);
}
export function updateStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: DataModels.StoredProcedure,
options: any
): Q.Promise<DataModels.StoredProcedure> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedure(storedProcedure.id)
.replace(storedProcedure, options)
.then(response => response.resource as DataModels.StoredProcedure)
);
}
export function updateUserDefinedFunction(
collection: ViewModels.Collection,
userDefinedFunction: DataModels.UserDefinedFunction,
options?: any
): Q.Promise<DataModels.UserDefinedFunction> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.userDefinedFunction(userDefinedFunction.id)
.replace(userDefinedFunction, options)
.then(response => response.resource as DataModels.StoredProcedure)
);
}
export function updateTrigger(
collection: ViewModels.Collection,
trigger: DataModels.Trigger,
options?: any
): Q.Promise<DataModels.Trigger> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.trigger(trigger.id)
.replace(trigger as TriggerDefinition, options)
.then(response => response.resource as DataModels.Trigger)
);
}
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.items.create(newDocument)
.then(response => response.resource as DataModels.StoredProcedure)
);
}
export function createStoredProcedure(
collection: ViewModels.Collection,
newStoredProcedure: DataModels.StoredProcedure,
options?: any
): Q.Promise<DataModels.StoredProcedure> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedures.create(newStoredProcedure, options)
.then(response => response.resource as DataModels.StoredProcedure)
);
}
export function createUserDefinedFunction(
collection: ViewModels.Collection,
newUserDefinedFunction: DataModels.UserDefinedFunction,
options: any
): Q.Promise<DataModels.UserDefinedFunction> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.userDefinedFunctions.create(newUserDefinedFunction, options)
.then(response => response.resource as DataModels.UserDefinedFunction)
);
}
export function createTrigger(
collection: ViewModels.Collection,
newTrigger: DataModels.Trigger,
options?: any
): Q.Promise<DataModels.Trigger> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.triggers.create(newTrigger as TriggerDefinition, options)
.then(response => response.resource as DataModels.Trigger)
);
}
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> { export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue; const partitionKey = documentId.partitionKeyValue;
@@ -355,48 +184,6 @@ export function deleteConflict(
); );
} }
export function deleteStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: DataModels.StoredProcedure,
options: any
): Q.Promise<any> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedure(storedProcedure.id)
.delete()
);
}
export function deleteUserDefinedFunction(
collection: ViewModels.Collection,
userDefinedFunction: DataModels.UserDefinedFunction,
options: any
): Q.Promise<any> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.userDefinedFunction(userDefinedFunction.id)
.delete()
);
}
export function deleteTrigger(
collection: ViewModels.Collection,
trigger: DataModels.Trigger,
options: any
): Q.Promise<any> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.trigger(trigger.id)
.delete()
);
}
export function readCollectionQuotaInfo( export function readCollectionQuotaInfo(
collection: ViewModels.Collection, collection: ViewModels.Collection,
options: any options: any

View File

@@ -1,18 +1,17 @@
import * as Constants from "./Constants"; import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import Q from "q";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import Q from "q";
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import * as DataAccessUtilityBase from "./DataAccessUtilityBase";
import * as Logger from "./Logger";
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import ConflictId from "../Explorer/Tree/ConflictId"; import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import * as Constants from "./Constants";
import { sendNotificationForError } from "./dataAccess/sendNotificationForError"; import { sendNotificationForError } from "./dataAccess/sendNotificationForError";
import * as DataAccessUtilityBase from "./DataAccessUtilityBase";
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
import * as Logger from "./Logger";
// TODO: Log all promise resolutions and errors with verbosity levels // TODO: Log all promise resolutions and errors with verbosity levels
export function queryDocuments( export function queryDocuments(
@@ -42,121 +41,6 @@ export function getEntityName() {
return "item"; return "item";
} }
export function readStoredProcedures(
collection: ViewModels.Collection,
options: any = {}
): Q.Promise<DataModels.StoredProcedure[]> {
var deferred = Q.defer<DataModels.StoredProcedure[]>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Querying stored procedures for container ${collection.id()}`
);
DataAccessUtilityBase.readStoredProcedures(collection, options)
.then(
(storedProcedures: DataModels.StoredProcedure[]) => {
deferred.resolve(storedProcedures);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to query stored procedures for container ${collection.id()}: ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadStoredProcedures", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
export function readStoredProcedure(
collection: ViewModels.Collection,
requestedResource: DataModels.Resource,
options?: any
): Q.Promise<DataModels.StoredProcedure> {
return DataAccessUtilityBase.readStoredProcedure(collection, requestedResource, options);
}
export function readUserDefinedFunctions(
collection: ViewModels.Collection,
options: any = {}
): Q.Promise<DataModels.UserDefinedFunction[]> {
var deferred = Q.defer<DataModels.UserDefinedFunction[]>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Querying user defined functions for collection ${collection.id()}`
);
DataAccessUtilityBase.readUserDefinedFunctions(collection, options)
.then(
(userDefinedFunctions: DataModels.UserDefinedFunction[]) => {
deferred.resolve(userDefinedFunctions);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to query user defined functions for container ${collection.id()}: ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadUDFs", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
export function readUserDefinedFunction(
collection: ViewModels.Collection,
requestedResource: DataModels.Resource,
options: any
): Q.Promise<DataModels.UserDefinedFunction> {
return DataAccessUtilityBase.readUserDefinedFunction(collection, requestedResource, options);
}
export function readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> {
var deferred = Q.defer<DataModels.Trigger[]>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Querying triggers for container ${collection.id()}`
);
DataAccessUtilityBase.readTriggers(collection, options)
.then(
(triggers: DataModels.Trigger[]) => {
deferred.resolve(triggers);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to query triggers for container ${collection.id()}: ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadTriggers", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
export function readTrigger(
collection: ViewModels.Collection,
requestedResource: DataModels.Resource,
options?: any
): Q.Promise<DataModels.Trigger> {
return DataAccessUtilityBase.readTrigger(collection, requestedResource, options);
}
export function executeStoredProcedure( export function executeStoredProcedure(
collection: ViewModels.Collection, collection: ViewModels.Collection,
storedProcedure: StoredProcedure, storedProcedure: StoredProcedure,
@@ -165,22 +49,17 @@ export function executeStoredProcedure(
): Q.Promise<any> { ): Q.Promise<any> {
var deferred = Q.defer<any>(); var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`);
ConsoleDataType.InProgress,
`Executing stored procedure ${storedProcedure.id()}`
);
DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params) DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params)
.then( .then(
(response: any) => { (response: any) => {
deferred.resolve(response); deferred.resolve(response);
NotificationConsoleUtils.logConsoleMessage( logConsoleInfo(
ConsoleDataType.Info,
`Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}` `Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
); );
}, },
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleError(
ConsoleDataType.Error,
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}: ${JSON.stringify( `Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}: ${JSON.stringify(
error error
)}` )}`
@@ -191,7 +70,7 @@ export function executeStoredProcedure(
} }
) )
.finally(() => { .finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id); clearMessage();
}); });
return deferred.promise; return deferred.promise;
@@ -205,32 +84,23 @@ export function queryDocumentsPage(
): Q.Promise<ViewModels.QueryResults> { ): Q.Promise<ViewModels.QueryResults> {
var deferred = Q.defer<ViewModels.QueryResults>(); var deferred = Q.defer<ViewModels.QueryResults>();
const entityName = getEntityName(); const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
ConsoleDataType.InProgress,
`Querying ${entityName} for container ${resourceName}`
);
Q(nextPage(documentsIterator, firstItemIndex)) Q(nextPage(documentsIterator, firstItemIndex))
.then( .then(
(result: ViewModels.QueryResults) => { (result: ViewModels.QueryResults) => {
const itemCount = (result.documents && result.documents.length) || 0; const itemCount = (result.documents && result.documents.length) || 0;
NotificationConsoleUtils.logConsoleMessage( logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
ConsoleDataType.Info,
`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`
);
deferred.resolve(result); deferred.resolve(result);
}, },
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Failed to query ${entityName} for container ${resourceName}: ${JSON.stringify(error)}`);
ConsoleDataType.Error,
`Failed to query ${entityName} for container ${resourceName}: ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "QueryDocumentsPage", error.code); Logger.logError(JSON.stringify(error), "QueryDocumentsPage", error.code);
sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
.finally(() => { .finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id); clearMessage();
}); });
return deferred.promise; return deferred.promise;
@@ -239,27 +109,21 @@ export function queryDocumentsPage(
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> { export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>(); var deferred = Q.defer<any>();
const entityName = getEntityName(); const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
ConsoleDataType.InProgress,
`Reading ${entityName} ${documentId.id()}`
);
DataAccessUtilityBase.readDocument(collection, documentId) DataAccessUtilityBase.readDocument(collection, documentId)
.then( .then(
(document: any) => { (document: any) => {
deferred.resolve(document); deferred.resolve(document);
}, },
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Failed to read ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`);
ConsoleDataType.Error,
`Failed to read ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadDocument", error.code); Logger.logError(JSON.stringify(error), "ReadDocument", error.code);
sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
.finally(() => { .finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id); clearMessage();
}); });
return deferred.promise; return deferred.promise;
@@ -272,31 +136,22 @@ export function updateDocument(
): Q.Promise<any> { ): Q.Promise<any> {
var deferred = Q.defer<any>(); var deferred = Q.defer<any>();
const entityName = getEntityName(); const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
ConsoleDataType.InProgress,
`Updating ${entityName} ${documentId.id()}`
);
DataAccessUtilityBase.updateDocument(collection, documentId, newDocument) DataAccessUtilityBase.updateDocument(collection, documentId, newDocument)
.then( .then(
(updatedDocument: any) => { (updatedDocument: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
ConsoleDataType.Info,
`Successfully updated ${entityName} ${documentId.id()}`
);
deferred.resolve(updatedDocument); deferred.resolve(updatedDocument);
}, },
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Failed to update ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`);
ConsoleDataType.Error,
`Failed to update ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "UpdateDocument", error.code); Logger.logError(JSON.stringify(error), "UpdateDocument", error.code);
sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
.finally(() => { .finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id); clearMessage();
}); });
return deferred.promise; return deferred.promise;
@@ -308,24 +163,15 @@ export function updateOffer(
options: RequestOptions options: RequestOptions
): Q.Promise<DataModels.Offer> { ): Q.Promise<DataModels.Offer> {
var deferred = Q.defer<any>(); var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = logConsoleProgress(`Updating offer for resource ${offer.resource}`);
ConsoleDataType.InProgress,
`Updating offer for resource ${offer.resource}`
);
DataAccessUtilityBase.updateOffer(offer, newOffer, options) DataAccessUtilityBase.updateOffer(offer, newOffer, options)
.then( .then(
(replacedOffer: DataModels.Offer) => { (replacedOffer: DataModels.Offer) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleInfo(`Successfully updated offer for resource ${offer.resource}`);
ConsoleDataType.Info,
`Successfully updated offer for resource ${offer.resource}`
);
deferred.resolve(replacedOffer); deferred.resolve(replacedOffer);
}, },
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Error updating offer for resource ${offer.resource}: ${JSON.stringify(error)}`);
ConsoleDataType.Error,
`Error updating offer for resource ${offer.resource}: ${JSON.stringify(error)}`
);
Logger.logError( Logger.logError(
JSON.stringify({ JSON.stringify({
oldOffer: offer, oldOffer: offer,
@@ -340,108 +186,7 @@ export function updateOffer(
} }
) )
.finally(() => { .finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id); clearMessage();
});
return deferred.promise;
}
export function updateStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: DataModels.StoredProcedure,
options?: any
): Q.Promise<DataModels.StoredProcedure> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Updating stored procedure ${storedProcedure.id}`
);
DataAccessUtilityBase.updateStoredProcedure(collection, storedProcedure, options)
.then(
(updatedStoredProcedure: DataModels.StoredProcedure) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully updated stored procedure ${storedProcedure.id}`
);
deferred.resolve(updatedStoredProcedure);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while updating stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "UpdateStoredProcedure", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
export function updateUserDefinedFunction(
collection: ViewModels.Collection,
userDefinedFunction: DataModels.UserDefinedFunction,
options: any = {}
): Q.Promise<DataModels.UserDefinedFunction> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Updating user defined function ${userDefinedFunction.id}`
);
DataAccessUtilityBase.updateUserDefinedFunction(collection, userDefinedFunction, options)
.then(
(updatedUserDefinedFunction: DataModels.UserDefinedFunction) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully updated user defined function ${userDefinedFunction.id}`
);
deferred.resolve(updatedUserDefinedFunction);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while updating user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "UpdateUDF", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
export function updateTrigger(
collection: ViewModels.Collection,
trigger: DataModels.Trigger
): Q.Promise<DataModels.Trigger> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, `Updating trigger ${trigger.id}`);
DataAccessUtilityBase.updateTrigger(collection, trigger)
.then(
(updatedTrigger: DataModels.Trigger) => {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Updated trigger ${trigger.id}`);
deferred.resolve(updatedTrigger);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while updating trigger ${trigger.id}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "UpdateTrigger", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
}); });
return deferred.promise; return deferred.promise;
@@ -450,22 +195,15 @@ export function updateTrigger(
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> { export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
var deferred = Q.defer<any>(); var deferred = Q.defer<any>();
const entityName = getEntityName(); const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
ConsoleDataType.InProgress,
`Creating new ${entityName} for container ${collection.id()}`
);
DataAccessUtilityBase.createDocument(collection, newDocument) DataAccessUtilityBase.createDocument(collection, newDocument)
.then( .then(
(savedDocument: any) => { (savedDocument: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`);
ConsoleDataType.Info,
`Successfully created new ${entityName} for container ${collection.id()}`
);
deferred.resolve(savedDocument); deferred.resolve(savedDocument);
}, },
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleError(
ConsoleDataType.Error,
`Error while creating new ${entityName} for container ${collection.id()}:\n ${JSON.stringify(error)}` `Error while creating new ${entityName} for container ${collection.id()}:\n ${JSON.stringify(error)}`
); );
Logger.logError(JSON.stringify(error), "CreateDocument", error.code); Logger.logError(JSON.stringify(error), "CreateDocument", error.code);
@@ -474,115 +212,7 @@ export function createDocument(collection: ViewModels.CollectionBase, newDocumen
} }
) )
.finally(() => { .finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id); clearMessage();
});
return deferred.promise;
}
export function createStoredProcedure(
collection: ViewModels.Collection,
newStoredProcedure: DataModels.StoredProcedure,
options?: any
): Q.Promise<DataModels.StoredProcedure> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Creating stored procedure for container ${collection.id()}`
);
DataAccessUtilityBase.createStoredProcedure(collection, newStoredProcedure, options)
.then(
(createdStoredProcedure: DataModels.StoredProcedure) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully created stored procedure for container ${collection.id()}`
);
deferred.resolve(createdStoredProcedure);
},
error => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while creating stored procedure for container ${collection.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "CreateStoredProcedure", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
export function createUserDefinedFunction(
collection: ViewModels.Collection,
newUserDefinedFunction: DataModels.UserDefinedFunction,
options?: any
): Q.Promise<DataModels.UserDefinedFunction> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Creating user defined function for container ${collection.id()}`
);
DataAccessUtilityBase.createUserDefinedFunction(collection, newUserDefinedFunction, options)
.then(
(createdUserDefinedFunction: DataModels.UserDefinedFunction) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully created user defined function for container ${collection.id()}`
);
deferred.resolve(createdUserDefinedFunction);
},
error => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while creating user defined function for container ${collection.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "CreateUDF", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
export function createTrigger(
collection: ViewModels.Collection,
newTrigger: DataModels.Trigger,
options: any = {}
): Q.Promise<DataModels.Trigger> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Creating trigger for container ${collection.id()}`
);
DataAccessUtilityBase.createTrigger(collection, newTrigger, options)
.then(
(createdTrigger: DataModels.Trigger) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully created trigger for container ${collection.id()}`
);
deferred.resolve(createdTrigger);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while creating trigger for container ${collection.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "CreateTrigger", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
}); });
return deferred.promise; return deferred.promise;
@@ -591,31 +221,22 @@ export function createTrigger(
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> { export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>(); var deferred = Q.defer<any>();
const entityName = getEntityName(); const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
ConsoleDataType.InProgress,
`Deleting ${entityName} ${documentId.id()}`
);
DataAccessUtilityBase.deleteDocument(collection, documentId) DataAccessUtilityBase.deleteDocument(collection, documentId)
.then( .then(
(response: any) => { (response: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
ConsoleDataType.Info,
`Successfully deleted ${entityName} ${documentId.id()}`
);
deferred.resolve(response); deferred.resolve(response);
}, },
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Error while deleting ${entityName} ${documentId.id()}:\n ${JSON.stringify(error)}`);
ConsoleDataType.Error,
`Error while deleting ${entityName} ${documentId.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "DeleteDocument", error.code); Logger.logError(JSON.stringify(error), "DeleteDocument", error.code);
sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
.finally(() => { .finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id); clearMessage();
}); });
return deferred.promise; return deferred.promise;
@@ -628,134 +249,22 @@ export function deleteConflict(
): Q.Promise<any> { ): Q.Promise<any> {
var deferred = Q.defer<any>(); var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
ConsoleDataType.InProgress,
`Deleting conflict ${conflictId.id()}`
);
DataAccessUtilityBase.deleteConflict(collection, conflictId, options) DataAccessUtilityBase.deleteConflict(collection, conflictId, options)
.then( .then(
(response: any) => { (response: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`);
ConsoleDataType.Info,
`Successfully deleted conflict ${conflictId.id()}`
);
deferred.resolve(response); deferred.resolve(response);
}, },
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Error while deleting conflict ${conflictId.id()}:\n ${JSON.stringify(error)}`);
ConsoleDataType.Error,
`Error while deleting conflict ${conflictId.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "DeleteConflict", error.code); Logger.logError(JSON.stringify(error), "DeleteConflict", error.code);
sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
.finally(() => { .finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id); clearMessage();
});
return deferred.promise;
}
export function deleteStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: DataModels.StoredProcedure,
options: any = {}
): Q.Promise<DataModels.StoredProcedure> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting stored procedure ${storedProcedure.id}`
);
DataAccessUtilityBase.deleteStoredProcedure(collection, storedProcedure, options)
.then(
(response: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully deleted stored procedure ${storedProcedure.id}`
);
deferred.resolve(response);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "DeleteStoredProcedure", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
export function deleteUserDefinedFunction(
collection: ViewModels.Collection,
userDefinedFunction: DataModels.UserDefinedFunction,
options: any = {}
): Q.Promise<DataModels.UserDefinedFunction> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting user defined function ${userDefinedFunction.id}`
);
DataAccessUtilityBase.deleteUserDefinedFunction(collection, userDefinedFunction, options)
.then(
(response: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully deleted user defined function ${userDefinedFunction.id}`
);
deferred.resolve(response);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "DeleteUDF", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
}
export function deleteTrigger(
collection: ViewModels.Collection,
trigger: DataModels.Trigger,
options: any = {}
): Q.Promise<DataModels.Trigger> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, `Deleting trigger ${trigger.id}`);
DataAccessUtilityBase.deleteTrigger(collection, trigger, options)
.then(
(response: any) => {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted trigger ${trigger.id}`);
deferred.resolve(response);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting trigger ${trigger.id}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "DeleteTrigger", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
}); });
return deferred.promise; return deferred.promise;
@@ -775,27 +284,21 @@ export function readCollectionQuotaInfo(
): Q.Promise<DataModels.CollectionQuotaInfo> { ): Q.Promise<DataModels.CollectionQuotaInfo> {
var deferred = Q.defer<DataModels.CollectionQuotaInfo>(); var deferred = Q.defer<DataModels.CollectionQuotaInfo>();
const id = NotificationConsoleUtils.logConsoleMessage( const clearMessage = logConsoleProgress(`Querying quota info for container ${collection.id}`);
ConsoleDataType.InProgress,
`Querying quota info for container ${collection.id}`
);
DataAccessUtilityBase.readCollectionQuotaInfo(collection, options) DataAccessUtilityBase.readCollectionQuotaInfo(collection, options)
.then( .then(
(quota: DataModels.CollectionQuotaInfo) => { (quota: DataModels.CollectionQuotaInfo) => {
deferred.resolve(quota); deferred.resolve(quota);
}, },
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Error while querying quota info for container ${collection.id}:\n ${JSON.stringify(error)}`);
ConsoleDataType.Error,
`Error while querying quota info for container ${collection.id}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadCollectionQuotaInfo", error.code); Logger.logError(JSON.stringify(error), "ReadCollectionQuotaInfo", error.code);
sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
.finally(() => { .finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id); clearMessage();
}); });
return deferred.promise; return deferred.promise;
@@ -804,24 +307,21 @@ export function readCollectionQuotaInfo(
export function readOffers(options: any = {}): Q.Promise<DataModels.Offer[]> { export function readOffers(options: any = {}): Q.Promise<DataModels.Offer[]> {
var deferred = Q.defer<DataModels.Offer[]>(); var deferred = Q.defer<DataModels.Offer[]>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying offers"); const clearMessage = logConsoleProgress("Querying offers");
DataAccessUtilityBase.readOffers(options) DataAccessUtilityBase.readOffers(options)
.then( .then(
(offers: DataModels.Offer[]) => { (offers: DataModels.Offer[]) => {
deferred.resolve(offers); deferred.resolve(offers);
}, },
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Error while querying offers:\n ${JSON.stringify(error)}`);
ConsoleDataType.Error,
`Error while querying offers:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadOffers", error.code); Logger.logError(JSON.stringify(error), "ReadOffers", error.code);
sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
.finally(() => { .finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id); clearMessage();
}); });
return deferred.promise; return deferred.promise;
@@ -833,24 +333,21 @@ export function readOffer(
): Q.Promise<DataModels.OfferWithHeaders> { ): Q.Promise<DataModels.OfferWithHeaders> {
var deferred = Q.defer<DataModels.OfferWithHeaders>(); var deferred = Q.defer<DataModels.OfferWithHeaders>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying offer"); const clearMessage = logConsoleProgress("Querying offer");
DataAccessUtilityBase.readOffer(requestedResource, options) DataAccessUtilityBase.readOffer(requestedResource, options)
.then( .then(
(offer: DataModels.OfferWithHeaders) => { (offer: DataModels.OfferWithHeaders) => {
deferred.resolve(offer); deferred.resolve(offer);
}, },
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage( logConsoleError(`Error while querying offer:\n ${JSON.stringify(error)}`);
ConsoleDataType.Error,
`Error while querying offer:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "ReadOffer", error.code); Logger.logError(JSON.stringify(error), "ReadOffer", error.code);
sendNotificationForError(error); sendNotificationForError(error);
deferred.reject(error); deferred.reject(error);
} }
) )
.finally(() => { .finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id); clearMessage();
}); });
return deferred.promise; return deferred.promise;

View File

@@ -1,6 +1,5 @@
import Q from "q"; import Q from "q";
import * as MessageHandler from "./MessageHandler"; import * as MessageHandler from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts";
describe("Message Handler", () => { describe("Message Handler", () => {
it("should handle cached message", async () => { it("should handle cached message", async () => {
@@ -26,4 +25,34 @@ describe("Message Handler", () => {
MessageHandler.runGarbageCollector(); MessageHandler.runGarbageCollector();
expect(MessageHandler.RequestMap["123"]).toBeUndefined(); expect(MessageHandler.RequestMap["123"]).toBeUndefined();
}); });
describe("getDataExplorerWindow", () => {
it("should return current window if current window has dataExplorerPlatform property", () => {
const currentWindow: Window = { dataExplorerPlatform: 0 } as any;
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toEqual(currentWindow);
});
it("should return current window's parent if current window's parent has dataExplorerPlatform property", () => {
const parentWindow: Window = { dataExplorerPlatform: 0 } as any;
const currentWindow: Window = { parent: parentWindow } as any;
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toEqual(parentWindow);
});
it("should return undefined if none of the windows in the hierarchy have dataExplorerPlatform property and window's parent is reference to itself", () => {
const parentWindow: Window = {} as any;
(parentWindow as any).parent = parentWindow; // If a window does not have a parent, its parent property is a reference to itself.
const currentWindow: Window = { parent: parentWindow } as any;
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toBeUndefined();
});
it("should return undefined if none of the windows in the hierarchy have dataExplorerPlatform property and window's parent is not defined", () => {
const parentWindow: Window = {} as any;
const currentWindow: Window = { parent: parentWindow } as any;
expect(MessageHandler.getDataExplorerWindow(currentWindow)).toBeUndefined();
});
});
}); });

View File

@@ -48,15 +48,37 @@ export function sendCachedDataMessage<TResponseDataModel>(
export function sendMessage(data: any): void { export function sendMessage(data: any): void {
if (canSendMessage()) { if (canSendMessage()) {
window.parent.postMessage( const dataExplorerWindow = getDataExplorerWindow(window);
if (dataExplorerWindow) {
dataExplorerWindow.parent.postMessage(
{ {
signature: "pcIframe", signature: "pcIframe",
data: data data: data
}, },
window.document.referrer dataExplorerWindow.document.referrer
); );
} }
} }
}
// Only exported for unit tests
export const getDataExplorerWindow = (currentWindow: Window): Window | undefined => {
// Start with the current window and traverse up the parent hierarchy to find a window
// with `dataExplorerPlatform` property
let dataExplorerWindow: Window | undefined = currentWindow;
// TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet
// eslint-disable-next-line @typescript-eslint/no-explicit-any
while (dataExplorerWindow && (dataExplorerWindow as any).dataExplorerPlatform == undefined) {
// If a window does not have a parent, its parent property is a reference to itself.
if (dataExplorerWindow.parent == dataExplorerWindow) {
dataExplorerWindow = undefined;
} else {
dataExplorerWindow = dataExplorerWindow.parent;
}
}
return dataExplorerWindow;
};
export function canSendMessage(): boolean { export function canSendMessage(): boolean {
return window.parent !== window; return window.parent !== window;

View File

@@ -0,0 +1,28 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function createStoredProcedure(
databaseId: string,
collectionId: string,
storedProcedure: StoredProcedureDefinition
): Promise<StoredProcedureDefinition & Resource> {
let createdStoredProcedure: StoredProcedureDefinition & Resource;
const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`);
try {
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.storedProcedures.create(storedProcedure);
createdStoredProcedure = response.resource;
} catch (error) {
logConsoleError(`Error while creating stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "CreateStoredProcedure", error.code);
sendNotificationForError(error);
}
clearMessage();
return createdStoredProcedure;
}

View File

@@ -0,0 +1,28 @@
import { Resource, TriggerDefinition } from "@azure/cosmos";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function createTrigger(
databaseId: string,
collectionId: string,
trigger: TriggerDefinition
): Promise<TriggerDefinition & Resource> {
let createdTrigger: TriggerDefinition & Resource;
const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`);
try {
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.triggers.create(trigger);
createdTrigger = response.resource;
} catch (error) {
logConsoleError(`Error while creating trigger ${trigger.id}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "CreateTrigger", error.code);
sendNotificationForError(error);
}
clearMessage();
return createdTrigger;
}

View File

@@ -0,0 +1,28 @@
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function createUserDefinedFunction(
databaseId: string,
collectionId: string,
userDefinedFunction: UserDefinedFunctionDefinition
): Promise<UserDefinedFunctionDefinition & Resource> {
let createdUserDefinedFunction: UserDefinedFunctionDefinition & Resource;
const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`);
try {
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunctions.create(userDefinedFunction);
createdUserDefinedFunction = response.resource;
} catch (error) {
logConsoleError(`Error while creating user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "CreateUserupdateUserDefinedFunction", error.code);
sendNotificationForError(error);
}
clearMessage();
return createdUserDefinedFunction;
}

View File

@@ -0,0 +1,26 @@
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function deleteStoredProcedure(
databaseId: string,
collectionId: string,
storedProcedureId: string
): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting stored procedure ${storedProcedureId}`);
try {
await client()
.database(databaseId)
.container(collectionId)
.scripts.storedProcedure(storedProcedureId)
.delete();
} catch (error) {
logConsoleError(`Error while deleting stored procedure ${storedProcedureId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "DeleteStoredProcedure", error.code);
sendNotificationForError(error);
}
clearMessage();
return undefined;
}

View File

@@ -0,0 +1,22 @@
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function deleteTrigger(databaseId: string, collectionId: string, triggerId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting trigger ${triggerId}`);
try {
await client()
.database(databaseId)
.container(collectionId)
.scripts.trigger(triggerId)
.delete();
} catch (error) {
logConsoleError(`Error while deleting trigger ${triggerId}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "DeleteTrigger", error.code);
sendNotificationForError(error);
}
clearMessage();
return undefined;
}

View File

@@ -0,0 +1,22 @@
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function deleteUserDefinedFunction(databaseId: string, collectionId: string, id: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting user defined function ${id}`);
try {
await client()
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunction(id)
.delete();
} catch (error) {
logConsoleError(`Error while deleting user defined function ${id}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "DeleteUserDefinedFunction", error.code);
sendNotificationForError(error);
}
clearMessage();
return undefined;
}

View File

@@ -0,0 +1,83 @@
import * as DataModels from "../../Contracts/DataModels";
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { readOffers } from "./readOffers";
import { userContext } from "../../UserContext";
export const readDatabaseOffer = async (
params: DataModels.ReadDatabaseOfferParams
): Promise<DataModels.OfferWithHeaders> => {
let offerId = params.offerId;
if (!offerId) {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
try {
offerId = await getDatabaseOfferIdWithARM(params.databaseId);
} catch (error) {
if (error.code !== "NotFound") {
throw new Error(error);
}
return undefined;
}
} else {
offerId = await getDatabaseOfferIdWithSDK(params.databaseResourceId, params.isServerless);
if (!offerId) {
return undefined;
}
}
}
const options: RequestOptions = {
initialHeaders: {
[HttpHeaders.populateCollectionThroughputInfo]: true
}
};
const response = await client()
.offer(offerId)
.read(options);
return (
response && {
...response.resource,
headers: response.headers
}
);
};
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
let rpResponse;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
switch (defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
rpResponse = await getSqlDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.MongoDB:
rpResponse = await getMongoDBDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.Cassandra:
rpResponse = await getCassandraKeyspaceThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
case DefaultAccountExperienceType.Graph:
rpResponse = await getGremlinDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
break;
default:
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
}
return rpResponse?.name;
};
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string, isServerless: boolean): Promise<string> => {
const offers = await readOffers(isServerless);
const offer = offers.find(offer => offer.resource === databaseResourceId);
return offer?.id;
};

View File

@@ -0,0 +1,36 @@
import { Offer } from "../../Contracts/DataModels";
import { ClientDefaults } from "../Constants";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { Platform, configContext } from "../../ConfigContext";
import { client } from "../CosmosClient";
import { sendCachedDataMessage } from "../MessageHandler";
import { userContext } from "../../UserContext";
export const readOffers = async (isServerless?: boolean): Promise<Offer[]> => {
if (isServerless) {
return []; // Reading offers is not supported for serverless accounts
}
try {
if (configContext.platform === Platform.Portal) {
return sendCachedDataMessage<Offer[]>(MessageTypes.AllOffers, [
userContext.databaseAccount.id,
ClientDefaults.portalCacheTimeoutMs
]);
}
} catch (error) {
// If error getting cached Offers, continue on and read via SDK
}
return client()
.offers.readAll()
.fetchAll()
.then(response => response.resources)
.catch(error => {
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) {
return [];
}
throw error;
});
};

View File

@@ -0,0 +1,27 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function readStoredProcedures(
databaseId: string,
collectionId: string
): Promise<(StoredProcedureDefinition & Resource)[]> {
let sprocs: (StoredProcedureDefinition & Resource)[];
const clearMessage = logConsoleProgress(`Querying stored procedures for container ${collectionId}`);
try {
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.storedProcedures.readAll()
.fetchAll();
sprocs = response.resources;
} catch (error) {
logConsoleError(`Failed to query stored procedures for container ${collectionId}: ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadStoredProcedures", error.code);
sendNotificationForError(error);
}
clearMessage();
return sprocs;
}

View File

@@ -0,0 +1,27 @@
import { Resource, TriggerDefinition } from "@azure/cosmos";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function readTriggers(
databaseId: string,
collectionId: string
): Promise<(TriggerDefinition & Resource)[]> {
let triggers: (TriggerDefinition & Resource)[];
const clearMessage = logConsoleProgress(`Querying triggers for container ${collectionId}`);
try {
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.triggers.readAll()
.fetchAll();
triggers = response.resources;
} catch (error) {
logConsoleError(`Failed to query triggers for container ${collectionId}: ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadTriggers", error.code);
sendNotificationForError(error);
}
clearMessage();
return triggers;
}

View File

@@ -0,0 +1,27 @@
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function readUserDefinedFunctions(
databaseId: string,
collectionId: string
): Promise<(UserDefinedFunctionDefinition & Resource)[]> {
let udfs: (UserDefinedFunctionDefinition & Resource)[];
const clearMessage = logConsoleProgress(`Querying user defined functions for container ${collectionId}`);
try {
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunctions.readAll()
.fetchAll();
udfs = response.resources;
} catch (error) {
logConsoleError(`Failed to query user defined functions for container ${collectionId}: ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "ReadUserDefinedFunctions", error.code);
sendNotificationForError(error);
}
clearMessage();
return udfs;
}

View File

@@ -0,0 +1,29 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function updateStoredProcedure(
databaseId: string,
collectionId: string,
storedProcedure: StoredProcedureDefinition
): Promise<StoredProcedureDefinition & Resource> {
let updatedStoredProcedure: StoredProcedureDefinition & Resource;
const clearMessage = logConsoleProgress(`Updating stored procedure ${storedProcedure.id}`);
try {
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.storedProcedure(storedProcedure.id)
.replace(storedProcedure);
updatedStoredProcedure = response.resource;
} catch (error) {
logConsoleError(`Error while updating stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "UpdateStoredProcedure", error.code);
sendNotificationForError(error);
}
clearMessage();
return updatedStoredProcedure;
}

View File

@@ -0,0 +1,29 @@
import { TriggerDefinition } from "@azure/cosmos";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function updateTrigger(
databaseId: string,
collectionId: string,
trigger: TriggerDefinition
): Promise<TriggerDefinition> {
let updatedTrigger: TriggerDefinition;
const clearMessage = logConsoleProgress(`Updating trigger ${trigger.id}`);
try {
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.trigger(trigger.id)
.replace(trigger);
updatedTrigger = response.resource;
} catch (error) {
logConsoleError(`Error while updating trigger ${trigger.id}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "UpdateTrigger", error.code);
sendNotificationForError(error);
}
clearMessage();
return updatedTrigger;
}

View File

@@ -0,0 +1,29 @@
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
export async function updateUserDefinedFunction(
databaseId: string,
collectionId: string,
userDefinedFunction: UserDefinedFunctionDefinition
): Promise<UserDefinedFunctionDefinition & Resource> {
let updatedUserDefinedFunction: UserDefinedFunctionDefinition & Resource;
const clearMessage = logConsoleProgress(`Updating user defined function ${userDefinedFunction.id}`);
try {
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunction(userDefinedFunction.id)
.replace(userDefinedFunction);
updatedUserDefinedFunction = response.resource;
} catch (error) {
logConsoleError(`Error while updating user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "UpdateUserupdateUserDefinedFunction", error.code);
sendNotificationForError(error);
}
clearMessage();
return updatedUserDefinedFunction;
}

View File

@@ -6,7 +6,7 @@ export enum Platform {
interface ConfigContext { interface ConfigContext {
platform: Platform; platform: Platform;
allowedParentFrameOrigins: RegExp; allowedParentFrameOrigins: string[];
gitSha?: string; gitSha?: string;
proxyPath?: string; proxyPath?: string;
AAD_ENDPOINT: string; AAD_ENDPOINT: string;
@@ -30,7 +30,12 @@ interface ConfigContext {
// Default configuration // Default configuration
let configContext: Readonly<ConfigContext> = { let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal, platform: Platform.Portal,
allowedParentFrameOrigins: /^https:\/\/portal\.azure\.com$|^https:\/\/portal\.azure\.us$|^https:\/\/portal\.azure\.cn$|^https:\/\/portal\.microsoftazure\.de$|^https:\/\/.+\.portal\.azure\.com$|^https:\/\/.+\.portal\.azure\.us$|^https:\/\/.+\.portal\.azure\.cn$|^https:\/\/.+\.portal\.microsoftazure\.de$|^https:\/\/main\.documentdb\.ext\.azure\.com$|^https:\/\/main\.documentdb\.ext\.microsoftazure\.de$|^https:\/\/main\.documentdb\.ext\.azure\.cn$|^https:\/\/main\.documentdb\.ext\.azure\.us$/, allowedParentFrameOrigins: [
`^https:\\/\\/cosmos.azure.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]+.portal.azure.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]+.ext.azure.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]+microsoftazure.de$`
],
// Webpack injects this at build time // Webpack injects this at build time
gitSha: process.env.GIT_SHA, gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/", hostedExplorerURL: "https://cosmos.azure.com/",
@@ -73,8 +78,13 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
const response = await fetch("./config.json"); const response = await fetch("./config.json");
if (response.status === 200) { if (response.status === 200) {
try { try {
const externalConfig = await response.json(); const { allowedParentFrameOrigins, ...externalConfig } = await response.json();
Object.assign(configContext, externalConfig); Object.assign(configContext, externalConfig);
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
updateConfigContext({
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins]
});
}
} catch (error) { } catch (error) {
console.error("Unable to parse json in config file"); console.error("Unable to parse json in config file");
console.error(error); console.error(error);

View File

@@ -88,10 +88,6 @@ export interface Resource {
id: string; id: string;
} }
export interface ResourceRequest {
id: string;
}
export interface Collection extends Resource { export interface Collection extends Resource {
defaultTtl?: number; defaultTtl?: number;
indexingPolicy?: IndexingPolicy; indexingPolicy?: IndexingPolicy;
@@ -104,39 +100,12 @@ export interface Collection extends Resource {
geospatialConfig?: GeospatialConfig; geospatialConfig?: GeospatialConfig;
} }
export interface CreateCollectionWithRpResponse extends Resource {
properties: Collection;
name: string;
type: string;
}
export interface CollectionRequest extends ResourceRequest {
defaultTtl?: number;
indexingPolicy?: IndexingPolicy;
partitionKey?: PartitionKey;
uniqueKeyPolicy?: UniqueKeyPolicy;
conflictResolutionPolicy?: ConflictResolutionPolicy;
}
export interface Database extends Resource { export interface Database extends Resource {
collections?: Collection[]; collections?: Collection[];
} }
export interface DocumentId extends Resource {} export interface DocumentId extends Resource {}
export interface Script extends Resource {
body: string;
}
export interface StoredProcedure extends Script {}
export interface UserDefinedFunction extends Script {}
export interface Trigger extends Script {
triggerType: string;
triggerOperation: string;
}
export interface ConflictId extends Resource { export interface ConflictId extends Resource {
resourceId?: string; resourceId?: string;
resourceType?: string; resourceType?: string;
@@ -259,28 +228,6 @@ export interface ErrorDataModel {
code?: string; code?: string;
} }
/**
* Defines a property bag for telemetry e.g. see ITelemetryError.
*/
export interface ITelemetryProperties {
[propertyName: string]: string;
}
/**
* Defines a property bag for telemetry e.g. see ITelemetryError.
*/
export interface ITelemetryEvent {
name: string;
properties?: ITelemetryProperties;
}
/**
* Defines an error to be logged as telemetry data.
*/
export interface ITelemetryError extends ITelemetryEvent {
error: any;
}
export interface CreateDatabaseAndCollectionRequest { export interface CreateDatabaseAndCollectionRequest {
databaseId: string; databaseId: string;
collectionId: string; collectionId: string;
@@ -307,11 +254,6 @@ export enum AutopilotTier {
Tier4 = 4 Tier4 = 4
} }
export interface RpOptions {
// tier is sent as string, autoscale as object (AutoPilotCreationSettings)
[key: string]: string | AutoPilotCreationSettings;
}
export interface Query { export interface Query {
id: string; id: string;
resourceId: string; resourceId: string;
@@ -347,10 +289,11 @@ export interface CreateCollectionParams {
uniqueKeyPolicy?: UniqueKeyPolicy; uniqueKeyPolicy?: UniqueKeyPolicy;
} }
export interface SharedThroughputRange { export interface ReadDatabaseOfferParams {
minimumRU: number; databaseId: string;
maximumRU: number; databaseResourceId?: string;
defaultRU: number; isServerless?: boolean;
offerId?: string;
} }
export interface Notification { export interface Notification {
@@ -495,25 +438,6 @@ export interface NotebookConfigurationEndpointInfo {
token: string; token: string;
} }
export interface SparkCluster {
id: string;
name: string;
type: string;
properties: {
kind: string;
driverSize: string;
workerSize: string;
workerInstanceCount: number;
creationTime: string;
status: string;
libraries?: SparkClusterLibrary[];
};
}
export interface SparkClusterFeedResponse {
value: SparkCluster[];
}
export interface SparkClusterConnectionInfo { export interface SparkClusterConnectionInfo {
userName: string; userName: string;
password: string; password: string;
@@ -555,79 +479,10 @@ export interface MongoParameters extends RpParameters {
analyticalStorageTtl?: number; analyticalStorageTtl?: number;
} }
export interface GraphParameters extends RpParameters {
pk: string;
coll: string;
cd: Boolean;
indexingPolicy?: IndexingPolicy;
}
export interface CreationRequest {
properties: {
resource: {
id: string;
};
options: RpOptions;
};
}
export interface SqlCollectionParameters extends RpParameters {
uniqueKeyPolicy?: UniqueKeyPolicy;
pk: string;
coll: string;
cd: Boolean;
analyticalStorageTtl?: number;
indexingPolicy?: IndexingPolicy;
}
export interface MongoCreationRequest extends CreationRequest {
properties: {
resource: {
id: string;
analyticalStorageTtl?: number;
shardKey?: {};
};
options: RpOptions;
};
}
export interface GraphCreationRequest extends CreationRequest {
properties: {
resource: {
id: string;
partitionKey: {};
indexingPolicy?: IndexingPolicy;
};
options: RpOptions;
};
}
export interface CreateDatabaseWithRpResponse {
id: string;
name: string;
type: string;
properties: {
id: string;
};
}
export interface SparkClusterLibrary { export interface SparkClusterLibrary {
name: string; name: string;
} }
export interface SqlCollectionCreationRequest extends CreationRequest {
properties: {
resource: {
uniqueKeyPolicy?: UniqueKeyPolicy;
id: string;
partitionKey: {};
analyticalStorageTtl?: number;
indexingPolicy?: IndexingPolicy;
};
options: RpOptions;
};
}
export interface Library extends SparkClusterLibrary { export interface Library extends SparkClusterLibrary {
properties: { properties: {
kind: "Jar"; kind: "Jar";

View File

@@ -1,16 +1,22 @@
import * as DataModels from "./DataModels"; import {
QueryMetrics,
Resource,
StoredProcedureDefinition,
TriggerDefinition,
UserDefinedFunctionDefinition
} from "@azure/cosmos";
import Q from "q"; import Q from "q";
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { QueryMetrics } from "@azure/cosmos";
import { UploadDetails } from "../workers/upload/definitions";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction"; import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
import StoredProcedure from "../Explorer/Tree/StoredProcedure"; import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import Trigger from "../Explorer/Tree/Trigger"; import Trigger from "../Explorer/Tree/Trigger";
import DocumentId from "../Explorer/Tree/DocumentId"; import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
import ConflictId from "../Explorer/Tree/ConflictId"; import { UploadDetails } from "../workers/upload/definitions";
import * as DataModels from "./DataModels";
export interface TokenProvider { export interface TokenProvider {
getAuthHeader(): Promise<Headers>; getAuthHeader(): Promise<Headers>;
@@ -75,15 +81,15 @@ export interface Database extends TreeNode {
selectedSubnodeKind: ko.Observable<CollectionTabKind>; selectedSubnodeKind: ko.Observable<CollectionTabKind>;
selectDatabase(): void; selectDatabase(): void;
expandDatabase(): void; expandDatabase(): Promise<void>;
collapseDatabase(): void; collapseDatabase(): void;
loadCollections(): Q.Promise<void>; loadCollections(): Promise<void>;
findCollectionWithId(collectionRid: string): Collection; findCollectionWithId(collectionRid: string): Collection;
openAddCollection(database: Database, event: MouseEvent): void; openAddCollection(database: Database, event: MouseEvent): void;
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void; onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
readSettings(): void;
onSettingsClick: () => void; onSettingsClick: () => void;
loadOffer(): Promise<void>;
} }
export interface CollectionBase extends TreeNode { export interface CollectionBase extends TreeNode {
@@ -153,13 +159,13 @@ export interface Collection extends CollectionBase {
collapseUserDefinedFunctions(): void; collapseUserDefinedFunctions(): void;
collapseTriggers(): void; collapseTriggers(): void;
loadUserDefinedFunctions(): Q.Promise<any>; loadUserDefinedFunctions(): Promise<any>;
loadStoredProcedures(): Q.Promise<any>; loadStoredProcedures(): Promise<any>;
loadTriggers(): Q.Promise<any>; loadTriggers(): Promise<any>;
createStoredProcedureNode(data: DataModels.StoredProcedure): StoredProcedure; createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure;
createUserDefinedFunctionNode(data: DataModels.UserDefinedFunction): UserDefinedFunction; createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction;
createTriggerNode(data: DataModels.Trigger): Trigger; createTriggerNode(data: TriggerDefinition & Resource): Trigger;
findStoredProcedureWithId(sprocRid: string): StoredProcedure; findStoredProcedureWithId(sprocRid: string): StoredProcedure;
findTriggerWithId(triggerRid: string): Trigger; findTriggerWithId(triggerRid: string): Trigger;
findUserDefinedFunctionWithId(udfRid: string): UserDefinedFunction; findUserDefinedFunctionWithId(udfRid: string): UserDefinedFunction;

View File

@@ -42,7 +42,8 @@ export class ResourceTreeContextMenuButtonFactory {
const deleteDatabaseMenuItem = { const deleteDatabaseMenuItem = {
iconSrc: DeleteDatabaseIcon, iconSrc: DeleteDatabaseIcon,
onClick: () => container.deleteDatabaseConfirmationPane.open(), onClick: () => container.deleteDatabaseConfirmationPane.open(),
label: container.deleteDatabaseText() label: container.deleteDatabaseText(),
styleClass: "deleteDatabaseMenuItem"
}; };
return [newCollectionMenuItem, deleteDatabaseMenuItem]; return [newCollectionMenuItem, deleteDatabaseMenuItem];
} }
@@ -112,7 +113,8 @@ export class ResourceTreeContextMenuButtonFactory {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection(); const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null); selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
}, },
label: container.deleteCollectionText() label: container.deleteCollectionText(),
styleClass: "deleteCollectionMenuItem"
}); });
return items; return items;

View File

@@ -86,6 +86,7 @@ export class DynamicListViewModel extends WaitsForTemplateViewModel {
public onRemoveItemKeyPress = (data: any, event: KeyboardEvent, source: any): boolean => { public onRemoveItemKeyPress = (data: any, event: KeyboardEvent, source: any): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.removeItem(data, event); this.removeItem(data, event);
(document.querySelector(".dynamicListItem:last-of-type input") as HTMLElement).focus();
event.stopPropagation(); event.stopPropagation();
return false; return false;
} }
@@ -94,7 +95,7 @@ export class DynamicListViewModel extends WaitsForTemplateViewModel {
public addItem(): void { public addItem(): void {
this.listItems.push({ value: ko.observable("") }); this.listItems.push({ value: ko.observable("") });
document.getElementById("uniqueKeyItems").focus(); (document.querySelector(".dynamicListItem:last-of-type input") as HTMLElement).focus();
} }
public onAddItemKeyPress = (source: any, event: KeyboardEvent): boolean => { public onAddItemKeyPress = (source: any, event: KeyboardEvent): boolean => {

View File

@@ -66,7 +66,9 @@ export class GitHubReposComponent extends React.Component<GitHubReposComponentPr
return ( return (
<> <>
<div className={"firstdivbg headerline"}>{header}</div> <div className={"firstdivbg headerline"} role="heading" aria-level={2}>
{header}
</div>
<div className={"paneMainContent"}>{content}</div> <div className={"paneMainContent"}>{content}</div>
{!this.props.showAuthorizeAccess && ( {!this.props.showAuthorizeAccess && (
<> <>

View File

@@ -30,6 +30,7 @@ export interface NotebookViewerComponentProps {
isFavorite?: boolean; isFavorite?: boolean;
backNavigationText: string; backNavigationText: string;
hideInputs?: boolean; hideInputs?: boolean;
hidePrompts?: boolean;
onBackClick: () => void; onBackClick: () => void;
onTagClick: (tag: string) => void; onTagClick: (tag: string) => void;
} }
@@ -148,7 +149,8 @@ export class NotebookViewerComponent extends React.Component<
{this.state.showProgressBar && <ProgressIndicator />} {this.state.showProgressBar && <ProgressIndicator />}
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, { {this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
hideInputs: this.props.hideInputs hideInputs: this.props.hideInputs,
hidePrompts: this.props.hidePrompts
})} })}
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />} {this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />}

View File

@@ -84,7 +84,7 @@
step: step, step: step,
'class':'migration collid select-font-size', 'class':'migration collid select-font-size',
min: minAutoPilotThroughput, min: minAutoPilotThroughput,
'aria-label': ariaLabel, 'aria-label': 'Max request units per second',
type: isAutoscaleThroughputInputFieldRequired() ? 'number' : 'hidden', type: isAutoscaleThroughputInputFieldRequired() ? 'number' : 'hidden',
css: { css: {
dirty: maxAutoPilotThroughputSet.editableIsDirty dirty: maxAutoPilotThroughputSet.editableIsDirty

View File

@@ -159,4 +159,20 @@ describe("TreeNodeComponent", () => {
const wrapper = shallow(<TreeNodeComponent {...props} />); const wrapper = shallow(<TreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("renders loading icon", () => {
const node: TreeNode = {
label: "label",
children: [],
isExpanded: true
};
const props = {
node,
generation: 2,
paddingLeft: 9
};
const wrapper = shallow(<TreeNodeComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
}); });

View File

@@ -17,12 +17,14 @@ import {
import TriangleDownIcon from "../../../../images/Triangle-down.svg"; import TriangleDownIcon from "../../../../images/Triangle-down.svg";
import TriangleRightIcon from "../../../../images/Triangle-right.svg"; import TriangleRightIcon from "../../../../images/Triangle-right.svg";
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
export interface TreeNodeMenuItem { export interface TreeNodeMenuItem {
label: string; label: string;
onClick: () => void; onClick: () => void;
iconSrc?: string; iconSrc?: string;
isDisabled?: boolean; isDisabled?: boolean;
styleClass?: string;
} }
export interface TreeNode { export interface TreeNode {
@@ -37,6 +39,7 @@ export interface TreeNode {
data?: any; // Piece of data corresponding to this node data?: any; // Piece of data corresponding to this node
timestamp?: number; timestamp?: number;
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
isLoading?: boolean;
isSelected?: () => boolean; isSelected?: () => boolean;
onClick?: (isExpanded: boolean) => void; // Only if a leaf, other click will expand/collapse onClick?: (isExpanded: boolean) => void; // Only if a leaf, other click will expand/collapse
onExpanded?: () => void; onExpanded?: () => void;
@@ -183,6 +186,9 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
)} )}
{node.contextMenu && this.renderContextMenuButton(node)} {node.contextMenu && this.renderContextMenuButton(node)}
</div> </div>
<div className="loadingIconContainer">
<img className="loadingIcon" src={LoadingIndicator_3Squares} hidden={!this.props.node.isLoading} />
</div>
{node.children && ( {node.children && (
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}> <AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
<div className="nodeChildren" data-test={node.label}> <div className="nodeChildren" data-test={node.label}>
@@ -256,13 +262,20 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
onContextMenu={e => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())} onContextMenu={e => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
> >
{props.item.onRenderIcon()} {props.item.onRenderIcon()}
<span className="treeComponentMenuItemLabel">{props.item.text}</span> <span
className={
"treeComponentMenuItemLabel" + (props.item.className ? ` ${props.item.className}Label` : "")
}
>
{props.item.text}
</span>
</div> </div>
), ),
items: node.contextMenu.map((menuItem: TreeNodeMenuItem) => ({ items: node.contextMenu.map((menuItem: TreeNodeMenuItem) => ({
key: menuItem.label, key: menuItem.label,
text: menuItem.label, text: menuItem.label,
disabled: menuItem.isDisabled, disabled: menuItem.isDisabled,
className: menuItem.styleClass,
onClick: menuItem.onClick, onClick: menuItem.onClick,
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" /> onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
})) }))
@@ -282,7 +295,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
<img <img
className="expandCollapseIcon" className="expandCollapseIcon"
src={this.state.isExpanded ? TriangleDownIcon : TriangleRightIcon} src={this.state.isExpanded ? TriangleDownIcon : TriangleRightIcon}
alt={this.state.isExpanded ? "Branch is expanded" : "Branch is collapsed"} alt={this.state.isExpanded ? `${node.label} branch is expanded` : `${node.label} branch is collapsed`}
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onCollapseExpandIconKeyPress(event, node)} onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onCollapseExpandIconKeyPress(event, node)}
tabIndex={0} tabIndex={0}
role="button" role="button"

View File

@@ -49,7 +49,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
tabIndex={-1} tabIndex={-1}
> >
<img <img
alt="Branch is collapsed" alt="label branch is collapsed"
className="expandCollapseIcon" className="expandCollapseIcon"
onKeyPress={[Function]} onKeyPress={[Function]}
role="button" role="button"
@@ -63,6 +63,15 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
label label
</span> </span>
</div> </div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight <AnimateHeight
animateOpacity={false} animateOpacity={false}
animationStateClasses={ animationStateClasses={
@@ -140,7 +149,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
tabIndex={-1} tabIndex={-1}
> >
<img <img
alt="Branch is expanded" alt="label branch is expanded"
className="expandCollapseIcon" className="expandCollapseIcon"
onKeyPress={[Function]} onKeyPress={[Function]}
role="button" role="button"
@@ -179,6 +188,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
"isBeakVisible": false, "isBeakVisible": false,
"items": Array [ "items": Array [
Object { Object {
"className": undefined,
"disabled": true, "disabled": true,
"key": "menuLabel", "key": "menuLabel",
"onClick": undefined, "onClick": undefined,
@@ -201,6 +211,15 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
/> />
</div> </div>
</div> </div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight <AnimateHeight
animateOpacity={false} animateOpacity={false}
animationStateClasses={ animationStateClasses={
@@ -261,6 +280,77 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
</div> </div>
`; `;
exports[`TreeNodeComponent renders loading icon 1`] = `
<div
className=" main2 nodeItem "
onClick={[Function]}
onKeyPress={[Function]}
>
<div
className="treeNodeHeader "
data-test="label"
style={
Object {
"paddingLeft": 9,
}
}
tabIndex={-1}
>
<img
alt="label branch is expanded"
className="expandCollapseIcon"
onKeyPress={[Function]}
role="button"
src=""
tabIndex={0}
/>
<span
className="nodeLabel"
title="label"
>
label
</span>
</div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight
animateOpacity={false}
animationStateClasses={
Object {
"animating": "rah-animating",
"animatingDown": "rah-animating--down",
"animatingToHeightAuto": "rah-animating--to-height-auto",
"animatingToHeightSpecific": "rah-animating--to-height-specific",
"animatingToHeightZero": "rah-animating--to-height-zero",
"animatingUp": "rah-animating--up",
"static": "rah-static",
"staticHeightAuto": "rah-static--height-auto",
"staticHeightSpecific": "rah-static--height-specific",
"staticHeightZero": "rah-static--height-zero",
}
}
applyInlineTransitions={true}
delay={0}
duration={200}
easing="ease"
height="auto"
style={Object {}}
>
<div
className="nodeChildren"
data-test="label"
/>
</AnimateHeight>
</div>
`;
exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = ` exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
<div <div
className="nodeClassname main12 nodeItem " className="nodeClassname main12 nodeItem "
@@ -278,7 +368,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
tabIndex={-1} tabIndex={-1}
> >
<img <img
alt="Branch is expanded" alt="label branch is expanded"
className="expandCollapseIcon" className="expandCollapseIcon"
onKeyPress={[Function]} onKeyPress={[Function]}
role="button" role="button"
@@ -331,6 +421,15 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
/> />
</div> </div>
</div> </div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight <AnimateHeight
animateOpacity={false} animateOpacity={false}
animationStateClasses={ animationStateClasses={
@@ -436,7 +535,7 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
tabIndex={-1} tabIndex={-1}
> >
<img <img
alt="Branch is expanded" alt="label branch is expanded"
className="expandCollapseIcon" className="expandCollapseIcon"
onKeyPress={[Function]} onKeyPress={[Function]}
role="button" role="button"
@@ -450,6 +549,15 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
label label
</span> </span>
</div> </div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight <AnimateHeight
animateOpacity={false} animateOpacity={false}
animationStateClasses={ animationStateClasses={

View File

@@ -20,7 +20,7 @@
} }
&.showingMenu { &.showingMenu {
background-color: #EEE; background-color: #eee;
} }
.treeMenuEllipsis { .treeMenuEllipsis {
@@ -78,3 +78,12 @@
vertical-align: text-bottom; vertical-align: text-bottom;
} }
} }
.loadingIconContainer {
width: 100%;
.loadingIcon {
height: 6px;
margin-left: 38px;
}
}

View File

@@ -15,7 +15,7 @@ import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import Database from "./Tree/Database"; import Database from "./Tree/Database";
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane"; import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane"; import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import { readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase"; import { refreshCachedResources } from "../Common/DocumentClientUtilityBase";
import { readCollection } from "../Common/dataAccess/readCollection"; import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases"; import { readDatabases } from "../Common/dataAccess/readDatabases";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
@@ -87,6 +87,7 @@ import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import TabsBase from "./Tabs/TabsBase"; import TabsBase from "./Tabs/TabsBase";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { updateUserContext, userContext } from "../UserContext"; import { updateUserContext, userContext } from "../UserContext";
import { stringToBlob } from "../Utils/BlobUtils";
BindingHandlersRegisterer.registerBindingHandlers(); BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import // Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -1423,8 +1424,6 @@ export default class Explorer {
// TODO: Refactor // TODO: Refactor
const deferred: Q.Deferred<any> = Q.defer(); const deferred: Q.Deferred<any> = Q.defer();
const refreshDatabases = (offers?: DataModels.Offer[]) => {
this._setLoadingStatusText("Fetching databases..."); this._setLoadingStatusText("Fetching databases...");
readDatabases().then( readDatabases().then(
(databases: DataModels.Database[]) => { (databases: DataModels.Database[]) => {
@@ -1439,7 +1438,7 @@ export default class Explorer {
startKey startKey
); );
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode(); const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
const deltaDatabases = this.getDeltaDatabases(databases, offers); const deltaDatabases = this.getDeltaDatabases(databases);
this.addDatabasesToList(deltaDatabases.toAdd); this.addDatabasesToList(deltaDatabases.toAdd);
this.deleteDatabasesFromList(deltaDatabases.toDelete); this.deleteDatabasesFromList(deltaDatabases.toDelete);
this.selectedNode(currentlySelectedNode); this.selectedNode(currentlySelectedNode);
@@ -1477,35 +1476,6 @@ export default class Explorer {
); );
} }
); );
};
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers({ isServerless: this.isServerlessEnabled() });
this._setLoadingStatusText("Fetching offers...");
offerPromise.then(
(offers: DataModels.Offer[]) => {
this._setLoadingStatusText("Successfully fetched offers.");
refreshDatabases(offers);
},
error => {
this._setLoadingStatusText("Failed to fetch offers.");
this.isRefreshingExplorer(false);
deferred.reject(error);
TelemetryProcessor.traceFailure(
Action.LoadDatabases,
{
databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree,
error: JSON.stringify(error)
},
startKey
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while refreshing databases: ${JSON.stringify(error)}`
);
}
);
return deferred.promise.then( return deferred.promise.then(
() => { () => {
@@ -2102,16 +2072,13 @@ export default class Explorer {
defaultExperience: this.defaultExperience && this.defaultExperience(), defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree dataExplorerArea: Constants.Areas.ResourceTree
}); });
databasesToLoad.forEach((database: ViewModels.Database) => { databasesToLoad.forEach(async (database: ViewModels.Database) => {
loadCollectionPromises.push( await database.loadCollections();
database.loadCollections().finally(() => {
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid); const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid);
if (isNewDatabase) { if (isNewDatabase) {
database.expandDatabase(); database.expandDatabase();
} }
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid); this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
})
);
}); });
Q.all(loadCollectionPromises).done( Q.all(loadCollectionPromises).done(
@@ -2256,8 +2223,7 @@ export default class Explorer {
} }
private getDeltaDatabases( private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[], updatedDatabaseList: DataModels.Database[]
updatedOffersList: DataModels.Offer[]
): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } { ): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } {
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
const databaseExists = _.some( const databaseExists = _.some(
@@ -2266,10 +2232,9 @@ export default class Explorer {
); );
return !databaseExists; return !databaseExists;
}); });
const databasesToAdd: ViewModels.Database[] = _.map(newDatabases, (newDatabase: DataModels.Database) => { const databasesToAdd: ViewModels.Database[] = newDatabases.map(
const databaseOffer: DataModels.Offer = this.getOfferForResource(updatedOffersList, newDatabase._self); (newDatabase: DataModels.Database) => new Database(this, newDatabase)
return new Database(this, newDatabase, databaseOffer); );
});
let databasesToDelete: ViewModels.Database[] = []; let databasesToDelete: ViewModels.Database[] = [];
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
@@ -2319,10 +2284,6 @@ export default class Explorer {
return null; return null;
} }
private getOfferForResource(offers: DataModels.Offer[], resourceId: string): DataModels.Offer {
return _.find(offers, (offer: DataModels.Offer) => offer.resource === resourceId);
}
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> { public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to upload notebook, but notebook is not enabled"; const error = "Attempt to upload notebook, but notebook is not enabled";
@@ -2621,9 +2582,11 @@ export default class Explorer {
throw new Error(error); throw new Error(error);
} }
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`);
return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then( return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then(
(content: string) => { (content: string) => {
const blob = new Blob([content], { type: "octet/stream" }); const blob = stringToBlob(content, "text/plain");
if (navigator.msSaveBlob) { if (navigator.msSaveBlob) {
// for IE and Edge // for IE and Edge
navigator.msSaveBlob(blob, notebookFile.name); navigator.msSaveBlob(blob, notebookFile.name);
@@ -2640,12 +2603,16 @@ export default class Explorer {
downloadLink.click(); downloadLink.click();
downloadLink.remove(); downloadLink.remove();
} }
clearMessage();
}, },
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Could not download notebook ${JSON.stringify(error)}` `Could not download notebook ${JSON.stringify(error)}`
); );
clearMessage();
} }
); );
} }
@@ -3153,4 +3120,15 @@ export default class Explorer {
} }
} }
} }
public async loadSelectedDatabaseOffer(): Promise<void> {
const database = this.findSelectedDatabase();
await database?.loadOffer();
}
public async loadDatabaseOffers(): Promise<void> {
this.databases()?.forEach(async (database: ViewModels.Database) => {
await database.loadOffer();
});
}
} }

View File

@@ -391,31 +391,6 @@ export class CommandBarComponentButtonFactory {
return buttons; return buttons;
} }
private static createScaleAndSettingsButton(container: Explorer): CommandButtonComponentProps {
let isShared = false;
if (container.isDatabaseNodeSelected()) {
isShared = container.findSelectedDatabase().isDatabaseShared();
} else if (container.isNodeKindSelected("Collection")) {
const database: ViewModels.Database = container.findSelectedCollection().getDatabase();
isShared = database && database.isDatabaseShared();
}
const label = isShared ? "Settings" : "Scale & Settings";
return {
iconSrc: ScaleIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && (<any>selectedCollection).onSettingsClick();
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected()
};
}
private static createNewNotebookButton(container: Explorer): CommandButtonComponentProps { private static createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook"; const label = "New Notebook";
return { return {

View File

@@ -130,11 +130,14 @@ export class NotificationConsoleComponent extends React.Component<
<span className="headerStatusEllipsis">{this.state.headerStatus}</span> <span className="headerStatusEllipsis">{this.state.headerStatus}</span>
</span> </span>
</div> </div>
<div className="expandCollapseButton" role="button" tabIndex={0}> <div
<img className="expandCollapseButton"
src={this.state.isExpanded ? ChevronDownIcon : ChevronUpIcon} role="button"
alt={this.state.isExpanded ? "collapse console" : "expand console"} tabIndex={0}
/> aria-label={this.state.isExpanded ? "collapse console" : "expand console"}
aria-expanded={this.state.isExpanded}
>
<img src={this.state.isExpanded ? ChevronDownIcon : ChevronUpIcon} alt="" />
</div> </div>
</div> </div>
<AnimateHeight <AnimateHeight

View File

@@ -68,12 +68,14 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
</span> </span>
</div> </div>
<div <div
aria-expanded={true}
aria-label="collapse console"
className="expandCollapseButton" className="expandCollapseButton"
role="button" role="button"
tabIndex={0} tabIndex={0}
> >
<img <img
alt="collapse console" alt=""
src="" src=""
/> />
</div> </div>

View File

@@ -194,17 +194,24 @@ export class NotebookContentClient {
}); });
} }
public readFileContent(filePath: string): Promise<string> { public async readFileContent(filePath: string): Promise<string> {
return this.contentProvider const xhr = await this.contentProvider.get(this.getServerConfig(), filePath, { content: 1 }).toPromise();
.get(this.getServerConfig(), filePath, { type: "notebook", format: "text", content: 1 })
.toPromise()
.then(xhr => {
const content = (xhr.response as any).content; const content = (xhr.response as any).content;
if (!content) { if (!content) {
throw new Error("No content read"); throw new Error("No content read");
} }
const format = (xhr.response as any).format;
switch (format) {
case "text":
return content;
case "base64":
return atob(content);
case "json":
return stringifyNotebook(content); return stringifyNotebook(content);
}); default:
throw new Error(`Unsupported content format ${format}`);
}
} }
private deleteNotebookFile(path: string): Promise<string> { private deleteNotebookFile(path: string): Promise<string> {

View File

@@ -113,11 +113,14 @@ export default class NotebookManager {
this.params.resourceTree.initializeGitHubRepos(pinnedRepos); this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
this.params.resourceTree.triggerRender(); this.params.resourceTree.triggerRender();
}); });
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope); this.refreshPinnedRepos();
} }
public refreshPinnedRepos(): void { public refreshPinnedRepos(): void {
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope); const token = this.gitHubOAuthService.getTokenObservable()();
if (token) {
this.junoClient.getPinnedRepos(token.scope);
}
} }
public async openPublishNotebookPane( public async openPublishNotebookPane(

View File

@@ -3,6 +3,7 @@ import "./base.css";
import "./default.css"; import "./default.css";
import { CodeCell, RawCell, Cells, MarkdownCell } from "@nteract/stateful-components"; import { CodeCell, RawCell, Cells, MarkdownCell } from "@nteract/stateful-components";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import { AzureTheme } from "./AzureTheme"; import { AzureTheme } from "./AzureTheme";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -15,6 +16,7 @@ import "./NotebookReadOnlyRenderer.less";
export interface NotebookRendererProps { export interface NotebookRendererProps {
contentRef: any; contentRef: any;
hideInputs?: boolean; hideInputs?: boolean;
hidePrompts?: boolean;
} }
interface PassedEditorProps { interface PassedEditorProps {
@@ -38,6 +40,29 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
loadTransform(this.props as any); loadTransform(this.props as any);
} }
private renderPrompt(id: string, contentRef: string): JSX.Element {
if (this.props.hidePrompts) {
return <></>;
}
return (
<Prompt id={id} contentRef={contentRef}>
{(props: PassedPromptProps) => {
if (props.status === "busy") {
return <React.Fragment>{"[*]"}</React.Fragment>;
}
if (props.status === "queued") {
return <React.Fragment>{"[…]"}</React.Fragment>;
}
if (typeof props.executionCount === "number") {
return <React.Fragment>{`[${props.executionCount}]`}</React.Fragment>;
}
return <React.Fragment>{"[ ]"}</React.Fragment>;
}}
</Prompt>
);
}
render(): JSX.Element { render(): JSX.Element {
return ( return (
<div className="NotebookReadOnlyRender"> <div className="NotebookReadOnlyRender">
@@ -46,6 +71,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
code: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => ( code: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
<CodeCell id={id} contentRef={contentRef}> <CodeCell id={id} contentRef={contentRef}>
{{ {{
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
editor: { editor: {
codemirror: (props: PassedEditorProps) => codemirror: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} /> this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} />

View File

@@ -24,7 +24,7 @@
<script type="text/html" id="add-collection-inputs"> <script type="text/html" id="add-collection-inputs">
<!-- Add collection header - Start --> <!-- Add collection header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span id="containerTitle" data-bind="text: title"></span> <span id="containerTitle" role="heading" aria-level="2" data-bind="text: title" ></span>
<div class="closeImg" id="closeBtnAddCollection" role="button" aria-label="Add collection close pane" <div class="closeImg" id="closeBtnAddCollection" role="button" aria-label="Add collection close pane"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }" tabindex="0"> data-bind="click: cancel, event: { keypress: onCloseKeyPress }" tabindex="0">
<img src="../../../images/close-black.svg" title="Close" alt="Close" /> <img src="../../../images/close-black.svg" title="Close" alt="Close" />
@@ -115,10 +115,10 @@
<!-- Database provisioned throughput - Start --> <!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput --> <!-- ko if: canConfigureThroughput -->
<div class="databaseProvision" aria-label="New database provision support" <div class="databaseProvision" aria-label="Provision database throughput"
data-bind="visible: databaseCreateNew"> data-bind="visible: databaseCreateNew">
<input tabindex="0" type="checkbox" data-test="addCollectionPane-databaseSharedThroughput" <input tabindex="0" type="checkbox" data-test="addCollectionPane-databaseSharedThroughput"
id="addCollection-databaseSharedThroughput" title="Provision shared throughput" id="addCollection-databaseSharedThroughput" title="Provision database throughput"
data-bind="checked: databaseCreateNewShared" /> data-bind="checked: databaseCreateNewShared" />
<span class="databaseProvisionText" for="databaseSharedThroughput">Provision database throughput</span> <span class="databaseProvisionText" for="databaseSharedThroughput">Provision database throughput</span>
<span class="infoTooltip" role="tooltip" tabindex="0"> <span class="infoTooltip" role="tooltip" tabindex="0">
@@ -517,13 +517,13 @@
<div> <div>
<span class="mandatoryStar">*</span> <span class="mandatoryStar">*</span>
<span class="addCollectionLabel">Analytical store</span> <span class="addCollectionLabel">Analytical store</span>
<span class="infoTooltip" role="tooltip" tabindex="0"> <span class="infoTooltip" role="tooltip" tabindex="0" data-bind="event: { focus: function(data, event) { transferFocus('tooltip1', 'link1') } }">
<img class="infoImg" src="/info-bubble.svg" alt="More information"> <img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext infoTooltipWidth"> <span id="tooltip1" class="tooltiptext infoTooltipWidth" data-bind="event: { mouseout: onMouseOut }">
Enable analytical store capability to perform near real-time analytics on your operational Enable analytical store capability to perform near real-time analytics on your operational
data, without impacting the performance of transactional workloads. data, without impacting the performance of transactional workloads.
Learn more <a class="errorLink" href="https://aka.ms/analytical-store-overview" Learn more <a id="link1" class="errorLink" href="https://aka.ms/analytical-store-overview"
target="_blank">here</a> target="_blank" data-bind="event: { focusout: onFocusOut, keydown: onKeyDown.bind($data, 'largePartitionKey') }">here</a>
</span> </span>
</span> </span>
</div> </div>
@@ -537,9 +537,11 @@
attr: { attr: {
'aria-checked': isAnalyticalStorageOn() ? 'true' : 'false' 'aria-checked': isAnalyticalStorageOn() ? 'true' : 'false'
}" /> }" />
<span for="enableAnalyticalStorageRadioOn" data-bind="disable: showEnableSynapseLink"> <label for="enableAnalyticalStorageRadioOn" class="enableAnalyticalStorageRadioLabel">
<span data-bind="disable: showEnableSynapseLink">
On On
</span> </span>
</label>
<input class="enableAnalyticalStorageRadio" id="enableAnalyticalStorageRadioOff" <input class="enableAnalyticalStorageRadio" id="enableAnalyticalStorageRadioOff"
name="analyticalStore" type="radio" role="radio" tabindex="0" data-bind=" name="analyticalStore" type="radio" role="radio" tabindex="0" data-bind="
@@ -549,9 +551,11 @@
attr: { attr: {
'aria-checked': isAnalyticalStorageOn() ? 'false' : 'true' 'aria-checked': isAnalyticalStorageOn() ? 'false' : 'true'
}" /> }" />
<span for="enableAnalyticalStorageRadioOff" data-bind="disable: showEnableSynapseLink"> <label for="enableAnalyticalStorageRadioOff" class="enableAnalyticalStorageRadioLabel">
<span data-bind="disable: showEnableSynapseLink">
Off Off
</span> </span>
</label>
</div> </div>
<div class="paragraph italic" data-bind="visible: ttl90DaysEnabled() && isAnalyticalStorageOn()"> <div class="paragraph italic" data-bind="visible: ttl90DaysEnabled() && isAnalyticalStorageOn()">

View File

@@ -681,7 +681,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return true; return true;
}; };
public open(databaseId?: string) { public async open(databaseId?: string) {
super.open(); super.open();
// 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("");
@@ -715,18 +715,40 @@ export default class AddCollectionPane extends ContextualPaneBase {
dataExplorerArea: Constants.Areas.ContextualPane dataExplorerArea: Constants.Areas.ContextualPane
}; };
await this.container.loadDatabaseOffers();
this._onDatabasesChange(this.container.databases()); this._onDatabasesChange(this.container.databases());
this._setFocus(); this._setFocus();
TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage); TelemetryProcessor.trace(Action.CreateCollection, ActionModifiers.Open, addCollectionPaneOpenMessage);
} }
private transferFocus(elementIdToKeepVisible: string, elementIdToFocus: string): void {
document.getElementById(elementIdToKeepVisible).style.visibility = "visible";
document.getElementById(elementIdToFocus).focus();
}
private onFocusOut(_: any, event: any): void {
event.target.parentElement.style.visibility = "";
}
private onMouseOut(_: any, event: any): void {
event.target.style.visibility = "";
}
private onKeyDown(previousActiveElementId: string, _: any, event: KeyboardEvent): boolean {
if (event.shiftKey && event.keyCode == Constants.KeyCodes.Tab) {
document.getElementById(previousActiveElementId).focus();
return false;
} else {
// Execute default action
return true;
}
}
private _onDatabasesChange(newDatabaseIds: ViewModels.Database[]) { private _onDatabasesChange(newDatabaseIds: ViewModels.Database[]) {
const cachedDatabaseIdsList = _.map(newDatabaseIds, (database: ViewModels.Database) => { const cachedDatabaseIdsList = _.map(newDatabaseIds, (database: ViewModels.Database) => {
if (database && database.offer && database.offer()) { if (database && database.offer && database.offer()) {
this._databaseOffers.set(database.id(), database.offer()); this._databaseOffers.set(database.id(), database.offer());
} else if (database && database.isDatabaseShared && database.isDatabaseShared()) {
database.readSettings();
} }
return database.id(); return database.id();

View File

@@ -24,7 +24,7 @@
<script type="text/html" id="add-database-inputs"> <script type="text/html" id="add-database-inputs">
<!-- Add database header - Start --> <!-- Add database header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span id="databaseTitle" data-bind="text: title"></span> <span id="databaseTitle" role="heading" aria-level="2" data-bind="text: title"></span>
<div class="closeImg" role="button" aria-label="Close pane" <div class="closeImg" role="button" aria-label="Close pane"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }" tabindex="0"> data-bind="click: cancel, event: { keypress: onCloseKeyPress }" tabindex="0">
<img src="../../../images/close-black.svg" title="Close" alt="Close" /> <img src="../../../images/close-black.svg" title="Close" alt="Close" />
@@ -73,8 +73,8 @@
</p> </p>
<input id="database-id" type="text" aria-required="true" autocomplete="off" pattern="[^/?#\\]*[^/?# \\]" <input id="database-id" type="text" aria-required="true" autocomplete="off" pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'" placeholder="Type a new database id" title="May not end with space nor contain characters '\' '/' '#' '?'"
size="40" class="collid" data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus" size="40" class="collid" data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus, attr: { placeholder: databaseIdPlaceHolder }"
aria-label="Database id" autofocus> aria-label="Database id" autofocus>
<!-- Database provisioned throughput - Start --> <!-- Database provisioned throughput - Start -->

View File

@@ -16,6 +16,7 @@ import { PlatformType } from "../../PlatformType";
export default class AddDatabasePane extends ContextualPaneBase { export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>; public defaultExperience: ko.Computed<string>;
public databaseIdLabel: ko.Computed<string>; public databaseIdLabel: ko.Computed<string>;
public databaseIdPlaceHolder: ko.Computed<string>;
public databaseId: ko.Observable<string>; public databaseId: ko.Observable<string>;
public databaseIdTooltipText: ko.Computed<string>; public databaseIdTooltipText: ko.Computed<string>;
public databaseLevelThroughputTooltipText: ko.Computed<string>; public databaseLevelThroughputTooltipText: ko.Computed<string>;
@@ -70,6 +71,11 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.databaseIdLabel = ko.computed<string>(() => this.databaseIdLabel = ko.computed<string>(() =>
this.container.isPreferredApiCassandra() ? "Keyspace id" : "Database id" this.container.isPreferredApiCassandra() ? "Keyspace id" : "Database id"
); );
this.databaseIdPlaceHolder = ko.computed<string>(() =>
this.container.isPreferredApiCassandra() ? "Type a new keyspace id" : "Type a new database id"
);
this.databaseIdTooltipText = ko.computed<string>(() => { this.databaseIdTooltipText = ko.computed<string>(() => {
const isCassandraAccount: boolean = this.container.isPreferredApiCassandra(); const isCassandraAccount: boolean = this.container.isPreferredApiCassandra();
return `A ${isCassandraAccount ? "keyspace" : "database"} is a logical container of one or more ${ return `A ${isCassandraAccount ? "keyspace" : "database"} is a logical container of one or more ${

View File

@@ -6,7 +6,7 @@
<div class="paneContentContainer"> <div class="paneContentContainer">
<!-- Save Query header - Start --> <!-- Save Query header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div <div
class="closeImg" class="closeImg"
role="button" role="button"

View File

@@ -17,7 +17,7 @@
> >
<!-- Add Cassandra collection header - Start --> <!-- Add Cassandra collection header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div <div
class="closeImg" class="closeImg"
role="button" role="button"

View File

@@ -268,8 +268,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => { const cachedKeyspaceIdsList = _.map(newKeyspaceIds, (keyspace: ViewModels.Database) => {
if (keyspace && keyspace.offer && !!keyspace.offer()) { if (keyspace && keyspace.offer && !!keyspace.offer()) {
this.keyspaceOffers.set(keyspace.id(), keyspace.offer()); this.keyspaceOffers.set(keyspace.id(), keyspace.offer());
} else if (keyspace && keyspace.isDatabaseShared && keyspace.isDatabaseShared()) {
keyspace.readSettings();
} }
return keyspace.id(); return keyspace.id();
}); });

View File

@@ -9,6 +9,7 @@ import Explorer from "../Explorer";
// TODO: Use specific actions for logging telemetry data // TODO: Use specific actions for logging telemetry data
export abstract class ContextualPaneBase extends WaitsForTemplateViewModel { export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
private initalFocusedElement: HTMLElement | undefined;
public id: string; public id: string;
public container: Explorer; public container: Explorer;
public firstFieldHasFocus: ko.Observable<boolean>; public firstFieldHasFocus: ko.Observable<boolean>;
@@ -49,9 +50,11 @@ export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
this.visible(false); this.visible(false);
this.isExecuting(false); this.isExecuting(false);
this.resetData(); this.resetData();
this.resetFocus();
} }
public open() { public open() {
this.initalFocusedElement = document.activeElement as HTMLElement;
this.visible(true); this.visible(true);
this.firstFieldHasFocus(true); this.firstFieldHasFocus(true);
this.resizePane(); this.resizePane();
@@ -123,4 +126,11 @@ export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
$(paneElement).height(newPaneElementHeight); $(paneElement).height(newPaneElementHeight);
} }
private resetFocus(): void {
if (this.initalFocusedElement) {
this.initalFocusedElement.focus();
this.initalFocusedElement = undefined;
}
}
} }

View File

@@ -15,7 +15,7 @@
> >
<!-- Delete Collection Confirmation header - Start --> <!-- Delete Collection Confirmation header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div <div
class="closeImg" class="closeImg"
role="button" role="button"
@@ -67,8 +67,7 @@
name="collectionIdConfirmation" name="collectionIdConfirmation"
required required
class="collid" class="collid"
data-bind="value: collectionIdConfirmation, hasFocus: firstFieldHasFocus" data-bind="value: collectionIdConfirmation, hasFocus: firstFieldHasFocus, attr: { 'aria-label': collectionIdConfirmationText }"
aria-label="Confirm by typing the collection id"
/> />
</p> </p>
</div> </div>

View File

@@ -15,7 +15,7 @@
> >
<!-- Delete Database Confirmation header - Start --> <!-- Delete Database Confirmation header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div <div
class="closeImg" class="closeImg"
role="button" role="button"

View File

@@ -13,6 +13,7 @@ import DeleteFeedback from "../../Common/DeleteFeedback";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase"; import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
import { ARMError } from "../../Utils/arm/request";
export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase { export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
public databaseIdConfirmationText: ko.Observable<string>; public databaseIdConfirmationText: ko.Observable<string>;
@@ -105,11 +106,12 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
this.databaseDeleteFeedback(""); this.databaseDeleteFeedback("");
} }
}, },
(reason: any) => { (reason: unknown) => {
this.isExecuting(false); this.isExecuting(false);
const message = ErrorParserUtility.parse(reason);
this.formErrors(message[0].message); const message = reason instanceof ARMError ? reason.message : ErrorParserUtility.parse(reason)[0].message;
this.formErrorsDetails(message[0].message); this.formErrors(message);
this.formErrorsDetails(message);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.DeleteDatabase, Action.DeleteDatabase,
{ {
@@ -130,7 +132,8 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
super.resetData(); super.resetData();
} }
public open() { public async open() {
await this.container.loadSelectedDatabaseOffer();
this.recordDeleteFeedback(this.shouldRecordFeedback()); this.recordDeleteFeedback(this.shouldRecordFeedback());
super.open(); super.open();
} }

View File

@@ -11,7 +11,7 @@
<form class="paneContentContainer" data-bind="submit: execute"> <form class="paneContentContainer" data-bind="submit: execute">
<!-- Input params header - Start --> <!-- Input params header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div <div
class="closeImg" class="closeImg"
role="button" role="button"

View File

@@ -70,7 +70,9 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
private renderPanelHeader = (): JSX.Element => { private renderPanelHeader = (): JSX.Element => {
return ( return (
<div className="firstdivbg headerline"> <div className="firstdivbg headerline">
<span id="databaseTitle">{this.props.title}</span> <span id="databaseTitle" role="heading" aria-level={2}>
{this.props.title}
</span>
<IconButton <IconButton
ariaLabel="Close pane" ariaLabel="Close pane"
title="Close pane" title="Close pane"

View File

@@ -6,7 +6,7 @@
<form class="paneContentContainer" data-bind="submit: submit"> <form class="paneContentContainer" data-bind="submit: submit">
<!-- New Vertex header - Start --> <!-- New Vertex header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span>New Vertex</span> <span role="heading" aria-level="2">New Vertex</span>
<div <div
class="closeImg" class="closeImg"
role="button" role="button"

View File

@@ -6,7 +6,7 @@
<form class="paneContentContainer" data-bind="submit: submit"> <form class="paneContentContainer" data-bind="submit: submit">
<!-- Graph Styling header - Start --> <!-- Graph Styling header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span>Graph Styling</span> <span role="heading" aria-level="2">Graph Styling</span>
<div <div
class="closeImg" class="closeImg"
role="button" role="button"

View File

@@ -6,7 +6,7 @@
<form class="paneContentContainer" data-bind="submit: submit"> <form class="paneContentContainer" data-bind="submit: submit">
<!-- Load Query header - Start --> <!-- Load Query header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div <div
class="closeImg" class="closeImg"
role="button" role="button"

View File

@@ -11,7 +11,7 @@
<form class="paneContentContainer" data-bind="submit: submit"> <form class="paneContentContainer" data-bind="submit: submit">
<!-- Renew ad-hoc access header - Start --> <!-- Renew ad-hoc access header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div <div
class="closeImg" class="closeImg"
role="button" role="button"

View File

@@ -6,7 +6,7 @@
<form class="paneContentContainer" data-bind="submit: submit"> <form class="paneContentContainer" data-bind="submit: submit">
<!-- Save Query header - Start --> <!-- Save Query header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div class="closeImg" role="button" aria-label="Close pane" tabindex="0" data-bind="click: cancel"> <div class="closeImg" role="button" aria-label="Close pane" tabindex="0" data-bind="click: cancel">
<img src="../../../images/close-black.svg" title="Close" alt="Close" /> <img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div> </div>

View File

@@ -12,7 +12,7 @@
<form class="paneContentContainer" data-bind="submit: submit"> <form class="paneContentContainer" data-bind="submit: submit">
<!-- Settings Confirmation header - Start --> <!-- Settings Confirmation header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div <div
class="closeImg" class="closeImg"
role="button" role="button"

View File

@@ -6,7 +6,7 @@
<div class="paneContentContainer"> <div class="paneContentContainer">
<!-- Setup notebooks header - Start --> <!-- Setup notebooks header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div <div
class="closeImg" class="closeImg"
role="button" role="button"

View File

@@ -6,7 +6,7 @@
<form class="paneContentContainer" data-bind="submit: submit"> <form class="paneContentContainer" data-bind="submit: submit">
<!-- String Input header - Start --> <!-- String Input header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div class="closeImg" role="button" aria-label="Close pane" tabindex="0" data-bind="click: cancel"></div> <div class="closeImg" role="button" aria-label="Close pane" tabindex="0" data-bind="click: cancel"></div>
</div> </div>
<!-- String Input header - End --> <!-- String Input header - End -->

View File

@@ -5,7 +5,7 @@
<div class="contextual-pane-in"> <div class="contextual-pane-in">
<!-- Switch Directory header - Start --> <!-- Switch Directory header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div class="closeImg" role="button" aria-label="Close pane" tabindex="0" data-bind="click: close"> <div class="closeImg" role="button" aria-label="Close pane" tabindex="0" data-bind="click: close">
<img src="../../../images/close-black.svg" title="Close" alt="Close" /> <img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div> </div>

View File

@@ -80,7 +80,7 @@ export default class AddTableEntityPane extends TableEntityPane {
this.updateIsActionEnabled(); this.updateIsActionEnabled();
super.open(); super.open();
} }
const focusElement = document.getElementById("addTableEntityValue"); const focusElement = document.getElementById("closeAddEntityPane");
focusElement && focusElement.focus(); focusElement && focusElement.focus();
} }

View File

@@ -21,6 +21,7 @@
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div <div
id="closeAddEntityPane"
class="closeImg" class="closeImg"
role="button" role="button"
aria-label="Close pane" aria-label="Close pane"

View File

@@ -19,7 +19,7 @@
> >
<!-- Edit table entity header - Start --> <!-- Edit table entity header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div <div
class="closeImg" class="closeImg"
role="button" role="button"

View File

@@ -6,7 +6,7 @@
<form class="paneContentContainer" data-bind="submit: submit"> <form class="paneContentContainer" data-bind="submit: submit">
<!-- Upload File header - Start --> <!-- Upload File header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div class="closeImg" role="button" aria-label="Close pane" tabindex="0" data-bind="click: cancel"> <div class="closeImg" role="button" aria-label="Close pane" tabindex="0" data-bind="click: cancel">
<img src="../../../images/close-black.svg" title="Close" alt="Close" /> <img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div> </div>

View File

@@ -11,7 +11,7 @@
<form class="paneContentContainer" data-bind="submit: submit"> <form class="paneContentContainer" data-bind="submit: submit">
<!-- Upload items header - Start --> <!-- Upload items header - Start -->
<div class="firstdivbg headerline"> <div class="firstdivbg headerline">
<span data-bind="text: title"></span> <span role="heading" aria-level="2" data-bind="text: title"></span>
<div <div
class="closeImg" class="closeImg"
role="button" role="button"

View File

@@ -100,7 +100,7 @@
</table> </table>
</div> </div>
<div class="loadMore"> <div class="loadMore">
<a role="link" data-bind="click: loadNextPage, event: { keypress: onLoadMoreKeyInput }" tabindex="0" <a role="button" data-bind="click: loadNextPage, event: { keypress: onLoadMoreKeyInput }" tabindex="0"
>Load more</a >Load more</a
> >
</div> </div>

View File

@@ -598,7 +598,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
() => { () => {
this.container.isRefreshingExplorer(false); this.container.isRefreshingExplorer(false);
this._setBaseline(); this._setBaseline();
this.database.readSettings();
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.UpdateSettings, Action.UpdateSettings,
{ {
@@ -643,8 +642,9 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
}; };
public onActivate(): Q.Promise<any> { public onActivate(): Q.Promise<any> {
return super.onActivate().then(() => { return super.onActivate().then(async () => {
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
await this.database.loadOffer();
}); });
} }

View File

@@ -200,7 +200,7 @@
</table> </table>
</div> </div>
<div class="loadMore"> <div class="loadMore">
<a role="link" data-bind="click: loadNextPage, event: { keypress: onLoadMoreKeyInput }" tabindex="0" <a role="button" data-bind="click: loadNextPage, event: { keypress: onLoadMoreKeyInput }" tabindex="0"
>Load more</a >Load more</a
> >
</div> </div>

View File

@@ -390,7 +390,7 @@
</table> </table>
<div class="loadMore"> <div class="loadMore">
<a role="link" data-bind="click: loadNextPage, event: { keypress: onLoadMoreKeyInput }" tabindex="0" <a role="button" data-bind="click: loadNextPage, event: { keypress: onLoadMoreKeyInput }" tabindex="0"
>Load more</a >Load more</a
> >
</div> </div>

View File

@@ -111,17 +111,18 @@
data-bind="visible: isMetricsToggled() && allResultsMetadata().length > 0 && errors().length === 0" data-bind="visible: isMetricsToggled() && allResultsMetadata().length > 0 && errors().length === 0"
> >
<table class="queryMetricsSummary"> <table class="queryMetricsSummary">
<caption>
Query Statistics
</caption>
<thead class="queryMetricsSummaryHead"> <thead class="queryMetricsSummaryHead">
<tr class="queryMetricsSummaryHeader queryMetricsSummaryTuple"> <tr class="queryMetricsSummaryHeader queryMetricsSummaryTuple">
<th title="METRIC">METRIC</th> <th title="METRIC" scope="col">METRIC</th>
<th></th> <th title="VALUE" scope="col">VALUE</th>
<th title="VALUE">VALUE</th>
</tr> </tr>
</thead> </thead>
<tbody class="queryMetricsSummaryBody" data-bind="with: aggregatedQueryMetrics"> <tbody class="queryMetricsSummaryBody" data-bind="with: aggregatedQueryMetrics">
<tr class="queryMetricsSummaryTuple"> <tr class="queryMetricsSummaryTuple">
<td title="Request Charge">Request Charge</td> <td title="Request Charge">Request Charge</td>
<td></td>
<td> <td>
<span <span
data-bind="text: $parent.requestChargeDisplayText, attr: { title: $parent.requestChargeDisplayText }" data-bind="text: $parent.requestChargeDisplayText, attr: { title: $parent.requestChargeDisplayText }"
@@ -130,7 +131,6 @@
</tr> </tr>
<tr class="queryMetricsSummaryTuple"> <tr class="queryMetricsSummaryTuple">
<td title="Showing Results">Showing Results</td> <td title="Showing Results">Showing Results</td>
<td></td>
<td> <td>
<span <span
data-bind="text: $parent.showingDocumentsDisplayText, attr: { title: $parent.showingDocumentsDisplayText }" data-bind="text: $parent.showingDocumentsDisplayText, attr: { title: $parent.showingDocumentsDisplayText }"
@@ -138,8 +138,8 @@
</td> </td>
</tr> </tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled"> <tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td><span title="Retrieved document count">Retrieved document count</span></td>
<td> <td>
<span title="Retrieved document count">Retrieved document count</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0"> <span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" /> <img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total number of retrieved documents</span> <span class="queryMetricTooltipText">Total number of retrieved documents</span>
@@ -148,8 +148,8 @@
<td><span data-bind="text: retrievedDocumentCount, attr: { title: retrievedDocumentCount }"></span></td> <td><span data-bind="text: retrievedDocumentCount, attr: { title: retrievedDocumentCount }"></span></td>
</tr> </tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled"> <tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td><span title="Retrieved document size">Retrieved document size</span></td>
<td> <td>
<span title="Retrieved document size">Retrieved document size</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0"> <span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" /> <img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total size of retrieved documents in bytes</span> <span class="queryMetricTooltipText">Total size of retrieved documents in bytes</span>
@@ -161,8 +161,8 @@
</td> </td>
</tr> </tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled"> <tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td><span title="Output document count">Output document count</span></td>
<td> <td>
<span title="Output document count">Output document count</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0"> <span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" /> <img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Number of output documents</span> <span class="queryMetricTooltipText">Number of output documents</span>
@@ -171,8 +171,8 @@
<td><span data-bind="text: outputDocumentCount, attr: { title: outputDocumentCount }"></span></td> <td><span data-bind="text: outputDocumentCount, attr: { title: outputDocumentCount }"></span></td>
</tr> </tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled"> <tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td><span title="Output document size">Output document size</span></td>
<td> <td>
<span title="Output document size">Output document size</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0"> <span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" /> <img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total size of output documents in bytes</span> <span class="queryMetricTooltipText">Total size of output documents in bytes</span>
@@ -184,8 +184,8 @@
</td> </td>
</tr> </tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled"> <tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td><span title="Index hit document count">Index hit document count</span></td>
<td> <td>
<span title="Index hit document count">Index hit document count</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0"> <span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" /> <img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total number of documents matched by the filter</span> <span class="queryMetricTooltipText">Total number of documents matched by the filter</span>
@@ -194,8 +194,8 @@
<td><span data-bind="text: indexHitDocumentCount, attr: { title: indexHitDocumentCount }"></span></td> <td><span data-bind="text: indexHitDocumentCount, attr: { title: indexHitDocumentCount }"></span></td>
</tr> </tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled"> <tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td><span title="Index lookup time">Index lookup time</span></td>
<td> <td>
<span title="Index lookup time">Index lookup time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0"> <span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" /> <img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Time spent in physical index layer</span> <span class="queryMetricTooltipText">Time spent in physical index layer</span>
@@ -206,8 +206,8 @@
</td> </td>
</tr> </tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled"> <tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td><span title="Document load time">Document load time</span></td>
<td> <td>
<span title="Document load time">Document load time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0"> <span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" /> <img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Time spent in loading documents</span> <span class="queryMetricTooltipText">Time spent in loading documents</span>
@@ -218,8 +218,8 @@
</td> </td>
</tr> </tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled"> <tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td><span title="Query engine execution time">Query engine execution time</span></td>
<td> <td>
<span title="Query engine execution time">Query engine execution time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0"> <span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" /> <img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText queryEngineExeTimeInfo" <span class="queryMetricTooltipText queryEngineExeTimeInfo"
@@ -236,8 +236,8 @@
</td> </td>
</tr> </tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled"> <tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td><span title="System function execution time">System function execution time</span></td>
<td> <td>
<span title="System function execution time">System function execution time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0"> <span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" /> <img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total time spent executing system (built-in) functions</span> <span class="queryMetricTooltipText">Total time spent executing system (built-in) functions</span>
@@ -251,8 +251,8 @@
</td> </td>
</tr> </tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled"> <tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td><span title="User defined function execution time">User defined function execution time</span></td>
<td> <td>
<span title="User defined function execution time">User defined function execution time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0"> <span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" /> <img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total time spent executing user-defined functions</span> <span class="queryMetricTooltipText">Total time spent executing user-defined functions</span>
@@ -266,8 +266,8 @@
</td> </td>
</tr> </tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled"> <tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td><span title="Document write time">Document write time</span></td>
<td> <td>
<span title="Document write time">Document write time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0"> <span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" /> <img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Time spent to write query result set to response buffer</span> <span class="queryMetricTooltipText">Time spent to write query result set to response buffer</span>
@@ -279,7 +279,6 @@
</tr> </tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.roundTrips() != null"> <tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.roundTrips() != null">
<td title="Round Trips">Round Trips</td> <td title="Round Trips">Round Trips</td>
<td></td>
<td><span data-bind="text: $parent.roundTrips, attr: { title: $parent.roundTrips }"></span></td> <td><span data-bind="text: $parent.roundTrips, attr: { title: $parent.roundTrips }"></span></td>
</tr> </tr>
<!-- TODO: Report activity id for mongo queries --> <!-- TODO: Report activity id for mongo queries -->

View File

@@ -195,6 +195,10 @@
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
caption {
width: 100px;
}
.queryMetricsSummaryHead { .queryMetricsSummaryHead {
.flex-display(); .flex-display();
} }
@@ -221,11 +225,7 @@
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
flex: 0 1 auto; flex: 0 0 50%;
}
&:nth-child(2) {
flex: 1 1 auto;
} }
&:nth-child(3) { &:nth-child(3) {

View File

@@ -17,7 +17,7 @@
<div class="error-bar"> <div class="error-bar">
<div class="error-message" aria-label="Error Message" data-bind="visible: hasQueryError"> <div class="error-message" aria-label="Error Message" data-bind="visible: hasQueryError">
<span><img class="entity-error-Img" src="/error_red.svg"/></span> <span><img class="entity-error-Img" src="/error_red.svg"/></span>
<span class="error-text" data-bind="text: queryErrorMessage"></span> <span class="error-text" role="alert" data-bind="text: queryErrorMessage"></span>
</div> </div>
</div> </div>
<!-- Tables Query Tab Errors - End--> <!-- Tables Query Tab Errors - End-->

View File

@@ -1,15 +1,14 @@
import * as Constants from "../../Common/Constants";
import * as ko from "knockout"; import * as ko from "knockout";
import * as monaco from "monaco-editor";
import Q from "q"; import Q from "q";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants";
import editable from "../../Common/EditableUtility";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import TabsBase from "./TabsBase";
import editable from "../../Common/EditableUtility";
import * as monaco from "monaco-editor";
import SaveIcon from "../../../images/save-cosmos.svg";
import DiscardIcon from "../../../images/discard.svg";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import TabsBase from "./TabsBase";
export default abstract class ScriptTabBase extends TabsBase implements ViewModels.WaitsForTemplate { export default abstract class ScriptTabBase extends TabsBase implements ViewModels.WaitsForTemplate {
public ariaLabel: ko.Observable<string>; public ariaLabel: ko.Observable<string>;
@@ -30,7 +29,8 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
public formIsValid: ko.Computed<boolean>; public formIsValid: ko.Computed<boolean>;
public formIsDirty: ko.Computed<boolean>; public formIsDirty: ko.Computed<boolean>;
public isNew: ko.Observable<boolean>; public isNew: ko.Observable<boolean>;
public resource: ko.Observable<DataModels.Script>; // TODO: Remove any. The SDK types for all the script.body are slightly incorrect which makes this REALLY hard to type correct.
public resource: ko.Observable<any>;
public isTemplateReady: ko.Observable<boolean>; public isTemplateReady: ko.Observable<boolean>;
protected _partitionKey: DataModels.PartitionKey; protected _partitionKey: DataModels.PartitionKey;
@@ -194,8 +194,8 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
}); });
} }
public abstract onSaveClick: () => Q.Promise<any>; public abstract onSaveClick: () => Promise<any>;
public abstract onUpdateClick: () => Q.Promise<any>; public abstract onUpdateClick: () => Promise<any>;
public onDiscard = (): Q.Promise<any> => { public onDiscard = (): Q.Promise<any> => {
this.setBaselines(); this.setBaselines();
@@ -206,14 +206,14 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
return Q(); return Q();
}; };
public onSaveOrUpdateClick(): Q.Promise<any> { public onSaveOrUpdateClick(): Promise<any> {
if (this.saveButton.visible()) { if (this.saveButton.visible()) {
return this.onSaveClick(); return this.onSaveClick();
} else if (this.updateButton.visible()) { } else if (this.updateButton.visible()) {
return this.onUpdateClick(); return this.onUpdateClick();
} }
return Q(); return undefined;
} }
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {

View File

@@ -346,7 +346,6 @@ describe("Settings tab", () => {
const offer: DataModels.Offer = null; const offer: DataModels.Offer = null;
const defaultTtl = 200; const defaultTtl = 200;
const database = new Database(explorer, baseDatabase, null);
const conflictResolutionPolicy = { const conflictResolutionPolicy = {
mode: DataModels.ConflictResolutionMode.LastWriterWins, mode: DataModels.ConflictResolutionMode.LastWriterWins,
conflictResolutionPath: "/_ts" conflictResolutionPath: "/_ts"
@@ -507,7 +506,6 @@ describe("Settings tab", () => {
} }
} }
}; };
const database = new Database(explorer, baseDatabase, null);
const container: DataModels.Collection = { const container: DataModels.Collection = {
_rid: "_rid", _rid: "_rid",
_self: "", _self: "",

View File

@@ -1270,8 +1270,10 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
} }
public onActivate(): Q.Promise<any> { public onActivate(): Q.Promise<any> {
return super.onActivate().then(() => { return super.onActivate().then(async () => {
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings); this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
const database: ViewModels.Database = this.collection.getDatabase();
await database.loadOffer();
}); });
} }

View File

@@ -1,17 +1,18 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import * as _ from "underscore";
import Q from "q"; import Q from "q";
import * as _ from "underscore";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import { createStoredProcedure } from "../../Common/dataAccess/createStoredProcedure";
import { updateStoredProcedure } from "../../Common/dataAccess/updateStoredProcedure";
import editable from "../../Common/EditableUtility";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import editable from "../../Common/EditableUtility";
import ScriptTabBase from "./ScriptTabBase";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import StoredProcedure from "../Tree/StoredProcedure";
import { createStoredProcedure, updateStoredProcedure } from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import StoredProcedure from "../Tree/StoredProcedure";
import ScriptTabBase from "./ScriptTabBase";
enum ToggleState { enum ToggleState {
Result = "result", Result = "result",
@@ -24,7 +25,6 @@ export default class StoredProcedureTab extends ScriptTabBase {
public executeResultsEditorId: string; public executeResultsEditorId: string;
public executeLogsEditorId: string; public executeLogsEditorId: string;
public toggleState: ko.Observable<ToggleState>; public toggleState: ko.Observable<ToggleState>;
public originalSprocBody: ViewModels.Editable<string>; public originalSprocBody: ViewModels.Editable<string>;
public resultsData: ko.Observable<string>; public resultsData: ko.Observable<string>;
public logsData: ko.Observable<string>; public logsData: ko.Observable<string>;
@@ -54,13 +54,11 @@ export default class StoredProcedureTab extends ScriptTabBase {
this.buildCommandBarOptions(); this.buildCommandBarOptions();
} }
public onSaveClick = (): Q.Promise<DataModels.StoredProcedure> => { public onSaveClick = (): Promise<StoredProcedureDefinition & Resource> => {
const resource: DataModels.StoredProcedure = <DataModels.StoredProcedure>{ return this._createStoredProcedure({
id: this.id(), id: this.id(),
body: this.editorContent() body: this.editorContent()
}; });
return this._createStoredProcedure(resource);
}; };
public onDiscard = (): Q.Promise<any> => { public onDiscard = (): Q.Promise<any> => {
@@ -72,8 +70,8 @@ export default class StoredProcedureTab extends ScriptTabBase {
return Q(); return Q();
}; };
public onUpdateClick = (): Q.Promise<any> => { public onUpdateClick = (): Promise<any> => {
const data: DataModels.StoredProcedure = this._getResource(); const data = this._getResource();
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
@@ -83,18 +81,18 @@ export default class StoredProcedureTab extends ScriptTabBase {
dataExplorerArea: Constants.Areas.Tab, dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle() tabTitle: this.tabTitle()
}); });
return updateStoredProcedure(this.collection, data) return updateStoredProcedure(this.collection.databaseId, this.collection.id(), data)
.then( .then(
(updatedResource: DataModels.StoredProcedure) => { updatedResource => {
this.resource(updatedResource); this.resource(updatedResource);
this.tabTitle(updatedResource.id); this.tabTitle(updatedResource.id);
this.node.id(updatedResource.id); this.node.id(updatedResource.id);
this.node.body(updatedResource.body); this.node.body(updatedResource.body as string);
this.setBaselines(); this.setBaselines();
const editorModel = this.editor() && this.editor().getModel(); const editorModel = this.editor() && this.editor().getModel();
editorModel && editorModel.setValue(updatedResource.body); editorModel && editorModel.setValue(updatedResource.body as string);
this.editorContent.setBaseline(updatedResource.body); this.editorContent.setBaseline(updatedResource.body as string);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.UpdateStoredProcedure, Action.UpdateStoredProcedure,
{ {
@@ -220,18 +218,14 @@ export default class StoredProcedureTab extends ScriptTabBase {
}); });
} }
private _getResource(): DataModels.StoredProcedure { private _getResource() {
const resource: DataModels.StoredProcedure = <DataModels.StoredProcedure>{ return {
_rid: this.resource()._rid,
_self: this.resource()._self,
id: this.id(), id: this.id(),
body: this.editorContent() body: this.editorContent()
}; };
return resource;
} }
private _createStoredProcedure(resource: DataModels.StoredProcedure): Q.Promise<DataModels.StoredProcedure> { private _createStoredProcedure(resource: StoredProcedureDefinition): Promise<StoredProcedureDefinition & Resource> {
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateStoredProcedure, { const startKey: number = TelemetryProcessor.traceStart(Action.CreateStoredProcedure, {
@@ -241,7 +235,7 @@ export default class StoredProcedureTab extends ScriptTabBase {
tabTitle: this.tabTitle() tabTitle: this.tabTitle()
}); });
return createStoredProcedure(this.collection, resource) return createStoredProcedure(this.collection.databaseId, this.collection.id(), resource)
.then( .then(
createdResource => { createdResource => {
this.tabTitle(createdResource.id); this.tabTitle(createdResource.id);
@@ -256,8 +250,8 @@ export default class StoredProcedureTab extends ScriptTabBase {
this.setBaselines(); this.setBaselines();
const editorModel = this.editor() && this.editor().getModel(); const editorModel = this.editor() && this.editor().getModel();
editorModel && editorModel.setValue(createdResource.body); editorModel && editorModel.setValue(createdResource.body as string);
this.editorContent.setBaseline(createdResource.body); this.editorContent.setBaseline(createdResource.body as string);
this.node = this.collection.createStoredProcedureNode(createdResource); this.node = this.collection.createStoredProcedureNode(createdResource);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.CreateStoredProcedure, Action.CreateStoredProcedure,
@@ -284,7 +278,7 @@ export default class StoredProcedureTab extends ScriptTabBase {
}, },
startKey startKey
); );
return Q.reject(createError); return Promise.reject(createError);
} }
) )
.finally(() => this.isExecuting(false)); .finally(() => this.isExecuting(false));

View File

@@ -1,13 +1,13 @@
import Q from "q"; import { Resource, TriggerDefinition, TriggerOperation, TriggerType } from "@azure/cosmos";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import { createTrigger } from "../../Common/dataAccess/createTrigger";
import { updateTrigger } from "../../Common/dataAccess/updateTrigger";
import editable from "../../Common/EditableUtility";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import ScriptTabBase from "./ScriptTabBase";
import editable from "../../Common/EditableUtility";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Trigger from "../Tree/Trigger"; import Trigger from "../Tree/Trigger";
import { createTrigger, updateTrigger } from "../../Common/DocumentClientUtilityBase"; import ScriptTabBase from "./ScriptTabBase";
export default class TriggerTab extends ScriptTabBase { export default class TriggerTab extends ScriptTabBase {
public collection: ViewModels.Collection; public collection: ViewModels.Collection;
@@ -27,13 +27,17 @@ export default class TriggerTab extends ScriptTabBase {
super.buildCommandBarOptions(); super.buildCommandBarOptions();
} }
public onSaveClick = (): Q.Promise<DataModels.Trigger> => { public onSaveClick = (): Promise<TriggerDefinition & Resource> => {
const data: DataModels.Trigger = this._getResource(); return this._createTrigger({
return this._createTrigger(data); id: this.id(),
body: this.editorContent(),
triggerOperation: this.triggerOperation() as TriggerOperation,
triggerType: this.triggerType() as TriggerType
});
}; };
public onUpdateClick = (): Q.Promise<any> => { public onUpdateClick = (): Promise<any> => {
const data: DataModels.Trigger = this._getResource(); const data = this._getResource();
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateTrigger, { const startKey: number = TelemetryProcessor.traceStart(Action.UpdateTrigger, {
@@ -42,14 +46,19 @@ export default class TriggerTab extends ScriptTabBase {
tabTitle: this.tabTitle() tabTitle: this.tabTitle()
}); });
return updateTrigger(this.collection, data) return updateTrigger(this.collection.databaseId, this.collection.id(), {
id: this.id(),
body: this.editorContent(),
triggerOperation: this.triggerOperation() as TriggerOperation,
triggerType: this.triggerType() as TriggerType
})
.then( .then(
(createdResource: DataModels.Trigger) => { createdResource => {
this.resource(createdResource); this.resource(createdResource);
this.tabTitle(createdResource.id); this.tabTitle(createdResource.id);
this.node.id(createdResource.id); this.node.id(createdResource.id);
this.node.body(createdResource.body); this.node.body(createdResource.body as string);
this.node.triggerType(createdResource.triggerOperation); this.node.triggerType(createdResource.triggerOperation);
this.node.triggerOperation(createdResource.triggerOperation); this.node.triggerOperation(createdResource.triggerOperation);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
@@ -66,8 +75,8 @@ export default class TriggerTab extends ScriptTabBase {
this.setBaselines(); this.setBaselines();
const editorModel = this.editor().getModel(); const editorModel = this.editor().getModel();
editorModel.setValue(createdResource.body); editorModel.setValue(createdResource.body as string);
this.editorContent.setBaseline(createdResource.body); this.editorContent.setBaseline(createdResource.body as string);
}, },
(createError: any) => { (createError: any) => {
this.isExecutionError(true); this.isExecutionError(true);
@@ -89,7 +98,7 @@ export default class TriggerTab extends ScriptTabBase {
public setBaselines() { public setBaselines() {
super.setBaselines(); super.setBaselines();
const resource = <DataModels.Trigger>this.resource(); const resource = this.resource();
this.triggerOperation.setBaseline(resource.triggerOperation); this.triggerOperation.setBaseline(resource.triggerOperation);
this.triggerType.setBaseline(resource.triggerType); this.triggerType.setBaseline(resource.triggerType);
} }
@@ -109,7 +118,7 @@ export default class TriggerTab extends ScriptTabBase {
} }
} }
private _createTrigger(resource: DataModels.Trigger): Q.Promise<DataModels.Trigger> { private _createTrigger(resource: TriggerDefinition): Promise<TriggerDefinition & Resource> {
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateTrigger, { const startKey: number = TelemetryProcessor.traceStart(Action.CreateTrigger, {
@@ -119,9 +128,9 @@ export default class TriggerTab extends ScriptTabBase {
tabTitle: this.tabTitle() tabTitle: this.tabTitle()
}); });
return createTrigger(this.collection, resource) return createTrigger(this.collection.databaseId, this.collection.id(), resource)
.then( .then(
(createdResource: DataModels.Trigger) => { createdResource => {
this.tabTitle(createdResource.id); this.tabTitle(createdResource.id);
this.isNew(false); this.isNew(false);
this.resource(createdResource); this.resource(createdResource);
@@ -134,8 +143,8 @@ export default class TriggerTab extends ScriptTabBase {
this.setBaselines(); this.setBaselines();
const editorModel = this.editor().getModel(); const editorModel = this.editor().getModel();
editorModel.setValue(createdResource.body); editorModel.setValue(createdResource.body as string);
this.editorContent.setBaseline(createdResource.body); this.editorContent.setBaseline(createdResource.body as string);
this.node = this.collection.createTriggerNode(createdResource); this.node = this.collection.createTriggerNode(createdResource);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
@@ -163,22 +172,18 @@ export default class TriggerTab extends ScriptTabBase {
}, },
startKey startKey
); );
return Q.reject(createError); return Promise.reject(createError);
} }
) )
.finally(() => this.isExecuting(false)); .finally(() => this.isExecuting(false));
} }
private _getResource(): DataModels.Trigger { private _getResource() {
const resource: DataModels.Trigger = <DataModels.Trigger>{ return {
_rid: this.resource()._rid,
_self: this.resource()._self,
id: this.id(), id: this.id(),
body: this.editorContent(), body: this.editorContent(),
triggerOperation: this.triggerOperation(), triggerOperation: this.triggerOperation(),
triggerType: this.triggerType() triggerType: this.triggerType()
}; };
return resource;
} }
} }

View File

@@ -1,12 +1,12 @@
import Q from "q"; import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import ScriptTabBase from "./ScriptTabBase";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import UserDefinedFunction from "../Tree/UserDefinedFunction"; import UserDefinedFunction from "../Tree/UserDefinedFunction";
import { createUserDefinedFunction, updateUserDefinedFunction } from "../../Common/DocumentClientUtilityBase"; import ScriptTabBase from "./ScriptTabBase";
export default class UserDefinedFunctionTab extends ScriptTabBase { export default class UserDefinedFunctionTab extends ScriptTabBase {
public collection: ViewModels.Collection; public collection: ViewModels.Collection;
@@ -19,13 +19,13 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
super.buildCommandBarOptions(); super.buildCommandBarOptions();
} }
public onSaveClick = (): Q.Promise<DataModels.UserDefinedFunction> => { public onSaveClick = (): Promise<UserDefinedFunctionDefinition & Resource> => {
const data: DataModels.UserDefinedFunction = this._getResource(); const data = this._getResource();
return this._createUserDefinedFunction(data); return this._createUserDefinedFunction(data);
}; };
public onUpdateClick = (): Q.Promise<any> => { public onUpdateClick = (): Promise<any> => {
const data: DataModels.UserDefinedFunction = this._getResource(); const data = this._getResource();
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateUDF, { const startKey: number = TelemetryProcessor.traceStart(Action.UpdateUDF, {
@@ -35,14 +35,14 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
tabTitle: this.tabTitle() tabTitle: this.tabTitle()
}); });
return updateUserDefinedFunction(this.collection, data) return updateUserDefinedFunction(this.collection.databaseId, this.collection.id(), data)
.then( .then(
(createdResource: DataModels.UserDefinedFunction) => { createdResource => {
this.resource(createdResource); this.resource(createdResource);
this.tabTitle(createdResource.id); this.tabTitle(createdResource.id);
this.node.id(createdResource.id); this.node.id(createdResource.id);
this.node.body(createdResource.body); this.node.body(createdResource.body as string);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.UpdateUDF, Action.UpdateUDF,
{ {
@@ -57,8 +57,8 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
this.setBaselines(); this.setBaselines();
const editorModel = this.editor().getModel(); const editorModel = this.editor().getModel();
editorModel.setValue(createdResource.body); editorModel.setValue(createdResource.body as string);
this.editorContent.setBaseline(createdResource.body); this.editorContent.setBaseline(createdResource.body as string);
}, },
(createError: any) => { (createError: any) => {
this.isExecutionError(true); this.isExecutionError(true);
@@ -93,8 +93,8 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
} }
private _createUserDefinedFunction( private _createUserDefinedFunction(
resource: DataModels.UserDefinedFunction resource: UserDefinedFunctionDefinition
): Q.Promise<DataModels.UserDefinedFunction> { ): Promise<UserDefinedFunctionDefinition & Resource> {
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateUDF, { const startKey: number = TelemetryProcessor.traceStart(Action.CreateUDF, {
@@ -104,9 +104,9 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
tabTitle: this.tabTitle() tabTitle: this.tabTitle()
}); });
return createUserDefinedFunction(this.collection, resource) return createUserDefinedFunction(this.collection.databaseId, this.collection.id(), resource)
.then( .then(
(createdResource: DataModels.UserDefinedFunction) => { createdResource => {
this.tabTitle(createdResource.id); this.tabTitle(createdResource.id);
this.isNew(false); this.isNew(false);
this.resource(createdResource); this.resource(createdResource);
@@ -118,8 +118,8 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
this.setBaselines(); this.setBaselines();
const editorModel = this.editor().getModel(); const editorModel = this.editor().getModel();
editorModel.setValue(createdResource.body); editorModel.setValue(createdResource.body as string);
this.editorContent.setBaseline(createdResource.body); this.editorContent.setBaseline(createdResource.body as string);
this.node = this.collection.createUserDefinedFunctionNode(createdResource); this.node = this.collection.createUserDefinedFunctionNode(createdResource);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
@@ -147,14 +147,14 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
}, },
startKey startKey
); );
return Q.reject(createError); return Promise.reject(createError);
} }
) )
.finally(() => this.isExecuting(false)); .finally(() => this.isExecuting(false));
} }
private _getResource() { private _getResource() {
const resource: DataModels.UserDefinedFunction = <DataModels.UserDefinedFunction>{ const resource = {
_rid: this.resource()._rid, _rid: this.resource()._rid,
_self: this.resource()._self, _self: this.resource()._self,
id: this.id(), id: this.id(),

View File

@@ -1,23 +1,28 @@
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q"; import Q from "q";
import * as _ from "underscore"; import * as _ from "underscore";
import UploadWorker from "worker-loader!../../workers/upload"; import UploadWorker from "worker-loader!../../workers/upload";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures";
import { readTriggers } from "../../Common/dataAccess/readTriggers";
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
import { createDocument, readCollectionQuotaInfo, readOffer, readOffers } from "../../Common/DocumentClientUtilityBase";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { PlatformType } from "../../PlatformType"; import { PlatformType } from "../../PlatformType";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { OfferUtils } from "../../Utils/OfferUtils"; import { OfferUtils } from "../../Utils/OfferUtils";
import { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions"; import { StartUploadMessageParams, UploadDetails, UploadDetailsRecord } from "../../workers/upload/definitions";
import Explorer from "../Explorer";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient"; import { CassandraAPIDataClient, CassandraTableKey, CassandraTableKeys } from "../Tables/TableDataClient";
import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId";
import ConflictsTab from "../Tabs/ConflictsTab"; import ConflictsTab from "../Tabs/ConflictsTab";
import DocumentsTab from "../Tabs/DocumentsTab"; import DocumentsTab from "../Tabs/DocumentsTab";
import GraphTab from "../Tabs/GraphTab"; import GraphTab from "../Tabs/GraphTab";
@@ -27,21 +32,11 @@ import MongoShellTab from "../Tabs/MongoShellTab";
import QueryTab from "../Tabs/QueryTab"; import QueryTab from "../Tabs/QueryTab";
import QueryTablesTab from "../Tabs/QueryTablesTab"; import QueryTablesTab from "../Tabs/QueryTablesTab";
import SettingsTab from "../Tabs/SettingsTab"; import SettingsTab from "../Tabs/SettingsTab";
import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId";
import StoredProcedure from "./StoredProcedure"; import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger"; import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction"; import UserDefinedFunction from "./UserDefinedFunction";
import { configContext } from "../../ConfigContext";
import Explorer from "../Explorer";
import {
createDocument,
readTriggers,
readUserDefinedFunctions,
readStoredProcedures,
readCollectionQuotaInfo,
readOffer,
readOffers
} from "../../Common/DocumentClientUtilityBase";
import { userContext } from "../../UserContext";
export default class Collection implements ViewModels.Collection { export default class Collection implements ViewModels.Collection {
public nodeKind: string; public nodeKind: string;
@@ -854,21 +849,21 @@ export default class Collection implements ViewModels.Collection {
Trigger.create(source, event); Trigger.create(source, event);
} }
public createStoredProcedureNode(data: DataModels.StoredProcedure): StoredProcedure { public createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure {
const node = new StoredProcedure(this.container, this, data); const node = new StoredProcedure(this.container, this, data);
this.container.selectedNode(node); this.container.selectedNode(node);
this.children.push(node); this.children.push(node);
return node; return node;
} }
public createUserDefinedFunctionNode(data: DataModels.UserDefinedFunction): UserDefinedFunction { public createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction {
const node = new UserDefinedFunction(this.container, this, data); const node = new UserDefinedFunction(this.container, this, data);
this.container.selectedNode(node); this.container.selectedNode(node);
this.children.push(node); this.children.push(node);
return node; return node;
} }
public createTriggerNode(data: DataModels.Trigger): Trigger { public createTriggerNode(data: TriggerDefinition & Resource): Trigger {
const node = new Trigger(this.container, this, data); const node = new Trigger(this.container, this, data);
this.container.selectedNode(node); this.container.selectedNode(node);
this.children.push(node); this.children.push(node);
@@ -1062,8 +1057,8 @@ export default class Collection implements ViewModels.Collection {
}); });
} }
public loadStoredProcedures(): Q.Promise<any> { public loadStoredProcedures(): Promise<any> {
return readStoredProcedures(this).then((storedProcedures: DataModels.StoredProcedure[]) => { return readStoredProcedures(this.databaseId, this.id()).then(storedProcedures => {
const storedProceduresNodes: ViewModels.TreeNode[] = storedProcedures.map( const storedProceduresNodes: ViewModels.TreeNode[] = storedProcedures.map(
storedProcedure => new StoredProcedure(this.container, this, storedProcedure) storedProcedure => new StoredProcedure(this.container, this, storedProcedure)
); );
@@ -1073,8 +1068,8 @@ export default class Collection implements ViewModels.Collection {
}); });
} }
public loadUserDefinedFunctions(): Q.Promise<any> { public loadUserDefinedFunctions(): Promise<any> {
return readUserDefinedFunctions(this).then((userDefinedFunctions: DataModels.UserDefinedFunction[]) => { return readUserDefinedFunctions(this.databaseId, this.id()).then(userDefinedFunctions => {
const userDefinedFunctionsNodes: ViewModels.TreeNode[] = userDefinedFunctions.map( const userDefinedFunctionsNodes: ViewModels.TreeNode[] = userDefinedFunctions.map(
udf => new UserDefinedFunction(this.container, this, udf) udf => new UserDefinedFunction(this.container, this, udf)
); );
@@ -1084,8 +1079,8 @@ export default class Collection implements ViewModels.Collection {
}); });
} }
public loadTriggers(): Q.Promise<any> { public loadTriggers(): Promise<any> {
return readTriggers(this, null /*options*/).then((triggers: DataModels.Trigger[]) => { return readTriggers(this.databaseId, this.id()).then(triggers => {
const triggerNodes: ViewModels.TreeNode[] = triggers.map(trigger => new Trigger(this.container, this, trigger)); const triggerNodes: ViewModels.TreeNode[] = triggers.map(trigger => new Trigger(this.container, this, trigger));
const otherNodes = this.children().filter(node => node.nodeKind !== "Trigger"); const otherNodes = this.children().filter(node => node.nodeKind !== "Trigger");
const allNodes = otherNodes.concat(triggerNodes); const allNodes = otherNodes.concat(triggerNodes);

View File

@@ -12,8 +12,8 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { readOffers, readOffer } from "../../Common/DocumentClientUtilityBase";
import { readCollections } from "../../Common/dataAccess/readCollections"; import { readCollections } from "../../Common/dataAccess/readCollections";
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
export default class Database implements ViewModels.Database { export default class Database implements ViewModels.Database {
public nodeKind: string; public nodeKind: string;
@@ -27,13 +27,13 @@ export default class Database implements ViewModels.Database {
public isDatabaseShared: ko.Computed<boolean>; public isDatabaseShared: ko.Computed<boolean>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>; public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
constructor(container: Explorer, data: any, offer: DataModels.Offer) { constructor(container: Explorer, data: any) {
this.nodeKind = "Database"; this.nodeKind = "Database";
this.container = container; this.container = container;
this.self = data._self; this.self = data._self;
this.rid = data._rid; this.rid = data._rid;
this.id = ko.observable(data.id); this.id = ko.observable(data.id);
this.offer = ko.observable(offer); this.offer = ko.observable();
this.collections = ko.observableArray<Collection>(); this.collections = ko.observableArray<Collection>();
this.isDatabaseExpanded = ko.observable<boolean>(false); this.isDatabaseExpanded = ko.observable<boolean>(false);
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>(); this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
@@ -66,7 +66,7 @@ export default class Database implements ViewModels.Database {
dataExplorerArea: Constants.Areas.Tab, dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Scale" tabTitle: "Scale"
}); });
Q.all([pendingNotificationsPromise, this.readSettings()]).then( pendingNotificationsPromise.then(
(data: any) => { (data: any) => {
const pendingNotification: DataModels.Notification = data && data[0]; const pendingNotification: DataModels.Notification = data && data[0];
settingsTab = new DatabaseSettingsTab({ settingsTab = new DatabaseSettingsTab({
@@ -121,80 +121,6 @@ export default class Database implements ViewModels.Database {
} }
}; };
public readSettings(): Q.Promise<void> {
const deferred: Q.Deferred<void> = Q.defer<void>();
this.container.isRefreshingExplorer(true);
const databaseDataModel: DataModels.Database = <DataModels.Database>{
id: this.id(),
_rid: this.rid,
_self: this.self
};
const startKey: number = TelemetryProcessor.traceStart(Action.LoadOffers, {
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience()
});
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers({
isServerless: this.container.isServerlessEnabled()
});
Q.all([offerInfoPromise]).then(
() => {
this.container.isRefreshingExplorer(false);
const databaseOffer: DataModels.Offer = this._getOfferForDatabase(
offerInfoPromise.valueOf(),
databaseDataModel
);
if (!databaseOffer) {
return;
}
readOffer(databaseOffer).then((offerDetail: DataModels.OfferWithHeaders) => {
const offerThroughputInfo: DataModels.OfferThroughputInfo = {
minimumRUForCollection:
offerDetail.content &&
offerDetail.content.collectionThroughputInfo &&
offerDetail.content.collectionThroughputInfo.minimumRUForCollection,
numPhysicalPartitions:
offerDetail.content &&
offerDetail.content.collectionThroughputInfo &&
offerDetail.content.collectionThroughputInfo.numPhysicalPartitions
};
databaseOffer.content.collectionThroughputInfo = offerThroughputInfo;
(databaseOffer as DataModels.OfferWithHeaders).headers = offerDetail.headers;
this.offer(databaseOffer);
this.offer.valueHasMutated();
TelemetryProcessor.traceSuccess(
Action.LoadOffers,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience()
},
startKey
);
deferred.resolve();
});
},
(error: any) => {
this.container.isRefreshingExplorer(false);
deferred.reject(error);
TelemetryProcessor.traceFailure(
Action.LoadOffers,
{
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience()
},
startKey
);
}
);
return deferred.promise;
}
public isDatabaseNodeSelected(): boolean { public isDatabaseNodeSelected(): boolean {
return ( return (
!this.isDatabaseExpanded() && !this.isDatabaseExpanded() &&
@@ -219,23 +145,13 @@ export default class Database implements ViewModels.Database {
}); });
} }
public expandCollapseDatabase() { public async expandDatabase() {
this.selectDatabase();
if (this.isDatabaseExpanded()) {
this.collapseDatabase();
} else {
this.expandDatabase();
}
this.container.onUpdateTabsButtons([]);
this.container.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === this.rid);
}
public expandDatabase() {
if (this.isDatabaseExpanded()) { if (this.isDatabaseExpanded()) {
return; return;
} }
this.loadCollections(); await this.loadOffer();
await this.loadCollections();
this.isDatabaseExpanded(true); this.isDatabaseExpanded(true);
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
description: "Database node", description: "Database node",
@@ -259,14 +175,10 @@ export default class Database implements ViewModels.Database {
}); });
} }
public loadCollections(): Q.Promise<void> { public async loadCollections(): Promise<void> {
let collectionVMs: Collection[] = []; const collectionVMs: Collection[] = [];
let deferred: Q.Deferred<void> = Q.defer<void>(); const collections: DataModels.Collection[] = await readCollections(this.id());
const deltaCollections = this.getDeltaCollections(collections);
readCollections(this.id()).then(
(collections: DataModels.Collection[]) => {
let collectionsToAddVMPromises: Q.Promise<any>[] = [];
let deltaCollections = this.getDeltaCollections(collections);
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => { deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null); const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
@@ -276,15 +188,6 @@ export default class Database implements ViewModels.Database {
//merge collections //merge collections
this.addCollectionsToList(collectionVMs); this.addCollectionsToList(collectionVMs);
this.deleteCollectionsFromList(deltaCollections.toDelete); this.deleteCollectionsFromList(deltaCollections.toDelete);
deferred.resolve();
},
(error: any) => {
deferred.reject(error);
}
);
return deferred.promise;
} }
public openAddCollection(database: Database, event: MouseEvent) { public openAddCollection(database: Database, event: MouseEvent) {
@@ -296,6 +199,17 @@ export default class Database implements ViewModels.Database {
return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId); return _.find(this.collections(), (collection: ViewModels.Collection) => collection.id() === collectionId);
} }
public async loadOffer(): Promise<void> {
if (!this.offer()) {
const params: DataModels.ReadDatabaseOfferParams = {
databaseId: this.id(),
databaseResourceId: this.self,
isServerless: this.container.isServerlessEnabled()
};
this.offer(await readDatabaseOffer(params));
}
}
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> { private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
if (!this.container) { if (!this.container) {
return Q.resolve(undefined); return Q.resolve(undefined);
@@ -387,8 +301,4 @@ export default class Database implements ViewModels.Database {
this.collections(collectionsToKeep); this.collections(collectionsToKeep);
} }
private _getOfferForDatabase(offers: DataModels.Offer[], database: DataModels.Database): DataModels.Offer {
return _.find(offers, (offer: DataModels.Offer) => offer.resource === database._self);
}
} }

View File

@@ -170,14 +170,17 @@ export class ResourceTreeAdapter implements ReactAdapter {
children: [], children: [],
isSelected: () => this.isDataNodeSelected(database.rid, "Database", undefined), isSelected: () => this.isDataNodeSelected(database.rid, "Database", undefined),
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database), contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database),
onClick: isExpanded => { onClick: async isExpanded => {
// Rewritten version of expandCollapseDatabase(): // Rewritten version of expandCollapseDatabase():
if (!isExpanded) { if (isExpanded) {
database.expandDatabase();
database.loadCollections();
} else {
database.collapseDatabase(); database.collapseDatabase();
} else {
if (databaseNode.children?.length === 0) {
databaseNode.isLoading = true;
} }
await database.expandDatabase();
}
databaseNode.isLoading = false;
database.selectDatabase(); database.selectDatabase();
this.container.onUpdateTabsButtons([]); this.container.onUpdateTabsButtons([]);
this.container.tabsManager.refreshActiveTab( this.container.tabsManager.refreshActiveTab(
@@ -203,6 +206,12 @@ export class ResourceTreeAdapter implements ReactAdapter {
databaseNode.children.push(this.buildCollectionNode(database, collection)) databaseNode.children.push(this.buildCollectionNode(database, collection))
); );
database.collections.subscribe((collections: ViewModels.Collection[]) => {
collections.forEach((collection: ViewModels.Collection) =>
databaseNode.children.push(this.buildCollectionNode(database, collection))
);
});
return databaseNode; return databaseNode;
}); });

View File

@@ -1,13 +1,13 @@
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure";
import { executeStoredProcedure } from "../../Common/DocumentClientUtilityBase";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import StoredProcedureTab from "../Tabs/StoredProcedureTab";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { deleteStoredProcedure, executeStoredProcedure } from "../../Common/DocumentClientUtilityBase"; import StoredProcedureTab from "../Tabs/StoredProcedureTab";
import TabsBase from "../Tabs/TabsBase"; import TabsBase from "../Tabs/TabsBase";
const sampleStoredProcedureBody: string = `// SAMPLE STORED PROCEDURE const sampleStoredProcedureBody: string = `// SAMPLE STORED PROCEDURE
@@ -47,20 +47,20 @@ export default class StoredProcedure {
public body: ko.Observable<string>; public body: ko.Observable<string>;
public isExecuteEnabled: boolean; public isExecuteEnabled: boolean;
constructor(container: Explorer, collection: ViewModels.Collection, data: DataModels.StoredProcedure) { constructor(container: Explorer, collection: ViewModels.Collection, data: StoredProcedureDefinition & Resource) {
this.nodeKind = "StoredProcedure"; this.nodeKind = "StoredProcedure";
this.container = container; this.container = container;
this.collection = collection; this.collection = collection;
this.self = data._self; this.self = data._self;
this.rid = data._rid; this.rid = data._rid;
this.id = ko.observable(data.id); this.id = ko.observable(data.id);
this.body = ko.observable(data.body); this.body = ko.observable(data.body as string);
this.isExecuteEnabled = this.container.isFeatureEnabled(Constants.Features.executeSproc); this.isExecuteEnabled = this.container.isFeatureEnabled(Constants.Features.executeSproc);
} }
public static create(source: ViewModels.Collection, event: MouseEvent) { public static create(source: ViewModels.Collection, event: MouseEvent) {
const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.StoredProcedures).length + 1; const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.StoredProcedures).length + 1;
const storedProcedure = <DataModels.StoredProcedure>{ const storedProcedure = <StoredProcedureDefinition>{
id: "", id: "",
body: sampleStoredProcedureBody body: sampleStoredProcedureBody
}; };
@@ -104,7 +104,7 @@ export default class StoredProcedure {
if (storedProcedureTab) { if (storedProcedureTab) {
this.container.tabsManager.activateTab(storedProcedureTab); this.container.tabsManager.activateTab(storedProcedureTab);
} else { } else {
const storedProcedureData = <DataModels.StoredProcedure>{ const storedProcedureData = <StoredProcedureDefinition>{
_rid: this.rid, _rid: this.rid,
_self: this.self, _self: this.self,
id: this.id(), id: this.id(),
@@ -137,14 +137,7 @@ export default class StoredProcedure {
return; return;
} }
const storedProcedureData = <DataModels.StoredProcedure>{ deleteStoredProcedure(this.collection.databaseId, this.collection.id(), this.id()).then(
_rid: this.rid,
_self: this.self,
id: this.id(),
body: this.body()
};
deleteStoredProcedure(this.collection, storedProcedureData).then(
() => { () => {
this.container.tabsManager.removeTabByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid); this.container.tabsManager.removeTabByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this); this.collection.children.remove(this);

View File

@@ -1,12 +1,12 @@
import { StoredProcedureDefinition } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import { deleteTrigger } from "../../Common/dataAccess/deleteTrigger";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import TriggerTab from "../Tabs/TriggerTab";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { deleteTrigger } from "../../Common/DocumentClientUtilityBase"; import TriggerTab from "../Tabs/TriggerTab";
export default class Trigger { export default class Trigger {
public nodeKind: string; public nodeKind: string;
@@ -43,7 +43,7 @@ export default class Trigger {
public static create(source: ViewModels.Collection, event: MouseEvent) { public static create(source: ViewModels.Collection, event: MouseEvent) {
const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Triggers).length + 1; const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Triggers).length + 1;
const trigger = <DataModels.Trigger>{ const trigger = <StoredProcedureDefinition>{
id: "", id: "",
body: "function trigger(){}", body: "function trigger(){}",
triggerOperation: "All", triggerOperation: "All",
@@ -79,7 +79,7 @@ export default class Trigger {
if (triggerTab) { if (triggerTab) {
this.container.tabsManager.activateTab(triggerTab); this.container.tabsManager.activateTab(triggerTab);
} else { } else {
const triggerData = <DataModels.Trigger>{ const triggerData = <StoredProcedureDefinition>{
_rid: this.rid, _rid: this.rid,
_self: this.self, _self: this.self,
id: this.id(), id: this.id(),
@@ -114,16 +114,7 @@ export default class Trigger {
return; return;
} }
const triggerData = <DataModels.Trigger>{ deleteTrigger(this.collection.databaseId, this.collection.id(), this.id()).then(
_rid: this.rid,
_self: this.self,
id: this.id(),
body: this.body(),
triggerOperation: this.triggerOperation(),
triggerType: this.triggerType()
};
deleteTrigger(this.collection, triggerData).then(
() => { () => {
this.container.tabsManager.removeTabByComparator(tab => tab.node && tab.node.rid === this.rid); this.container.tabsManager.removeTabByComparator(tab => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this); this.collection.children.remove(this);

View File

@@ -1,12 +1,12 @@
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels"; import { deleteUserDefinedFunction } from "../../Common/dataAccess/deleteUserDefinedFunction";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { deleteUserDefinedFunction } from "../../Common/DocumentClientUtilityBase"; import UserDefinedFunctionTab from "../Tabs/UserDefinedFunctionTab";
export default class UserDefinedFunction { export default class UserDefinedFunction {
public nodeKind: string; public nodeKind: string;
@@ -17,7 +17,7 @@ export default class UserDefinedFunction {
public id: ko.Observable<string>; public id: ko.Observable<string>;
public body: ko.Observable<string>; public body: ko.Observable<string>;
constructor(container: Explorer, collection: ViewModels.Collection, data: DataModels.UserDefinedFunction) { constructor(container: Explorer, collection: ViewModels.Collection, data: UserDefinedFunctionDefinition & Resource) {
this.nodeKind = "UserDefinedFunction"; this.nodeKind = "UserDefinedFunction";
this.container = container; this.container = container;
@@ -25,12 +25,12 @@ export default class UserDefinedFunction {
this.self = data._self; this.self = data._self;
this.rid = data._rid; this.rid = data._rid;
this.id = ko.observable(data.id); this.id = ko.observable(data.id);
this.body = ko.observable(data.body); this.body = ko.observable(data.body as string);
} }
public static create(source: ViewModels.Collection, event: MouseEvent) { public static create(source: ViewModels.Collection, event: MouseEvent) {
const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.UserDefinedFunctions).length + 1; const id = source.container.tabsManager.getTabs(ViewModels.CollectionTabKind.UserDefinedFunctions).length + 1;
const userDefinedFunction = <DataModels.UserDefinedFunction>{ const userDefinedFunction = {
id: "", id: "",
body: "function userDefinedFunction(){}" body: "function userDefinedFunction(){}"
}; };
@@ -64,7 +64,7 @@ export default class UserDefinedFunction {
if (userDefinedFunctionTab) { if (userDefinedFunctionTab) {
this.container.tabsManager.activateTab(userDefinedFunctionTab); this.container.tabsManager.activateTab(userDefinedFunctionTab);
} else { } else {
const userDefinedFunctionData = <DataModels.UserDefinedFunction>{ const userDefinedFunctionData = {
_rid: this.rid, _rid: this.rid,
_self: this.self, _self: this.self,
id: this.id(), id: this.id(),
@@ -107,13 +107,7 @@ export default class UserDefinedFunction {
return; return;
} }
const userDefinedFunctionData = <DataModels.UserDefinedFunction>{ deleteUserDefinedFunction(this.collection.databaseId, this.collection.id(), this.id()).then(
_rid: this.rid,
_self: this.self,
id: this.id(),
body: this.body()
};
deleteUserDefinedFunction(this.collection, userDefinedFunctionData).then(
() => { () => {
this.container.tabsManager.removeTabByComparator(tab => tab.node && tab.node.rid === this.rid); this.container.tabsManager.removeTabByComparator(tab => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this); this.collection.children.remove(this);

View File

@@ -35,13 +35,19 @@ const onInit = async () => {
const galleryItemJunoResponse = await junoClient.getNotebookInfo(galleryItemId); const galleryItemJunoResponse = await junoClient.getNotebookInfo(galleryItemId);
galleryItem = galleryItemJunoResponse.data; galleryItem = galleryItemJunoResponse.data;
} }
render(notebookUrl, backNavigationText, hideInputs, galleryItem, onBackClick);
// The main purpose of hiding the prompt is to hide everything when hiding inputs.
// It is generally not very useful to just hide the prompt.
const hidePrompts = hideInputs;
render(notebookUrl, backNavigationText, hideInputs, hidePrompts, galleryItem, onBackClick);
}; };
const render = ( const render = (
notebookUrl: string, notebookUrl: string,
backNavigationText: string, backNavigationText: string,
hideInputs: boolean, hideInputs?: boolean,
hidePrompts?: boolean,
galleryItem?: IGalleryItem, galleryItem?: IGalleryItem,
onBackClick?: () => void onBackClick?: () => void
) => { ) => {
@@ -51,6 +57,7 @@ const render = (
galleryItem, galleryItem,
backNavigationText, backNavigationText,
hideInputs, hideInputs,
hidePrompts,
onBackClick: onBackClick, onBackClick: onBackClick,
onTagClick: undefined onTagClick: undefined
}; };

View File

@@ -70,7 +70,8 @@ export enum Action {
NotebooksGitHubManualRepoAdd, NotebooksGitHubManualRepoAdd,
NotebooksGitHubManageRepo, NotebooksGitHubManageRepo,
NotebooksGitHubCommit, NotebooksGitHubCommit,
NotebooksGitHubDisconnect NotebooksGitHubDisconnect,
OpenTerminal
} }
export const ActionModifiers = { export const ActionModifiers = {

View File

@@ -19,7 +19,7 @@ export function trace(action: Action, actionModifier: string = ActionModifiers.M
} }
}); });
appInsights.trackEvent({ name: Action[action] }, getData(data)); appInsights.trackEvent({ name: Action[action] }, getData(actionModifier, data));
} }
export function traceStart(action: Action, data?: unknown): number { export function traceStart(action: Action, data?: unknown): number {
@@ -49,7 +49,7 @@ export function traceSuccess(action: Action, data?: unknown, timestamp?: number)
} }
}); });
appInsights.stopTrackEvent(Action[action], getData(data)); appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Success, data));
} }
export function traceFailure(action: Action, data?: unknown, timestamp?: number): void { export function traceFailure(action: Action, data?: unknown, timestamp?: number): void {
@@ -63,7 +63,7 @@ export function traceFailure(action: Action, data?: unknown, timestamp?: number)
} }
}); });
appInsights.stopTrackEvent(Action[action], getData(data)); appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Failed, data));
} }
export function traceCancel(action: Action, data?: unknown, timestamp?: number): void { export function traceCancel(action: Action, data?: unknown, timestamp?: number): void {
@@ -77,7 +77,7 @@ export function traceCancel(action: Action, data?: unknown, timestamp?: number):
} }
}); });
appInsights.stopTrackEvent(Action[action], getData(data)); appInsights.stopTrackEvent(Action[action], getData(ActionModifiers.Cancel, data));
} }
export function traceOpen(action: Action, data?: unknown, timestamp?: number): number { export function traceOpen(action: Action, data?: unknown, timestamp?: number): number {
@@ -112,7 +112,7 @@ export function traceMark(action: Action, data?: unknown, timestamp?: number): n
return validTimestamp; return validTimestamp;
} }
function getData(data: unknown = {}): { [key: string]: string } | undefined { function getData(actionModifier: string, data: unknown = {}): { [key: string]: string } | undefined {
if (typeof data === "string") { if (typeof data === "string") {
data = { message: data }; data = { message: data };
} }
@@ -124,6 +124,7 @@ function getData(data: unknown = {}): { [key: string]: string } | undefined {
subscriptionId: userContext.subscriptionId as string, subscriptionId: userContext.subscriptionId as string,
platform: configContext.platform, platform: configContext.platform,
env: process.env.NODE_ENV as string, env: process.env.NODE_ENV as string,
actionModifier,
...data ...data
}; };
} }

View File

@@ -2,8 +2,7 @@ import { ApplicationInsights } from "@microsoft/applicationinsights-web";
const appInsights = new ApplicationInsights({ const appInsights = new ApplicationInsights({
config: { config: {
instrumentationKey: "fa645d97-6237-4656-9559-0ee0cb55ee49", instrumentationKey: "fa645d97-6237-4656-9559-0ee0cb55ee49"
disableFetchTracking: false
} }
}); });
appInsights.loadAppInsights(); appInsights.loadAppInsights();

View File

@@ -4,6 +4,8 @@ import "@jupyterlab/terminal/style/index.css";
import "./index.css"; import "./index.css";
import { ServerConnection } from "@jupyterlab/services"; import { ServerConnection } from "@jupyterlab/services";
import { JupyterLabAppFactory } from "./JupyterLabAppFactory"; import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
import { Action } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
const getUrlVars = (): { [key: string]: string } => { const getUrlVars = (): { [key: string]: string } => {
const vars: { [key: string]: string } = {}; const vars: { [key: string]: string } = {};
@@ -14,10 +16,7 @@ const getUrlVars = (): { [key: string]: string } => {
return vars; return vars;
}; };
const main = (): void => { const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => {
const urlVars = getUrlVars();
console.log("URL parameters", urlVars);
let body: BodyInit; let body: BodyInit;
if (urlVars.hasOwnProperty("terminalEndpoint")) { if (urlVars.hasOwnProperty("terminalEndpoint")) {
body = JSON.stringify({ body = JSON.stringify({
@@ -39,14 +38,29 @@ const main = (): void => {
fetch: window.parent.fetch fetch: window.parent.fetch
}; };
} }
const serverSettings = ServerConnection.makeSettings(options);
return ServerConnection.makeSettings(options);
};
const main = async (): Promise<void> => {
const urlVars = getUrlVars();
const serverSettings = createServerSettings(urlVars);
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, {
baseUrl: serverSettings.baseUrl
});
try {
if (urlVars.hasOwnProperty("terminal")) { if (urlVars.hasOwnProperty("terminal")) {
JupyterLabAppFactory.createTerminalApp(serverSettings); await JupyterLabAppFactory.createTerminalApp(serverSettings);
return; } else {
throw new Error("Only terminal is supported");
} }
throw new Error("Only terminal is supported"); TelemetryProcessor.traceSuccess(Action.OpenTerminal, startTime);
} catch (error) {
TelemetryProcessor.traceFailure(Action.OpenTerminal, startTime);
}
}; };
window.addEventListener("load", main); window.addEventListener("load", main);

17
src/Utils/BlobUtils.ts Normal file
View File

@@ -0,0 +1,17 @@
export const stringToBlob = (data: string, contentType: string, sliceSize = 512): Blob => {
const byteArrays = [];
for (let offset = 0; offset < data.length; offset += sliceSize) {
const slice = data.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: contentType });
};

View File

@@ -0,0 +1,21 @@
import { isInvalidParentFrameOrigin } from "./MessageValidation";
test.each`
domain | expected
${"https://cosmos.azure.com"} | ${false}
${"https://cosmos.azure.us"} | ${false}
${"https://cosmos.azure.cn"} | ${false}
${"https://cosmos.microsoftazure.de"} | ${false}
${"https://subdomain.portal.azure.com"} | ${false}
${"https://subdomain.portal.azure.us"} | ${false}
${"https://subdomain.portal.azure.cn"} | ${false}
${"https://subdomain.microsoftazure.de"} | ${false}
${"https://main.documentdb.ext.azure.com"} | ${false}
${"https://main.documentdb.ext.azure.us"} | ${false}
${"https://main.documentdb.ext.azure.cn"} | ${false}
${"https://main.documentdb.ext.microsoftazure.de"} | ${false}
${"https://random.domain"} | ${true}
${"https://malicious.cloudapp.azure.com"} | ${true}
`("returns $expected when called with $domain", ({ domain, expected }) => {
expect(isInvalidParentFrameOrigin({ origin: domain } as MessageEvent)).toBe(expected);
});

View File

@@ -4,13 +4,18 @@ export function isInvalidParentFrameOrigin(event: MessageEvent): boolean {
return !isValidOrigin(configContext.allowedParentFrameOrigins, event); return !isValidOrigin(configContext.allowedParentFrameOrigins, event);
} }
function isValidOrigin(allowedOrigins: RegExp, event: MessageEvent): boolean { function isValidOrigin(allowedOrigins: string[], event: MessageEvent): boolean {
const eventOrigin = (event && event.origin) || ""; const eventOrigin = (event && event.origin) || "";
const windowOrigin = (window && window.origin) || ""; const windowOrigin = (window && window.origin) || "";
if (eventOrigin === windowOrigin) { if (eventOrigin === windowOrigin) {
return true; return true;
} }
const result = allowedOrigins && allowedOrigins.test(eventOrigin); for (const origin of allowedOrigins) {
return result; const result = new RegExp(origin).test(eventOrigin);
if (result) {
return true;
}
}
return false;
} }

View File

@@ -6,11 +6,30 @@ Instead, generate ARM clients that consume this function with stricter typing.
*/ */
import promiseRetry, { AbortError } from "p-retry"; import promiseRetry, { AbortError } from "p-retry";
import { ErrorResponse } from "./generatedClients/2020-04-01/types";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
interface ARMError extends Error { interface ErrorResponse {
code: string; code: string;
message: string;
}
// ARM sometimes returns an error wrapped in a top level error object
// Example: 409 Conflict error when trying to delete a locked resource
interface WrappedErrorResponse {
error: ErrorResponse;
}
type ParsedErrorResponse = ErrorResponse | WrappedErrorResponse;
export class ARMError extends Error {
constructor(message: string) {
super(message);
// Set the prototype explicitly.
// https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work
Object.setPrototypeOf(this, ARMError.prototype);
}
public code: string | number;
} }
interface Options { interface Options {
@@ -33,9 +52,20 @@ export async function armRequest<T>({ host, path, apiVersion, method, body: requ
body: requestBody ? JSON.stringify(requestBody) : undefined body: requestBody ? JSON.stringify(requestBody) : undefined
}); });
if (!response.ok) { if (!response.ok) {
const errorResponse = (await response.json()) as ErrorResponse; let error: ARMError;
const error = new Error(errorResponse.message) as ARMError; try {
const errorResponse = (await response.json()) as ParsedErrorResponse;
if ("error" in errorResponse) {
error = new ARMError(errorResponse.error.message);
error.code = errorResponse.error.code;
} else {
error = new ARMError(errorResponse.message);
error.code = errorResponse.code; error.code = errorResponse.code;
}
} catch (error) {
throw new Error(await response.text());
}
throw error; throw error;
} }

View File

@@ -56,24 +56,27 @@ describe('Collection Add and Delete SQL spec', () => {
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.click(`div[data-test="${dbId}"]`); await frame.click(`div[data-test="${dbId}"]`);
await frame.waitFor(`span[title="${collectionId}"]`); await frame.waitFor(`span[title="${collectionId}"]`, { visible: true });
await frame.waitFor(3000)
await frame.waitFor(`span[title="${collectionId}"]`, { visible: true });
// delete container // delete container
// click context menu for container // click context menu for container
await frame.waitFor(`div[data-test="${collectionId}"] > div > button`); await frame.waitFor(`div[data-test="${collectionId}"] > div > button`, { visible: true });
await frame.waitFor(`span[title="${collectionId}"]`, { visible: true });
await frame.click(`div[data-test="${collectionId}"] > div > button`); await frame.click(`div[data-test="${collectionId}"] > div > button`);
await frame.waitFor(2000)
// click delete container // click delete container
await frame.waitForSelector('body > div.ms-Layer.ms-Layer--fixed'); await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]', { visible: true });
await frame.waitFor(1000); await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]');
const elements = await frame.$$('span[class="treeComponentMenuItemLabel"]')
await elements[4].click();
// confirm delete container // confirm delete container
await frame.waitFor('input[data-test="confirmCollectionId"]', { visible: true })
await frame.type('input[data-test="confirmCollectionId"]', collectionId.trim()); await frame.type('input[data-test="confirmCollectionId"]', collectionId.trim());
// click delete // click delete
await frame.waitFor('input[data-test="deleteCollection"]', { visible: true })
await frame.click('input[data-test="deleteCollection"]'); await frame.click('input[data-test="deleteCollection"]');
await frame.waitFor(5000); await frame.waitFor(5000);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
@@ -87,9 +90,8 @@ describe('Collection Add and Delete SQL spec', () => {
await button.asElement().click(); await button.asElement().click();
// click delete database // click delete database
await frame.waitFor(1000); await frame.waitFor('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
const dbElements = await frame.$$('span[class="treeComponentMenuItemLabel"]') await frame.click('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
await dbElements[1].click();
// confirm delete database // confirm delete database
await frame.type('input[data-test="confirmDatabaseId"]', dbId.trim()); await frame.type('input[data-test="confirmDatabaseId"]', dbId.trim());