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();
}
.infoTooltip a {
color: @AccentHigh;
}
.nowrap {
white-space: nowrap;
}
@@ -1739,7 +1743,7 @@ input::-webkit-calendar-picker-indicator {
padding-right: 34px;
color: @BaseDark;
overflow-y: auto;
overflow-x: hidden;
overflow-x: auto;
margin: (2 * @MediumSpace) 0px;
}
@@ -2972,6 +2976,10 @@ settings-pane {
.enableAnalyticalStorageRadio:nth-child(n+2) {
margin-left: @LargeSpace;
}
.enableAnalyticalStorageRadioLabel {
padding: 0px
}
}
.addCollectionLabel {
@@ -3009,4 +3017,4 @@ settings-pane {
.infoBoxContent a {
color: @AccentMediumHigh
}
}

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 {
ConflictDefinition,
FeedOptions,
ItemDefinition,
OfferDefinition,
QueryIterator,
Resource,
TriggerDefinition,
OfferDefinition
Resource
} 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 StoredProcedure from "../Explorer/Tree/StoredProcedure";
import { Platform, configContext } from "../ConfigContext";
import DocumentId from "../Explorer/Tree/DocumentId";
import Q from "q";
import { configContext, Platform } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import * as ViewModels from "../Contracts/ViewModels";
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 {
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
@@ -55,85 +53,55 @@ export function queryDocuments(
return Q(documentsIterator);
}
export function readStoredProcedures(
collection: ViewModels.Collection,
options?: any
): Q.Promise<DataModels.StoredProcedure[]> {
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 updateOffer(
offer: DataModels.Offer,
newOffer: DataModels.Offer,
options?: RequestOptions
): Q.Promise<DataModels.Offer> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedures.readAll(options)
.fetchAll()
.then(response => response.resources as DataModels.StoredProcedure[])
.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 readStoredProcedure(
collection: ViewModels.Collection,
requestedResource: DataModels.Resource,
options?: any
): Q.Promise<DataModels.StoredProcedure> {
return Q(
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 updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
newDocument: any
): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue;
export function readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.triggers.readAll(options)
.fetchAll()
.then(response => response.resources as DataModels.Trigger[])
);
}
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)
.item(documentId.id(), partitionKey)
.replace(newDocument)
.then(response => response.resource)
);
}
@@ -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> {
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> {
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(
collection: ViewModels.Collection,
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 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 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 * as DataAccessUtilityBase from "./DataAccessUtilityBase";
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
import * as Logger from "./Logger";
// TODO: Log all promise resolutions and errors with verbosity levels
export function queryDocuments(
@@ -42,121 +41,6 @@ export function getEntityName() {
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(
collection: ViewModels.Collection,
storedProcedure: StoredProcedure,
@@ -165,22 +49,17 @@ export function executeStoredProcedure(
): Q.Promise<any> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Executing stored procedure ${storedProcedure.id()}`
);
const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`);
DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params)
.then(
(response: any) => {
deferred.resolve(response);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
logConsoleInfo(
`Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
logConsoleError(
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}: ${JSON.stringify(
error
)}`
@@ -191,7 +70,7 @@ export function executeStoredProcedure(
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
clearMessage();
});
return deferred.promise;
@@ -205,32 +84,23 @@ export function queryDocumentsPage(
): Q.Promise<ViewModels.QueryResults> {
var deferred = Q.defer<ViewModels.QueryResults>();
const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Querying ${entityName} for container ${resourceName}`
);
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
Q(nextPage(documentsIterator, firstItemIndex))
.then(
(result: ViewModels.QueryResults) => {
const itemCount = (result.documents && result.documents.length) || 0;
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`
);
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
deferred.resolve(result);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to query ${entityName} for container ${resourceName}: ${JSON.stringify(error)}`
);
logConsoleError(`Failed to query ${entityName} for container ${resourceName}: ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "QueryDocumentsPage", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
clearMessage();
});
return deferred.promise;
@@ -239,27 +109,21 @@ export function queryDocumentsPage(
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Reading ${entityName} ${documentId.id()}`
);
const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
DataAccessUtilityBase.readDocument(collection, documentId)
.then(
(document: any) => {
deferred.resolve(document);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to read ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`
);
logConsoleError(`Failed to read ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "ReadDocument", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
clearMessage();
});
return deferred.promise;
@@ -272,31 +136,22 @@ export function updateDocument(
): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Updating ${entityName} ${documentId.id()}`
);
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
DataAccessUtilityBase.updateDocument(collection, documentId, newDocument)
.then(
(updatedDocument: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully updated ${entityName} ${documentId.id()}`
);
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
deferred.resolve(updatedDocument);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to update ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`
);
logConsoleError(`Failed to update ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "UpdateDocument", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
clearMessage();
});
return deferred.promise;
@@ -308,24 +163,15 @@ export function updateOffer(
options: RequestOptions
): Q.Promise<DataModels.Offer> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Updating offer for resource ${offer.resource}`
);
const clearMessage = logConsoleProgress(`Updating offer for resource ${offer.resource}`);
DataAccessUtilityBase.updateOffer(offer, newOffer, options)
.then(
(replacedOffer: DataModels.Offer) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully updated offer for resource ${offer.resource}`
);
logConsoleInfo(`Successfully updated offer for resource ${offer.resource}`);
deferred.resolve(replacedOffer);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error updating offer for resource ${offer.resource}: ${JSON.stringify(error)}`
);
logConsoleError(`Error updating offer for resource ${offer.resource}: ${JSON.stringify(error)}`);
Logger.logError(
JSON.stringify({
oldOffer: offer,
@@ -340,108 +186,7 @@ export function updateOffer(
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
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);
clearMessage();
});
return deferred.promise;
@@ -450,22 +195,15 @@ export function updateTrigger(
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Creating new ${entityName} for container ${collection.id()}`
);
const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
DataAccessUtilityBase.createDocument(collection, newDocument)
.then(
(savedDocument: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully created new ${entityName} for container ${collection.id()}`
);
logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`);
deferred.resolve(savedDocument);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
logConsoleError(
`Error while creating new ${entityName} for container ${collection.id()}:\n ${JSON.stringify(error)}`
);
Logger.logError(JSON.stringify(error), "CreateDocument", error.code);
@@ -474,115 +212,7 @@ export function createDocument(collection: ViewModels.CollectionBase, newDocumen
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
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);
clearMessage();
});
return deferred.promise;
@@ -591,31 +221,22 @@ export function createTrigger(
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting ${entityName} ${documentId.id()}`
);
const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
DataAccessUtilityBase.deleteDocument(collection, documentId)
.then(
(response: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully deleted ${entityName} ${documentId.id()}`
);
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
deferred.resolve(response);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting ${entityName} ${documentId.id()}:\n ${JSON.stringify(error)}`
);
logConsoleError(`Error while deleting ${entityName} ${documentId.id()}:\n ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "DeleteDocument", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
clearMessage();
});
return deferred.promise;
@@ -628,134 +249,22 @@ export function deleteConflict(
): Q.Promise<any> {
var deferred = Q.defer<any>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting conflict ${conflictId.id()}`
);
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
DataAccessUtilityBase.deleteConflict(collection, conflictId, options)
.then(
(response: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully deleted conflict ${conflictId.id()}`
);
logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`);
deferred.resolve(response);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while deleting conflict ${conflictId.id()}:\n ${JSON.stringify(error)}`
);
logConsoleError(`Error while deleting conflict ${conflictId.id()}:\n ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "DeleteConflict", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
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);
clearMessage();
});
return deferred.promise;
@@ -775,27 +284,21 @@ export function readCollectionQuotaInfo(
): Q.Promise<DataModels.CollectionQuotaInfo> {
var deferred = Q.defer<DataModels.CollectionQuotaInfo>();
const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Querying quota info for container ${collection.id}`
);
const clearMessage = logConsoleProgress(`Querying quota info for container ${collection.id}`);
DataAccessUtilityBase.readCollectionQuotaInfo(collection, options)
.then(
(quota: DataModels.CollectionQuotaInfo) => {
deferred.resolve(quota);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while querying quota info for container ${collection.id}:\n ${JSON.stringify(error)}`
);
logConsoleError(`Error while querying quota info for container ${collection.id}:\n ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "ReadCollectionQuotaInfo", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
clearMessage();
});
return deferred.promise;
@@ -804,24 +307,21 @@ export function readCollectionQuotaInfo(
export function readOffers(options: any = {}): Q.Promise<DataModels.Offer[]> {
var deferred = Q.defer<DataModels.Offer[]>();
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying offers");
const clearMessage = logConsoleProgress("Querying offers");
DataAccessUtilityBase.readOffers(options)
.then(
(offers: DataModels.Offer[]) => {
deferred.resolve(offers);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while querying offers:\n ${JSON.stringify(error)}`
);
logConsoleError(`Error while querying offers:\n ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "ReadOffers", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
clearMessage();
});
return deferred.promise;
@@ -833,24 +333,21 @@ export function readOffer(
): Q.Promise<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)
.then(
(offer: DataModels.OfferWithHeaders) => {
deferred.resolve(offer);
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while querying offer:\n ${JSON.stringify(error)}`
);
logConsoleError(`Error while querying offer:\n ${JSON.stringify(error)}`);
Logger.logError(JSON.stringify(error), "ReadOffer", error.code);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
clearMessage();
});
return deferred.promise;

View File

@@ -1,6 +1,5 @@
import Q from "q";
import * as MessageHandler from "./MessageHandler";
import { MessageTypes } from "../Contracts/ExplorerContracts";
describe("Message Handler", () => {
it("should handle cached message", async () => {
@@ -26,4 +25,34 @@ describe("Message Handler", () => {
MessageHandler.runGarbageCollector();
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,16 +48,38 @@ export function sendCachedDataMessage<TResponseDataModel>(
export function sendMessage(data: any): void {
if (canSendMessage()) {
window.parent.postMessage(
{
signature: "pcIframe",
data: data
},
window.document.referrer
);
const dataExplorerWindow = getDataExplorerWindow(window);
if (dataExplorerWindow) {
dataExplorerWindow.parent.postMessage(
{
signature: "pcIframe",
data: data
},
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 {
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 {
platform: Platform;
allowedParentFrameOrigins: RegExp;
allowedParentFrameOrigins: string[];
gitSha?: string;
proxyPath?: string;
AAD_ENDPOINT: string;
@@ -30,7 +30,12 @@ interface ConfigContext {
// Default configuration
let configContext: Readonly<ConfigContext> = {
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
gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/",
@@ -73,8 +78,13 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
const response = await fetch("./config.json");
if (response.status === 200) {
try {
const externalConfig = await response.json();
const { allowedParentFrameOrigins, ...externalConfig } = await response.json();
Object.assign(configContext, externalConfig);
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
updateConfigContext({
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins]
});
}
} catch (error) {
console.error("Unable to parse json in config file");
console.error(error);

View File

@@ -88,10 +88,6 @@ export interface Resource {
id: string;
}
export interface ResourceRequest {
id: string;
}
export interface Collection extends Resource {
defaultTtl?: number;
indexingPolicy?: IndexingPolicy;
@@ -104,39 +100,12 @@ export interface Collection extends Resource {
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 {
collections?: Collection[];
}
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 {
resourceId?: string;
resourceType?: string;
@@ -259,28 +228,6 @@ export interface ErrorDataModel {
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 {
databaseId: string;
collectionId: string;
@@ -307,11 +254,6 @@ export enum AutopilotTier {
Tier4 = 4
}
export interface RpOptions {
// tier is sent as string, autoscale as object (AutoPilotCreationSettings)
[key: string]: string | AutoPilotCreationSettings;
}
export interface Query {
id: string;
resourceId: string;
@@ -347,10 +289,11 @@ export interface CreateCollectionParams {
uniqueKeyPolicy?: UniqueKeyPolicy;
}
export interface SharedThroughputRange {
minimumRU: number;
maximumRU: number;
defaultRU: number;
export interface ReadDatabaseOfferParams {
databaseId: string;
databaseResourceId?: string;
isServerless?: boolean;
offerId?: string;
}
export interface Notification {
@@ -495,25 +438,6 @@ export interface NotebookConfigurationEndpointInfo {
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 {
userName: string;
password: string;
@@ -555,79 +479,10 @@ export interface MongoParameters extends RpParameters {
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 {
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 {
properties: {
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 { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
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 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 Trigger from "../Explorer/Tree/Trigger";
import DocumentId from "../Explorer/Tree/DocumentId";
import ConflictId from "../Explorer/Tree/ConflictId";
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
import { UploadDetails } from "../workers/upload/definitions";
import * as DataModels from "./DataModels";
export interface TokenProvider {
getAuthHeader(): Promise<Headers>;
@@ -75,15 +81,15 @@ export interface Database extends TreeNode {
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
selectDatabase(): void;
expandDatabase(): void;
expandDatabase(): Promise<void>;
collapseDatabase(): void;
loadCollections(): Q.Promise<void>;
loadCollections(): Promise<void>;
findCollectionWithId(collectionRid: string): Collection;
openAddCollection(database: Database, event: MouseEvent): void;
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
readSettings(): void;
onSettingsClick: () => void;
loadOffer(): Promise<void>;
}
export interface CollectionBase extends TreeNode {
@@ -153,13 +159,13 @@ export interface Collection extends CollectionBase {
collapseUserDefinedFunctions(): void;
collapseTriggers(): void;
loadUserDefinedFunctions(): Q.Promise<any>;
loadStoredProcedures(): Q.Promise<any>;
loadTriggers(): Q.Promise<any>;
loadUserDefinedFunctions(): Promise<any>;
loadStoredProcedures(): Promise<any>;
loadTriggers(): Promise<any>;
createStoredProcedureNode(data: DataModels.StoredProcedure): StoredProcedure;
createUserDefinedFunctionNode(data: DataModels.UserDefinedFunction): UserDefinedFunction;
createTriggerNode(data: DataModels.Trigger): Trigger;
createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure;
createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction;
createTriggerNode(data: TriggerDefinition & Resource): Trigger;
findStoredProcedureWithId(sprocRid: string): StoredProcedure;
findTriggerWithId(triggerRid: string): Trigger;
findUserDefinedFunctionWithId(udfRid: string): UserDefinedFunction;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -159,4 +159,20 @@ describe("TreeNodeComponent", () => {
const wrapper = shallow(<TreeNodeComponent {...props} />);
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 TriangleRightIcon from "../../../../images/Triangle-right.svg";
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
export interface TreeNodeMenuItem {
label: string;
onClick: () => void;
iconSrc?: string;
isDisabled?: boolean;
styleClass?: string;
}
export interface TreeNode {
@@ -37,6 +39,7 @@ export interface TreeNode {
data?: any; // Piece of data corresponding to this node
timestamp?: number;
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
isLoading?: boolean;
isSelected?: () => boolean;
onClick?: (isExpanded: boolean) => void; // Only if a leaf, other click will expand/collapse
onExpanded?: () => void;
@@ -183,6 +186,9 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
)}
{node.contextMenu && this.renderContextMenuButton(node)}
</div>
<div className="loadingIconContainer">
<img className="loadingIcon" src={LoadingIndicator_3Squares} hidden={!this.props.node.isLoading} />
</div>
{node.children && (
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
<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())}
>
{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>
),
items: node.contextMenu.map((menuItem: TreeNodeMenuItem) => ({
key: menuItem.label,
text: menuItem.label,
disabled: menuItem.isDisabled,
className: menuItem.styleClass,
onClick: menuItem.onClick,
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
}))
@@ -282,7 +295,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
<img
className="expandCollapseIcon"
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)}
tabIndex={0}
role="button"

View File

@@ -49,7 +49,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
tabIndex={-1}
>
<img
alt="Branch is collapsed"
alt="label branch is collapsed"
className="expandCollapseIcon"
onKeyPress={[Function]}
role="button"
@@ -63,6 +63,15 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
label
</span>
</div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight
animateOpacity={false}
animationStateClasses={
@@ -140,7 +149,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
tabIndex={-1}
>
<img
alt="Branch is expanded"
alt="label branch is expanded"
className="expandCollapseIcon"
onKeyPress={[Function]}
role="button"
@@ -179,6 +188,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
"isBeakVisible": false,
"items": Array [
Object {
"className": undefined,
"disabled": true,
"key": "menuLabel",
"onClick": undefined,
@@ -201,6 +211,15 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
/>
</div>
</div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight
animateOpacity={false}
animationStateClasses={
@@ -261,6 +280,77 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
</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`] = `
<div
className="nodeClassname main12 nodeItem "
@@ -278,7 +368,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
tabIndex={-1}
>
<img
alt="Branch is expanded"
alt="label branch is expanded"
className="expandCollapseIcon"
onKeyPress={[Function]}
role="button"
@@ -331,6 +421,15 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
/>
</div>
</div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight
animateOpacity={false}
animationStateClasses={
@@ -436,7 +535,7 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
tabIndex={-1}
>
<img
alt="Branch is expanded"
alt="label branch is expanded"
className="expandCollapseIcon"
onKeyPress={[Function]}
role="button"
@@ -450,6 +549,15 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
label
</span>
</div>
<div
className="loadingIconContainer"
>
<img
className="loadingIcon"
hidden={true}
src=""
/>
</div>
<AnimateHeight
animateOpacity={false}
animationStateClasses={

View File

@@ -20,7 +20,7 @@
}
&.showingMenu {
background-color: #EEE;
background-color: #eee;
}
.treeMenuEllipsis {
@@ -78,3 +78,12 @@
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 DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import { readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase";
import { refreshCachedResources } from "../Common/DocumentClientUtilityBase";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
@@ -87,6 +87,7 @@ import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import TabsBase from "./Tabs/TabsBase";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { updateUserContext, userContext } from "../UserContext";
import { stringToBlob } from "../Utils/BlobUtils";
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -1423,71 +1424,40 @@ export default class Explorer {
// TODO: Refactor
const deferred: Q.Deferred<any> = Q.defer();
const refreshDatabases = (offers?: DataModels.Offer[]) => {
this._setLoadingStatusText("Fetching databases...");
readDatabases().then(
(databases: DataModels.Database[]) => {
this._setLoadingStatusText("Successfully fetched databases.");
TelemetryProcessor.traceSuccess(
Action.LoadDatabases,
{
databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree
this._setLoadingStatusText("Fetching databases...");
readDatabases().then(
(databases: DataModels.Database[]) => {
this._setLoadingStatusText("Successfully fetched databases.");
TelemetryProcessor.traceSuccess(
Action.LoadDatabases,
{
databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree
},
startKey
);
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
const deltaDatabases = this.getDeltaDatabases(databases);
this.addDatabasesToList(deltaDatabases.toAdd);
this.deleteDatabasesFromList(deltaDatabases.toDelete);
this.selectedNode(currentlySelectedNode);
this._setLoadingStatusText("Fetching containers...");
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd)
.then(
() => {
this._setLoadingStatusText("Successfully fetched containers.");
deferred.resolve();
},
startKey
);
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
const deltaDatabases = this.getDeltaDatabases(databases, offers);
this.addDatabasesToList(deltaDatabases.toAdd);
this.deleteDatabasesFromList(deltaDatabases.toDelete);
this.selectedNode(currentlySelectedNode);
this._setLoadingStatusText("Fetching containers...");
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd)
.then(
() => {
this._setLoadingStatusText("Successfully fetched containers.");
deferred.resolve();
},
reason => {
this._setLoadingStatusText("Failed to fetch containers.");
deferred.reject(reason);
}
)
.finally(() => this.isRefreshingExplorer(false));
},
error => {
this._setLoadingStatusText("Failed to fetch databases.");
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)}`
);
}
);
};
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);
reason => {
this._setLoadingStatusText("Failed to fetch containers.");
deferred.reject(reason);
}
)
.finally(() => this.isRefreshingExplorer(false));
},
error => {
this._setLoadingStatusText("Failed to fetch offers.");
this._setLoadingStatusText("Failed to fetch databases.");
this.isRefreshingExplorer(false);
deferred.reject(error);
TelemetryProcessor.traceFailure(
@@ -2102,16 +2072,13 @@ export default class Explorer {
defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree
});
databasesToLoad.forEach((database: ViewModels.Database) => {
loadCollectionPromises.push(
database.loadCollections().finally(() => {
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid);
if (isNewDatabase) {
database.expandDatabase();
}
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
})
);
databasesToLoad.forEach(async (database: ViewModels.Database) => {
await database.loadCollections();
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid);
if (isNewDatabase) {
database.expandDatabase();
}
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
});
Q.all(loadCollectionPromises).done(
@@ -2256,8 +2223,7 @@ export default class Explorer {
}
private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[],
updatedOffersList: DataModels.Offer[]
updatedDatabaseList: DataModels.Database[]
): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } {
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
const databaseExists = _.some(
@@ -2266,10 +2232,9 @@ export default class Explorer {
);
return !databaseExists;
});
const databasesToAdd: ViewModels.Database[] = _.map(newDatabases, (newDatabase: DataModels.Database) => {
const databaseOffer: DataModels.Offer = this.getOfferForResource(updatedOffersList, newDatabase._self);
return new Database(this, newDatabase, databaseOffer);
});
const databasesToAdd: ViewModels.Database[] = newDatabases.map(
(newDatabase: DataModels.Database) => new Database(this, newDatabase)
);
let databasesToDelete: ViewModels.Database[] = [];
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
@@ -2319,10 +2284,6 @@ export default class Explorer {
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> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to upload notebook, but notebook is not enabled";
@@ -2621,9 +2582,11 @@ export default class Explorer {
throw new Error(error);
}
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`);
return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then(
(content: string) => {
const blob = new Blob([content], { type: "octet/stream" });
const blob = stringToBlob(content, "text/plain");
if (navigator.msSaveBlob) {
// for IE and Edge
navigator.msSaveBlob(blob, notebookFile.name);
@@ -2640,12 +2603,16 @@ export default class Explorer {
downloadLink.click();
downloadLink.remove();
}
clearMessage();
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.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;
}
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 {
const label = "New Notebook";
return {

View File

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

View File

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

View File

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

View File

@@ -113,11 +113,14 @@ export default class NotebookManager {
this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
this.params.resourceTree.triggerRender();
});
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
this.refreshPinnedRepos();
}
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(

View File

@@ -3,6 +3,7 @@ import "./base.css";
import "./default.css";
import { CodeCell, RawCell, Cells, MarkdownCell } from "@nteract/stateful-components";
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
import { AzureTheme } from "./AzureTheme";
import { connect } from "react-redux";
@@ -15,6 +16,7 @@ import "./NotebookReadOnlyRenderer.less";
export interface NotebookRendererProps {
contentRef: any;
hideInputs?: boolean;
hidePrompts?: boolean;
}
interface PassedEditorProps {
@@ -38,6 +40,29 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
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 {
return (
<div className="NotebookReadOnlyRender">
@@ -46,6 +71,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
code: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
<CodeCell id={id} contentRef={contentRef}>
{{
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
editor: {
codemirror: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} />

View File

@@ -24,7 +24,7 @@
<script type="text/html" id="add-collection-inputs">
<!-- Add collection header - Start -->
<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"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }" tabindex="0">
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
@@ -115,10 +115,10 @@
<!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput -->
<div class="databaseProvision" aria-label="New database provision support"
<div class="databaseProvision" aria-label="Provision database throughput"
data-bind="visible: databaseCreateNew">
<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" />
<span class="databaseProvisionText" for="databaseSharedThroughput">Provision database throughput</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
@@ -517,13 +517,13 @@
<div>
<span class="mandatoryStar">*</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">
<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
data, without impacting the performance of transactional workloads.
Learn more <a class="errorLink" href="https://aka.ms/analytical-store-overview"
target="_blank">here</a>
Learn more <a id="link1" class="errorLink" href="https://aka.ms/analytical-store-overview"
target="_blank" data-bind="event: { focusout: onFocusOut, keydown: onKeyDown.bind($data, 'largePartitionKey') }">here</a>
</span>
</span>
</div>
@@ -537,9 +537,11 @@
attr: {
'aria-checked': isAnalyticalStorageOn() ? 'true' : 'false'
}" />
<span for="enableAnalyticalStorageRadioOn" data-bind="disable: showEnableSynapseLink">
On
</span>
<label for="enableAnalyticalStorageRadioOn" class="enableAnalyticalStorageRadioLabel">
<span data-bind="disable: showEnableSynapseLink">
On
</span>
</label>
<input class="enableAnalyticalStorageRadio" id="enableAnalyticalStorageRadioOff"
name="analyticalStore" type="radio" role="radio" tabindex="0" data-bind="
@@ -549,9 +551,11 @@
attr: {
'aria-checked': isAnalyticalStorageOn() ? 'false' : 'true'
}" />
<span for="enableAnalyticalStorageRadioOff" data-bind="disable: showEnableSynapseLink">
Off
</span>
<label for="enableAnalyticalStorageRadioOff" class="enableAnalyticalStorageRadioLabel">
<span data-bind="disable: showEnableSynapseLink">
Off
</span>
</label>
</div>
<div class="paragraph italic" data-bind="visible: ttl90DaysEnabled() && isAnalyticalStorageOn()">

View File

@@ -681,7 +681,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return true;
};
public open(databaseId?: string) {
public async open(databaseId?: string) {
super.open();
// TODO: Figure out if a database level partition split is about to happen once shared throughput read is available
this.formWarnings("");
@@ -715,18 +715,40 @@ export default class AddCollectionPane extends ContextualPaneBase {
dataExplorerArea: Constants.Areas.ContextualPane
};
await this.container.loadDatabaseOffers();
this._onDatabasesChange(this.container.databases());
this._setFocus();
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[]) {
const cachedDatabaseIdsList = _.map(newDatabaseIds, (database: ViewModels.Database) => {
if (database && database.offer && database.offer()) {
this._databaseOffers.set(database.id(), database.offer());
} else if (database && database.isDatabaseShared && database.isDatabaseShared()) {
database.readSettings();
}
return database.id();

View File

@@ -24,7 +24,7 @@
<script type="text/html" id="add-database-inputs">
<!-- Add database header - Start -->
<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"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }" tabindex="0">
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
@@ -73,8 +73,8 @@
</p>
<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"
size="40" class="collid" data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus"
title="May not end with space nor contain characters '\' '/' '#' '?'"
size="40" class="collid" data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus, attr: { placeholder: databaseIdPlaceHolder }"
aria-label="Database id" autofocus>
<!-- Database provisioned throughput - Start -->

View File

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

View File

@@ -6,7 +6,7 @@
<div class="paneContentContainer">
<!-- Save Query header - Start -->
<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"

View File

@@ -17,7 +17,7 @@
>
<!-- Add Cassandra collection header - Start -->
<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"

View File

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

View File

@@ -9,6 +9,7 @@ import Explorer from "../Explorer";
// TODO: Use specific actions for logging telemetry data
export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
private initalFocusedElement: HTMLElement | undefined;
public id: string;
public container: Explorer;
public firstFieldHasFocus: ko.Observable<boolean>;
@@ -49,9 +50,11 @@ export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
this.visible(false);
this.isExecuting(false);
this.resetData();
this.resetFocus();
}
public open() {
this.initalFocusedElement = document.activeElement as HTMLElement;
this.visible(true);
this.firstFieldHasFocus(true);
this.resizePane();
@@ -123,4 +126,11 @@ export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
$(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 -->
<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"
@@ -67,8 +67,7 @@
name="collectionIdConfirmation"
required
class="collid"
data-bind="value: collectionIdConfirmation, hasFocus: firstFieldHasFocus"
aria-label="Confirm by typing the collection id"
data-bind="value: collectionIdConfirmation, hasFocus: firstFieldHasFocus, attr: { 'aria-label': collectionIdConfirmationText }"
/>
</p>
</div>

View File

@@ -15,7 +15,7 @@
>
<!-- Delete Database Confirmation header - Start -->
<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"

View File

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

View File

@@ -11,7 +11,7 @@
<form class="paneContentContainer" data-bind="submit: execute">
<!-- Input params header - Start -->
<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"

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Load Query header - Start -->
<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"

View File

@@ -11,7 +11,7 @@
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Renew ad-hoc access header - Start -->
<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"

View File

@@ -6,7 +6,7 @@
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Save Query header - Start -->
<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">
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>

View File

@@ -12,7 +12,7 @@
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Settings Confirmation header - Start -->
<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"

View File

@@ -6,7 +6,7 @@
<div class="paneContentContainer">
<!-- Setup notebooks header - Start -->
<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"

View File

@@ -6,7 +6,7 @@
<form class="paneContentContainer" data-bind="submit: submit">
<!-- String Input header - Start -->
<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>
<!-- String Input header - End -->

View File

@@ -5,7 +5,7 @@
<div class="contextual-pane-in">
<!-- Switch Directory header - Start -->
<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">
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>

View File

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

View File

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

View File

@@ -19,7 +19,7 @@
>
<!-- Edit table entity header - Start -->
<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"

View File

@@ -6,7 +6,7 @@
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Upload File header - Start -->
<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">
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>

View File

@@ -11,7 +11,7 @@
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Upload items header - Start -->
<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"

View File

@@ -100,7 +100,7 @@
</table>
</div>
<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
>
</div>

View File

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

View File

@@ -200,7 +200,7 @@
</table>
</div>
<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
>
</div>

View File

@@ -390,7 +390,7 @@
</table>
<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
>
</div>

View File

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

View File

@@ -93,7 +93,7 @@
margin-left: @MediumSpace;
.flex-display();
.flex-direction();
.togglesWithMetadata {
margin-top: @MediumSpace;
@@ -102,23 +102,23 @@
height: @ToggleHeight;
width: @ToggleWidth;
margin-left: @MediumSpace;
&:focus {
.focus();
}
.tab {
margin-right: @MediumSpace;
}
.toggleSwitch {
.toggleSwitch();
}
.selectedToggle {
.selectedToggle();
}
.unselectedToggle {
.unselectedToggle();
}
@@ -136,7 +136,7 @@
.queryResultNextEnable {
color: @AccentMediumHigh;
font-size: @mediumFontSize;
cursor: pointer;
cursor: pointer;
img {
height: @ImgHeight;
@@ -153,7 +153,7 @@
img {
height: @ImgHeight;
width: @ImgWidth;
margin-left: @SmallSpace;
margin-left: @SmallSpace;
}
}
}
@@ -181,7 +181,7 @@
cursor: pointer;
padding: @SmallSpace;
}
.queryMetricsSummaryContainer {
.flex-display();
.flex-direction();
@@ -195,10 +195,14 @@
overflow-y: auto;
overflow-x: hidden;
caption {
width: 100px;
}
.queryMetricsSummaryHead {
.flex-display();
}
.queryMetricsSummaryHeader.queryMetricsSummaryTuple {
font-size: 10px;
}
@@ -207,7 +211,7 @@
.flex-display();
.flex-direction();
}
.queryMetricsSummaryTuple {
border-bottom: 1px solid @BaseMedium;
height: 32px;
@@ -221,18 +225,14 @@
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex: 0 1 auto;
}
&:nth-child(2) {
flex: 1 1 auto;
flex: 0 0 50%;
}
&:nth-child(3) {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex: 0 0 50%;
flex: 0 0 50%;
}
.queryMetricInfoTooltip {
@@ -264,7 +264,7 @@
.tooltipTextAfter();
}
}
.queryEngineExeTimeInfo {
width: @QueryEngineExeInfo;
top: -85px;

View File

@@ -17,7 +17,7 @@
<div class="error-bar">
<div class="error-message" aria-label="Error Message" data-bind="visible: hasQueryError">
<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>
<!-- Tables Query Tab Errors - End-->

View File

@@ -1,15 +1,14 @@
import * as Constants from "../../Common/Constants";
import * as ko from "knockout";
import * as monaco from "monaco-editor";
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 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 TabsBase from "./TabsBase";
export default abstract class ScriptTabBase extends TabsBase implements ViewModels.WaitsForTemplate {
public ariaLabel: ko.Observable<string>;
@@ -30,7 +29,8 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
public formIsValid: ko.Computed<boolean>;
public formIsDirty: ko.Computed<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>;
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 onUpdateClick: () => Q.Promise<any>;
public abstract onSaveClick: () => Promise<any>;
public abstract onUpdateClick: () => Promise<any>;
public onDiscard = (): Q.Promise<any> => {
this.setBaselines();
@@ -206,14 +206,14 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
return Q();
};
public onSaveOrUpdateClick(): Q.Promise<any> {
public onSaveOrUpdateClick(): Promise<any> {
if (this.saveButton.visible()) {
return this.onSaveClick();
} else if (this.updateButton.visible()) {
return this.onUpdateClick();
}
return Q();
return undefined;
}
protected getTabsButtons(): CommandButtonComponentProps[] {

View File

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

View File

@@ -1270,8 +1270,10 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
}
public onActivate(): Q.Promise<any> {
return super.onActivate().then(() => {
return super.onActivate().then(async () => {
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 _ from "underscore";
import Q from "q";
import * as _ from "underscore";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
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 { Action } from "../../Shared/Telemetry/TelemetryConstants";
import editable from "../../Common/EditableUtility";
import ScriptTabBase from "./ScriptTabBase";
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 StoredProcedure from "../Tree/StoredProcedure";
import ScriptTabBase from "./ScriptTabBase";
enum ToggleState {
Result = "result",
@@ -24,7 +25,6 @@ export default class StoredProcedureTab extends ScriptTabBase {
public executeResultsEditorId: string;
public executeLogsEditorId: string;
public toggleState: ko.Observable<ToggleState>;
public originalSprocBody: ViewModels.Editable<string>;
public resultsData: ko.Observable<string>;
public logsData: ko.Observable<string>;
@@ -54,13 +54,11 @@ export default class StoredProcedureTab extends ScriptTabBase {
this.buildCommandBarOptions();
}
public onSaveClick = (): Q.Promise<DataModels.StoredProcedure> => {
const resource: DataModels.StoredProcedure = <DataModels.StoredProcedure>{
public onSaveClick = (): Promise<StoredProcedureDefinition & Resource> => {
return this._createStoredProcedure({
id: this.id(),
body: this.editorContent()
};
return this._createStoredProcedure(resource);
});
};
public onDiscard = (): Q.Promise<any> => {
@@ -72,8 +70,8 @@ export default class StoredProcedureTab extends ScriptTabBase {
return Q();
};
public onUpdateClick = (): Q.Promise<any> => {
const data: DataModels.StoredProcedure = this._getResource();
public onUpdateClick = (): Promise<any> => {
const data = this._getResource();
this.isExecutionError(false);
this.isExecuting(true);
@@ -83,18 +81,18 @@ export default class StoredProcedureTab extends ScriptTabBase {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle()
});
return updateStoredProcedure(this.collection, data)
return updateStoredProcedure(this.collection.databaseId, this.collection.id(), data)
.then(
(updatedResource: DataModels.StoredProcedure) => {
updatedResource => {
this.resource(updatedResource);
this.tabTitle(updatedResource.id);
this.node.id(updatedResource.id);
this.node.body(updatedResource.body);
this.node.body(updatedResource.body as string);
this.setBaselines();
const editorModel = this.editor() && this.editor().getModel();
editorModel && editorModel.setValue(updatedResource.body);
this.editorContent.setBaseline(updatedResource.body);
editorModel && editorModel.setValue(updatedResource.body as string);
this.editorContent.setBaseline(updatedResource.body as string);
TelemetryProcessor.traceSuccess(
Action.UpdateStoredProcedure,
{
@@ -220,18 +218,14 @@ export default class StoredProcedureTab extends ScriptTabBase {
});
}
private _getResource(): DataModels.StoredProcedure {
const resource: DataModels.StoredProcedure = <DataModels.StoredProcedure>{
_rid: this.resource()._rid,
_self: this.resource()._self,
private _getResource() {
return {
id: this.id(),
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.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateStoredProcedure, {
@@ -241,7 +235,7 @@ export default class StoredProcedureTab extends ScriptTabBase {
tabTitle: this.tabTitle()
});
return createStoredProcedure(this.collection, resource)
return createStoredProcedure(this.collection.databaseId, this.collection.id(), resource)
.then(
createdResource => {
this.tabTitle(createdResource.id);
@@ -256,8 +250,8 @@ export default class StoredProcedureTab extends ScriptTabBase {
this.setBaselines();
const editorModel = this.editor() && this.editor().getModel();
editorModel && editorModel.setValue(createdResource.body);
this.editorContent.setBaseline(createdResource.body);
editorModel && editorModel.setValue(createdResource.body as string);
this.editorContent.setBaseline(createdResource.body as string);
this.node = this.collection.createStoredProcedureNode(createdResource);
TelemetryProcessor.traceSuccess(
Action.CreateStoredProcedure,
@@ -284,7 +278,7 @@ export default class StoredProcedureTab extends ScriptTabBase {
},
startKey
);
return Q.reject(createError);
return Promise.reject(createError);
}
)
.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 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 { Action } from "../../Shared/Telemetry/TelemetryConstants";
import ScriptTabBase from "./ScriptTabBase";
import editable from "../../Common/EditableUtility";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Trigger from "../Tree/Trigger";
import { createTrigger, updateTrigger } from "../../Common/DocumentClientUtilityBase";
import ScriptTabBase from "./ScriptTabBase";
export default class TriggerTab extends ScriptTabBase {
public collection: ViewModels.Collection;
@@ -27,13 +27,17 @@ export default class TriggerTab extends ScriptTabBase {
super.buildCommandBarOptions();
}
public onSaveClick = (): Q.Promise<DataModels.Trigger> => {
const data: DataModels.Trigger = this._getResource();
return this._createTrigger(data);
public onSaveClick = (): Promise<TriggerDefinition & Resource> => {
return this._createTrigger({
id: this.id(),
body: this.editorContent(),
triggerOperation: this.triggerOperation() as TriggerOperation,
triggerType: this.triggerType() as TriggerType
});
};
public onUpdateClick = (): Q.Promise<any> => {
const data: DataModels.Trigger = this._getResource();
public onUpdateClick = (): Promise<any> => {
const data = this._getResource();
this.isExecutionError(false);
this.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateTrigger, {
@@ -42,14 +46,19 @@ export default class TriggerTab extends ScriptTabBase {
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(
(createdResource: DataModels.Trigger) => {
createdResource => {
this.resource(createdResource);
this.tabTitle(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.triggerOperation(createdResource.triggerOperation);
TelemetryProcessor.traceSuccess(
@@ -66,8 +75,8 @@ export default class TriggerTab extends ScriptTabBase {
this.setBaselines();
const editorModel = this.editor().getModel();
editorModel.setValue(createdResource.body);
this.editorContent.setBaseline(createdResource.body);
editorModel.setValue(createdResource.body as string);
this.editorContent.setBaseline(createdResource.body as string);
},
(createError: any) => {
this.isExecutionError(true);
@@ -89,7 +98,7 @@ export default class TriggerTab extends ScriptTabBase {
public setBaselines() {
super.setBaselines();
const resource = <DataModels.Trigger>this.resource();
const resource = this.resource();
this.triggerOperation.setBaseline(resource.triggerOperation);
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.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateTrigger, {
@@ -119,9 +128,9 @@ export default class TriggerTab extends ScriptTabBase {
tabTitle: this.tabTitle()
});
return createTrigger(this.collection, resource)
return createTrigger(this.collection.databaseId, this.collection.id(), resource)
.then(
(createdResource: DataModels.Trigger) => {
createdResource => {
this.tabTitle(createdResource.id);
this.isNew(false);
this.resource(createdResource);
@@ -134,8 +143,8 @@ export default class TriggerTab extends ScriptTabBase {
this.setBaselines();
const editorModel = this.editor().getModel();
editorModel.setValue(createdResource.body);
this.editorContent.setBaseline(createdResource.body);
editorModel.setValue(createdResource.body as string);
this.editorContent.setBaseline(createdResource.body as string);
this.node = this.collection.createTriggerNode(createdResource);
TelemetryProcessor.traceSuccess(
@@ -163,22 +172,18 @@ export default class TriggerTab extends ScriptTabBase {
},
startKey
);
return Q.reject(createError);
return Promise.reject(createError);
}
)
.finally(() => this.isExecuting(false));
}
private _getResource(): DataModels.Trigger {
const resource: DataModels.Trigger = <DataModels.Trigger>{
_rid: this.resource()._rid,
_self: this.resource()._self,
private _getResource() {
return {
id: this.id(),
body: this.editorContent(),
triggerOperation: this.triggerOperation(),
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 DataModels from "../../Contracts/DataModels";
import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import ScriptTabBase from "./ScriptTabBase";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import UserDefinedFunction from "../Tree/UserDefinedFunction";
import { createUserDefinedFunction, updateUserDefinedFunction } from "../../Common/DocumentClientUtilityBase";
import ScriptTabBase from "./ScriptTabBase";
export default class UserDefinedFunctionTab extends ScriptTabBase {
public collection: ViewModels.Collection;
@@ -19,13 +19,13 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
super.buildCommandBarOptions();
}
public onSaveClick = (): Q.Promise<DataModels.UserDefinedFunction> => {
const data: DataModels.UserDefinedFunction = this._getResource();
public onSaveClick = (): Promise<UserDefinedFunctionDefinition & Resource> => {
const data = this._getResource();
return this._createUserDefinedFunction(data);
};
public onUpdateClick = (): Q.Promise<any> => {
const data: DataModels.UserDefinedFunction = this._getResource();
public onUpdateClick = (): Promise<any> => {
const data = this._getResource();
this.isExecutionError(false);
this.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateUDF, {
@@ -35,14 +35,14 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
tabTitle: this.tabTitle()
});
return updateUserDefinedFunction(this.collection, data)
return updateUserDefinedFunction(this.collection.databaseId, this.collection.id(), data)
.then(
(createdResource: DataModels.UserDefinedFunction) => {
createdResource => {
this.resource(createdResource);
this.tabTitle(createdResource.id);
this.node.id(createdResource.id);
this.node.body(createdResource.body);
this.node.body(createdResource.body as string);
TelemetryProcessor.traceSuccess(
Action.UpdateUDF,
{
@@ -57,8 +57,8 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
this.setBaselines();
const editorModel = this.editor().getModel();
editorModel.setValue(createdResource.body);
this.editorContent.setBaseline(createdResource.body);
editorModel.setValue(createdResource.body as string);
this.editorContent.setBaseline(createdResource.body as string);
},
(createError: any) => {
this.isExecutionError(true);
@@ -93,8 +93,8 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
}
private _createUserDefinedFunction(
resource: DataModels.UserDefinedFunction
): Q.Promise<DataModels.UserDefinedFunction> {
resource: UserDefinedFunctionDefinition
): Promise<UserDefinedFunctionDefinition & Resource> {
this.isExecutionError(false);
this.isExecuting(true);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateUDF, {
@@ -104,9 +104,9 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
tabTitle: this.tabTitle()
});
return createUserDefinedFunction(this.collection, resource)
return createUserDefinedFunction(this.collection.databaseId, this.collection.id(), resource)
.then(
(createdResource: DataModels.UserDefinedFunction) => {
createdResource => {
this.tabTitle(createdResource.id);
this.isNew(false);
this.resource(createdResource);
@@ -118,8 +118,8 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
this.setBaselines();
const editorModel = this.editor().getModel();
editorModel.setValue(createdResource.body);
this.editorContent.setBaseline(createdResource.body);
editorModel.setValue(createdResource.body as string);
this.editorContent.setBaseline(createdResource.body as string);
this.node = this.collection.createUserDefinedFunctionNode(createdResource);
TelemetryProcessor.traceSuccess(
@@ -147,14 +147,14 @@ export default class UserDefinedFunctionTab extends ScriptTabBase {
},
startKey
);
return Q.reject(createError);
return Promise.reject(createError);
}
)
.finally(() => this.isExecuting(false));
}
private _getResource() {
const resource: DataModels.UserDefinedFunction = <DataModels.UserDefinedFunction>{
const resource = {
_rid: this.resource()._rid,
_self: this.resource()._self,
id: this.id(),

View File

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

View File

@@ -12,8 +12,8 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as Logger from "../../Common/Logger";
import Explorer from "../Explorer";
import { readOffers, readOffer } from "../../Common/DocumentClientUtilityBase";
import { readCollections } from "../../Common/dataAccess/readCollections";
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
export default class Database implements ViewModels.Database {
public nodeKind: string;
@@ -27,13 +27,13 @@ export default class Database implements ViewModels.Database {
public isDatabaseShared: ko.Computed<boolean>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
constructor(container: Explorer, data: any, offer: DataModels.Offer) {
constructor(container: Explorer, data: any) {
this.nodeKind = "Database";
this.container = container;
this.self = data._self;
this.rid = data._rid;
this.id = ko.observable(data.id);
this.offer = ko.observable(offer);
this.offer = ko.observable();
this.collections = ko.observableArray<Collection>();
this.isDatabaseExpanded = ko.observable<boolean>(false);
this.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
@@ -66,7 +66,7 @@ export default class Database implements ViewModels.Database {
dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Scale"
});
Q.all([pendingNotificationsPromise, this.readSettings()]).then(
pendingNotificationsPromise.then(
(data: any) => {
const pendingNotification: DataModels.Notification = data && data[0];
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 {
return (
!this.isDatabaseExpanded() &&
@@ -219,23 +145,13 @@ export default class Database implements ViewModels.Database {
});
}
public expandCollapseDatabase() {
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() {
public async expandDatabase() {
if (this.isDatabaseExpanded()) {
return;
}
this.loadCollections();
await this.loadOffer();
await this.loadCollections();
this.isDatabaseExpanded(true);
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
description: "Database node",
@@ -259,32 +175,19 @@ export default class Database implements ViewModels.Database {
});
}
public loadCollections(): Q.Promise<void> {
let collectionVMs: Collection[] = [];
let deferred: Q.Deferred<void> = Q.defer<void>();
public async loadCollections(): Promise<void> {
const collectionVMs: Collection[] = [];
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) => {
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
collectionVMs.push(collectionVM);
});
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
collectionVMs.push(collectionVM);
});
//merge collections
this.addCollectionsToList(collectionVMs);
this.deleteCollectionsFromList(deltaCollections.toDelete);
deferred.resolve();
},
(error: any) => {
deferred.reject(error);
}
);
return deferred.promise;
//merge collections
this.addCollectionsToList(collectionVMs);
this.deleteCollectionsFromList(deltaCollections.toDelete);
}
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);
}
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> {
if (!this.container) {
return Q.resolve(undefined);
@@ -387,8 +301,4 @@ export default class Database implements ViewModels.Database {
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: [],
isSelected: () => this.isDataNodeSelected(database.rid, "Database", undefined),
contextMenu: ResourceTreeContextMenuButtonFactory.createDatabaseContextMenu(this.container, database),
onClick: isExpanded => {
onClick: async isExpanded => {
// Rewritten version of expandCollapseDatabase():
if (!isExpanded) {
database.expandDatabase();
database.loadCollections();
} else {
if (isExpanded) {
database.collapseDatabase();
} else {
if (databaseNode.children?.length === 0) {
databaseNode.isLoading = true;
}
await database.expandDatabase();
}
databaseNode.isLoading = false;
database.selectDatabase();
this.container.onUpdateTabsButtons([]);
this.container.tabsManager.refreshActiveTab(
@@ -203,6 +206,12 @@ export class ResourceTreeAdapter implements ReactAdapter {
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;
});

View File

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

View File

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

View File

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

View File

@@ -35,13 +35,19 @@ const onInit = async () => {
const galleryItemJunoResponse = await junoClient.getNotebookInfo(galleryItemId);
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 = (
notebookUrl: string,
backNavigationText: string,
hideInputs: boolean,
hideInputs?: boolean,
hidePrompts?: boolean,
galleryItem?: IGalleryItem,
onBackClick?: () => void
) => {
@@ -51,6 +57,7 @@ const render = (
galleryItem,
backNavigationText,
hideInputs,
hidePrompts,
onBackClick: onBackClick,
onTagClick: undefined
};

View File

@@ -70,7 +70,8 @@ export enum Action {
NotebooksGitHubManualRepoAdd,
NotebooksGitHubManageRepo,
NotebooksGitHubCommit,
NotebooksGitHubDisconnect
NotebooksGitHubDisconnect,
OpenTerminal
}
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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -112,7 +112,7 @@ export function traceMark(action: Action, data?: unknown, timestamp?: number): n
return validTimestamp;
}
function getData(data: unknown = {}): { [key: string]: string } | undefined {
function getData(actionModifier: string, data: unknown = {}): { [key: string]: string } | undefined {
if (typeof data === "string") {
data = { message: data };
}
@@ -124,6 +124,7 @@ function getData(data: unknown = {}): { [key: string]: string } | undefined {
subscriptionId: userContext.subscriptionId as string,
platform: configContext.platform,
env: process.env.NODE_ENV as string,
actionModifier,
...data
};
}

View File

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

View File

@@ -4,6 +4,8 @@ import "@jupyterlab/terminal/style/index.css";
import "./index.css";
import { ServerConnection } from "@jupyterlab/services";
import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
import { Action } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
const getUrlVars = (): { [key: string]: string } => {
const vars: { [key: string]: string } = {};
@@ -14,10 +16,7 @@ const getUrlVars = (): { [key: string]: string } => {
return vars;
};
const main = (): void => {
const urlVars = getUrlVars();
console.log("URL parameters", urlVars);
const createServerSettings = (urlVars: { [key: string]: string }): ServerConnection.ISettings => {
let body: BodyInit;
if (urlVars.hasOwnProperty("terminalEndpoint")) {
body = JSON.stringify({
@@ -39,14 +38,29 @@ const main = (): void => {
fetch: window.parent.fetch
};
}
const serverSettings = ServerConnection.makeSettings(options);
if (urlVars.hasOwnProperty("terminal")) {
JupyterLabAppFactory.createTerminalApp(serverSettings);
return;
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")) {
await JupyterLabAppFactory.createTerminalApp(serverSettings);
} else {
throw new Error("Only terminal is supported");
}
TelemetryProcessor.traceSuccess(Action.OpenTerminal, startTime);
} catch (error) {
TelemetryProcessor.traceFailure(Action.OpenTerminal, startTime);
}
throw new Error("Only terminal is supported");
};
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);
}
function isValidOrigin(allowedOrigins: RegExp, event: MessageEvent): boolean {
function isValidOrigin(allowedOrigins: string[], event: MessageEvent): boolean {
const eventOrigin = (event && event.origin) || "";
const windowOrigin = (window && window.origin) || "";
if (eventOrigin === windowOrigin) {
return true;
}
const result = allowedOrigins && allowedOrigins.test(eventOrigin);
return result;
for (const origin of allowedOrigins) {
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 { ErrorResponse } from "./generatedClients/2020-04-01/types";
import { userContext } from "../../UserContext";
interface ARMError extends Error {
interface ErrorResponse {
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 {
@@ -33,9 +52,20 @@ export async function armRequest<T>({ host, path, apiVersion, method, body: requ
body: requestBody ? JSON.stringify(requestBody) : undefined
});
if (!response.ok) {
const errorResponse = (await response.json()) as ErrorResponse;
const error = new Error(errorResponse.message) as ARMError;
error.code = errorResponse.code;
let error: 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;
}
} catch (error) {
throw new Error(await response.text());
}
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.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
// 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.waitFor(2000)
// click delete container
await frame.waitForSelector('body > div.ms-Layer.ms-Layer--fixed');
await frame.waitFor(1000);
const elements = await frame.$$('span[class="treeComponentMenuItemLabel"]')
await elements[4].click();
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]', { visible: true });
await frame.click('span[class="treeComponentMenuItemLabel deleteCollectionMenuItemLabel"]');
// confirm delete container
await frame.waitFor('input[data-test="confirmCollectionId"]', { visible: true })
await frame.type('input[data-test="confirmCollectionId"]', collectionId.trim());
// click delete
await frame.waitFor('input[data-test="deleteCollection"]', { visible: true })
await frame.click('input[data-test="deleteCollection"]');
await frame.waitFor(5000);
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();
// click delete database
await frame.waitFor(1000);
const dbElements = await frame.$$('span[class="treeComponentMenuItemLabel"]')
await dbElements[1].click();
await frame.waitFor('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
await frame.click('span[class="treeComponentMenuItemLabel deleteDatabaseMenuItemLabel"]');
// confirm delete database
await frame.type('input[data-test="confirmDatabaseId"]', dbId.trim());