Compare commits

..

4 Commits

Author SHA1 Message Date
Steve Faulkner
8180c03969 Merge master into remove-redundant-e2e-tests 2020-10-07 09:22:08 -05:00
Steve Faulkner
ff1cb5603b Merge branch 'master' into remove-redundant-e2e-tests 2020-10-06 22:32:42 -05:00
Steve Faulkner
2dbbf6baab Remove deps 2020-10-05 18:53:31 -05:00
Steve Faulkner
4c11a940a0 Remove redundant E2E tests 2020-10-05 18:51:21 -05:00
157 changed files with 3678 additions and 2587 deletions

29
.github/workflows/automerge.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: automerge
on:
pull_request:
types:
- labeled
- unlabeled
- synchronize
- opened
- edited
- ready_for_review
- reopened
- unlocked
pull_request_review:
types:
- submitted
check_suite:
types:
- completed
status: {}
jobs:
automerge:
runs-on: ubuntu-latest
steps:
- name: automerge
uses: "pascalgn/automerge-action@v0.11.0"
env:
GITHUB_TOKEN: "${{ secrets.AUTOMERGE_GITHUB_PAT }}"
MERGE_METHOD: "squash"
MERGE_COMMIT_MESSAGE: "pull-request-title"

View File

@@ -1,4 +1,4 @@
# Cosmos DB Explorer
# CosmosDB Explorer
UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), https://cosmos.azure.com/, and the [Cosmos DB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator)

View File

@@ -7,7 +7,7 @@ trigger:
- master
pool:
vmImage: "ubuntu-latest"
vmImage: 'ubuntu-latest'
steps:
- task: ComponentGovernanceComponentDetection@0
- task: ComponentGovernanceComponentDetection@0

177
externals/jquery.contextMenu.css vendored Normal file
View File

@@ -0,0 +1,177 @@
/*!
* jQuery contextMenu - Plugin for simple contextMenu handling
*
* Version: 1.6.6
*
* Authors: Rodney Rehm, Addy Osmani (patches for FF)
* Web: http://medialize.github.com/jQuery-contextMenu/
*
* Licensed under
* MIT License http://www.opensource.org/licenses/mit-license
* GPL v3 http://opensource.org/licenses/GPL-3.0
*
*/
.context-menu-list {
z-index: 1001;
position: fixed;
background: white;
border: solid 1px gainsboro;
box-shadow: 4px 4px 4px -2px #888888;
padding: 8px 0px 8px 0px;
line-height: 25px;
width: 254px;
list-style: none;
margin-left: -10px;
outline: 0px #fff;
}
.context-menu-item {
padding: 2px 2px 2px 31px;
background-color: #fff;
position: relative;
-webkit-user-select: none;
-moz-user-select: -moz-none;
-ms-user-select: none;
user-select: none;
}
.context-menu-separator {
padding-bottom: 0;
border-bottom: 1px solid #DDD;
}
.context-menu-item>label>input,
.context-menu-item>label>textarea {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
margin-left: -10px;
}
.context-menu-item:hover {
cursor: pointer;
background-color: #eeeeee;
}
.context-menu-item.disabled {
color: #666;
}
.context-menu-input.hover,
.context-menu-item.disabled.hover {
cursor: default;
background-color: #EEE;
}
.context-menu-submenu:after {
content: ">";
color: #666;
position: absolute;
top: 0;
right: 3px;
z-index: 1;
}
/* icons
#protip:
In case you want to use sprites for icons (which I would suggest you do) have a look at
http://css-tricks.com/13224-pseudo-spriting/ to get an idea of how to implement
.context-menu-item.icon:before {}
*/
.context-menu-item.icon {
min-height: 18px;
background-repeat: no-repeat;
background-position: 10px 7px;
}
.context-menu-item.icon:hover {
min-height: 18px;
background-repeat: no-repeat;
background-position: 10px 7px;
}
/*.context-menu-item.icon-edit {
background-image: url(images/page_white_edit.png);
}
.context-menu-item.icon-cut {
background-image: url(images/cut.png);
}
.context-menu-item.icon-copy {
background-image: url(images/page_white_copy.png);
}
.context-menu-item.icon-paste {
background-image: url(images/page_white_paste.png);
}
.context-menu-item.icon-delete {
background-image: url(images/page_white_delete.png);
}
.context-menu-item.icon-add {
background-image: url(images/page_white_add.png);
}
.context-menu-item.icon-quit {
background-image: url(images/door.png);
}*/
/* vertically align inside labels */
.context-menu-input>label>* {
vertical-align: top;
}
/* position checkboxes and radios as icons */
.context-menu-input>label>input[type="checkbox"],
.context-menu-input>label>input[type="radio"] {
margin-left: -17px;
}
.context-menu-input>label>span {
margin-left: 5px;
}
.context-menu-input>label,
.context-menu-input>label>input[type="text"],
.context-menu-input>label>textarea,
.context-menu-input>label>select {
display: block;
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
}
.context-menu-input>label>textarea {
height: 100px;
}
.context-menu-item>.context-menu-list {
display: none;
/* re-positioned by js */
right: -5px;
top: 5px;
}
/*.context-menu-item.hover>.context-menu-list {
display: block;
padding-left: 5px;
}*/
.context-menu-accesskey {
text-decoration: underline;
}

1686
externals/jquery.contextMenu.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ const isCI = require("is-ci");
module.exports = {
launch: {
headless: isCI,
slowMo: 55,
slowMo: 30,
defaultViewport: null,
ignoreHTTPSErrors: true,
args: ["--disable-web-security"]

View File

@@ -2082,8 +2082,7 @@ a:link {
.resourceTreeAndTabs {
display: flex;
flex: 1 1 auto;
overflow-x: auto;
overflow-y: hidden;
overflow: auto;
height: 100%;
}

92
package-lock.json generated
View File

@@ -2326,83 +2326,83 @@
}
},
"@microsoft/applicationinsights-analytics-js": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-2.5.9.tgz",
"integrity": "sha512-9L3fb1H1as+J3J2j2EDx1HEMdrucjgR4INqahy+ZAxDPFvR3HCOedYzx645zObBIPu7QkH2LAjPk4fuNGHR1rg==",
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-2.5.8.tgz",
"integrity": "sha512-2RsftiXa5ojNXuHRIC7RybWbN+Z7TMrLK2XGTza1Wq/KHRJNB48WmQuxjd6SzsNguqxRoHsH0sUogIwlK+NO8A==",
"requires": {
"@microsoft/applicationinsights-common": "2.5.9",
"@microsoft/applicationinsights-core-js": "2.5.9",
"@microsoft/applicationinsights-shims": "1.0.3",
"@microsoft/applicationinsights-common": "2.5.8",
"@microsoft/applicationinsights-core-js": "2.5.8",
"@microsoft/applicationinsights-shims": "1.0.1",
"@microsoft/dynamicproto-js": "^1.0.0"
}
},
"@microsoft/applicationinsights-channel-js": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.5.9.tgz",
"integrity": "sha512-NAQ/2wWmD+gaIZDCMzzwxm8RcbswDvUO5BYeuW9UHJaFuEZ9o9xpztKVz32u4CMv7OI/mLOqnmR4rb0d+kUMwQ==",
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-2.5.8.tgz",
"integrity": "sha512-etVGzhNluflTikOpW9dlDxdg+B+8gt/nxmGeXqti9Hsqq0fTxcY9bmNklFIEKhOIwai4DodgJjflaDdgR0ObUQ==",
"requires": {
"@microsoft/applicationinsights-common": "2.5.9",
"@microsoft/applicationinsights-core-js": "2.5.9",
"@microsoft/applicationinsights-shims": "1.0.3",
"@microsoft/applicationinsights-common": "2.5.8",
"@microsoft/applicationinsights-core-js": "2.5.8",
"@microsoft/applicationinsights-shims": "1.0.1",
"@microsoft/dynamicproto-js": "^1.0.0"
}
},
"@microsoft/applicationinsights-common": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.5.9.tgz",
"integrity": "sha512-dKmXO9m55uRDhpoa0P7l+BApf+lsrqjgoLeKv+ABM8ygIyd9JH6CDcdaT3af+kUFtt9Oj3ChyfueKr1EVOdGkQ==",
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-2.5.8.tgz",
"integrity": "sha512-RIMAJTsF0H5wiRZxYIF9H8sbMe2W5Ig4yMuH0/lho69DcNZpHf8p6PSa4Qhhli0AnoWYfLE7/WlWO1eR5SkByw==",
"requires": {
"@microsoft/applicationinsights-core-js": "2.5.9",
"@microsoft/applicationinsights-shims": "1.0.3"
"@microsoft/applicationinsights-core-js": "2.5.8",
"@microsoft/applicationinsights-shims": "1.0.1"
}
},
"@microsoft/applicationinsights-core-js": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.5.9.tgz",
"integrity": "sha512-KE9h1wmC/Ckm7jYjsMF1SEWQnk0v0CRzZq1upSARgPH7BgmyClXz1kdnLtuTWz8Aha8IIH9dW2hUOfPCdR+BpQ==",
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.5.8.tgz",
"integrity": "sha512-NxxIViHKuqgpla+KdQk7Qy48Dm8lN4oy2mMEpv9kM5GW5MBJ8nZ4A5RV1kokF3kXuDmTUTHlWBXeLR8hauA3qQ==",
"requires": {
"@microsoft/applicationinsights-shims": "1.0.3",
"@microsoft/applicationinsights-shims": "1.0.1",
"@microsoft/dynamicproto-js": "^1.0.0"
}
},
"@microsoft/applicationinsights-dependencies-js": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-dependencies-js/-/applicationinsights-dependencies-js-2.5.9.tgz",
"integrity": "sha512-pNM/dkUOscV0ul/YJe928+77EBtRkRXO/le/VWzlunoUFaEEo4pirc7NycvPx9w/KxA62JMEogbQsWE6nAmqPg==",
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-dependencies-js/-/applicationinsights-dependencies-js-2.5.8.tgz",
"integrity": "sha512-jnXpjE/bnlLeew78OsN8aPTLOPZQJ4y1MOO8R3E+eUXdproD2TemynSk5kUfrMdry91DZOBZnrmJ2NCB+g5ArQ==",
"requires": {
"@microsoft/applicationinsights-common": "2.5.9",
"@microsoft/applicationinsights-core-js": "2.5.9",
"@microsoft/applicationinsights-shims": "1.0.3",
"@microsoft/applicationinsights-common": "2.5.8",
"@microsoft/applicationinsights-core-js": "2.5.8",
"@microsoft/applicationinsights-shims": "1.0.1",
"@microsoft/dynamicproto-js": "^1.0.0"
}
},
"@microsoft/applicationinsights-properties-js": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-properties-js/-/applicationinsights-properties-js-2.5.9.tgz",
"integrity": "sha512-mZxaC8CZsURn38IwsPaUx+o9QXQU2vm81THZL+1Lc+7scPo55ATDTFgZ2awIj7CdTp69oGzUkpB7maOn6+OVOw==",
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-properties-js/-/applicationinsights-properties-js-2.5.8.tgz",
"integrity": "sha512-BAOSguXe07ua6SqYmAW+8+vVvBZb4qmmUP6s5zc7kNMMgEoEGsCn2VHmM8wHHxq4J/TmYxIqwoufS+XKbUvCeQ==",
"requires": {
"@microsoft/applicationinsights-common": "2.5.9",
"@microsoft/applicationinsights-core-js": "2.5.9",
"@microsoft/applicationinsights-shims": "1.0.3"
"@microsoft/applicationinsights-common": "2.5.8",
"@microsoft/applicationinsights-core-js": "2.5.8",
"@microsoft/applicationinsights-shims": "1.0.1"
}
},
"@microsoft/applicationinsights-shims": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-1.0.3.tgz",
"integrity": "sha512-+S17aqEkOYpyBpmclhgwcEplwnxSo5AxYBdRg38GBobI1GKPSpZfnLssLzcjJ6XZCS5tqB5xjyTZs6gHj7ZJWQ=="
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-1.0.1.tgz",
"integrity": "sha512-nPjUBSpvX5Dnkovp2lZzrg0/uSvYNtbAclWwVP7t8J1hy5OJ3xr3KPNaz79+b84G16Rj861ybau9Gbk7inXkTg=="
},
"@microsoft/applicationinsights-web": {
"version": "2.5.9",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web/-/applicationinsights-web-2.5.9.tgz",
"integrity": "sha512-dxg5XXbQqjWw9QmGdgbd7knb1qFA58FFYj9ObqRmlqiihk25kper7H15HH8LaV0lV6goClmBWc9KsNGA2veyeA==",
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web/-/applicationinsights-web-2.5.8.tgz",
"integrity": "sha512-ZxCUkBJrCFNHa0LgWWbtwqu4TxJPlukuSvDrdLv6XV1yX2ETq6Q1kw/IUEtKhbtNbTZQ8aJ+x8Nc/iqbssdXTA==",
"requires": {
"@microsoft/applicationinsights-analytics-js": "2.5.9",
"@microsoft/applicationinsights-channel-js": "2.5.9",
"@microsoft/applicationinsights-common": "2.5.9",
"@microsoft/applicationinsights-core-js": "2.5.9",
"@microsoft/applicationinsights-dependencies-js": "2.5.9",
"@microsoft/applicationinsights-properties-js": "2.5.9",
"@microsoft/applicationinsights-shims": "1.0.3",
"@microsoft/applicationinsights-analytics-js": "2.5.8",
"@microsoft/applicationinsights-channel-js": "2.5.8",
"@microsoft/applicationinsights-common": "2.5.8",
"@microsoft/applicationinsights-core-js": "2.5.8",
"@microsoft/applicationinsights-dependencies-js": "2.5.8",
"@microsoft/applicationinsights-properties-js": "2.5.8",
"@microsoft/applicationinsights-shims": "1.0.1",
"@microsoft/dynamicproto-js": "^1.0.0"
}
},

View File

@@ -8,7 +8,7 @@
"@azure/cosmos-language-service": "0.0.4",
"@jupyterlab/services": "6.0.0-rc.2",
"@jupyterlab/terminal": "3.0.0-rc.2",
"@microsoft/applicationinsights-web": "2.5.9",
"@microsoft/applicationinsights-web": "2.5.8",
"@nteract/commutable": "7.3.2",
"@nteract/connected-components": "6.8.2",
"@nteract/core": "15.1.0",
@@ -88,8 +88,8 @@
"styled-components": "4.3.2",
"text-encoding": "0.7.0",
"underscore": "1.9.1",
"url-polyfill": "1.1.7",
"utility-types": "3.10.0",
"url-polyfill": "1.1.7",
"webcrypto-liner": "1.1.4",
"webfontloader": "1.6.28",
"whatwg-fetch": "3.0.0"

View File

@@ -117,6 +117,7 @@ export class Features {
public static readonly enableGalleryPublish = "enablegallerypublish";
public static readonly enableCodeOfConduct = "enablecodeofconduct";
public static readonly enableLinkInjection = "enablelinkinjection";
public static readonly enableSettingsV2 = "enablesettingsv2";
public static readonly enableSpark = "enablespark";
public static readonly livyEndpoint = "livyendpoint";
public static readonly notebookServerUrl = "notebookserverurl";
@@ -130,11 +131,6 @@ export class Features {
public static readonly enableSDKoperations = "enablesdkoperations";
}
// flight names returned from the portal are always lowercase
export class Flights {
public static readonly SettingsV2 = "settingsv2";
}
export class AfecFeatures {
public static readonly Spark = "spark-public-preview";
public static readonly Notebooks = "sparknotebooks-public-preview";

View File

@@ -112,6 +112,7 @@ describe("endpoint", () => {
describe("requestPlugin", () => {
beforeEach(() => {
delete window.dataExplorerPlatform;
resetConfigContext();
});

View File

@@ -72,6 +72,22 @@ export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.Partiti
return [partitionKeyValue];
}
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 updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,

View File

@@ -157,6 +157,41 @@ export function updateDocument(
return deferred.promise;
}
export function updateOffer(
offer: DataModels.Offer,
newOffer: DataModels.Offer,
options: RequestOptions
): Q.Promise<DataModels.Offer> {
var deferred = Q.defer<any>();
const clearMessage = logConsoleProgress(`Updating offer for resource ${offer.resource}`);
DataAccessUtilityBase.updateOffer(offer, newOffer, options)
.then(
(replacedOffer: DataModels.Offer) => {
logConsoleInfo(`Successfully updated offer for resource ${offer.resource}`);
deferred.resolve(replacedOffer);
},
(error: any) => {
logConsoleError(`Error updating offer for resource ${offer.resource}: ${JSON.stringify(error)}`);
Logger.logError(
JSON.stringify({
oldOffer: offer,
newOffer: newOffer,
error: error
}),
"UpdateOffer",
error.code
);
sendNotificationForError(error);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();

View File

@@ -0,0 +1,46 @@
import "jquery";
import * as Q from "q";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { userContext } from "../UserContext";
export class NotificationsClientBase {
private _extensionEndpoint: string;
private _notificationsApiSuffix: string;
protected constructor(notificationsApiSuffix: string) {
this._notificationsApiSuffix = notificationsApiSuffix;
}
public fetchNotifications(): Q.Promise<DataModels.Notification[]> {
const deferred: Q.Deferred<DataModels.Notification[]> = Q.defer<DataModels.Notification[]>();
const databaseAccount = userContext.databaseAccount;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const url = `${this._extensionEndpoint}${this._notificationsApiSuffix}?accountName=${databaseAccount.name}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers: any = {};
headers[authorizationHeader.header] = authorizationHeader.token;
$.ajax({
url: url,
type: "GET",
headers: headers,
cache: false
}).then(
(notifications: DataModels.Notification[], textStatus: string, xhr: JQueryXHR<any>) => {
deferred.resolve(notifications);
},
(xhr: JQueryXHR<any>, textStatus: string, error: any) => {
deferred.reject(xhr.responseText);
}
);
return deferred.promise;
}
public setExtensionEndpoint(extensionEndpoint: string): void {
this._extensionEndpoint = extensionEndpoint;
}
}

View File

@@ -1,41 +0,0 @@
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { userContext } from "../UserContext";
import { configContext, Platform } from "../ConfigContext";
const notificationsPath = () => {
switch (configContext.platform) {
case Platform.Hosted:
return "/api/guest/notifications";
case Platform.Portal:
return "/api/notifications";
default:
throw new Error(`Unknown platform: ${configContext.platform}`);
}
};
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
if (configContext.platform === Platform.Emulator) {
return [];
}
const databaseAccount = userContext.databaseAccount;
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const url = `${configContext.BACKEND_ENDPOINT}${notificationsPath()}?accountName=${
databaseAccount.name
}&subscriptionId=${subscriptionId}&resourceGroup=${resourceGroup}`;
const authorizationHeader: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
const response = await window.fetch(url, {
headers
});
if (!response.ok) {
throw new Error(await response.text());
}
return (await response.json()) as DataModels.Notification[];
};

View File

@@ -1,76 +1,28 @@
import { AuthType } from "../../AuthType";
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import {
SqlStoredProcedureCreateUpdateParameters,
SqlStoredProcedureResource
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient";
import {
createUpdateSqlStoredProcedure,
getSqlStoredProcedure
} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
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 {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
try {
const getResponse = await getSqlStoredProcedure(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
storedProcedure.id
);
if (getResponse?.properties?.resource) {
throw new Error(
`Create stored procedure failed: stored procedure with id ${storedProcedure.id} already exists`
);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const createSprocParams: SqlStoredProcedureCreateUpdateParameters = {
properties: {
resource: storedProcedure as SqlStoredProcedureResource,
options: {}
}
};
const rpResponse = await createUpdateSqlStoredProcedure(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
storedProcedure.id,
createSprocParams
);
return rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
}
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.storedProcedures.create(storedProcedure);
return response?.resource;
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);
throw error;
} finally {
clearMessage();
}
clearMessage();
return createdStoredProcedure;
}

View File

@@ -1,71 +1,28 @@
import { AuthType } from "../../AuthType";
import { Resource, TriggerDefinition } from "@azure/cosmos";
import {
SqlTriggerCreateUpdateParameters,
SqlTriggerResource
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient";
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
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 {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
try {
const getResponse = await getSqlTrigger(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
trigger.id
);
if (getResponse?.properties?.resource) {
throw new Error(`Create trigger failed: trigger with id ${trigger.id} already exists`);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const createTriggerParams: SqlTriggerCreateUpdateParameters = {
properties: {
resource: trigger as SqlTriggerResource,
options: {}
}
};
const rpResponse = await createUpdateSqlTrigger(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
trigger.id,
createTriggerParams
);
return rpResponse && (rpResponse.properties?.resource as TriggerDefinition & Resource);
}
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.triggers.create(trigger);
return response.resource;
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);
throw error;
} finally {
clearMessage();
}
clearMessage();
return createdTrigger;
}

View File

@@ -1,76 +1,28 @@
import { AuthType } from "../../AuthType";
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
import {
SqlUserDefinedFunctionCreateUpdateParameters,
SqlUserDefinedFunctionResource
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient";
import {
createUpdateSqlUserDefinedFunction,
getSqlUserDefinedFunction
} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
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 {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
try {
const getResponse = await getSqlUserDefinedFunction(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
userDefinedFunction.id
);
if (getResponse?.properties?.resource) {
throw new Error(
`Create user defined function failed: user defined function with id ${userDefinedFunction.id} already exists`
);
}
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
}
const createUDFParams: SqlUserDefinedFunctionCreateUpdateParameters = {
properties: {
resource: userDefinedFunction as SqlUserDefinedFunctionResource,
options: {}
}
};
const rpResponse = await createUpdateSqlUserDefinedFunction(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
userDefinedFunction.id,
createUDFParams
);
return rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
}
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunctions.create(userDefinedFunction);
return response?.resource;
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);
throw error;
} finally {
clearMessage();
}
clearMessage();
return createdUserDefinedFunction;
}

View File

@@ -1,10 +1,7 @@
import { AuthType } from "../../AuthType";
import { client } from "../CosmosClient";
import { deleteSqlStoredProcedure } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export async function deleteStoredProcedure(
databaseId: string,
@@ -13,28 +10,17 @@ export async function deleteStoredProcedure(
): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting stored procedure ${storedProcedureId}`);
try {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
await deleteSqlStoredProcedure(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
storedProcedureId
);
} else {
await client()
.database(databaseId)
.container(collectionId)
.scripts.storedProcedure(storedProcedureId)
.delete();
}
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);
throw error;
} finally {
clearMessage();
}
clearMessage();
return undefined;
}

View File

@@ -1,36 +1,22 @@
import { AuthType } from "../../AuthType";
import { client } from "../CosmosClient";
import { deleteSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export async function deleteTrigger(databaseId: string, collectionId: string, triggerId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting trigger ${triggerId}`);
try {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
await deleteSqlTrigger(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
triggerId
);
} else {
await client()
.database(databaseId)
.container(collectionId)
.scripts.trigger(triggerId)
.delete();
}
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);
throw error;
} finally {
clearMessage();
}
clearMessage();
return undefined;
}

View File

@@ -1,36 +1,22 @@
import { AuthType } from "../../AuthType";
import { client } from "../CosmosClient";
import { deleteSqlUserDefinedFunction } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export async function deleteUserDefinedFunction(databaseId: string, collectionId: string, id: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting user defined function ${id}`);
try {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
await deleteSqlUserDefinedFunction(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
id
);
} else {
await client()
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunction(id)
.delete();
}
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);
throw error;
} finally {
clearMessage();
}
clearMessage();
return undefined;
}

View File

@@ -20,14 +20,26 @@ export const readDatabaseOffer = async (
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
let offerId = params.offerId;
if (!offerId) {
offerId = await (window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
? getDatabaseOfferIdWithARM(params.databaseId)
: getDatabaseOfferIdWithSDK(params.databaseResourceId));
if (!offerId) {
clearMessage();
return undefined;
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
try {
offerId = await getDatabaseOfferIdWithARM(params.databaseId);
} catch (error) {
clearMessage();
if (error.code !== "NotFound") {
throw new error();
}
return undefined;
}
} else {
offerId = await getDatabaseOfferIdWithSDK(params.databaseResourceId);
if (!offerId) {
clearMessage();
return undefined;
}
}
}
@@ -63,32 +75,24 @@ const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> =>
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const defaultExperience = userContext.defaultExperience;
try {
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;
} catch (error) {
if (error.code !== "NotFound") {
throw error;
}
return undefined;
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): Promise<string> => {

View File

@@ -18,7 +18,9 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table &&
userContext.defaultExperience !== DefaultAccountExperienceType.Cassandra
) {
databases = await readDatabasesWithARM();
} else {

View File

@@ -13,13 +13,10 @@ export const readOffers = async (): Promise<Offer[]> => {
const clearMessage = logConsoleProgress(`Querying offers`);
try {
if (configContext.platform === Platform.Portal) {
const offers = sendCachedDataMessage<Offer[]>(MessageTypes.AllOffers, [
return sendCachedDataMessage<Offer[]>(MessageTypes.AllOffers, [
userContext.databaseAccount.id,
ClientDefaults.portalCacheTimeoutMs
]);
clearMessage();
return offers;
}
} catch (error) {
// If error getting cached Offers, continue on and read via SDK

View File

@@ -1,41 +1,27 @@
import { AuthType } from "../../AuthType";
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import { client } from "../CosmosClient";
import { listSqlStoredProcedures } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
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 {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
const rpResponse = await listSqlStoredProcedures(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId
);
return rpResponse?.value?.map(sproc => sproc.properties?.resource as StoredProcedureDefinition & Resource);
}
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.storedProcedures.readAll()
.fetchAll();
return response?.resources;
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);
throw error;
} finally {
clearMessage();
}
clearMessage();
return sprocs;
}

View File

@@ -1,41 +1,27 @@
import { AuthType } from "../../AuthType";
import { Resource, TriggerDefinition } from "@azure/cosmos";
import { client } from "../CosmosClient";
import { listSqlTriggers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export async function readTriggers(
databaseId: string,
collectionId: string
): Promise<(TriggerDefinition & Resource)[]> {
let triggers: (TriggerDefinition & Resource)[];
const clearMessage = logConsoleProgress(`Querying triggers for container ${collectionId}`);
try {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
const rpResponse = await listSqlTriggers(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId
);
return rpResponse?.value?.map(trigger => trigger.properties?.resource as TriggerDefinition & Resource);
}
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.triggers.readAll()
.fetchAll();
return response?.resources;
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);
throw error;
} finally {
clearMessage();
}
clearMessage();
return triggers;
}

View File

@@ -1,41 +1,27 @@
import { AuthType } from "../../AuthType";
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { client } from "../CosmosClient";
import { listSqlUserDefinedFunctions } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
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 {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
const rpResponse = await listSqlUserDefinedFunctions(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId
);
return rpResponse?.value?.map(udf => udf.properties?.resource as UserDefinedFunctionDefinition & Resource);
}
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunctions.readAll()
.fetchAll();
return response?.resources;
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);
throw error;
} finally {
clearMessage();
}
clearMessage();
return udfs;
}

View File

@@ -41,7 +41,6 @@ export async function updateCollection(
try {
if (
window.authType === AuthType.AAD &&
!userContext.useSDKOperations &&
userContext.defaultExperience !== DefaultAccountExperienceType.MongoDB &&
userContext.defaultExperience !== DefaultAccountExperienceType.Table
) {
@@ -53,18 +52,16 @@ export async function updateCollection(
.replace(newCollection as ContainerDefinition, options);
collection = sdkResponse.resource as Collection;
}
logConsoleInfo(`Successfully updated container ${collectionId}`);
await refreshCachedResources();
return collection;
} catch (error) {
logConsoleError(`Failed to update container ${collectionId}: ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "UpdateCollection", error.code);
sendNotificationForError(error);
throw error;
} finally {
clearMessage();
}
logConsoleInfo(`Successfully updated container ${collectionId}`);
clearMessage();
await refreshCachedResources();
return collection;
}
async function updateCollectionWithARM(

View File

@@ -1,425 +0,0 @@
import { AuthType } from "../../AuthType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { HttpHeaders } from "../Constants";
import { Offer, UpdateOfferParams } from "../../Contracts/DataModels";
import { OfferDefinition } from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { logError } from "../Logger";
import { readCollectionOffer } from "./readCollectionOffer";
import { readDatabaseOffer } from "./readDatabaseOffer";
import { refreshCachedOffers, refreshCachedResources } from "../DataAccessUtilityBase";
import { sendNotificationForError } from "./sendNotificationForError";
import {
updateSqlDatabaseThroughput,
migrateSqlDatabaseToAutoscale,
migrateSqlDatabaseToManualThroughput,
migrateSqlContainerToAutoscale,
migrateSqlContainerToManualThroughput,
updateSqlContainerThroughput
} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import {
updateCassandraKeyspaceThroughput,
migrateCassandraKeyspaceToAutoscale,
migrateCassandraKeyspaceToManualThroughput,
migrateCassandraTableToAutoscale,
migrateCassandraTableToManualThroughput,
updateCassandraTableThroughput
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
import {
updateMongoDBDatabaseThroughput,
migrateMongoDBDatabaseToAutoscale,
migrateMongoDBDatabaseToManualThroughput,
migrateMongoDBCollectionToAutoscale,
migrateMongoDBCollectionToManualThroughput,
updateMongoDBCollectionThroughput
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import {
updateGremlinDatabaseThroughput,
migrateGremlinDatabaseToAutoscale,
migrateGremlinDatabaseToManualThroughput,
migrateGremlinGraphToAutoscale,
migrateGremlinGraphToManualThroughput,
updateGremlinGraphThroughput
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { userContext } from "../../UserContext";
import {
migrateTableToAutoscale,
migrateTableToManualThroughput,
updateTableThroughput
} from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> => {
let updatedOffer: Offer;
const offerResourceText: string = params.collectionId
? `collection ${params.collectionId}`
: `database ${params.databaseId}`;
const clearMessage = logConsoleProgress(`Updating offer for ${offerResourceText}`);
try {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
if (params.collectionId) {
updatedOffer = await updateCollectionOfferWithARM(params);
} else if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {
// update table's database offer with SDK since RP doesn't support it
updatedOffer = await updateOfferWithSDK(params);
} else {
updatedOffer = await updateDatabaseOfferWithARM(params);
}
} else {
updatedOffer = await updateOfferWithSDK(params);
}
await refreshCachedOffers();
await refreshCachedResources();
logConsoleInfo(`Successfully updated offer for ${offerResourceText}`);
return updatedOffer;
} catch (error) {
logConsoleError(`Error updating offer for ${offerResourceText}: ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "UpdateCollection", error.code);
sendNotificationForError(error);
throw error;
} finally {
clearMessage();
}
};
const updateCollectionOfferWithARM = async (params: UpdateOfferParams): Promise<Offer> => {
try {
switch (userContext.defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
await updateSqlContainerOffer(params);
break;
case DefaultAccountExperienceType.MongoDB:
await updateMongoCollectionOffer(params);
break;
case DefaultAccountExperienceType.Cassandra:
await updateCassandraTableOffer(params);
break;
case DefaultAccountExperienceType.Graph:
await updateGremlinGraphOffer(params);
break;
case DefaultAccountExperienceType.Table:
await updateTableOffer(params);
break;
default:
throw new Error(`Unsupported default experience type: ${userContext.defaultExperience}`);
}
} catch (error) {
if (error.code !== "MethodNotAllowed") {
throw error;
}
}
return await readCollectionOffer({
collectionId: params.collectionId,
databaseId: params.databaseId,
offerId: params.currentOffer.id
});
};
const updateDatabaseOfferWithARM = async (params: UpdateOfferParams): Promise<Offer> => {
try {
switch (userContext.defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
await updateSqlDatabaseOffer(params);
break;
case DefaultAccountExperienceType.MongoDB:
await updateMongoDatabaseOffer(params);
break;
case DefaultAccountExperienceType.Cassandra:
await updateCassandraKeyspaceOffer(params);
break;
case DefaultAccountExperienceType.Graph:
await updateGremlinDatabaseOffer(params);
break;
default:
throw new Error(`Unsupported default experience type: ${userContext.defaultExperience}`);
}
} catch (error) {
if (error.code !== "MethodNotAllowed") {
throw error;
}
}
return await readDatabaseOffer({
databaseId: params.databaseId,
offerId: params.currentOffer.id
});
};
const updateSqlContainerOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateSqlContainerToAutoscale(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else if (params.migrateToManual) {
await migrateSqlContainerToManualThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateSqlContainerThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId,
body
);
}
};
const updateMongoCollectionOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateMongoDBCollectionToAutoscale(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else if (params.migrateToManual) {
await migrateMongoDBCollectionToManualThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateMongoDBCollectionThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId,
body
);
}
};
const updateCassandraTableOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateCassandraTableToAutoscale(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else if (params.migrateToManual) {
await migrateCassandraTableToManualThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateCassandraTableThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId,
body
);
}
};
const updateGremlinGraphOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateGremlinGraphToAutoscale(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else if (params.migrateToManual) {
await migrateGremlinGraphToManualThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId
);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateGremlinGraphThroughput(
subscriptionId,
resourceGroup,
accountName,
params.databaseId,
params.collectionId,
body
);
}
};
const updateTableOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateTableToAutoscale(subscriptionId, resourceGroup, accountName, params.collectionId);
} else if (params.migrateToManual) {
await migrateTableToManualThroughput(subscriptionId, resourceGroup, accountName, params.collectionId);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateTableThroughput(subscriptionId, resourceGroup, accountName, params.collectionId, body);
}
};
const updateSqlDatabaseOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateSqlDatabaseToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId);
} else if (params.migrateToManual) {
await migrateSqlDatabaseToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateSqlDatabaseThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body);
}
};
const updateMongoDatabaseOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateMongoDBDatabaseToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId);
} else if (params.migrateToManual) {
await migrateMongoDBDatabaseToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateMongoDBDatabaseThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body);
}
};
const updateCassandraKeyspaceOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateCassandraKeyspaceToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId);
} else if (params.migrateToManual) {
await migrateCassandraKeyspaceToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateCassandraKeyspaceThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body);
}
};
const updateGremlinDatabaseOffer = async (params: UpdateOfferParams): Promise<void> => {
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
if (params.migrateToAutoPilot) {
await migrateGremlinDatabaseToAutoscale(subscriptionId, resourceGroup, accountName, params.databaseId);
} else if (params.migrateToManual) {
await migrateGremlinDatabaseToManualThroughput(subscriptionId, resourceGroup, accountName, params.databaseId);
} else {
const body: ThroughputSettingsUpdateParameters = createUpdateOfferBody(params);
await updateGremlinDatabaseThroughput(subscriptionId, resourceGroup, accountName, params.databaseId, body);
}
};
const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpdateParameters => {
const body: ThroughputSettingsUpdateParameters = {
properties: {
resource: {}
}
};
if (params.autopilotThroughput) {
body.properties.resource.autoscaleSettings = {
maxThroughput: params.autopilotThroughput
};
} else {
body.properties.resource.throughput = params.manualThroughput;
}
return body;
};
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
const currentOffer = params.currentOffer;
const newOffer: Offer = {
content: {
offerThroughput: undefined,
offerIsRUPerMinuteThroughputEnabled: false
},
_etag: undefined,
_ts: undefined,
_rid: currentOffer._rid,
_self: currentOffer._self,
id: currentOffer.id,
offerResourceId: currentOffer.offerResourceId,
offerVersion: currentOffer.offerVersion,
offerType: currentOffer.offerType,
resource: currentOffer.resource
};
if (params.autopilotThroughput) {
newOffer.content.offerAutopilotSettings = {
maxThroughput: params.autopilotThroughput
};
} else {
newOffer.content.offerThroughput = params.manualThroughput;
}
const options: RequestOptions = {};
if (params.migrateToAutoPilot) {
options.initialHeaders = {
[HttpHeaders.migrateOfferToAutopilot]: "true"
};
delete newOffer.content.offerAutopilotSettings;
} else if (params.migrateToManual) {
options.initialHeaders = {
[HttpHeaders.migrateOfferToManualThroughput]: "true"
};
newOffer.content.offerAutopilotSettings = { maxThroughput: 0 };
}
const sdkResponse = await client()
.offer(params.currentOffer.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);
return sdkResponse?.resource;
};

View File

@@ -1,71 +1,29 @@
import { AuthType } from "../../AuthType";
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import {
SqlStoredProcedureCreateUpdateParameters,
SqlStoredProcedureResource
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient";
import {
createUpdateSqlStoredProcedure,
getSqlStoredProcedure
} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
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 {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
const getResponse = await getSqlStoredProcedure(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
storedProcedure.id
);
if (getResponse?.properties?.resource) {
const createSprocParams: SqlStoredProcedureCreateUpdateParameters = {
properties: {
resource: storedProcedure as SqlStoredProcedureResource,
options: {}
}
};
const rpResponse = await createUpdateSqlStoredProcedure(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
storedProcedure.id,
createSprocParams
);
return rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
}
throw new Error(`Failed to update stored procedure: ${storedProcedure.id} does not exist.`);
}
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.storedProcedure(storedProcedure.id)
.replace(storedProcedure);
return response?.resource;
updatedStoredProcedure = response.resource;
} catch (error) {
const errorMessage = error.code === "NotFound" ? `${storedProcedure.id} does not exist.` : JSON.stringify(error);
logConsoleError(`Error while updating stored procedure ${storedProcedure.id}:\n ${errorMessage}`);
logError(errorMessage, "UpdateStoredProcedure", error.code);
logConsoleError(`Error while updating stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "UpdateStoredProcedure", error.code);
sendNotificationForError(error);
throw error;
} finally {
clearMessage();
}
clearMessage();
return updatedStoredProcedure;
}

View File

@@ -1,68 +1,29 @@
import { AuthType } from "../../AuthType";
import {
SqlTriggerCreateUpdateParameters,
SqlTriggerResource
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { TriggerDefinition } from "@azure/cosmos";
import { client } from "../CosmosClient";
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
export async function updateTrigger(
databaseId: string,
collectionId: string,
trigger: TriggerDefinition
): Promise<TriggerDefinition> {
let updatedTrigger: TriggerDefinition;
const clearMessage = logConsoleProgress(`Updating trigger ${trigger.id}`);
try {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
const getResponse = await getSqlTrigger(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
trigger.id
);
if (getResponse?.properties?.resource) {
const createTriggerParams: SqlTriggerCreateUpdateParameters = {
properties: {
resource: trigger as SqlTriggerResource,
options: {}
}
};
const rpResponse = await createUpdateSqlTrigger(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
trigger.id,
createTriggerParams
);
return rpResponse && (rpResponse.properties?.resource as TriggerDefinition);
}
throw new Error(`Failed to update trigger: ${trigger.id} does not exist.`);
}
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.trigger(trigger.id)
.replace(trigger);
return response?.resource;
updatedTrigger = response.resource;
} catch (error) {
const errorMessage = error.code === "NotFound" ? `${trigger.id} does not exist.` : JSON.stringify(error);
logConsoleError(`Error while updating trigger ${trigger.id}:\n ${errorMessage}`);
logError(errorMessage, "UpdateTrigger", error.code);
logConsoleError(`Error while updating trigger ${trigger.id}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "UpdateTrigger", error.code);
sendNotificationForError(error);
throw error;
} finally {
clearMessage();
}
clearMessage();
return updatedTrigger;
}

View File

@@ -1,72 +1,29 @@
import { AuthType } from "../../AuthType";
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
import {
SqlUserDefinedFunctionCreateUpdateParameters,
SqlUserDefinedFunctionResource
} from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient";
import {
createUpdateSqlUserDefinedFunction,
getSqlUserDefinedFunction
} from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { logError } from "../Logger";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext";
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 {
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
const getResponse = await getSqlUserDefinedFunction(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
userDefinedFunction.id
);
if (getResponse?.properties?.resource) {
const createUDFParams: SqlUserDefinedFunctionCreateUpdateParameters = {
properties: {
resource: userDefinedFunction as SqlUserDefinedFunctionResource,
options: {}
}
};
const rpResponse = await createUpdateSqlUserDefinedFunction(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
databaseId,
collectionId,
userDefinedFunction.id,
createUDFParams
);
return rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
}
throw new Error(`Failed to update user defined function: ${userDefinedFunction.id} does not exist.`);
}
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunction(userDefinedFunction.id)
.replace(userDefinedFunction);
return response?.resource;
updatedUserDefinedFunction = response.resource;
} catch (error) {
const errorMessage =
error.code === "NotFound" ? `${userDefinedFunction.id} does not exist.` : JSON.stringify(error);
logConsoleError(`Error while updating user defined function ${userDefinedFunction.id}:\n ${errorMessage}`);
logError(errorMessage, "UpdateUserupdateUserDefinedFunction", error.code);
logConsoleError(`Error while updating user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`);
logError(JSON.stringify(error), "UpdateUserupdateUserDefinedFunction", error.code);
sendNotificationForError(error);
throw error;
} finally {
clearMessage();
}
clearMessage();
return updatedUserDefinedFunction;
}

View File

@@ -34,7 +34,6 @@ let configContext: Readonly<ConfigContext> = {
allowedParentFrameOrigins: [
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*portal\\.microsoftazure.de$`,
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`
@@ -51,8 +50,7 @@ let configContext: Readonly<ConfigContext> = {
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com"
JUNO_ENDPOINT: "https://tools.cosmos.azure.com"
};
export function resetConfigContext(): void {

View File

@@ -303,16 +303,6 @@ export interface ReadCollectionOfferParams {
offerId?: string;
}
export interface UpdateOfferParams {
currentOffer: Offer;
databaseId: string;
autopilotThroughput: number;
manualThroughput: number;
collectionId?: string;
migrateToAutoPilot?: boolean;
migrateToManual?: boolean;
}
export interface Notification {
id: string;
kind: string;

View File

@@ -85,7 +85,7 @@ export interface Database extends TreeNode {
collapseDatabase(): void;
loadCollections(): Promise<void>;
findCollectionWithId(collectionId: string): Collection;
findCollectionWithId(collectionRid: string): Collection;
openAddCollection(database: Database, event: MouseEvent): void;
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
onSettingsClick: () => void;
@@ -266,6 +266,7 @@ export interface TabOptions {
tabKind: CollectionTabKind;
title: string;
tabPath: string;
selfLink: string;
isActive: ko.Observable<boolean>;
hashLocation: string;
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]) => void;
@@ -304,6 +305,7 @@ export interface QueryTabOptions extends TabOptions {
export interface ScriptTabOption extends TabOptions {
resource: any;
isNew: boolean;
collectionSelfLink?: string;
partitionKey?: DataModels.PartitionKey;
}
@@ -386,7 +388,6 @@ export interface DataExplorerInputsFrame {
dataExplorerVersion?: string;
isAuthWithresourceToken?: boolean;
defaultCollectionThroughput?: CollectionCreationDefaults;
flights?: readonly string[];
}
export interface CollectionCreationDefaults {

42
src/Definitions/jquery.contextmenu.d.ts vendored Normal file
View File

@@ -0,0 +1,42 @@
// Type definitions for jQuery contextMenu 1.7.0
// Project: http://medialize.github.com/jQuery-contextMenu/
// Definitions by: Natan Vivo <https://github.com/nvivo/>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
/// <reference path="jquery.d.ts" />
interface JQueryContextMenuOptions {
selector: string;
appendTo?: string;
trigger?: string;
autoHide?: boolean;
delay?: number;
determinePosition?: (menu: JQuery) => void;
position?: (opt: JQuery, x: number, y: number) => void;
positionSubmenu?: (menu: JQuery) => void;
zIndex?: number;
animation?: {
duration?: number;
show?: string;
hide?: string;
};
events?: {
show?: () => void;
hide?: () => void;
};
callback?: (key: any, options: any) => any;
items?: any;
build?: (triggerElement: JQuery, e: Event) => any;
reposition?: boolean;
className?: string;
itemClickEvent?: string;
}
interface JQueryStatic {
contextMenu(options?: JQueryContextMenuOptions): JQuery;
contextMenu(type: string, selector?: any): JQuery;
}
interface JQuery {
contextMenu(options?: any): JQuery;
}

View File

@@ -3,7 +3,7 @@ import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
import { Link } from "office-ui-fabric-react/lib/Link";
import { ChoiceGroup, FontIcon, IChoiceGroupProps } from "office-ui-fabric-react";
import { FontIcon } from "office-ui-fabric-react";
export interface TextFieldProps extends ITextFieldProps {
label: string;
@@ -24,7 +24,6 @@ export interface DialogProps {
subText: string;
isModal: boolean;
visible: boolean;
choiceGroupProps?: IChoiceGroupProps;
textFieldProps?: TextFieldProps;
linkProps?: LinkProps;
primaryButtonText: string;
@@ -66,7 +65,6 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
minWidth: DIALOG_MIN_WIDTH,
maxWidth: DIALOG_MAX_WIDTH
};
const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps;
const textFieldProps: ITextFieldProps = this.props.textFieldProps;
const linkProps: LinkProps = this.props.linkProps;
const primaryButtonProps: IButtonProps = {
@@ -84,7 +82,6 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
return (
<Dialog {...dialogProps}>
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
{textFieldProps && <TextField {...textFieldProps} />}
{linkProps && (
<Link href={linkProps.linkUrl} target="_blank">

View File

@@ -55,6 +55,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
label: "Enable Injecting Notebook Viewer Link into the first cell",
value: "true"
},
{ key: "feature.enablesettingsv2", label: "Enable SettingsV2 Tab", value: "true" },
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
{
key: "feature.enablefixedcollectionwithsharedthroughput",

View File

@@ -178,6 +178,12 @@ exports[`Feature panel renders all flags 1`] = `
className="checkboxRow"
horizontalAlign="space-between"
>
<StyledCheckboxBase
checked={false}
key="feature.enablesettingsv2"
label="Enable SettingsV2 Tab"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.canexceedmaximumvalue"

View File

@@ -18,9 +18,7 @@ describe("GalleryCardComponent", () => {
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined
newCellId: undefined
},
isFavorite: false,
showDownload: true,

View File

@@ -1,7 +1,6 @@
import {
Dropdown,
FocusZone,
FontWeights,
IDropdownOption,
IPageSpecification,
IPivotItemProps,
@@ -12,8 +11,7 @@ import {
Pivot,
PivotItem,
SearchBox,
Stack,
Text
Stack
} from "office-ui-fabric-react";
import * as React from "react";
import * as Logger from "../../../Common/Logger";
@@ -153,7 +151,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
// Displaying code of conduct component on gallery load should not be the default behavior.
if (this.state.isCodeOfConductAccepted !== false) {
tabs.push(this.createPublishedNotebooksTab(GalleryTab.Published, this.state.publishedNotebooks));
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
}
}
@@ -199,59 +197,10 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
return {
tab,
content: this.createSearchBarHeader(this.createCardsTabContent(data))
content: this.createTabContent(data)
};
}
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
return {
tab,
content: this.createPublishedNotebooksTabContent(data)
};
};
private createPublishedNotebooksTabContent = (data: IGalleryItem[]): JSX.Element => {
const { published, underReview, removed } = GalleryUtils.filterPublishedNotebooks(data);
const content = (
<Stack tokens={{ childrenGap: 10 }}>
{published?.length > 0 &&
this.createPublishedNotebooksSectionContent(
undefined,
"You have successfully published the following notebook(s) to public gallery and shared with other Azure Cosmos DB users.",
this.createCardsTabContent(published)
)}
{underReview?.length > 0 &&
this.createPublishedNotebooksSectionContent(
"Under Review",
"Content of a notebook you published is currently being scanned for illegal content. It will not be available to public gallery until the review is completed (may take a few days)",
this.createCardsTabContent(underReview)
)}
{removed?.length > 0 &&
this.createPublishedNotebooksSectionContent(
"Removed",
"These notebooks were found to contain illegal content and has been taken down.",
this.createPolicyViolationsListContent(removed)
)}
</Stack>
);
return this.createSearchBarHeader(content);
};
private createPublishedNotebooksSectionContent = (
title: string,
description: string,
content: JSX.Element
): JSX.Element => {
return (
<Stack tokens={{ childrenGap: 5 }}>
{title && <Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{title}</Text>}
{description && <Text>{description}</Text>}
{content}
</Stack>
);
};
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
return acceptedCodeOfConduct === false ? (
<CodeOfConductComponent
@@ -261,11 +210,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}}
/>
) : (
this.createSearchBarHeader(this.createCardsTabContent(data))
this.createTabContent(data)
);
}
private createSearchBarHeader(content: JSX.Element): JSX.Element {
private createTabContent(data: IGalleryItem[]): JSX.Element {
return (
<Stack tokens={{ childrenGap: 10 }}>
<Stack horizontal tokens={{ childrenGap: 20, padding: 10 }}>
@@ -278,13 +227,13 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
<Stack.Item styles={{ root: { minWidth: 200 } }}>
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
</Stack.Item>
{(!this.props.container || this.props.container.isGalleryPublishEnabled()) && (
{this.props.container?.isGalleryPublishEnabled() && (
<Stack.Item>
<InfoComponent />
</Stack.Item>
)}
</Stack>
<Stack.Item>{content}</Stack.Item>
{data && this.createCardsTabContent(data)}
</Stack>
);
}
@@ -302,25 +251,6 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
);
}
private createPolicyViolationsListContent(data: IGalleryItem[]): JSX.Element {
return (
<table>
<tbody>
<tr>
<th>Name</th>
<th>Policy violations</th>
</tr>
{data.map(item => (
<tr key={`policy-violations-tr-${item.id}`}>
<td>{item.name}</td>
<td>{item.policyViolations.join(", ")}</td>
</tr>
))}
</tbody>
</table>
);
}
private loadTabContent(tab: GalleryTab, searchText: string, sortBy: SortBy, offline: boolean): void {
switch (tab) {
case GalleryTab.OfficialSamples:

View File

@@ -3,14 +3,10 @@ import { Icon, Label, Stack, HoverCard, HoverCardType, Link } from "office-ui-fa
import { CodeOfConductEndpoints } from "../../../../Common/Constants";
import "./InfoComponent.less";
export interface InfoComponentProps {
onReportAbuseClick?: () => void;
}
export class InfoComponent extends React.Component<InfoComponentProps> {
private getInfoPanel = (iconName: string, labelText: string, url?: string, onClick?: () => void): JSX.Element => {
export class InfoComponent extends React.Component {
private getInfoPanel = (iconName: string, labelText: string, url: string): JSX.Element => {
return (
<Link href={url} target={url && "_blank"} onClick={onClick}>
<Link href={url} target="_blank">
<div className="infoPanel">
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} />
<Label className="infoLabel">{labelText}</Label>
@@ -29,11 +25,6 @@ export class InfoComponent extends React.Component<InfoComponentProps> {
<Stack.Item>
{this.getInfoPanel("KnowledgeArticle", "Microsoft Terms of Use", CodeOfConductEndpoints.termsOfUse)}
</Stack.Item>
{this.props.onReportAbuseClick && (
<Stack.Item>
{this.getInfoPanel("ReportHacked", "Report Abuse", undefined, () => this.props.onReportAbuseClick())}
</Stack.Item>
)}
</Stack>
);
};

View File

@@ -77,25 +77,7 @@ exports[`GalleryViewerComponent renders 1`] = `
selectedKey={0}
/>
</StackItem>
<StackItem>
<InfoComponent />
</StackItem>
</Stack>
<StackItem>
<FocusZone
direction={2}
isCircularNavigation={false}
shouldRaiseClicks={true}
>
<List
getPageSpecification={[Function]}
onRenderCell={[Function]}
renderedWindowsAhead={3}
renderedWindowsBehind={2}
startIndex={0}
/>
</FocusZone>
</StackItem>
</Stack>
</PivotItem>
</StyledPivotBase>

View File

@@ -18,17 +18,14 @@ describe("NotebookMetadataComponent", () => {
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined
newCellId: undefined
},
isFavorite: false,
downloadButtonText: "Download",
onTagClick: undefined,
onDownloadClick: undefined,
onFavoriteClick: undefined,
onUnfavoriteClick: undefined,
onReportAbuseClick: undefined
onUnfavoriteClick: undefined
};
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
@@ -50,17 +47,14 @@ describe("NotebookMetadataComponent", () => {
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined
newCellId: undefined
},
isFavorite: true,
downloadButtonText: "Download",
onTagClick: undefined,
onDownloadClick: undefined,
onFavoriteClick: undefined,
onUnfavoriteClick: undefined,
onReportAbuseClick: undefined
onUnfavoriteClick: undefined
};
const wrapper = shallow(<NotebookMetadataComponent {...props} />);

View File

@@ -17,7 +17,6 @@ import { IGalleryItem } from "../../../Juno/JunoClient";
import { FileSystemUtil } from "../../Notebook/FileSystemUtil";
import "./NotebookViewerComponent.less";
import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg";
import { InfoComponent } from "../NotebookGallery/InfoComponent/InfoComponent";
export interface NotebookMetadataComponentProps {
data: IGalleryItem;
@@ -27,7 +26,6 @@ export interface NotebookMetadataComponentProps {
onFavoriteClick: () => void;
onUnfavoriteClick: () => void;
onDownloadClick: () => void;
onReportAbuseClick: () => void;
}
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
@@ -43,39 +41,24 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
return (
<Stack tokens={{ childrenGap: 10 }}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 30 }}>
<Stack.Item>
<Text variant="xxLarge" nowrap>
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
</Text>
</Stack.Item>
<Stack.Item>
<Text>
{this.props.isFavorite !== undefined && (
<>
<IconButton
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
/>
{this.props.data.favorites} likes
</>
)}
</Text>
</Stack.Item>
<Text variant="xxLarge" nowrap>
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
</Text>
<Text>
{this.props.isFavorite !== undefined && (
<>
<IconButton
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
/>
{this.props.data.favorites} likes
</>
)}
</Text>
{this.props.downloadButtonText && (
<Stack.Item>
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
</Stack.Item>
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
)}
<Stack.Item grow>
<></>
</Stack.Item>
<Stack.Item>
<InfoComponent onReportAbuseClick={this.props.onReportAbuseClick} />
</Stack.Item>
</Stack>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}>

View File

@@ -3,10 +3,11 @@
*/
import { Notebook } from "@nteract/commutable";
import { createContentRef } from "@nteract/core";
import { IChoiceGroupProps, Icon, Link, ProgressIndicator } from "office-ui-fabric-react";
import { Icon, Link, ProgressIndicator } from "office-ui-fabric-react";
import * as React from "react";
import { contents } from "rx-jupyter";
import * as Logger from "../../../Common/Logger";
import * as ViewModels from "../../../Contracts/ViewModels";
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
@@ -14,13 +15,12 @@ import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationCon
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
import { DialogComponent, DialogProps, TextFieldProps } from "../DialogReactComponent/DialogComponent";
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less";
import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
import { DialogHost } from "../../../Utils/GalleryUtils";
export interface NotebookViewerComponentProps {
container?: Explorer;
@@ -43,8 +43,10 @@ interface NotebookViewerComponentState {
showProgressBar: boolean;
}
export class NotebookViewerComponent extends React.Component<NotebookViewerComponentProps, NotebookViewerComponentState>
implements DialogHost {
export class NotebookViewerComponent extends React.Component<
NotebookViewerComponentProps,
NotebookViewerComponentState
> {
private clientManager: NotebookClientV2;
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
@@ -138,7 +140,6 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
onFavoriteClick={this.favoriteItem}
onUnfavoriteClick={this.unfavoriteItem}
onDownloadClick={this.downloadItem}
onReportAbuseClick={this.state.galleryItem.isSample ? undefined : this.reportAbuse}
/>
</div>
) : (
@@ -178,39 +179,6 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
};
}
// DialogHost
showOkCancelModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps
): void {
this.setState({
dialogProps: {
isModal: true,
visible: true,
title,
subText: msg,
primaryButtonText: okLabel,
secondaryButtonText: cancelLabel,
onPrimaryButtonClick: () => {
this.setState({ dialogProps: undefined });
onOk && onOk();
},
onSecondaryButtonClick: () => {
this.setState({ dialogProps: undefined });
onCancel && onCancel();
},
choiceGroupProps,
textFieldProps
}
});
}
private favoriteItem = async (): Promise<void> => {
GalleryUtils.favoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
this.setState({ galleryItem: item, isFavorite: true })
@@ -228,8 +196,4 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
this.setState({ galleryItem: item })
);
};
private reportAbuse = (): void => {
GalleryUtils.reportAbuse(this.props.junoClient, this.state.galleryItem, this, () => {});
};
}

View File

@@ -17,38 +17,26 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
}
verticalAlign="center"
>
<StackItem>
<Text
nowrap={true}
variant="xxLarge"
>
name
</Text>
</StackItem>
<StackItem>
<Text>
<CustomizedIconButton
iconProps={
Object {
"iconName": "HeartFill",
}
<Text
nowrap={true}
variant="xxLarge"
>
name
</Text>
<Text>
<CustomizedIconButton
iconProps={
Object {
"iconName": "HeartFill",
}
/>
0
likes
</Text>
</StackItem>
<StackItem>
<CustomizedPrimaryButton
text="Download"
}
/>
</StackItem>
<StackItem
grow={true}
0
likes
</Text>
<CustomizedPrimaryButton
text="Download"
/>
<StackItem>
<InfoComponent />
</StackItem>
</Stack>
<Stack
horizontal={true}
@@ -129,38 +117,26 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
}
verticalAlign="center"
>
<StackItem>
<Text
nowrap={true}
variant="xxLarge"
>
name
</Text>
</StackItem>
<StackItem>
<Text>
<CustomizedIconButton
iconProps={
Object {
"iconName": "Heart",
}
<Text
nowrap={true}
variant="xxLarge"
>
name
</Text>
<Text>
<CustomizedIconButton
iconProps={
Object {
"iconName": "Heart",
}
/>
0
likes
</Text>
</StackItem>
<StackItem>
<CustomizedPrimaryButton
text="Download"
}
/>
</StackItem>
<StackItem
grow={true}
0
likes
</Text>
<CustomizedPrimaryButton
text="Download"
/>
<StackItem>
<InfoComponent />
</StackItem>
</Stack>
<Stack
horizontal={true}

View File

@@ -6,7 +6,7 @@ import SettingsTabV2 from "../../Tabs/SettingsTabV2";
import { collection } from "./TestUtils";
import * as DataModels from "../../../Contracts/DataModels";
import ko from "knockout";
import { TtlType, isDirty } from "./SettingsUtils";
import { TtlType, isDirty, TtlOnNoDefault, TtlOn, TtlOff } from "./SettingsUtils";
import Explorer from "../../Explorer";
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
jest.mock("../../../Common/dataAccess/updateCollection", () => ({
@@ -20,8 +20,8 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
geospatialConfig: undefined
} as DataModels.Collection)
}));
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
jest.mock("../../../Common/dataAccess/updateOffer", () => ({
import { updateOffer } from "../../../Common/DocumentClientUtilityBase";
jest.mock("../../../Common/DocumentClientUtilityBase", () => ({
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer)
}));
@@ -33,6 +33,7 @@ describe("SettingsComponent", () => {
title: "Scale & Settings",
tabPath: "",
node: undefined,
selfLink: undefined,
hashLocation: "settings",
isActive: ko.observable(false),
onUpdateTabsButtons: undefined
@@ -102,7 +103,10 @@ describe("SettingsComponent", () => {
let settingsComponentInstance = new SettingsComponent(baseProps);
expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(false);
const newContainer = new Explorer();
const newContainer = new Explorer({
notificationsClient: undefined,
isEmulator: false
});
newContainer.isPreferredApiCassandra = ko.computed(() => true);
const newCollection = { ...collection };
@@ -143,7 +147,10 @@ describe("SettingsComponent", () => {
let settingsComponentInstance = new SettingsComponent(baseProps);
expect(settingsComponentInstance.hasConflictResolution()).toEqual(undefined);
const newContainer = new Explorer();
const newContainer = new Explorer({
notificationsClient: undefined,
isEmulator: false
});
newContainer.databaseAccount = ko.observable({
id: undefined,
name: undefined,
@@ -213,6 +220,13 @@ describe("SettingsComponent", () => {
expect(isDirty(state.throughput, state.throughputBaseline)).toEqual(false);
});
it("getTtlValue", async () => {
const settingsComponentInstance = new SettingsComponent(baseProps);
expect(settingsComponentInstance.getTtlValue(TtlType.OnNoDefault)).toEqual(TtlOnNoDefault);
expect(settingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn);
expect(settingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff);
});
it("getAnalyticalStorageTtl", () => {
const newCollection = { ...collection };
newCollection.analyticalStorageTtl = ko.observable(10);

View File

@@ -6,11 +6,11 @@ import * as SharedConstants from "../../../Shared/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import DiscardIcon from "../../../../images/discard.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceStart, traceFailure, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import Explorer from "../../Explorer";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { updateOffer } from "../../../Common/DocumentClientUtilityBase";
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { userContext } from "../../../UserContext";
@@ -27,6 +27,9 @@ import {
SettingsV2TabTypes,
getTabTitle,
isDirty,
TtlOff,
TtlOn,
TtlOnNoDefault,
parseConflictResolutionMode,
parseConflictResolutionProcedure
} from "./SettingsUtils";
@@ -35,7 +38,7 @@ import {
ConflictResolutionComponentProps
} from "./SettingsSubComponents/ConflictResolutionComponent";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric-react";
import { Pivot, PivotItem, IPivotProps, IPivotItemProps, IChoiceGroupOption } from "office-ui-fabric-react";
import "./SettingsComponent.less";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
@@ -82,6 +85,7 @@ export interface SettingsComponentState {
indexingPolicyContent: DataModels.IndexingPolicy;
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
shouldDiscardIndexingPolicy: boolean;
indexingPolicyElementFocussed: boolean;
isIndexingPolicyDirty: boolean;
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
@@ -98,6 +102,7 @@ export interface SettingsComponentState {
export class SettingsComponent extends React.Component<SettingsComponentProps, SettingsComponentState> {
private static readonly sixMonthsInSeconds = 15768000;
private static readonly zeroSeconds = 0;
public saveSettingsButton: ButtonV2;
public discardSettingsChangesButton: ButtonV2;
@@ -122,8 +127,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled(
Constants.Features.enableChangeFeedPolicy
);
// Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer =
!this.collection.partitionKey ||
(this.container.isPreferredApiMongoDB() && this.collection.partitionKey.systemKey);
@@ -155,6 +160,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
indexingPolicyContent: undefined,
indexingPolicyContentBaseline: undefined,
indexingPolicyElementFocussed: false,
shouldDiscardIndexingPolicy: false,
isIndexingPolicyDirty: false,
@@ -264,7 +270,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.props.settingsTab.isExecutionError(false);
this.props.settingsTab.isExecuting(true);
const startKey: number = traceStart(Action.SettingsV2Updated, {
const startKey: number = traceStart(Action.UpdateSettings, {
databaseAccountName: this.container.databaseAccount()?.name,
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
@@ -405,11 +411,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
} catch (error) {
traceFailure(
Action.SettingsV2Updated,
Action.UpdateSettings,
{
databaseAccountName: this.container.databaseAccount().name,
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id(),
databaseName: this.collection && this.collection.databaseId,
collectionName: this.collection && this.collection.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
@@ -420,21 +426,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
throw error;
}
} else {
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.collection.databaseId,
collectionId: this.collection.id(),
currentOffer: this.collection.offer(),
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
};
if (this.hasProvisioningTypeChanged()) {
if (this.state.isAutoPilotSelected) {
updateOfferParams.migrateToAutoPilot = true;
} else {
updateOfferParams.migrateToManual = true;
}
}
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
const updatedOffer: DataModels.Offer = await updateOffer(this.collection.offer(), newOffer, headerOptions);
this.collection.offer(updatedOffer);
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
if (this.state.isAutoPilotSelected) {
@@ -454,11 +446,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.setBaseline();
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
traceSuccess(
Action.SettingsV2Updated,
Action.UpdateSettings,
{
databaseAccountName: this.container.databaseAccount()?.name,
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle()
@@ -470,15 +460,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.props.settingsTab.isExecutionError(true);
console.error(reason);
traceFailure(
Action.SettingsV2Updated,
Action.UpdateSettings,
{
databaseAccountName: this.container.databaseAccount()?.name,
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
error: reason
tabTitle: this.props.settingsTab.tabTitle()
},
startKey
);
@@ -487,10 +474,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
};
public onRevertClick = (): void => {
trace(Action.SettingsV2Discarded, ActionModifiers.Mark, {
message: "Settings Discarded"
});
this.setState({
throughput: this.state.throughputBaseline,
timeToLive: this.state.timeToLiveBaseline,
@@ -521,6 +504,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onScaleDiscardableChange = (isScaleDiscardable: boolean): void =>
this.setState({ isScaleDiscardable: isScaleDiscardable });
private onIndexingPolicyElementFocusChange = (indexingPolicyElementFocussed: boolean): void =>
this.setState({ indexingPolicyElementFocussed: indexingPolicyElementFocussed });
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
this.setState({ indexingPolicyContent: newIndexingPolicy });
@@ -544,34 +530,79 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
};
private onConflictResolutionPolicyModeChange = (newMode: DataModels.ConflictResolutionMode): void =>
this.setState({ conflictResolutionPolicyMode: newMode });
private onConflictResolutionPolicyModeChange = (
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void =>
this.setState({
conflictResolutionPolicyMode:
DataModels.ConflictResolutionMode[option.key as keyof typeof DataModels.ConflictResolutionMode]
});
private onConflictResolutionPolicyPathChange = (newPath: string): void =>
this.setState({ conflictResolutionPolicyPath: newPath });
private onConflictResolutionPolicyPathChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => this.setState({ conflictResolutionPolicyPath: newValue });
private onConflictResolutionPolicyProcedureChange = (newProcedure: string): void =>
this.setState({ conflictResolutionPolicyProcedure: newProcedure });
private onConflictResolutionPolicyProcedureChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => this.setState({ conflictResolutionPolicyProcedure: newValue });
private onConflictResolutionDirtyChange = (isConflictResolutionDirty: boolean): void =>
this.setState({ isConflictResolutionDirty: isConflictResolutionDirty });
private onTtlChange = (newTtl: TtlType): void => this.setState({ timeToLive: newTtl });
public getTtlValue = (value: string): TtlType => {
switch (value) {
case TtlOn:
return TtlType.On;
case TtlOff:
return TtlType.Off;
case TtlOnNoDefault:
return TtlType.OnNoDefault;
}
return undefined;
};
private onTimeToLiveSecondsChange = (newTimeToLiveSeconds: number): void =>
private onTtlChange = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption): void =>
this.setState({ timeToLive: this.getTtlValue(option.key) });
private onTimeToLiveSecondsChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
let newTimeToLiveSeconds = parseInt(newValue);
newTimeToLiveSeconds = isNaN(newTimeToLiveSeconds) ? SettingsComponent.zeroSeconds : newTimeToLiveSeconds;
this.setState({ timeToLiveSeconds: newTimeToLiveSeconds });
};
private onGeoSpatialConfigTypeChange = (newGeoSpatialConfigType: GeospatialConfigType): void =>
this.setState({ geospatialConfigType: newGeoSpatialConfigType });
private onGeoSpatialConfigTypeChange = (
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void =>
this.setState({ geospatialConfigType: GeospatialConfigType[option.key as keyof typeof GeospatialConfigType] });
private onAnalyticalStorageTtlSelectionChange = (newAnalyticalStorageTtlSelection: TtlType): void =>
this.setState({ analyticalStorageTtlSelection: newAnalyticalStorageTtlSelection });
private onAnalyticalStorageTtlSelectionChange = (
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void => this.setState({ analyticalStorageTtlSelection: this.getTtlValue(option.key) });
private onAnalyticalStorageTtlSecondsChange = (newAnalyticalStorageTtlSeconds: number): void =>
private onAnalyticalStorageTtlSecondsChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
let newAnalyticalStorageTtlSeconds = parseInt(newValue);
newAnalyticalStorageTtlSeconds = isNaN(newAnalyticalStorageTtlSeconds)
? SettingsComponent.zeroSeconds
: newAnalyticalStorageTtlSeconds;
this.setState({ analyticalStorageTtlSeconds: newAnalyticalStorageTtlSeconds });
};
private onChangeFeedPolicyChange = (newChangeFeedPolicy: ChangeFeedPolicyState): void =>
this.setState({ changeFeedPolicy: newChangeFeedPolicy });
private onChangeFeedPolicyChange = (
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void =>
this.setState({ changeFeedPolicy: ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState] });
private onSubSettingsSaveableChange = (isSubSettingsSaveable: boolean): void =>
this.setState({ isSubSettingsSaveable: isSubSettingsSaveable });
@@ -799,6 +830,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
indexingPolicyContent: this.state.indexingPolicyContent,
indexingPolicyContentBaseline: this.state.indexingPolicyContentBaseline,
onIndexingPolicyElementFocusChange: this.onIndexingPolicyElementFocusChange,
onIndexingPolicyContentChange: this.onIndexingPolicyContentChange,
logIndexingPolicySuccessMessage: this.logIndexingPolicySuccessMessage,
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange

View File

@@ -18,15 +18,24 @@ export interface ConflictResolutionComponentProps {
container: Explorer;
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
onConflictResolutionPolicyModeChange: (newMode: DataModels.ConflictResolutionMode) => void;
onConflictResolutionPolicyModeChange: (
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
) => void;
conflictResolutionPolicyPath: string;
conflictResolutionPolicyPathBaseline: string;
onConflictResolutionPolicyPathChange: (newPath: string) => void;
onConflictResolutionPolicyPathChange: (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
) => void;
conflictResolutionPolicyProcedure: string;
conflictResolutionPolicyProcedureBaseline: string;
onConflictResolutionPolicyProcedureChange: (newProcedure: string) => void;
onConflictResolutionPolicyProcedureChange: (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
) => void;
onConflictResolutionDirtyChange: (isConflictResolutionDirty: boolean) => void;
}
@@ -68,30 +77,12 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
return false;
};
private onConflictResolutionPolicyModeChange = (
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void =>
this.props.onConflictResolutionPolicyModeChange(
DataModels.ConflictResolutionMode[option.key as keyof typeof DataModels.ConflictResolutionMode]
);
private onConflictResolutionPolicyPathChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => this.props.onConflictResolutionPolicyPathChange(newValue);
private onConflictResolutionPolicyProcedureChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => this.props.onConflictResolutionPolicyProcedureChange(newValue);
private getConflictResolutionModeComponent = (): JSX.Element => (
<ChoiceGroup
label="Mode"
selectedKey={this.props.conflictResolutionPolicyMode}
options={this.conflictResolutionChoiceGroupOptions}
onChange={this.onConflictResolutionPolicyModeChange}
onChange={this.props.onConflictResolutionPolicyModeChange}
styles={getChoiceGroupStyles(
this.props.conflictResolutionPolicyMode,
this.props.conflictResolutionPolicyModeBaseline
@@ -113,7 +104,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
this.props.conflictResolutionPolicyPathBaseline
)}
value={this.props.conflictResolutionPolicyPath}
onChange={this.onConflictResolutionPolicyPathChange}
onChange={this.props.onConflictResolutionPolicyPathChange}
/>
);
@@ -131,7 +122,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
this.props.conflictResolutionPolicyProcedureBaseline
)}
value={this.props.conflictResolutionPolicyProcedure}
onChange={this.onConflictResolutionPolicyProcedureChange}
onChange={this.props.onConflictResolutionPolicyProcedureChange}
/>
);

View File

@@ -17,6 +17,9 @@ describe("IndexingPolicyComponent", () => {
},
indexingPolicyContent: initialIndexingPolicyContent,
indexingPolicyContentBaseline: initialIndexingPolicyContent,
onIndexingPolicyElementFocusChange: () => {
return;
},
onIndexingPolicyContentChange: () => {
return;
},

View File

@@ -10,6 +10,7 @@ export interface IndexingPolicyComponentProps {
resetShouldDiscardIndexingPolicy: () => void;
indexingPolicyContent: DataModels.IndexingPolicy;
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
onIndexingPolicyElementFocusChange: (indexingPolicyContentFocussed: boolean) => void;
onIndexingPolicyContentChange: (newIndexingPolicy: DataModels.IndexingPolicy) => void;
logIndexingPolicySuccessMessage: () => void;
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
@@ -88,6 +89,8 @@ export class IndexingPolicyComponent extends React.Component<
ariaLabel: "Indexing Policy"
});
if (this.indexingPolicyEditor) {
this.indexingPolicyEditor.onDidFocusEditorText(() => this.props.onIndexingPolicyElementFocusChange(true));
this.indexingPolicyEditor.onDidBlurEditorText(() => this.props.onIndexingPolicyElementFocusChange(false));
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
indexingPolicyEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
this.props.logIndexingPolicySuccessMessage();

View File

@@ -5,13 +5,18 @@ import { container, collection } from "../TestUtils";
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
import Explorer from "../../../Explorer";
import * as Constants from "../../../../Common/Constants";
import { PlatformType } from "../../../../PlatformType";
import * as DataModels from "../../../../Contracts/DataModels";
import { throughputUnit } from "../SettingsRenderUtils";
import * as SharedConstants from "../../../../Shared/Constants";
import ko from "knockout";
describe("ScaleComponent", () => {
const nonNationalCloudContainer = new Explorer();
const nonNationalCloudContainer = new Explorer({
notificationsClient: undefined,
isEmulator: false
});
nonNationalCloudContainer.getPlatformType = () => PlatformType.Portal;
nonNationalCloudContainer.isRunningOnNationalCloud = () => false;
const targetThroughput = 6000;
@@ -83,7 +88,10 @@ describe("ScaleComponent", () => {
});
it("autoScale enabled", () => {
const newContainer = new Explorer();
const newContainer = new Explorer({
notificationsClient: undefined,
isEmulator: false
});
newContainer.databaseAccount({
id: undefined,
@@ -117,7 +125,7 @@ describe("ScaleComponent", () => {
it("getThroughputTitle", () => {
let scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - 40,000 RU/s)");
let newProps = { ...baseProps, container: nonNationalCloudContainer };
scaleComponent = new ScaleComponent(newProps);
@@ -130,7 +138,7 @@ describe("ScaleComponent", () => {
it("canThroughputExceedMaximumValue", () => {
let scaleComponent = new ScaleComponent(baseProps);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(false);
const newProps = { ...baseProps, container: nonNationalCloudContainer };
scaleComponent = new ScaleComponent(newProps);

View File

@@ -5,6 +5,7 @@ import * as ViewModels from "../../../../Contracts/ViewModels";
import * as DataModels from "../../../../Contracts/DataModels";
import * as SharedConstants from "../../../../Shared/Constants";
import Explorer from "../../../Explorer";
import { PlatformType } from "../../../../PlatformType";
import {
getTextFieldStyles,
subComponentStackProps,
@@ -18,7 +19,6 @@ import {
import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
import { configContext, Platform } from "../../../../ConfigContext";
export interface ScaleComponentProps {
collection: ViewModels.Collection;
@@ -43,7 +43,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
private isEmulator: boolean;
constructor(props: ScaleComponentProps) {
super(props);
this.isEmulator = configContext.platform === Platform.Emulator;
this.isEmulator = this.props.container.isEmulator;
}
public isAutoScaleEnabled = (): boolean => {
@@ -77,7 +77,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
};
public getMaxRUThroughputInputLimit = (): number => {
if (configContext.platform === Platform.Hosted && this.props.collection.partitionKey) {
if (this.props.container?.getPlatformType() === PlatformType.Hosted && this.props.collection.partitionKey) {
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
}
@@ -99,7 +99,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
public canThroughputExceedMaximumValue = (): boolean => {
const isPublicAzurePortal: boolean =
configContext.platform === Platform.Portal && !this.props.container.isRunningOnNationalCloud();
this.props.container.getPlatformType() === PlatformType.Portal &&
!this.props.container.isRunningOnNationalCloud();
const hasPartitionKey = !!this.props.collection.partitionKey;
return isPublicAzurePortal && hasPartitionKey;

View File

@@ -2,7 +2,7 @@ import { shallow } from "enzyme";
import React from "react";
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent";
import { container, collection } from "../TestUtils";
import { TtlType, GeospatialConfigType, ChangeFeedPolicyState, TtlOnNoDefault, TtlOn, TtlOff } from "../SettingsUtils";
import { TtlType, GeospatialConfigType, ChangeFeedPolicyState } from "../SettingsUtils";
import ko from "knockout";
import Explorer from "../../../Explorer";
@@ -105,7 +105,10 @@ describe("SubSettingsComponent", () => {
});
it("partitionKey not visible", () => {
const newContainer = new Explorer();
const newContainer = new Explorer({
notificationsClient: undefined,
isEmulator: false
});
newContainer.isPreferredApiCassandra = ko.computed(() => true);
const props = { ...baseProps, container: newContainer };
@@ -130,11 +133,4 @@ describe("SubSettingsComponent", () => {
expect(isComponentDirtyResult.isSaveable).toEqual(true);
expect(isComponentDirtyResult.isDiscardable).toEqual(true);
});
it("getTtlValue", async () => {
const subSettingsComponentInstance = new SubSettingsComponent(baseProps);
expect(subSettingsComponentInstance.getTtlValue(TtlType.OnNoDefault)).toEqual(TtlOnNoDefault);
expect(subSettingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn);
expect(subSettingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff);
});
});

View File

@@ -5,11 +5,7 @@ import {
TtlType,
ChangeFeedPolicyState,
isDirty,
IsComponentDirtyResult,
TtlOn,
TtlOff,
TtlOnNoDefault,
getSanitizedInputValue
IsComponentDirtyResult
} from "../SettingsUtils";
import Explorer from "../../../Explorer";
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
@@ -41,28 +37,40 @@ export interface SubSettingsComponentProps {
timeToLive: TtlType;
timeToLiveBaseline: TtlType;
onTtlChange: (newTtl: TtlType) => void;
onTtlChange: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption) => void;
timeToLiveSeconds: number;
timeToLiveSecondsBaseline: number;
onTimeToLiveSecondsChange: (newTimeToLiveSeconds: number) => void;
onTimeToLiveSecondsChange: (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
) => void;
geospatialConfigType: GeospatialConfigType;
geospatialConfigTypeBaseline: GeospatialConfigType;
onGeoSpatialConfigTypeChange: (newGeoSpatialConfigType: GeospatialConfigType) => void;
onGeoSpatialConfigTypeChange: (
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
) => void;
isAnalyticalStorageEnabled: boolean;
analyticalStorageTtlSelection: TtlType;
analyticalStorageTtlSelectionBaseline: TtlType;
onAnalyticalStorageTtlSelectionChange: (newAnalyticalStorageTtlSelection: TtlType) => void;
onAnalyticalStorageTtlSelectionChange: (
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
) => void;
analyticalStorageTtlSeconds: number;
analyticalStorageTtlSecondsBaseline: number;
onAnalyticalStorageTtlSecondsChange: (newAnalyticalStorageTtlSeconds: number) => void;
onAnalyticalStorageTtlSecondsChange: (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
) => void;
changeFeedPolicyVisible: boolean;
changeFeedPolicy: ChangeFeedPolicyState;
changeFeedPolicyBaseline: ChangeFeedPolicyState;
onChangeFeedPolicyChange: (newChangeFeedPolicyState: ChangeFeedPolicyState) => void;
onChangeFeedPolicyChange: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption) => void;
onSubSettingsSaveableChange: (isSubSettingsSaveable: boolean) => void;
onSubSettingsDiscardableChange: (isSubSettingsDiscardable: boolean) => void;
}
@@ -131,54 +139,6 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
{ key: TtlType.On, text: "On" }
];
public getTtlValue = (value: string): TtlType => {
switch (value) {
case TtlOn:
return TtlType.On;
case TtlOff:
return TtlType.Off;
case TtlOnNoDefault:
return TtlType.OnNoDefault;
}
return undefined;
};
private onTtlChange = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption): void =>
this.props.onTtlChange(this.getTtlValue(option.key));
private onTimeToLiveSecondsChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
const newTimeToLiveSeconds = getSanitizedInputValue(newValue, Int32.Max);
this.props.onTimeToLiveSecondsChange(newTimeToLiveSeconds);
};
private onGeoSpatialConfigTypeChange = (
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void =>
this.props.onGeoSpatialConfigTypeChange(GeospatialConfigType[option.key as keyof typeof GeospatialConfigType]);
private onAnalyticalStorageTtlSelectionChange = (
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void => this.props.onAnalyticalStorageTtlSelectionChange(this.getTtlValue(option.key));
private onAnalyticalStorageTtlSecondsChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
const newAnalyticalStorageTtlSeconds = getSanitizedInputValue(newValue, Int32.Max);
this.props.onAnalyticalStorageTtlSecondsChange(newAnalyticalStorageTtlSeconds);
};
private onChangeFeedPolicyChange = (
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
option?: IChoiceGroupOption
): void =>
this.props.onChangeFeedPolicyChange(ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState]);
private getTtlComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
<ChoiceGroup
@@ -186,7 +146,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
label="Time to Live"
selectedKey={this.props.timeToLive}
options={this.ttlChoiceGroupOptions}
onChange={this.onTtlChange}
onChange={this.props.onTtlChange}
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
/>
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
@@ -203,7 +163,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
min={1}
max={Int32.Max}
value={this.props.timeToLiveSeconds?.toString()}
onChange={this.onTimeToLiveSecondsChange}
onChange={this.props.onTimeToLiveSecondsChange}
suffix="second(s)"
/>
)}
@@ -223,7 +183,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
label="Analytical Storage Time to Live"
selectedKey={this.props.analyticalStorageTtlSelection}
options={this.analyticalTtlChoiceGroupOptions}
onChange={this.onAnalyticalStorageTtlSelectionChange}
onChange={this.props.onAnalyticalStorageTtlSelectionChange}
styles={getChoiceGroupStyles(
this.props.analyticalStorageTtlSelection,
this.props.analyticalStorageTtlSelectionBaseline
@@ -242,7 +202,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
max={Int32.Max}
value={this.props.analyticalStorageTtlSeconds?.toString()}
suffix="second(s)"
onChange={this.onAnalyticalStorageTtlSecondsChange}
onChange={this.props.onAnalyticalStorageTtlSecondsChange}
/>
)}
</Stack>
@@ -259,7 +219,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
label="Geospatial Configuration"
selectedKey={this.props.geospatialConfigType}
options={this.geoSpatialConfigTypeChoiceGroupOptions}
onChange={this.onGeoSpatialConfigTypeChange}
onChange={this.props.onGeoSpatialConfigTypeChange}
styles={getChoiceGroupStyles(this.props.geospatialConfigType, this.props.geospatialConfigTypeBaseline)}
/>
);
@@ -281,7 +241,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
id="changeFeedPolicy"
selectedKey={this.props.changeFeedPolicy}
options={this.changeFeedChoiceGroupOptions}
onChange={this.onChangeFeedPolicyChange}
onChange={this.props.onChangeFeedPolicyChange}
styles={getChoiceGroupStyles(this.props.changeFeedPolicy, this.props.changeFeedPolicyBaseline)}
aria-labelledby={labelId}
/>

View File

@@ -26,10 +26,9 @@ import {
MessageBarType
} from "office-ui-fabric-react";
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
import { IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
import * as SharedConstants from "../../../../../Shared/Constants";
import * as DataModels from "../../../../../Contracts/DataModels";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
export interface ThroughputInputAutoPilotV3Props {
databaseAccount: DataModels.DatabaseAccount;
@@ -72,9 +71,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
> {
private shouldCheckComponentIsDirty = true;
private static readonly defaultStep = 100;
private static readonly zeroThroughput = 0;
private step: number;
private throughputInputMaxValue: number;
private autoPilotInputMaxValue: number;
private choiceGroupFixedStyle = getChoiceGroupStyles(undefined, undefined);
private options: IChoiceGroupOption[] = [
{ key: "true", text: "Autoscale" },
{ key: "false", text: "Manual" }
@@ -141,8 +140,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
};
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
this.throughputInputMaxValue = this.props.canExceedMaximumValue ? Int32.Max : this.props.maximum;
this.autoPilotInputMaxValue = Int32.Max;
}
public hasProvisioningTypeChanged = (): boolean =>
@@ -203,7 +200,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
const newThroughput = getSanitizedInputValue(newValue, this.autoPilotInputMaxValue);
let newThroughput = parseInt(newValue);
newThroughput = isNaN(newThroughput) ? ThroughputInputAutoPilotV3Component.zeroThroughput : newThroughput;
this.props.onMaxAutoPilotThroughputChange(newThroughput);
};
@@ -211,7 +209,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
const newThroughput = getSanitizedInputValue(newValue, this.throughputInputMaxValue);
let newThroughput = parseInt(newValue);
newThroughput = isNaN(newThroughput) ? ThroughputInputAutoPilotV3Component.zeroThroughput : newThroughput;
if (this.overrideWithAutoPilotSettings()) {
this.props.onMaxAutoPilotThroughputChange(newThroughput);
} else {
@@ -245,7 +245,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onChange={this.onChoiceGroupChange}
required={this.props.showAsMandatory}
ariaLabelledBy={labelId}
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected)}
styles={this.choiceGroupFixedStyle}
/>
</Stack>
);
@@ -270,7 +270,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
key="auto pilot throughput input"
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
disabled={this.overrideWithProvisionedThroughputSettings()}
step={AutoPilotUtils.autoPilotIncrementStep}
step={this.step}
min={AutoPilotUtils.minAutoPilotThroughput}
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
onChange={this.onAutoPilotThroughputChange}
/>
@@ -297,6 +298,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
styles={getTextFieldStyles(this.props.throughput, this.props.throughputBaseline)}
disabled={this.overrideWithAutoPilotSettings()}
step={this.step}
min={this.props.minimum}
max={this.props.canExceedMaximumValue ? undefined : this.props.maximum}
value={
this.overrideWithAutoPilotSettings()
? this.props.maxAutoPilotThroughputBaseline?.toString()

View File

@@ -81,10 +81,10 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
Object {
"selectors": Object {
".ms-ChoiceField-field.is-checked::after": Object {
"borderColor": undefined,
"borderColor": "",
},
".ms-ChoiceField-field.is-checked::before": Object {
"borderColor": undefined,
"borderColor": "",
},
".ms-ChoiceField-wrapper label": Object {
"fontFamily": undefined,
@@ -113,9 +113,10 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
id="autopilotInput"
key="auto pilot throughput input"
label="Max RU/s"
min={4000}
onChange={[Function]}
required={true}
step={1000}
step={100}
styles={
Object {
"fieldGroup": Object {
@@ -218,6 +219,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
disabled={false}
id="throughputInput"
key="provisioned throughput input"
min={10000}
onChange={[Function]}
required={true}
step={100}
@@ -373,6 +375,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
disabled={false}
id="throughputInput"
key="provisioned throughput input"
min={10000}
onChange={[Function]}
required={true}
step={100}

View File

@@ -39,13 +39,13 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
}
>
<ThroughputInputAutoPilotV3Component
canExceedMaximumValue={true}
canExceedMaximumValue={false}
getThroughputWarningMessage={[Function]}
isAutoPilotSelected={false}
isEmulator={false}
isEnabled={true}
isFixed={false}
label="Throughput (6,000 - unlimited RU/s)"
label="Throughput (6,000 - 40,000 RU/s)"
maxAutoPilotThroughput={4000}
maxAutoPilotThroughputBaseline={4000}
maximum={40000}

View File

@@ -2,7 +2,6 @@ import { collection, container } from "./TestUtils";
import {
getMaxRUs,
getMinRUs,
getSanitizedInputValue,
hasDatabaseSharedThroughput,
isDirty,
isDirtyTypes,
@@ -87,11 +86,4 @@ describe("SettingsUtils", () => {
expect(isDirty(baseline, current)).toEqual(true);
});
});
it("getSanitizedInputValue", () => {
const max = 100;
expect(getSanitizedInputValue("", max)).toEqual(0);
expect(getSanitizedInputValue("999", max)).toEqual(99);
expect(getSanitizedInputValue("10", max)).toEqual(10);
});
});

View File

@@ -6,7 +6,6 @@ import * as PricingUtils from "../../../Utils/PricingUtils";
import Explorer from "../../Explorer";
const zeroValue = 0;
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy;
export const TtlOff = "off";
export const TtlOn = "on";
@@ -130,16 +129,6 @@ export const parseConflictResolutionProcedure = (procedureFromBackEnd: string):
return procedureFromBackEnd;
};
export const getSanitizedInputValue = (newValueString: string, max: number): number => {
let newValue = parseInt(newValueString);
if (isNaN(newValue)) {
newValue = zeroValue;
} else if (newValue > max) {
newValue = Math.floor(newValue / 10);
}
return newValue;
};
export const isDirty = (current: isDirtyTypes, baseline: isDirtyTypes): boolean => {
const currentType = typeof current;
const baselineType = typeof baseline;

View File

@@ -3,7 +3,10 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer";
import ko from "knockout";
export const container = new Explorer();
export const container = new Explorer({
notificationsClient: undefined,
isEmulator: false
});
export const collection = ({
container: container,

View File

@@ -84,6 +84,9 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function],
},
AddCollectionPane {
"_databaseOffers": HashMap {
"container": Object {},
},
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
@@ -583,6 +586,9 @@ exports[`SettingsComponent renders 1`] = `
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionPane": AddCollectionPane {
"_databaseOffers": HashMap {
"container": Object {},
},
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
@@ -976,6 +982,7 @@ exports[`SettingsComponent renders 1`] = `
"isAuthWithResourceToken": [Function],
"isCodeOfConductEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function],
"isEmulator": false,
"isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isGalleryPublishEnabled": [Function],
@@ -1051,6 +1058,7 @@ exports[`SettingsComponent renders 1`] = `
"parameters": [Function],
},
"notificationConsoleData": [Function],
"notificationsClient": undefined,
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
@@ -1390,6 +1398,9 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function],
},
AddCollectionPane {
"_databaseOffers": HashMap {
"container": Object {},
},
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
@@ -1889,6 +1900,9 @@ exports[`SettingsComponent renders 1`] = `
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionPane": AddCollectionPane {
"_databaseOffers": HashMap {
"container": Object {},
},
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
@@ -2282,6 +2296,7 @@ exports[`SettingsComponent renders 1`] = `
"isAuthWithResourceToken": [Function],
"isCodeOfConductEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function],
"isEmulator": false,
"isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isGalleryPublishEnabled": [Function],
@@ -2357,6 +2372,7 @@ exports[`SettingsComponent renders 1`] = `
"parameters": [Function],
},
"notificationConsoleData": [Function],
"notificationsClient": undefined,
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
@@ -2709,6 +2725,9 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function],
},
AddCollectionPane {
"_databaseOffers": HashMap {
"container": Object {},
},
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
@@ -3208,6 +3227,9 @@ exports[`SettingsComponent renders 1`] = `
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionPane": AddCollectionPane {
"_databaseOffers": HashMap {
"container": Object {},
},
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
@@ -3601,6 +3623,7 @@ exports[`SettingsComponent renders 1`] = `
"isAuthWithResourceToken": [Function],
"isCodeOfConductEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function],
"isEmulator": false,
"isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isGalleryPublishEnabled": [Function],
@@ -3676,6 +3699,7 @@ exports[`SettingsComponent renders 1`] = `
"parameters": [Function],
},
"notificationConsoleData": [Function],
"notificationsClient": undefined,
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
@@ -4015,6 +4039,9 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function],
},
AddCollectionPane {
"_databaseOffers": HashMap {
"container": Object {},
},
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
@@ -4514,6 +4541,9 @@ exports[`SettingsComponent renders 1`] = `
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
"addCollectionPane": AddCollectionPane {
"_databaseOffers": HashMap {
"container": Object {},
},
"_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
@@ -4907,6 +4937,7 @@ exports[`SettingsComponent renders 1`] = `
"isAuthWithResourceToken": [Function],
"isCodeOfConductEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function],
"isEmulator": false,
"isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isGalleryPublishEnabled": [Function],
@@ -4982,6 +5013,7 @@ exports[`SettingsComponent renders 1`] = `
"parameters": [Function],
},
"notificationConsoleData": [Function],
"notificationsClient": undefined,
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
@@ -5293,6 +5325,7 @@ exports[`SettingsComponent renders 1`] = `
logIndexingPolicySuccessMessage={[Function]}
onIndexingPolicyContentChange={[Function]}
onIndexingPolicyDirtyChange={[Function]}
onIndexingPolicyElementFocusChange={[Function]}
resetShouldDiscardIndexingPolicy={[Function]}
shouldDiscardIndexingPolicy={false}
/>

View File

@@ -45,7 +45,7 @@
<input
class="throughputModeRadio nonFirstRadio"
aria-label="Manual mode"
aria-label="Provisioned Throughput mode"
type="radio"
role="radio"
tabindex="0"

View File

@@ -44,7 +44,7 @@
<input
class="throughputModeRadio nonFirstRadio"
aria-label="Manual mode"
aria-label="Provisioned Throughput mode"
type="radio"
role="radio"
tabindex="0"

View File

@@ -18,8 +18,6 @@ import {
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
export interface TreeNodeMenuItem {
label: string;
@@ -278,12 +276,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
text: menuItem.label,
disabled: menuItem.isDisabled,
className: menuItem.styleClass,
onClick: () => {
menuItem.onClick();
TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, {
label: menuItem.label
});
},
onClick: menuItem.onClick,
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
}))
}}

View File

@@ -191,7 +191,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
"className": undefined,
"disabled": true,
"key": "menuLabel",
"onClick": [Function],
"onClick": undefined,
"onRenderIcon": [Function],
"text": "menuLabel",
},

View File

@@ -37,7 +37,7 @@ import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { configContext, Platform, updateConfigContext } from "../ConfigContext";
import { configContext, updateConfigContext } from "../ConfigContext";
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
@@ -58,6 +58,7 @@ import { NotebookUtil } from "./Notebook/NotebookUtil";
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { PlatformType } from "../PlatformType";
import { QueriesClient } from "../Common/QueriesClient";
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
import { RenewAdHocAccessPane } from "./Panes/RenewAdHocAccessPane";
@@ -81,12 +82,12 @@ import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import { NotificationsClientBase } from "../Common/NotificationsClientBase";
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";
import { IChoiceGroupProps } from "office-ui-fabric-react";
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -97,6 +98,10 @@ enum ShareAccessToggleState {
Read
}
interface ExplorerOptions {
notificationsClient: NotificationsClientBase;
isEmulator: boolean;
}
interface AdHocAccessData {
readWriteUrl: string;
readUrl: string;
@@ -130,12 +135,14 @@ export default class Explorer {
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
public isEnableMongoCapabilityPresent: ko.Computed<boolean>;
public isServerlessEnabled: ko.Computed<boolean>;
public isEmulator: boolean;
public isAccountReady: ko.Observable<boolean>;
public canSaveQueries: ko.Computed<boolean>;
public features: ko.Observable<any>;
public serverId: ko.Observable<string>;
public armEndpoint: ko.Observable<string>;
public isTryCosmosDBSubscription: ko.Observable<boolean>;
public notificationsClient: NotificationsClientBase;
public queriesClient: QueriesClient;
public tableDataClient: TableDataClient;
public splitter: Splitter;
@@ -205,7 +212,7 @@ export default class Explorer {
public isGalleryPublishEnabled: ko.Computed<boolean>;
public isCodeOfConductEnabled: ko.Computed<boolean>;
public isLinkInjectionEnabled: ko.Computed<boolean>;
public isSettingsV2Enabled: ko.Observable<boolean>;
public isSettingsV2Enabled: ko.Computed<boolean>;
public isGitHubPaneEnabled: ko.Observable<boolean>;
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
@@ -264,7 +271,7 @@ export default class Explorer {
private static readonly MaxNbDatabasesToAutoExpand = 5;
constructor() {
constructor(options: ExplorerOptions) {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree
});
@@ -370,6 +377,8 @@ export default class Explorer {
}
});
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
this.notificationsClient = options.notificationsClient;
this.isEmulator = options.isEmulator;
this.features = ko.observable();
this.serverId = ko.observable<string>();
@@ -412,8 +421,7 @@ export default class Explorer {
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
);
//this.isSettingsV2Enabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSettingsV2));
this.isSettingsV2Enabled = ko.observable(false);
this.isSettingsV2Enabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSettingsV2));
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
@@ -564,7 +572,9 @@ export default class Explorer {
this.isHostedDataExplorerEnabled = ko.computed<boolean>(
() =>
configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph()
this.getPlatformType() === PlatformType.Portal &&
!this.isRunningOnNationalCloud() &&
!this.isPreferredApiGraph()
);
this.isRightPanelV2Enabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableRightPanelV2)
@@ -1790,7 +1800,7 @@ export default class Explorer {
const message: any = event.data.data;
const inputs: ViewModels.DataExplorerInputsFrame = message.inputs;
const isRunningInPortal = configContext.platform === Platform.Portal;
const isRunningInPortal = window.dataExplorerPlatform == PlatformType.Portal;
const isRunningInDevMode = process.env.NODE_ENV === "development";
if (inputs && configContext.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) {
inputs.extensionEndpoint = configContext.PROXY_PATH;
@@ -1858,7 +1868,7 @@ export default class Explorer {
return null;
}
if (this.selectedNode().nodeKind === "Database") {
return _.find(this.databases(), (database: ViewModels.Database) => database.id() === this.selectedNode().id());
return _.find(this.databases(), (database: ViewModels.Database) => database.rid === this.selectedNode().rid);
}
return this.findSelectedCollection().database;
}
@@ -1901,6 +1911,7 @@ export default class Explorer {
this.features(inputs.features);
this.serverId(inputs.serverId);
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT));
this.notificationsClient.setExtensionEndpoint(configContext.BACKEND_ENDPOINT);
this.databaseAccount(databaseAccount);
this.subscriptionType(inputs.subscriptionType);
this.quotaId(inputs.quotaId);
@@ -1908,7 +1919,6 @@ export default class Explorer {
this.flight(inputs.addCollectionDefaultFlight);
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription);
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken);
this.setFeatureFlagsFromFlights(inputs.flights);
if (!!inputs.dataExplorerVersion) {
this.parentFrameDataExplorerVersion(inputs.dataExplorerVersion);
@@ -1943,20 +1953,12 @@ export default class Explorer {
return Q();
}
public setFeatureFlagsFromFlights(flights: readonly string[]): void {
if (!flights) {
return;
}
if (flights.indexOf(Constants.Flights.SettingsV2) !== -1) {
this.isSettingsV2Enabled(true);
}
}
public findSelectedCollection(): ViewModels.Collection {
return (this.selectedNode().nodeKind === "Collection"
? this.selectedNode()
: this.selectedNode().collection) as ViewModels.Collection;
if (this.selectedNode().nodeKind === "Collection") {
return this.findSelectedCollectionForSelectedNode();
} else {
return this.findSelectedCollectionForSubNode();
}
}
// TODO: Refactor below methods, minimize dependencies and add unit tests where necessary
@@ -2006,6 +2008,10 @@ export default class Explorer {
this._panes.forEach((pane: ContextualPaneBase) => pane.close());
}
public getPlatformType(): PlatformType {
return window.dataExplorerPlatform;
}
public isRunningOnNationalCloud(): boolean {
return (
this.serverId() === Constants.ServerIds.blackforest ||
@@ -2069,11 +2075,11 @@ export default class Explorer {
});
databasesToLoad.forEach(async (database: ViewModels.Database) => {
await database.loadCollections();
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id());
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().id() === database.id());
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
});
Q.all(loadCollectionPromises).done(
@@ -2185,11 +2191,21 @@ export default class Explorer {
}
}
public findCollection(databaseId: string, collectionId: string): ViewModels.Collection {
const database: ViewModels.Database = this.databases().find(
(database: ViewModels.Database) => database.id() === databaseId
);
return database?.collections().find((collection: ViewModels.Collection) => collection.id() === collectionId);
private findSelectedCollectionForSelectedNode(): ViewModels.Collection {
return this.findCollection(this.selectedNode().rid);
}
public findCollection(rid: string): ViewModels.Collection {
for (let i = 0; i < this.databases().length; i++) {
const database = this.databases()[i];
for (let j = 0; j < database.collections().length; j++) {
const collection = database.collections()[j];
if (collection.rid === rid) {
return collection;
}
}
}
return null;
}
public isLastCollection(): boolean {
@@ -2213,7 +2229,7 @@ export default class Explorer {
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
const databaseExists = _.some(
this.databases(),
(existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id
(existingDatabase: ViewModels.Database) => existingDatabase.rid === database._rid
);
return !databaseExists;
});
@@ -2225,7 +2241,7 @@ export default class Explorer {
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
const databasePresentInUpdatedList = _.some(
updatedDatabaseList,
(db: DataModels.Database) => db.id === database.id()
(db: DataModels.Database) => db._rid === database.rid
);
if (!databasePresentInUpdatedList) {
databasesToDelete.push(database);
@@ -2247,7 +2263,7 @@ export default class Explorer {
const databasesToKeep: ViewModels.Database[] = [];
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.id === database.id);
const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.rid === database.rid);
if (!shouldRemoveDatabase) {
databasesToKeep.push(database);
}
@@ -2256,6 +2272,19 @@ export default class Explorer {
this.databases(databasesToKeep);
}
private findSelectedCollectionForSubNode(): ViewModels.Collection {
for (let i = 0; i < this.databases().length; i++) {
const database = this.databases()[i];
for (let j = 0; j < database.collections().length; j++) {
const collection = database.collections()[j];
if (this.selectedNode().collection && collection.rid === this.selectedNode().collection.rid) {
return collection;
}
}
}
return null;
}
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";
@@ -2352,10 +2381,7 @@ export default class Explorer {
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
isPrimaryButtonDisabled?: boolean
onCancel: () => void
): void {
this._dialogProps({
isModal: true,
@@ -2371,10 +2397,38 @@ export default class Explorer {
onSecondaryButtonClick: () => {
this._closeModalDialog();
onCancel && onCancel();
}
});
}
public showOkCancelTextFieldModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
textFieldProps: TextFieldProps,
isPrimaryButtonDisabled?: boolean
): void {
let textFieldValue: string = null;
this._dialogProps({
isModal: true,
visible: true,
title,
subText: msg,
primaryButtonText: okLabel,
secondaryButtonText: cancelLabel,
onPrimaryButtonClick: () => {
this._closeModalDialog();
onOk && onOk();
},
choiceGroupProps,
textFieldProps,
primaryButtonDisabled: isPrimaryButtonDisabled
onSecondaryButtonClick: () => {
this._closeModalDialog();
onCancel && onCancel();
},
primaryButtonDisabled: isPrimaryButtonDisabled,
textFieldProps
});
}
@@ -2415,6 +2469,7 @@ export default class Explorer {
title: notebookContentItem.name,
tabPath: notebookContentItem.path,
collection: null,
selfLink: null,
masterKey: userContext.masterKey || "",
hashLocation: "notebooks",
isActive: ko.observable(false),
@@ -2866,6 +2921,7 @@ export default class Explorer {
title: title,
tabPath: title,
collection: null,
selfLink: null,
hashLocation: hashLocation,
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true),
@@ -2909,6 +2965,7 @@ export default class Explorer {
title: title,
tabPath: title,
documentClientUtility: null,
selfLink: null,
isActive: ko.observable(false),
hashLocation: hashLocation,
onUpdateTabsButtons: this.onUpdateTabsButtons,
@@ -2951,6 +3008,7 @@ export default class Explorer {
tabPath: title,
documentClientUtility: null,
collection: null,
selfLink: null,
hashLocation: hashLocation,
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true),

View File

@@ -141,6 +141,8 @@ describe("getPkIdFromDocumentId", () => {
});
describe("GraphExplorer", () => {
const COLLECTION_RID = "collectionRid";
const COLLECTION_SELF_LINK = "collectionSelfLink";
const gremlinRU = 789.12;
const createMockProps = (): GraphExplorerProps => {
@@ -158,6 +160,8 @@ describe("GraphExplorer", () => {
onIsValidQueryChange: (isValidQuery: boolean): void => {},
collectionPartitionKeyProperty: "collectionPartitionKeyProperty",
collectionRid: COLLECTION_RID,
collectionSelfLink: COLLECTION_SELF_LINK,
graphBackendEndpoint: "graphBackendEndpoint",
databaseId: "databaseId",
collectionId: "collectionId",

View File

@@ -47,6 +47,8 @@ export interface GraphExplorerProps {
onIsValidQueryChange: (isValidQuery: boolean) => void;
collectionPartitionKeyProperty: string;
collectionRid: string;
collectionSelfLink: string;
graphBackendEndpoint: string;
databaseId: string;
collectionId: string;
@@ -1759,7 +1761,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
return queryDocumentsPage(
this.props.collectionId,
this.props.collectionRid,
this.currentDocDBQueryInfo.iterator,
this.currentDocDBQueryInfo.index,
{

View File

@@ -17,6 +17,8 @@ interface Parameter {
graphConfig?: GraphConfig;
collectionPartitionKeyProperty: string;
collectionRid: string;
collectionSelfLink: string;
graphBackendEndpoint: string;
databaseId: string;
collectionId: string;
@@ -47,6 +49,8 @@ export class GraphExplorerAdapter implements ReactAdapter {
onIsGraphDisplayed={this.params.onIsGraphDisplayed}
onResetDefaultGraphConfigValues={this.params.onResetDefaultGraphConfigValues}
collectionPartitionKeyProperty={this.params.collectionPartitionKeyProperty}
collectionRid={this.params.collectionRid}
collectionSelfLink={this.params.collectionSelfLink}
graphBackendEndpoint={this.params.graphBackendEndpoint}
databaseId={this.params.databaseId}
collectionId={this.params.collectionId}

View File

@@ -10,7 +10,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory";
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { StyleConstants } from "../../../Common/Constants";
import * as CommandBarUtil from "./CommandBarUtil";
import { CommandBarUtil } from "./CommandBarUtil";
import Explorer from "../../Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";

View File

@@ -1,4 +1,5 @@
import * as ViewModels from "../../../Contracts/ViewModels";
import { PlatformType } from "../../../PlatformType";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../../Common/Constants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
@@ -158,7 +159,7 @@ export class CommandBarComponentButtonFactory {
public static createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (configContext.platform === Platform.Hosted) {
if (window.dataExplorerPlatform === PlatformType.Hosted) {
return buttons;
}
@@ -193,7 +194,7 @@ export class CommandBarComponentButtonFactory {
buttons.push(fullScreenButton);
}
if (configContext.platform !== Platform.Emulator) {
if (!container.hasOwnProperty("isEmulator") || !container.isEmulator) {
const label = "Feedback";
const feedbackButtonOptions: CommandButtonComponentProps = {
iconSrc: FeedbackIcon,

View File

@@ -1,4 +1,4 @@
import * as CommandBarUtil from "./CommandBarUtil";
import { CommandBarUtil } from "./CommandBarUtil";
import * as ViewModels from "../../../Contracts/ViewModels";
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
@@ -8,7 +8,7 @@ describe("CommandBarUtil tests", () => {
return {
iconSrc: "icon",
iconAlt: "label",
onCommandClick: jest.fn(),
onCommandClick: (e: React.SyntheticEvent): void => {},
commandButtonLabel: "label",
ariaLabel: "ariaLabel",
hasPopup: true,
@@ -29,14 +29,11 @@ describe("CommandBarUtil tests", () => {
expect(!converted.split);
expect(converted.iconProps.imageProps.src).toEqual(btn.iconSrc);
expect(converted.iconProps.imageProps.alt).toEqual(btn.iconAlt);
expect(converted.onClick).toEqual(btn.onCommandClick);
expect(converted.text).toEqual(btn.commandButtonLabel);
expect(converted.ariaLabel).toEqual(btn.ariaLabel);
expect(converted.disabled).toEqual(btn.disabled);
expect(converted.className).toEqual(btn.className);
// Click gets called
converted.onClick();
expect(btn.onCommandClick).toBeCalled();
});
it("should convert NavbarButtonConfig to split button", () => {

View File

@@ -11,187 +11,177 @@ import ChevronDownIcon from "../../../../images/Chevron_down.svg";
import { ArcadiaMenuPicker } from "../../Controls/Arcadia/ArcadiaMenuPicker";
import { MemoryTrackerComponent } from "./MemoryTrackerComponent";
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
/**
* Convert our NavbarButtonConfig to UI Fabric buttons
* @param btns
* Utilities for CommandBar
*/
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
export class CommandBarUtil {
/**
* Convert our NavbarButtonConfig to UI Fabric buttons
* @param btns
*/
public static convertButton(btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] {
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
return btns
.filter(btn => btn)
.map(
(btn: CommandButtonComponentProps, index: number): ICommandBarItemProps => {
if (btn.isDivider) {
return createDivider(btn.commandButtonLabel);
}
return btns
.filter(btn => btn)
.map(
(btn: CommandButtonComponentProps, index: number): ICommandBarItemProps => {
if (btn.isDivider) {
return CommandBarUtil.createDivider(btn.commandButtonLabel);
}
const isSplit = !!btn.children && btn.children.length > 0;
const label = btn.commandButtonLabel || btn.tooltipText;
const result: ICommandBarItemProps = {
iconProps: {
style: {
width: StyleConstants.CommandBarIconWidth, // 16
alignSelf: btn.iconName ? "baseline" : undefined
},
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
iconName: btn.iconName
},
onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
btn.onCommandClick(ev);
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label });
},
key: `${btn.commandButtonLabel}${index}`,
text: label,
"data-test": label,
title: btn.tooltipText,
name: label,
disabled: btn.disabled,
ariaLabel: btn.ariaLabel,
buttonStyles: {
root: {
backgroundColor: backgroundColor,
height: buttonHeightPx,
paddingRight: 0,
paddingLeft: 0,
minWidth: 24,
marginLeft: isSplit ? 0 : 5,
marginRight: isSplit ? 0 : 5
},
rootDisabled: {
backgroundColor: backgroundColor,
pointerEvents: "auto"
},
splitButtonMenuButton: {
backgroundColor: backgroundColor,
selectors: {
":hover": { backgroundColor: StyleConstants.AccentLight }
const isSplit = !!btn.children && btn.children.length > 0;
const result: ICommandBarItemProps = {
iconProps: {
style: {
width: StyleConstants.CommandBarIconWidth, // 16
alignSelf: btn.iconName ? "baseline" : undefined
},
width: 16
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
iconName: btn.iconName
},
label: { fontSize: StyleConstants.mediumFontSize },
rootHovered: { backgroundColor: StyleConstants.AccentLight },
rootPressed: { backgroundColor: StyleConstants.AccentLight },
splitButtonMenuButtonExpanded: {
backgroundColor: StyleConstants.AccentExtra,
selectors: {
":hover": { backgroundColor: StyleConstants.AccentLight }
onClick: btn.onCommandClick,
key: `${btn.commandButtonLabel}${index}`,
text: btn.commandButtonLabel || btn.tooltipText,
"data-test": btn.commandButtonLabel || btn.tooltipText,
title: btn.tooltipText,
name: btn.commandButtonLabel || btn.tooltipText,
disabled: btn.disabled,
ariaLabel: btn.ariaLabel,
buttonStyles: {
root: {
backgroundColor: backgroundColor,
height: buttonHeightPx,
paddingRight: 0,
paddingLeft: 0,
minWidth: 24,
marginLeft: isSplit ? 0 : 5,
marginRight: isSplit ? 0 : 5
},
rootDisabled: {
backgroundColor: backgroundColor,
pointerEvents: "auto"
},
splitButtonMenuButton: {
backgroundColor: backgroundColor,
selectors: {
":hover": { backgroundColor: StyleConstants.AccentLight }
},
width: 16
},
label: { fontSize: StyleConstants.mediumFontSize },
rootHovered: { backgroundColor: StyleConstants.AccentLight },
rootPressed: { backgroundColor: StyleConstants.AccentLight },
splitButtonMenuButtonExpanded: {
backgroundColor: StyleConstants.AccentExtra,
selectors: {
":hover": { backgroundColor: StyleConstants.AccentLight }
}
},
splitButtonDivider: {
display: "none"
},
icon: {
paddingLeft: 0,
paddingRight: 0
},
splitButtonContainer: {
marginLeft: 5,
marginRight: 5
}
},
splitButtonDivider: {
display: "none"
},
icon: {
paddingLeft: 0,
paddingRight: 0
},
splitButtonContainer: {
marginLeft: 5,
marginRight: 5
}
},
className: btn.className,
id: btn.id
};
className: btn.className,
id: btn.id
};
if (isSplit) {
// It's a split button
result.split = true;
if (isSplit) {
// It's a split button
result.split = true;
result.subMenuProps = {
items: convertButton(btn.children, backgroundColor),
styles: {
list: {
// TODO Figure out how to do it the proper way with subComponentStyles.
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
selectors: {
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
".ms-ContextualMenu-link:hover": { backgroundColor: StyleConstants.AccentLight },
".ms-ContextualMenu-icon": { width: 16, height: 16 }
result.subMenuProps = {
items: CommandBarUtil.convertButton(btn.children, backgroundColor),
styles: {
list: {
// TODO Figure out how to do it the proper way with subComponentStyles.
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
selectors: {
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
".ms-ContextualMenu-link:hover": { backgroundColor: StyleConstants.AccentLight },
".ms-ContextualMenu-icon": { width: 16, height: 16 }
}
}
}
}
};
};
result.menuIconProps = {
iconType: IconType.image,
style: {
width: 12,
paddingLeft: 1,
paddingTop: 6
},
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt }
};
result.menuIconProps = {
iconType: IconType.image,
style: {
width: 12,
paddingLeft: 1,
paddingTop: 6
},
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt }
};
}
if (btn.isDropdown) {
const selectedChild = _.find(btn.children, child => child.dropdownItemKey === btn.dropdownSelectedKey);
result.name = selectedChild?.commandButtonLabel || btn.dropdownPlaceholder;
const dropdownStyles: Partial<IDropdownStyles> = {
root: { margin: 5 },
dropdown: { width: btn.dropdownWidth },
title: { fontSize: 12, height: 30, lineHeight: 28 },
dropdownItem: { fontSize: 12, lineHeight: 28, minHeight: 30 },
dropdownItemSelected: { fontSize: 12, lineHeight: 28, minHeight: 30 }
};
result.commandBarButtonAs = (props: IComponentAsProps<ICommandBarItemProps>) => {
return (
<Dropdown
placeholder={btn.dropdownPlaceholder}
defaultSelectedKey={btn.dropdownSelectedKey}
onChange={(event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number): void =>
btn.children[index].onCommandClick(event)
}
options={btn.children.map((child: CommandButtonComponentProps) => ({
key: child.dropdownItemKey,
text: child.commandButtonLabel
}))}
styles={dropdownStyles}
/>
);
};
}
if (btn.isArcadiaPicker && btn.arcadiaProps) {
result.commandBarButtonAs = () => <ArcadiaMenuPicker {...btn.arcadiaProps} />;
}
return result;
}
);
}
if (btn.isDropdown) {
const selectedChild = _.find(btn.children, child => child.dropdownItemKey === btn.dropdownSelectedKey);
result.name = selectedChild?.commandButtonLabel || btn.dropdownPlaceholder;
public static createDivider(key: string): ICommandBarItemProps {
return {
onRender: () => (
<div className="dividerContainer">
<span />
</div>
),
iconOnly: true,
disabled: true,
key: key
};
}
const dropdownStyles: Partial<IDropdownStyles> = {
root: { margin: 5 },
dropdown: { width: btn.dropdownWidth },
title: { fontSize: 12, height: 30, lineHeight: 28 },
dropdownItem: { fontSize: 12, lineHeight: 28, minHeight: 30 },
dropdownItemSelected: { fontSize: 12, lineHeight: 28, minHeight: 30 }
};
const onDropdownChange = (
event: React.FormEvent<HTMLDivElement>,
option?: IDropdownOption,
index?: number
): void => {
btn.children[index].onCommandClick(event);
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
};
result.commandBarButtonAs = (props: IComponentAsProps<ICommandBarItemProps>) => {
return (
<Dropdown
placeholder={btn.dropdownPlaceholder}
defaultSelectedKey={btn.dropdownSelectedKey}
onChange={onDropdownChange}
options={btn.children.map((child: CommandButtonComponentProps) => ({
key: child.dropdownItemKey,
text: child.commandButtonLabel
}))}
styles={dropdownStyles}
/>
);
};
}
if (btn.isArcadiaPicker && btn.arcadiaProps) {
result.commandBarButtonAs = () => <ArcadiaMenuPicker {...btn.arcadiaProps} />;
}
return result;
}
);
};
export const createDivider = (key: string): ICommandBarItemProps => {
return {
onRender: () => (
<div className="dividerContainer">
<span />
</div>
),
iconOnly: true,
disabled: true,
key: key
};
};
export const createMemoryTracker = (
key: string,
memoryUsageInfo: Observable<MemoryUsageInfo>
): ICommandBarItemProps => {
return {
key,
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />
};
};
public static createMemoryTracker(key: string, memoryUsageInfo: Observable<MemoryUsageInfo>): ICommandBarItemProps {
return {
key,
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />
};
}
}

View File

@@ -15,10 +15,7 @@ export interface OpenNotebookItem {
path: string;
}
export interface OpenCollectionItem {
databaseId: string;
collectionId: string;
}
export type OpenCollectionItem = string;
export interface Item {
type: Type;
@@ -124,11 +121,7 @@ export class MostRecentActivity {
public onItemClicked(item: Item) {
switch (item.type) {
case Type.OpenCollection: {
const openCollectionitem = item.data as OpenCollectionItem;
const collection = this.container.findCollection(
openCollectionitem.databaseId,
openCollectionitem.collectionId
);
const collection = this.container.findCollection(item.data as OpenCollectionItem);
if (collection) {
collection.openTab();
}

View File

@@ -32,27 +32,24 @@ import {
import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging";
import { sessions, kernels } from "rx-jupyter";
import { RecordOf } from "immutable";
import { AnyAction } from "redux";
import * as Constants from "../../../Common/Constants";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as CdbActions from "./actions";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { Action as TelemetryAction } from "../../../Shared/Telemetry/TelemetryConstants";
import { CdbAppState } from "./types";
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
import * as TextFile from "./contents/file/text-file";
import { NotebookUtil } from "../NotebookUtil";
import { FileSystemUtil } from "../FileSystemUtil";
import * as cdbActions from "../NotebookComponent/actions";
import { Areas } from "../../../Common/Constants";
interface NotebookServiceConfig extends JupyterServerConfig {
userPuid?: string;
}
const logFailureToTelemetry = (state: CdbAppState, title: string, error?: string) => {
const logToTelemetry = (state: CdbAppState, title: string, error?: string) => {
TelemetryProcessor.traceFailure(TelemetryAction.NotebookErrorNotification, {
databaseAccountName: state.cdb.databaseAccountName,
defaultExperience: state.cdb.defaultExperience,
@@ -314,7 +311,7 @@ export const launchWebSocketKernelEpic = (
kernelSpecToLaunch = currentKernelspecs.defaultKernelName;
const msg = `No kernelspec name specified to launch, using default kernel: ${kernelSpecToLaunch}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
logToTelemetry(state$.value, "Launching alternate kernel", msg);
} else {
return of(
actions.launchKernelFailed({
@@ -340,7 +337,7 @@ export const launchWebSocketKernelEpic = (
msg += ` Using default kernel: ${kernelSpecToLaunch}`;
}
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
logToTelemetry(state$.value, "Launching alternate kernel", msg);
}
const sessionPayload = {
@@ -637,7 +634,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
const title = "Kernel restart";
const msg = "Kernel successfully restarted";
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
logFailureToTelemetry(state$.value, title, msg);
logToTelemetry(state$.value, title, msg);
break;
}
case actions.RESTART_KERNEL_FAILED:
@@ -648,7 +645,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
const title = "Save failure";
const msg = `Failed to save notebook: ${(action as actions.SaveFailed).payload.error}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logFailureToTelemetry(state$.value, title, msg);
logToTelemetry(state$.value, title, msg);
break;
}
case actions.FETCH_CONTENT_FAILED: {
@@ -657,7 +654,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
const title = "Fetching content failure";
const msg = `Failed to fetch notebook content: ${filepath}, error: ${typedAction.payload.error}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logFailureToTelemetry(state$.value, title, msg);
logToTelemetry(state$.value, title, msg);
break;
}
}
@@ -682,7 +679,7 @@ const handleKernelConnectionLostEpic = (
const msg = "Notebook was disconnected from kernel";
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logFailureToTelemetry(state, "Error", "Kernel connection error");
logToTelemetry(state, "Error", "Kernel connection error");
const host = selectors.currentHost(state);
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host as RecordOf<JupyterHostRecordProps>);
@@ -695,7 +692,7 @@ const handleKernelConnectionLostEpic = (
const msg =
"Restarted kernel too many times. Please reload the page to enable Data Explorer to restart the kernel automatically.";
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logFailureToTelemetry(state, "Kernel restart error", msg);
logToTelemetry(state, "Kernel restart error", msg);
const explorer = window.dataExplorer;
if (explorer) {
@@ -847,105 +844,6 @@ const closeContentFailedToFetchEpic = (
);
};
const traceNotebookTelemetryEpic = (
action$: Observable<cdbActions.TraceNotebookTelemetryAction>,
state$: StateObservable<CdbAppState>
): Observable<{}> => {
return action$.pipe(
ofType(cdbActions.TRACE_NOTEBOOK_TELEMETRY),
mergeMap((action: cdbActions.TraceNotebookTelemetryAction) => {
const state = state$.value;
TelemetryProcessor.trace(action.payload.action, action.payload.actionModifier, {
...action.payload.data,
databaseAccountName: state.cdb.databaseAccountName,
defaultExperience: state.cdb.defaultExperience,
dataExplorerArea: Areas.Notebook
});
return EMPTY;
})
);
};
/**
* Log notebook information to telemetry
* # raw cells, # markdown cells, # code cells, total
* @param action$
* @param state$
*/
const traceNotebookInfoEpic = (
action$: Observable<actions.FetchContentFulfilled>,
state$: StateObservable<AppState>
): Observable<{} | cdbActions.TraceNotebookTelemetryAction> => {
return action$.pipe(
ofType(actions.FETCH_CONTENT_FULFILLED),
mergeMap((action: { payload: any }) => {
const state = state$.value;
const contentRef = action.payload.contentRef;
const model = selectors.model(state, { contentRef });
// If it's not a notebook, we shouldn't be here
if (!model || model.type !== "notebook") {
return EMPTY;
}
const dataToLog = {
nbCodeCells: 0,
nbRawCells: 0,
nbMarkdownCells: 0,
nbCells: 0
};
for (let [id, cell] of selectors.notebook.cellMap(model)) {
switch (cell.cell_type) {
case "code":
dataToLog.nbCodeCells++;
break;
case "markdown":
dataToLog.nbMarkdownCells++;
break;
case "raw":
dataToLog.nbRawCells++;
break;
}
dataToLog.nbCells++;
}
return of(
cdbActions.traceNotebookTelemetry({
action: TelemetryAction.NotebooksFetched,
actionModifier: ActionModifiers.Mark,
data: dataToLog
})
);
})
);
};
/**
* Log Kernel spec to start
* @param action$
* @param state$
*/
const traceNotebookKernelEpic = (
action$: Observable<AnyAction>,
state$: StateObservable<AppState>
): Observable<cdbActions.TraceNotebookTelemetryAction> => {
return action$.pipe(
ofType(actions.LAUNCH_KERNEL_SUCCESSFUL),
mergeMap((action: { payload: any; type: string }) => {
return of(
cdbActions.traceNotebookTelemetry({
action: TelemetryAction.NotebooksKernelSpecName,
actionModifier: ActionModifiers.Mark,
data: {
kernelSpecName: action.payload.kernel.name
}
})
);
})
);
};
export const allEpics = [
addInitialCodeCellEpic,
focusInitialCodeCellEpic,
@@ -958,8 +856,5 @@ export const allEpics = [
executeFocusedCellAndFocusNextEpic,
closeUnsupportedMimetypesEpic,
closeContentFailedToFetchEpic,
restartWebSocketKernelEpic,
traceNotebookTelemetryEpic,
traceNotebookInfoEpic,
traceNotebookKernelEpic
restartWebSocketKernelEpic
];

View File

@@ -1,5 +1,7 @@
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
import { Action } from "redux";
import { Areas } from "../../../Common/Constants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as cdbActions from "./actions";
import { CdbRecord } from "./types";
@@ -70,6 +72,17 @@ export const cdbReducer = (state: CdbRecord, action: Action) => {
return state.set("hoveredCellId", typedAction.payload.cellId);
}
case cdbActions.TRACE_NOTEBOOK_TELEMETRY: {
const typedAction = action as cdbActions.TraceNotebookTelemetryAction;
TelemetryProcessor.trace(typedAction.payload.action, typedAction.payload.actionModifier, {
...typedAction.payload.data,
databaseAccountName: state.databaseAccountName,
defaultExperience: state.defaultExperience,
dataExplorerArea: Areas.Notebook
});
return state;
}
case cdbActions.UPDATE_NOTEBOOK_PARENT_DOM_ELTS: {
const typedAction = action as cdbActions.UpdateNotebookParentDomEltAction;
var parentEltsMap = state.get("currentNotebookParentElements");

View File

@@ -166,7 +166,7 @@ export default class NotebookManager {
private promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
return new Promise<string>((resolve, reject) => {
let commitMsg = "Committed from Azure Cosmos DB Notebooks";
this.params.container.showOkCancelModalDialog(
this.params.container.showOkCancelTextFieldModalDialog(
title || "Commit",
undefined,
primaryButtonLabel || "Commit",
@@ -181,7 +181,6 @@ export default class NotebookManager {
},
"Cancel",
() => reject(new Error("Commit dialog canceled")),
undefined,
{
label: "Commit message",
autoAdjustHeight: true,

View File

@@ -14,8 +14,6 @@ import { CellToolbarContext } from "@nteract/stateful-components";
import { CellType, CellId } from "@nteract/commutable";
import * as selectors from "@nteract/selectors";
import { RecordOf } from "immutable";
import * as cdbActions from "../NotebookComponent/actions";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
export interface ComponentProps {
contentRef: ContentRef;
@@ -31,7 +29,6 @@ interface DispatchProps {
moveCell: (destinationId: CellId, above: boolean) => void;
clearOutputs: () => void;
deleteCell: () => void;
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) => void;
}
interface StateProps {
@@ -51,18 +48,12 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
{
key: "Run",
text: "Run",
onClick: () => {
this.props.executeCell();
this.props.traceNotebookTelemetry(Action.NotebooksExecuteCellFromMenu, ActionModifiers.Mark);
}
onClick: this.props.executeCell
},
{
key: "Clear Outputs",
text: "Clear Outputs",
onClick: () => {
this.props.clearOutputs();
this.props.traceNotebookTelemetry(Action.NotebooksClearOutputsFromMenu, ActionModifiers.Mark);
}
onClick: this.props.clearOutputs
},
{
key: "Divider",
@@ -73,43 +64,31 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
items = items.concat([
{
key: "Divider2",
key: "Divider",
itemType: ContextualMenuItemType.Divider
},
{
key: "Insert Code Cell Above",
text: "Insert Code Cell Above",
onClick: () => {
this.props.insertCodeCellAbove();
this.props.traceNotebookTelemetry(Action.NotebooksInsertCodeCellAboveFromMenu, ActionModifiers.Mark);
}
onClick: this.props.insertCodeCellAbove
},
{
key: "Insert Code Cell Below",
text: "Insert Code Cell Below",
onClick: () => {
this.props.insertCodeCellBelow();
this.props.traceNotebookTelemetry(Action.NotebooksInsertCodeCellBelowFromMenu, ActionModifiers.Mark);
}
onClick: this.props.insertCodeCellBelow
},
{
key: "Insert Text Cell Above",
text: "Insert Text Cell Above",
onClick: () => {
this.props.insertTextCellAbove();
this.props.traceNotebookTelemetry(Action.NotebooksInsertTextCellAboveFromMenu, ActionModifiers.Mark);
}
onClick: this.props.insertTextCellAbove
},
{
key: "Insert Text Cell Below",
text: "Insert Text Cell Below",
onClick: () => {
this.props.insertTextCellBelow();
this.props.traceNotebookTelemetry(Action.NotebooksInsertTextCellBelowFromMenu, ActionModifiers.Mark);
}
onClick: this.props.insertTextCellBelow
},
{
key: "Divider3",
key: "Divider",
itemType: ContextualMenuItemType.Divider
}
]);
@@ -119,10 +98,7 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
moveItems.push({
key: "Move Cell Up",
text: "Move Cell Up",
onClick: () => {
this.props.moveCell(this.props.cellIdAbove, true);
this.props.traceNotebookTelemetry(Action.NotebooksMoveCellUpFromMenu, ActionModifiers.Mark);
}
onClick: () => this.props.moveCell(this.props.cellIdAbove, true)
});
}
@@ -130,16 +106,13 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
moveItems.push({
key: "Move Cell Down",
text: "Move Cell Down",
onClick: () => {
this.props.moveCell(this.props.cellIdBelow, false);
this.props.traceNotebookTelemetry(Action.NotebooksMoveCellDownFromMenu, ActionModifiers.Mark);
}
onClick: () => this.props.moveCell(this.props.cellIdBelow, false)
});
}
if (moveItems.length > 0) {
moveItems.push({
key: "Divider4",
key: "Divider",
itemType: ContextualMenuItemType.Divider
});
items = items.concat(moveItems);
@@ -148,10 +121,7 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
items.push({
key: "Delete Cell",
text: "Delete Cell",
onClick: () => {
this.props.deleteCell();
this.props.traceNotebookTelemetry(Action.DeleteCellFromMenu, ActionModifiers.Mark);
}
onClick: this.props.deleteCell
});
const menuItemLabel = "More";
@@ -186,9 +156,7 @@ const mapDispatchToProps = (
moveCell: (destinationId: CellId, above: boolean) =>
dispatch(actions.moveCell({ id, contentRef, destinationId, above })),
clearOutputs: () => dispatch(actions.clearOutputs({ id, contentRef })),
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef })),
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) =>
dispatch(cdbActions.traceNotebookTelemetry({ action, actionModifier, data }))
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef }))
});
const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => {

View File

@@ -49,7 +49,7 @@
.nteract-cell-input .nteract-cell-source {
flex: 1 1 auto;
overflow: visible;
overflow: auto;
}
/** Adaptation for the R kernel's inline lists **/

View File

@@ -276,7 +276,7 @@
<!-- Fixed option button - Start -->
<div class="tab">
<input type="radio" id="tab1" name="storage" value="10" class="radio" data-bind="checked: storage">
<label for="tab1">Fixed (20 GB)</label>
<label for="tab1">Fixed (10 GB)</label>
</div>
<!-- Fixed option button - End -->

View File

@@ -40,7 +40,7 @@ describe("Add Collection Pane", () => {
};
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
});

View File

@@ -14,6 +14,8 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import { configContext, Platform } from "../../ConfigContext";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
import { HashMap } from "../../Common/HashMap";
import { PlatformType } from "../../PlatformType";
import { refreshCachedResources } from "../../Common/DocumentClientUtilityBase";
import { createCollection } from "../../Common/dataAccess/createCollection";
@@ -99,10 +101,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
public showUpsellMessage: ko.PureComputed<boolean>;
public shouldCreateMongoWildcardIndex: ko.Observable<boolean>;
private _databaseOffers: HashMap<DataModels.Offer>;
private _isSynapseLinkEnabled: ko.Computed<boolean>;
constructor(options: AddCollectionPaneOptions) {
super(options);
this._databaseOffers = new HashMap<DataModels.Offer>();
this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag());
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag()));
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
@@ -323,9 +327,9 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform !== Platform.Emulator &&
!this.container.isEmulator &&
!this.container.isTryCosmosDBSubscription() &&
configContext.platform !== Platform.Portal
this.container.getPlatformType() !== PlatformType.Portal
) {
const offerThroughput: number = this._getThroughput();
return offerThroughput <= 100000;
@@ -335,7 +339,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
});
this.costsVisible = ko.pureComputed(() => {
return configContext.platform !== Platform.Emulator;
return !this.container.isEmulator;
});
this.maxCollectionsReached = ko.computed<boolean>(() => {
@@ -477,10 +481,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
}
if (!this.databaseCreateNew()) {
const selectedDatabase: ViewModels.Database = this.container
.databases()
.find((database: ViewModels.Database) => database.id() === selectedDatabaseId);
this.databaseHasSharedOffer(!!selectedDatabase?.offer());
this.databaseHasSharedOffer(this._databaseOffers.has(selectedDatabaseId));
}
});
@@ -748,7 +749,15 @@ export default class AddCollectionPane extends ContextualPaneBase {
}
private _onDatabasesChange(newDatabaseIds: ViewModels.Database[]) {
this.databaseIds(newDatabaseIds?.map((database: ViewModels.Database) => database.id()));
const cachedDatabaseIdsList = _.map(newDatabaseIds, (database: ViewModels.Database) => {
if (database && database.offer && database.offer()) {
this._databaseOffers.set(database.id(), database.offer());
}
return database.id();
});
this.databaseIds(cachedDatabaseIdsList);
}
private _computeOfferThroughput(): number {

View File

@@ -74,8 +74,8 @@
<input id="database-id" type="text" aria-required="true" autocomplete="off" pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
size="40" class="collid" data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus, attr: { 'aria-label': databaseIdLabel, 'placeholder': databaseIdPlaceHolder }"
autofocus>
size="40" class="collid" data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus, attr: { placeholder: databaseIdPlaceHolder }"
aria-label="Database id" autofocus>
<!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput -->

View File

@@ -40,7 +40,10 @@ describe("Add Database Pane", () => {
};
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({
notificationsClient: null,
isEmulator: false
});
});
it("should be true if subscription type is Benefits", () => {

View File

@@ -11,7 +11,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { createDatabase } from "../../Common/dataAccess/createDatabase";
import { configContext, Platform } from "../../ConfigContext";
import { PlatformType } from "../../PlatformType";
export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>;
@@ -180,9 +180,9 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.canRequestSupport = ko.pureComputed(() => {
if (
configContext.platform !== Platform.Emulator &&
!this.container.isEmulator &&
!this.container.isTryCosmosDBSubscription() &&
configContext.platform !== Platform.Portal
this.container.getPlatformType() !== PlatformType.Portal
) {
const offerThroughput: number = this.throughput();
return offerThroughput <= 100000;
@@ -203,7 +203,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
});
this.costsVisible = ko.pureComputed(() => {
return configContext.platform !== Platform.Emulator;
return !this.container.isEmulator;
});
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {

View File

@@ -12,7 +12,6 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { HashMap } from "../../Common/HashMap";
import { configContext, Platform } from "../../ConfigContext";
export default class CassandraAddCollectionPane extends ContextualPaneBase {
public createTableQuery: ko.Observable<string>;
@@ -232,11 +231,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
});
this.costsVisible = ko.pureComputed(() => {
return configContext.platform !== Platform.Emulator;
return !this.container.isEmulator;
});
this.canRequestSupport = ko.pureComputed(() => {
if (configContext.platform !== Platform.Emulator && !this.container.isTryCosmosDBSubscription()) {
if (!this.container.isEmulator && !this.container.isTryCosmosDBSubscription()) {
const offerThroughput: number = this.throughput();
return offerThroughput <= 100000;
}

View File

@@ -17,7 +17,7 @@ describe("Delete Collection Confirmation Pane", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
});
it("should be true if 1 database and 1 collection", () => {
@@ -56,7 +56,7 @@ describe("Delete Collection Confirmation Pane", () => {
describe("shouldRecordFeedback()", () => {
it("should return true if last collection and database does not have shared throughput else false", () => {
let fakeExplorer = new Explorer();
let fakeExplorer = new Explorer({ notificationsClient: null, isEmulator: false });
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
fakeExplorer.refreshAllDatabases = () => Q.resolve();

View File

@@ -66,11 +66,7 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
this.isExecuting(false);
this.close();
this.container.selectedNode(selectedCollection.database);
this.container.tabsManager?.closeTabsByComparator(
tab =>
tab.node?.id() === selectedCollection.id() &&
(tab.node as ViewModels.Collection).databaseId === selectedCollection.databaseId
);
this.container.tabsManager?.closeTabsByComparator(tab => tab.node && tab.node.rid === selectedCollection.rid);
this.container.refreshAllDatabases();
this.resetData();
TelemetryProcessor.traceSuccess(

View File

@@ -22,7 +22,7 @@ describe("Delete Database Confirmation Pane", () => {
});
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
});
it("should be true if only 1 database", () => {

View File

@@ -69,16 +69,12 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
this.isExecuting(false);
this.close();
this.container.refreshAllDatabases();
this.container.tabsManager.closeTabsByComparator(tab => tab.node?.id() === selectedDatabase.id());
this.container.tabsManager.closeTabsByComparator(tab => tab.node && tab.node.rid === selectedDatabase.rid);
this.container.selectedNode(null);
selectedDatabase
.collections()
.forEach((collection: ViewModels.Collection) =>
this.container.tabsManager.closeTabsByComparator(
tab =>
tab.node?.id() === collection.id() &&
(tab.node as ViewModels.Collection).databaseId === collection.databaseId
)
this.container.tabsManager.closeTabsByComparator(tab => tab.node && tab.node.rid === collection.rid)
);
this.resetData();
TelemetryProcessor.traceSuccess(

View File

@@ -140,7 +140,10 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
}
public async submit(): Promise<void> {
const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${this.name} to gallery`);
const notificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Publishing ${this.name} to gallery`
);
this.isExecuting = true;
this.triggerRender();
@@ -158,16 +161,8 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
this.content,
this.isLinkInjectionEnabled
);
const data = response.data;
if (data) {
if (data.pendingScanJobIds?.length > 0) {
NotificationConsoleUtils.logConsoleInfo(
`Content of ${this.name} is currently being scanned for illegal content. It will not be available in the public gallery until the review is complete (may take a few days).`
);
} else {
NotificationConsoleUtils.logConsoleInfo(`Published ${this.name} to gallery`);
}
if (response.data) {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
}
} catch (error) {
this.formError = `Failed to publish ${this.name} to gallery`;
@@ -175,10 +170,10 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
const message = `${this.formError}: ${this.formErrorDetail}`;
Logger.logError(message, "PublishNotebookPaneAdapter/submit");
NotificationConsoleUtils.logConsoleError(message);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
return;
} finally {
clearPublishingMessage();
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
this.isExecuting = false;
this.triggerRender();
}

View File

@@ -296,9 +296,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined
newCellId: undefined
}}
isFavorite={false}
showDownload={true}

View File

@@ -7,7 +7,7 @@ describe("Settings Pane", () => {
let explorer: Explorer;
beforeEach(() => {
explorer = new Explorer();
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
});
it("should be true for SQL API", () => {

View File

@@ -103,8 +103,6 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
"isSample": false,
"name": "SampleNotebook.ipynb",
"newCellId": undefined,
"pendingScanJobIds": undefined,
"policyViolations": undefined,
"tags": Array [
"",
],

View File

@@ -6,7 +6,7 @@ import Explorer from "../Explorer";
jest.mock("../Explorer");
const createExplorer = () => {
const mock = new Explorer();
const mock = new Explorer({} as any);
mock.selectedNode = ko.observable();
mock.isNotebookEnabled = ko.observable(false);
mock.addCollectionText = ko.observable("add collection");

View File

@@ -71,8 +71,6 @@ export var htmlSelectors = {
dataTableScrollContainerSelector: ".dataTables_scroll",
dataTableHeaderTypeSelector: "table thead th",
dataTablePaginationButtonSelector: ".paginate_button",
dataTableHeaderTableSelector: "#storageTable_wrapper .dataTables_scrollHeadInner table",
dataTableBodyTableSelector: "#storageTable_wrapper .dataTables_scrollBody table",
searchInputField: ".search-input",
uploadDropdownSelector: "#upload-dropdown",
navigationDropdownSelector: "#navigation-dropdown",

View File

@@ -1,18 +0,0 @@
import { getQuotedCqlIdentifier } from "./CqlUtilities";
describe("getQuotedCqlIdentifier", () => {
it("undefined id", () => {
const result = getQuotedCqlIdentifier(undefined);
expect(result).toBe(undefined);
});
it("id with no quotes", () => {
const result = getQuotedCqlIdentifier("foo");
expect(result).toBe('"foo"');
});
it("id with quotes", () => {
const result = getQuotedCqlIdentifier('"foo"');
expect(result).toBe('"""foo"""');
});
});

View File

@@ -1,12 +0,0 @@
export function getQuotedCqlIdentifier(identifier: string): string {
let result = identifier;
if (!identifier) {
return result;
}
if (identifier.includes('"')) {
result = identifier.replace(/"/g, '""');
}
return `"${result}"`;
}

Some files were not shown because too many files have changed in this diff Show More