Compare commits

..

28 Commits

Author SHA1 Message Date
Laurent Nguyen
4cc98f0b62 Initial implementation of dropzone component 2022-03-16 17:42:42 +01:00
Karthik chakravarthy
5c8016ecd6 Enable phoenix for MPAC by default, and for PROD will enable phoenix only based on flight (#1232)
* Enable phoenix to MPAC by default and for prod based on flight

* phoenix flag setting based of configContext
2022-03-01 13:38:30 -05:00
victor-meng
605117c62d Update autoscale throughput limits (#1229)
* Change minimum autoscale throughput and default autoscale throughput for free tier account to 1000

* Fix unit tests

* Update snapshot

* Fix cassandra add collection panel

* Address comments

* Add feature flag/flight

* Remove console.log
2022-02-25 18:21:58 -08:00
Tanuj Mittal
7a809cd2bc Always schedule memory call (#1231)
* Always schedule memory call

* Memory heart beat issue-fix

Co-authored-by: kcheekuri <kcheekuri@microsoft.com>
2022-02-25 13:41:43 -05:00
Karthik chakravarthy
0a51e24b94 Phoenix UX fixes (#1230) 2022-02-25 13:41:13 -05:00
victor-meng
f36a881679 Add List, Map, and Set column types for cassandra (#1228) 2022-02-18 15:25:47 -08:00
victor-meng
b7f0548cca Set toolTip for "Request Charge" and "Showing Results" metrics (#1212) 2022-01-31 10:02:04 -08:00
Armando Trejo Oliver
4728dc48d7 Add Mooncake and Fairfax BE and Mongo endpoints to allowed endpoints (#1213) 2022-01-28 18:43:34 -08:00
Karthik chakravarthy
9358fd5889 Clean computeV2 code (#1194)
Cleans compute V2 code used in the Phoenix notebooks flow.
Fix the issue with 'Setup Notebooks' in quick start menu.
2022-01-26 07:31:38 -05:00
Armando Trejo Oliver
f5da8bb276 Validate endpoints from feature flags (#1196)
Validate endpoints from feature flags
2022-01-24 13:06:43 -08:00
Asier Isayas
de5df90f75 Removing serverless check to show synapse link options (#1188)
* removing serverless check to show synapse link enablement

* fixing tests

* fixing fomatting

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2022-01-13 15:27:43 -05:00
victor-meng
66421ad276 Add headers to unblock merge private preview customers (#1190) 2022-01-13 11:35:20 -08:00
Deborah Chen
e70fa01a8b Updating text to match backend value for large partition keys(#1186) 2022-01-11 10:45:41 -08:00
Srinath Narayanan
79b6f3cf2f FIxed bugs in JupyterLabAppFactory (#1187)
* initial commit for closing terminal

* added extra case

* lint changes and hostee explorer fixes

* fixed lint errors

* fixed compile error

* fixed review comments

* modified mongo shell logic

* added cassandra hell changes

* fixed compile error
2022-01-11 18:24:27 +05:30
Srinath Narayanan
b765cae088 Close mongo and casssandra terminal tabs once the shells are exited (#1183)
* initial commit for closing terminal

* added extra case

* lint changes and hostee explorer fixes

* fixed lint errors

* fixed compile error

* fixed review comments
2022-01-11 01:28:35 +05:30
Srinath Narayanan
591782195d added phoenixfeatures flag (#1184) 2022-01-10 23:40:41 +05:30
Karthik chakravarthy
c7ceda3a3e Disable auto save for notebooks under temporary workspace (#1181)
* disable autosave only for temporary notebooks

* Add override epic description
2022-01-10 09:38:54 -05:00
Sunil Kumar Yadav
b19144f792 Fixed querytab corresponding command bar (#1180)
Co-authored-by: sunilyadav <v-yadavsunil@microsoft.com>
2021-12-27 11:41:42 -08:00
Sunil Kumar Yadav
e61f9f2a38 Fixed inconsistent use of collection id and name (#1179)
Co-authored-by: sunilyadav <v-yadavsunil@microsoft.com>
2021-12-27 11:40:12 -08:00
Karthik chakravarthy
025d5010b4 Add Pop-up on save click for temporary notebooks (#1177)
* Disable auto save for notebooks

* Changing auto save interval

* Remove auto save tabwise

* Remove auto save tabwise-1

* update file
2021-12-22 13:15:33 -05:00
Karthik chakravarthy
be28eb387b Removal of feature flag notebooksTemporarilyDown (#1178)
* Removal of feature flag notebooksTemporarilyDown

* Update flag

* Add Vnet/Firewall check for enabling phoenix
2021-12-22 13:15:12 -05:00
victor-meng
529202ba7e Add support for date type to cassandra column types (#1176) 2021-12-16 14:51:18 -08:00
vaidankarswapnil
de58f570cd Fix Radio buttons present under 'Settings' blade like ‘Custom and Unlimited’ along with its label ‘Page options’ are not enclosed in fieldset/legend tag (#1175)
* Fix a11y setting pane radiobuttons issue

* Update test snapshot issue

* Implemented fieldset and legend for ChoiceGroup in HTML

* cleanup
2021-12-15 12:22:15 -08:00
Sunil Kumar Yadav
6351e2bcd2 fixed unshared collection error for cassandra (#1172)
* fixed unshared collection error for cassandra

* fixed shared props value

Co-authored-by: sunilyadav <v-yadavsunil@microsoft.com>
2021-12-15 11:56:40 -08:00
Sunil Kumar Yadav
d97b991378 fixed screenreader copy issue (#1173)
Co-authored-by: sunilyadav <v-yadavsunil@microsoft.com>
2021-12-15 11:54:39 -08:00
Karthik chakravarthy
b7daadee20 Hide commandbar btns when phoenix flag is false (#1174)
* Hide commandbar btns when phoenix flag is false

* Showing notebooks based on phoenix
2021-12-14 09:02:49 -05:00
Karthik chakravarthy
b327bfd0d6 Update Api end points and add brs for allowlist (#1161)
* Update Api end points and add brs for allowlist
2021-12-13 09:23:33 -05:00
Karthik chakravarthy
469cd866e0 Bug Bash issues fixes (#1162)
* Bug Bash issues fixes

* Remove rename from root of Temporary Workspace context menu

* Update comments

* Update comments
2021-12-12 19:41:15 -05:00
75 changed files with 7798 additions and 2361 deletions

6
.vscode/launch.json vendored
View File

@@ -12,7 +12,8 @@
"--inspect-brk", "--inspect-brk",
"${workspaceRoot}/node_modules/jest/bin/jest.js", "${workspaceRoot}/node_modules/jest/bin/jest.js",
"--runInBand", "--runInBand",
"--coverage", "false" "--coverage",
"false"
], ],
"console": "integratedTerminal", "console": "integratedTerminal",
"internalConsoleOptions": "neverOpen", "internalConsoleOptions": "neverOpen",
@@ -26,7 +27,8 @@
"--inspect-brk", "--inspect-brk",
"${workspaceRoot}/node_modules/jest/bin/jest.js", "${workspaceRoot}/node_modules/jest/bin/jest.js",
"${fileBasenameNoExtension}", "${fileBasenameNoExtension}",
"--coverage", "false", "--coverage",
"false",
// "--watch", // "--watch",
// // --no-cache only used to make --watch work. Otherwise jest ignores the breakpoints. // // --no-cache only used to make --watch work. Otherwise jest ignores the breakpoints.
// // https://github.com/facebook/jest/issues/6683 // // https://github.com/facebook/jest/issues/6683

View File

@@ -1,4 +1,5 @@
{ {
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com", "JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
"IS_MPAC" : true "isTerminalEnabled" : true,
"isPhoenixEnabled" : true
} }

View File

@@ -1,4 +1,5 @@
{ {
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com", "JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"IS_MPAC" : false "isTerminalEnabled" : false,
"isPhoenixEnabled" : false
} }

View File

@@ -2895,6 +2895,14 @@ a:link {
.pageOptionsPart { .pageOptionsPart {
padding-bottom: @MediumSpace; padding-bottom: @MediumSpace;
} }
.legendLabel {
border-bottom: 0px;
width: auto;
font-size: @mediumFontSize;
display: inline !important;
float: left;
}
} }
// TODO: Remove these styles once we refactor all buttons to use the command button component // TODO: Remove these styles once we refactor all buttons to use the command button component

42
package-lock.json generated
View File

@@ -6866,6 +6866,11 @@
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
}, },
"attr-accept": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
},
"aws-sign2": { "aws-sign2": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@@ -10815,6 +10820,21 @@
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
}, },
"file-selector": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz",
"integrity": "sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==",
"requires": {
"tslib": "^2.0.3"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
"file-uri-to-path": { "file-uri-to-path": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -20978,6 +20998,28 @@
} }
} }
}, },
"react-dropzone": {
"version": "12.0.4",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.4.tgz",
"integrity": "sha512-fcqHEYe1MzAghU6/Hz86lHDlBNsA+lO48nAcm7/wA+kIzwS6uuJbUG33tBZjksj7GAZ1iUQ6NHwjUURPmSGang==",
"requires": {
"attr-accept": "^2.2.2",
"file-selector": "^0.4.0",
"prop-types": "^15.8.1"
},
"dependencies": {
"prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
}
}
},
"react-error-overlay": { "react-error-overlay": {
"version": "6.0.9", "version": "6.0.9",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",

View File

@@ -86,6 +86,7 @@
"react-dnd": "14.0.2", "react-dnd": "14.0.2",
"react-dnd-html5-backend": "14.0.0", "react-dnd-html5-backend": "14.0.0",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-dropzone": "12.0.4",
"react-hotkeys": "2.0.0", "react-hotkeys": "2.0.0",
"react-i18next": "11.8.5", "react-i18next": "11.8.5",
"react-notification-system": "0.2.17", "react-notification-system": "0.2.17",

View File

@@ -96,8 +96,10 @@ export class Flights {
public static readonly AutoscaleTest = "autoscaletest"; public static readonly AutoscaleTest = "autoscaletest";
public static readonly PartitionKeyTest = "partitionkeytest"; public static readonly PartitionKeyTest = "partitionkeytest";
public static readonly PKPartitionKeyTest = "pkpartitionkeytest"; public static readonly PKPartitionKeyTest = "pkpartitionkeytest";
public static readonly Phoenix = "phoenix"; public static readonly PhoenixNotebooks = "phoenixnotebooks";
public static readonly PhoenixFeatures = "phoenixfeatures";
public static readonly NotebooksDownBanner = "notebooksdownbanner"; public static readonly NotebooksDownBanner = "notebooksdownbanner";
public static readonly FreeTierAutoscaleThroughput = "freetierautoscalethroughput";
} }
export class AfecFeatures { export class AfecFeatures {
@@ -365,7 +367,7 @@ export class Notebook {
public static readonly containerStatusHeartbeatDelayMs = 30000; public static readonly containerStatusHeartbeatDelayMs = 30000;
public static readonly kernelRestartInitialDelayMs = 1000; public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000; public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000; public static readonly autoSaveIntervalMs = 300000;
public static readonly memoryGuageToGB = 1048576; public static readonly memoryGuageToGB = 1048576;
public static readonly lowMemoryThreshold = 0.8; public static readonly lowMemoryThreshold = 0.8;
public static readonly remainingTimeForAlert = 10; public static readonly remainingTimeForAlert = 10;
@@ -378,7 +380,7 @@ export class Notebook {
"We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation."; "We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation.";
public static saveNotebookModalTitle = "Save notebook in temporary workspace"; public static saveNotebookModalTitle = "Save notebook in temporary workspace";
public static saveNotebookModalContent = public static saveNotebookModalContent =
"This notebook will be saved in the temporary workspace and will be removed when the session expires. To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends."; "This notebook will be saved in the temporary workspace and will be removed when the session expires.";
public static newNotebookModalTitle = "Create notebook in temporary workspace"; public static newNotebookModalTitle = "Create notebook in temporary workspace";
public static newNotebookUploadModalTitle = "Upload notebook to temporary workspace"; public static newNotebookUploadModalTitle = "Upload notebook to temporary workspace";
public static newNotebookModalContent1 = public static newNotebookModalContent1 =

View File

@@ -1,5 +1,6 @@
import * as Cosmos from "@azure/cosmos"; import * as Cosmos from "@azure/cosmos";
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos"; import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
import { CosmosHeaders } from "@azure/cosmos/dist-esm";
import { configContext, Platform } from "../ConfigContext"; import { configContext, Platform } from "../ConfigContext";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
@@ -77,10 +78,21 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
} }
} }
// The Capability is a bitmap, which cosmosdb backend decodes as per the below enum
enum SDKSupportedCapabilities {
None = 0,
PartitionMerge = 1 << 0,
}
let _client: Cosmos.CosmosClient; let _client: Cosmos.CosmosClient;
export function client(): Cosmos.CosmosClient { export function client(): Cosmos.CosmosClient {
if (_client) return _client; if (_client) return _client;
let _defaultHeaders: CosmosHeaders = {};
_defaultHeaders["x-ms-cosmos-sdk-supported-capabilities"] =
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
const options: Cosmos.CosmosClientOptions = { const options: Cosmos.CosmosClientOptions = {
endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called endpoint: endpoint() || "https://cosmos.azure.com", // CosmosClient gets upset if we pass a bad URL. This should never actually get called
key: userContext.masterKey, key: userContext.masterKey,
@@ -89,6 +101,7 @@ export function client(): Cosmos.CosmosClient {
enableEndpointDiscovery: false, enableEndpointDiscovery: false,
}, },
userAgentSuffix: "Azure Portal", userAgentSuffix: "Azure Portal",
defaultHeaders: _defaultHeaders,
}; };
if (configContext.PROXY_PATH !== undefined) { if (configContext.PROXY_PATH !== undefined) {

View File

@@ -236,13 +236,12 @@ describe("MongoProxyClient", () => {
}); });
it("returns a production endpoint", () => { it("returns a production endpoint", () => {
const endpoint = getEndpoint(); const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer"); expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
}); });
it("returns a development endpoint", () => { it("returns a development endpoint", () => {
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" }); const endpoint = getEndpoint("https://localhost:1234");
const endpoint = getEndpoint();
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer"); expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
}); });
@@ -250,7 +249,7 @@ describe("MongoProxyClient", () => {
updateUserContext({ updateUserContext({
authType: AuthType.EncryptedToken, authType: AuthType.EncryptedToken,
}); });
const endpoint = getEndpoint(); const endpoint = getEndpoint("https://main.documentdb.ext.azure.com");
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer"); expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
}); });
}); });

View File

@@ -1,5 +1,6 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos"; import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import queryString from "querystring"; import queryString from "querystring";
import { allowedMongoProxyEndpoints, validateEndpoint } from "Utils/EndpointValidation";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
@@ -336,14 +337,17 @@ export function createMongoCollectionWithProxy(
} }
export function getFeatureEndpointOrDefault(feature: string): string { export function getFeatureEndpointOrDefault(feature: string): string {
return hasFlag(userContext.features.mongoProxyAPIs, feature) const endpoint =
? getEndpoint(userContext.features.mongoProxyEndpoint) hasFlag(userContext.features.mongoProxyAPIs, feature) &&
: getEndpoint(); validateEndpoint(userContext.features.mongoProxyEndpoint, allowedMongoProxyEndpoints)
? userContext.features.mongoProxyEndpoint
: configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
return getEndpoint(endpoint);
} }
export function getEndpoint(customEndpoint?: string): string { export function getEndpoint(endpoint: string): string {
let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT; let url = endpoint + "/api/mongo/explorer";
url += "/api/mongo/explorer";
if (userContext.authType === AuthType.EncryptedToken) { if (userContext.authType === AuthType.EncryptedToken) {
url = url.replace("api/mongo", "api/guest/mongo"); url = url.replace("api/mongo", "api/guest/mongo");

View File

@@ -1,4 +1,16 @@
import { JunoEndpoints } from "Common/Constants"; import {
allowedAadEndpoints,
allowedArcadiaEndpoints,
allowedArmEndpoints,
allowedBackendEndpoints,
allowedEmulatorEndpoints,
allowedGraphEndpoints,
allowedHostedExplorerEndpoints,
allowedJunoOrigins,
allowedMongoBackendEndpoints,
allowedMsalRedirectEndpoints,
validateEndpoint,
} from "Utils/EndpointValidation";
export enum Platform { export enum Platform {
Portal = "Portal", Portal = "Portal",
@@ -8,7 +20,7 @@ export enum Platform {
export interface ConfigContext { export interface ConfigContext {
platform: Platform; platform: Platform;
allowedParentFrameOrigins: string[]; allowedParentFrameOrigins: ReadonlyArray<string>;
gitSha?: string; gitSha?: string;
proxyPath?: string; proxyPath?: string;
AAD_ENDPOINT: string; AAD_ENDPOINT: string;
@@ -27,10 +39,10 @@ export interface ConfigContext {
GITHUB_CLIENT_ID: string; GITHUB_CLIENT_ID: string;
GITHUB_TEST_ENV_CLIENT_ID: string; GITHUB_TEST_ENV_CLIENT_ID: string;
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it. GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
IS_MPAC: boolean; isTerminalEnabled: boolean;
isPhoenixEnabled: boolean;
hostedExplorerURL: string; hostedExplorerURL: string;
armAPIVersion?: string; armAPIVersion?: string;
allowedJunoOrigins: string[];
msalRedirectURI?: string; msalRedirectURI?: string;
} }
@@ -44,8 +56,7 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`, `^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`, `^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`, `^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`,
], ], // Webpack injects this at build time
// Webpack injects this at build time
gitSha: process.env.GIT_SHA, gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/", hostedExplorerURL: "https://cosmos.azure.com/",
AAD_ENDPOINT: "https://login.microsoftonline.com/", AAD_ENDPOINT: "https://login.microsoftonline.com/",
@@ -58,17 +69,10 @@ let configContext: Readonly<ConfigContext> = {
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net", ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306 GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772 GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772
IS_MPAC: false,
JUNO_ENDPOINT: "https://tools.cosmos.azure.com", JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
allowedJunoOrigins: [ isTerminalEnabled: false,
JunoEndpoints.Test, isPhoenixEnabled: false,
JunoEndpoints.Test2,
JunoEndpoints.Test3,
JunoEndpoints.Prod,
JunoEndpoints.Stage,
"https://localhost",
],
}; };
export function resetConfigContext(): void { export function resetConfigContext(): void {
@@ -79,6 +83,50 @@ export function resetConfigContext(): void {
} }
export function updateConfigContext(newContext: Partial<ConfigContext>): void { export function updateConfigContext(newContext: Partial<ConfigContext>): void {
if (!newContext) {
return;
}
if (!validateEndpoint(newContext.ARM_ENDPOINT, allowedArmEndpoints)) {
delete newContext.ARM_ENDPOINT;
}
if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) {
delete newContext.AAD_ENDPOINT;
}
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
delete newContext.EMULATOR_ENDPOINT;
}
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
delete newContext.GRAPH_ENDPOINT;
}
if (!validateEndpoint(newContext.ARCADIA_ENDPOINT, allowedArcadiaEndpoints)) {
delete newContext.ARCADIA_ENDPOINT;
}
if (!validateEndpoint(newContext.BACKEND_ENDPOINT, allowedBackendEndpoints)) {
delete newContext.BACKEND_ENDPOINT;
}
if (!validateEndpoint(newContext.MONGO_BACKEND_ENDPOINT, allowedMongoBackendEndpoints)) {
delete newContext.MONGO_BACKEND_ENDPOINT;
}
if (!validateEndpoint(newContext.JUNO_ENDPOINT, allowedJunoOrigins)) {
delete newContext.JUNO_ENDPOINT;
}
if (!validateEndpoint(newContext.hostedExplorerURL, allowedHostedExplorerEndpoints)) {
delete newContext.hostedExplorerURL;
}
if (!validateEndpoint(newContext.msalRedirectURI, allowedMsalRedirectEndpoints)) {
delete newContext.msalRedirectURI;
}
Object.assign(configContext, newContext); Object.assign(configContext, newContext);
} }
@@ -102,18 +150,8 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
}); });
if (response.status === 200) { if (response.status === 200) {
try { try {
const { allowedParentFrameOrigins, allowedJunoOrigins, ...externalConfig } = await response.json(); const { ...externalConfig } = await response.json();
Object.assign(configContext, externalConfig); updateConfigContext(externalConfig);
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
updateConfigContext({
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins],
});
}
if (allowedJunoOrigins && allowedJunoOrigins.length > 0) {
updateConfigContext({
allowedJunoOrigins: [...configContext.allowedJunoOrigins, ...allowedJunoOrigins],
});
}
} catch (error) { } catch (error) {
console.error("Unable to parse json in config file"); console.error("Unable to parse json in config file");
console.error(error); console.error(error);

View File

@@ -9,6 +9,7 @@ export enum TabKind {
Graph, Graph,
SQLQuery, SQLQuery,
ScaleSettings, ScaleSettings,
DataUploader,
} }
/** /**

View File

@@ -33,6 +33,7 @@ export enum MessageTypes {
CreateWorkspace, CreateWorkspace,
CreateSparkPool, CreateSparkPool,
RefreshDatabaseAccount, RefreshDatabaseAccount,
CloseTab,
} }
export { Versions, ActionContracts, Diagnostics }; export { Versions, ActionContracts, Diagnostics };

View File

@@ -142,6 +142,7 @@ export interface Collection extends CollectionBase {
onGraphDocumentsClick(): void; onGraphDocumentsClick(): void;
onMongoDBDocumentsClick(): void; onMongoDBDocumentsClick(): void;
onSchemaAnalyzerClick(): void; onSchemaAnalyzerClick(): void;
onDataUploaderClick(): void;
openTab(): void; openTab(): void;
onSettingsClick: () => Promise<void>; onSettingsClick: () => Promise<void>;
@@ -364,6 +365,7 @@ export enum CollectionTabKind {
CollectionSettingsV2 = 20, CollectionSettingsV2 = 20,
DatabaseSettingsV2 = 21, DatabaseSettingsV2 = 21,
SchemaAnalyzer = 22, SchemaAnalyzer = 22,
DataUploader = 23,
} }
export enum TerminalKind { export enum TerminalKind {

View File

@@ -83,7 +83,6 @@ export const createCollectionContextMenuButton = (
items.push({ items.push({
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
isDisabled: useNotebook.getState().isShellEnabled && !useNotebook.getState().isPhoenix,
onClick: () => { onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (useNotebook.getState().isShellEnabled) { if (useNotebook.getState().isShellEnabled) {

View File

@@ -55,6 +55,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = { const props: NotebookTerminalComponentProps = {
databaseAccount: testAccount, databaseAccount: testAccount,
notebookServerInfo: testNotebookServerInfo, notebookServerInfo: testNotebookServerInfo,
tabId: undefined,
}; };
const wrapper = shallow(<NotebookTerminalComponent {...props} />); const wrapper = shallow(<NotebookTerminalComponent {...props} />);
@@ -65,6 +66,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = { const props: NotebookTerminalComponentProps = {
databaseAccount: testMongo32Account, databaseAccount: testMongo32Account,
notebookServerInfo: testMongoNotebookServerInfo, notebookServerInfo: testMongoNotebookServerInfo,
tabId: undefined,
}; };
const wrapper = shallow(<NotebookTerminalComponent {...props} />); const wrapper = shallow(<NotebookTerminalComponent {...props} />);
@@ -75,6 +77,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = { const props: NotebookTerminalComponentProps = {
databaseAccount: testMongo36Account, databaseAccount: testMongo36Account,
notebookServerInfo: testMongoNotebookServerInfo, notebookServerInfo: testMongoNotebookServerInfo,
tabId: undefined,
}; };
const wrapper = shallow(<NotebookTerminalComponent {...props} />); const wrapper = shallow(<NotebookTerminalComponent {...props} />);
@@ -85,6 +88,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = { const props: NotebookTerminalComponentProps = {
databaseAccount: testCassandraAccount, databaseAccount: testCassandraAccount,
notebookServerInfo: testCassandraNotebookServerInfo, notebookServerInfo: testCassandraNotebookServerInfo,
tabId: undefined,
}; };
const wrapper = shallow(<NotebookTerminalComponent {...props} />); const wrapper = shallow(<NotebookTerminalComponent {...props} />);

View File

@@ -12,6 +12,7 @@ import * as StringUtils from "../../../Utils/StringUtils";
export interface NotebookTerminalComponentProps { export interface NotebookTerminalComponentProps {
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
databaseAccount: DataModels.DatabaseAccount; databaseAccount: DataModels.DatabaseAccount;
tabId: string;
} }
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> { export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
@@ -55,6 +56,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
apiType: userContext.apiType, apiType: userContext.apiType,
authType: userContext.authType, authType: userContext.authType,
databaseAccount: userContext.databaseAccount, databaseAccount: userContext.databaseAccount,
tabId: this.props.tabId,
}; };
postRobot.send(this.terminalWindow, "props", props, { postRobot.send(this.terminalWindow, "props", props, {

View File

@@ -19,7 +19,7 @@ import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/Telemet
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../../../UserContext"; import { userContext } from "../../../../../UserContext";
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils"; import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
import { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils"; import { autoPilotThroughput1K, autoPilotThroughput4K } from "../../../../../Utils/AutoPilotUtils";
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils"; import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import { import {
@@ -540,7 +540,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
step={AutoPilotUtils.autoPilotIncrementStep} step={AutoPilotUtils.autoPilotIncrementStep}
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()} value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
onChange={this.onAutoPilotThroughputChange} onChange={this.onAutoPilotThroughputChange}
min={minAutoPilotThroughput} min={userContext.features.freetierAutoscaleThroughput ? autoPilotThroughput1K : autoPilotThroughput4K}
errorMessage={this.props.throughputError} errorMessage={this.props.throughputError}
/> />
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()} {!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}

View File

@@ -5,6 +5,7 @@ const props = {
isDatabase: false, isDatabase: false,
showFreeTierExceedThroughputTooltip: true, showFreeTierExceedThroughputTooltip: true,
isSharded: true, isSharded: true,
isFreeTier: false,
setThroughputValue: () => jest.fn(), setThroughputValue: () => jest.fn(),
setIsAutoscale: () => jest.fn(), setIsAutoscale: () => jest.fn(),
setIsThroughputCapExceeded: () => jest.fn(), setIsThroughputCapExceeded: () => jest.fn(),

View File

@@ -14,6 +14,7 @@ import "./ThroughputInput.less";
export interface ThroughputInputProps { export interface ThroughputInputProps {
isDatabase: boolean; isDatabase: boolean;
isSharded: boolean; isSharded: boolean;
isFreeTier: boolean;
showFreeTierExceedThroughputTooltip: boolean; showFreeTierExceedThroughputTooltip: boolean;
setThroughputValue: (throughput: number) => void; setThroughputValue: (throughput: number) => void;
setIsAutoscale: (isAutoscale: boolean) => void; setIsAutoscale: (isAutoscale: boolean) => void;
@@ -23,15 +24,20 @@ export interface ThroughputInputProps {
export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
isDatabase, isDatabase,
isSharded,
isFreeTier,
showFreeTierExceedThroughputTooltip, showFreeTierExceedThroughputTooltip,
setThroughputValue, setThroughputValue,
setIsAutoscale, setIsAutoscale,
setIsThroughputCapExceeded, setIsThroughputCapExceeded,
isSharded,
onCostAcknowledgeChange, onCostAcknowledgeChange,
}: ThroughputInputProps) => { }: ThroughputInputProps) => {
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true); const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput); const [throughput, setThroughput] = useState<number>(
isFreeTier && userContext.features.freetierAutoscaleThroughput
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K
);
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false); const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
const [throughputError, setThroughputError] = useState<string>(""); const [throughputError, setThroughputError] = useState<string>("");
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0); const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
@@ -151,11 +157,15 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => { const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
if (mode === "Autoscale") { if (mode === "Autoscale") {
setThroughput(AutoPilotUtils.minAutoPilotThroughput); const defaultThroughput =
isFreeTier && userContext.features.freetierAutoscaleThroughput
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K;
setThroughput(defaultThroughput);
setIsAutoScaleSelected(true); setIsAutoScaleSelected(true);
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput); setThroughputValue(defaultThroughput);
setIsAutoscale(true); setIsAutoscale(true);
checkThroughputCap(AutoPilotUtils.minAutoPilotThroughput); checkThroughputCap(defaultThroughput);
} else { } else {
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400); setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoScaleSelected(false); setIsAutoScaleSelected(false);
@@ -226,7 +236,11 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
}} }}
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)} onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
step={AutoPilotUtils.autoPilotIncrementStep} step={AutoPilotUtils.autoPilotIncrementStep}
min={AutoPilotUtils.minAutoPilotThroughput} min={
userContext.features.freetierAutoscaleThroughput
? AutoPilotUtils.autoPilotThroughput1K
: AutoPilotUtils.autoPilotThroughput4K
}
value={throughput.toString()} value={throughput.toString()}
aria-label="Max request units per second" aria-label="Max request units per second"
required={true} required={true}

View File

@@ -3,6 +3,7 @@
exports[`ThroughputInput Pane should render Default properly 1`] = ` exports[`ThroughputInput Pane should render Default properly 1`] = `
<ThroughputInput <ThroughputInput
isDatabase={false} isDatabase={false}
isFreeTier={false}
isSharded={true} isSharded={true}
onCostAcknowledgeChange={[Function]} onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]} setIsAutoscale={[Function]}

View File

@@ -1,8 +1,10 @@
import { Link } from "@fluentui/react/lib/Link"; import { Link } from "@fluentui/react/lib/Link";
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { IGalleryItem } from "Juno/JunoClient";
import * as ko from "knockout"; import * as ko from "knockout";
import React from "react"; import React from "react";
import _ from "underscore"; import _ from "underscore";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
import shallow from "zustand/shallow"; import shallow from "zustand/shallow";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
@@ -24,7 +26,6 @@ import * as ViewModels from "../Contracts/ViewModels";
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
import { useTabs } from "../hooks/useTabs"; import { useTabs } from "../hooks/useTabs";
import { IGalleryItem } from "../Juno/JunoClient";
import { PhoenixClient } from "../Phoenix/PhoenixClient"; import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings"; import * as ExplorerSettings from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
@@ -32,11 +33,7 @@ import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils"; import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
get as getWorkspace,
listByDatabaseAccount,
start,
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { stringToBlob } from "../Utils/BlobUtils"; import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
@@ -50,13 +47,12 @@ import * as FileSystemUtil from "./Notebook/FileSystemUtil";
import { SnapshotRequest } from "./Notebook/NotebookComponent/types"; import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import type NotebookManager from "./Notebook/NotebookManager"; import type NotebookManager from "./Notebook/NotebookManager";
import type { NotebookPaneContent } from "./Notebook/NotebookManager"; import { NotebookPaneContent } from "./Notebook/NotebookManager";
import { NotebookUtil } from "./Notebook/NotebookUtil"; import { NotebookUtil } from "./Notebook/NotebookUtil";
import { useNotebook } from "./Notebook/useNotebook"; import { useNotebook } from "./Notebook/useNotebook";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
@@ -178,7 +174,11 @@ export default class Explorer {
this.resourceTree = new ResourceTreeAdapter(this); this.resourceTree = new ResourceTreeAdapter(this);
// Override notebook server parameters from URL parameters // Override notebook server parameters from URL parameters
if (userContext.features.notebookServerUrl && userContext.features.notebookServerToken) { if (
userContext.features.notebookServerUrl &&
validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
userContext.features.notebookServerToken
) {
useNotebook.getState().setNotebookServerInfo({ useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl, notebookServerEndpoint: userContext.features.notebookServerUrl,
authToken: userContext.features.notebookServerToken, authToken: userContext.features.notebookServerToken,
@@ -190,19 +190,6 @@ export default class Explorer {
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath); useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
} }
if (userContext.features.livyEndpoint) {
useNotebook.getState().setSparkClusterConnectionInfo({
userName: undefined,
password: undefined,
endpoints: [
{
endpoint: userContext.features.livyEndpoint,
kind: DataModels.SparkClusterEndpointKind.Livy,
},
],
});
}
this.refreshExplorer(); this.refreshExplorer();
} }
@@ -399,6 +386,12 @@ export default class Explorer {
}); });
connectionStatus.status = ConnectionStatusType.Failed; connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetContainerConnection(connectionStatus); useNotebook.getState().resetContainerConnection(connectionStatus);
useDialog
.getState()
.showOkModalDialog(
"Connection Failed",
"We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket."
);
throw error; throw error;
} finally { } finally {
useNotebook.getState().setIsAllocating(false); useNotebook.getState().setIsAllocating(false);
@@ -422,7 +415,10 @@ export default class Explorer {
connectionStatus.status = ConnectionStatusType.Connected; connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus); useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({ useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl, notebookServerEndpoint:
(validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
userContext.features.notebookServerUrl) ||
connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken, authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
forwardingId: connectionInfo.data.forwardingId, forwardingId: connectionInfo.data.forwardingId,
}); });
@@ -439,7 +435,7 @@ export default class Explorer {
); );
return; return;
} }
const dialogContent = useNotebook.getState().isPhoenix const dialogContent = useNotebook.getState().isPhoenixNotebooks
? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?" ? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?"
: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?"; : "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?";
@@ -472,35 +468,6 @@ export default class Explorer {
} }
} }
private async ensureNotebookWorkspaceRunning() {
if (!userContext.databaseAccount) {
return;
}
let clearMessage;
try {
const notebookWorkspace = await getWorkspace(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
"default"
);
if (
notebookWorkspace &&
notebookWorkspace.properties &&
notebookWorkspace.properties.status &&
notebookWorkspace.properties.status.toLowerCase() === "stopped"
) {
clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace");
await start(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
}
} catch (error) {
handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace");
} finally {
clearMessage && clearMessage();
}
}
private _resetNotebookWorkspace = async () => { private _resetNotebookWorkspace = async () => {
useDialog.getState().closeDialog(); useDialog.getState().closeDialog();
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace"); const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
@@ -516,7 +483,7 @@ export default class Explorer {
TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, { TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook, dataExplorerArea: Areas.Notebook,
}); });
if (useNotebook.getState().isPhoenix) { if (useNotebook.getState().isPhoenixNotebooks) {
useTabs.getState().closeAllNotebookTabs(true); useTabs.getState().closeAllNotebookTabs(true);
connectionStatus = { connectionStatus = {
status: ConnectionStatusType.Connecting, status: ConnectionStatusType.Connecting,
@@ -530,7 +497,7 @@ export default class Explorer {
if (!connectionInfo?.data?.notebookServerUrl) { if (!connectionInfo?.data?.notebookServerUrl) {
throw new Error(`Reset Workspace: NotebookServerUrl is invalid!`); throw new Error(`Reset Workspace: NotebookServerUrl is invalid!`);
} }
if (useNotebook.getState().isPhoenix) { if (useNotebook.getState().isPhoenixNotebooks) {
await this.setNotebookInfo(connectionInfo, connectionStatus); await this.setNotebookInfo(connectionInfo, connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
} }
@@ -545,7 +512,7 @@ export default class Explorer {
error: getErrorMessage(error), error: getErrorMessage(error),
errorStack: getErrorStack(error), errorStack: getErrorStack(error),
}); });
if (useNotebook.getState().isPhoenix) { if (useNotebook.getState().isPhoenixNotebooks) {
connectionStatus = { connectionStatus = {
status: ConnectionStatusType.Failed, status: ConnectionStatusType.Failed,
}; };
@@ -743,7 +710,7 @@ export default class Explorer {
if (!notebookContentItem || !notebookContentItem.path) { if (!notebookContentItem || !notebookContentItem.path) {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`); throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
} }
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenix) { if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) {
await this.allocateContainer(); await this.allocateContainer();
} }
@@ -961,19 +928,17 @@ export default class Explorer {
/** /**
* This creates a new notebook file, then opens the notebook * This creates a new notebook file, then opens the notebook
*/ */
public onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): void { public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) { if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create new notebook, but notebook is not enabled"; const error = "Attempt to create new notebook, but notebook is not enabled";
handleError(error, "Explorer/onNewNotebookClicked"); handleError(error, "Explorer/onNewNotebookClicked");
throw new Error(error); throw new Error(error);
} }
if (useNotebook.getState().isPhoenix) { if (useNotebook.getState().isPhoenixNotebooks) {
if (isGithubTree) { if (isGithubTree) {
async () => { await this.allocateContainer();
await this.allocateContainer(); parent = parent || this.resourceTree.myNotebooksContentRoot;
parent = parent || this.resourceTree.myNotebooksContentRoot; this.createNewNoteBook(parent, isGithubTree);
this.createNewNoteBook(parent, isGithubTree);
};
} else { } else {
useDialog.getState().showOkCancelModalDialog( useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookModalTitle, Notebook.newNotebookModalTitle,
@@ -1058,7 +1023,7 @@ export default class Explorer {
} }
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> { public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
if (useNotebook.getState().isPhoenix) { if (useNotebook.getState().isPhoenixFeatures) {
await this.allocateContainer(); await this.allocateContainer();
const notebookServerInfo = useNotebook.getState().notebookServerInfo; const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) { if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
@@ -1098,7 +1063,7 @@ export default class Explorer {
const terminalTabs: TerminalTab[] = useTabs const terminalTabs: TerminalTab[] = useTabs
.getState() .getState()
.getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle() === title) as TerminalTab[]; .getTabs(ViewModels.CollectionTabKind.Terminal, (tab) => tab.tabTitle().startsWith(title)) as TerminalTab[];
let index = 1; let index = 1;
if (terminalTabs.length > 0) { if (terminalTabs.length > 0) {
@@ -1189,20 +1154,12 @@ export default class Explorer {
} }
} }
private _openSetupNotebooksPaneForQuickstart(): void {
const title = "Enable Notebooks (Preview)";
const description =
"You have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
useSidePanel
.getState()
.openSidePanel(title, <SetupNoteBooksPanel explorer={this} panelTitle={title} panelDescription={description} />);
}
public async handleOpenFileAction(path: string): Promise<void> { public async handleOpenFileAction(path: string): Promise<void> {
if (useNotebook.getState().isPhoenix) { if (useNotebook.getState().isPhoenixNotebooks === undefined) {
await useNotebook.getState().getPhoenixStatus();
}
if (useNotebook.getState().isPhoenixNotebooks) {
await this.allocateContainer(); await this.allocateContainer();
} else if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
this._openSetupNotebooksPaneForQuickstart();
} }
// We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb // We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb
@@ -1233,7 +1190,7 @@ export default class Explorer {
} }
public openUploadFilePanel(parent?: NotebookContentItem): void { public openUploadFilePanel(parent?: NotebookContentItem): void {
if (useNotebook.getState().isPhoenix) { if (useNotebook.getState().isPhoenixNotebooks) {
useDialog.getState().showOkCancelModalDialog( useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookUploadModalTitle, Notebook.newNotebookUploadModalTitle,
undefined, undefined,
@@ -1263,7 +1220,7 @@ export default class Explorer {
} }
public getDownloadModalConent(fileName: string): JSX.Element { public getDownloadModalConent(fileName: string): JSX.Element {
if (useNotebook.getState().isPhoenix) { if (useNotebook.getState().isPhoenixNotebooks) {
return ( return (
<> <>
<p>{Notebook.galleryNotebookDownloadContent1}</p> <p>{Notebook.galleryNotebookDownloadContent1}</p>
@@ -1286,21 +1243,22 @@ export default class Explorer {
: this.refreshAllDatabases(); : this.refreshAllDatabases();
await useNotebook.getState().refreshNotebooksEnabledStateForAccount(); await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
//Disable phoenix in case of Vnet or Firewall was enabled.
if (!isPublicInternetAccessAllowed()) {
useNotebook.getState().setIsPhoenix(false);
}
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount // TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
const isNotebookEnabled = userContext.features.notebooksDownBanner || useNotebook.getState().isPhoenix; const isNotebookEnabled =
userContext.features.notebooksDownBanner ||
useNotebook.getState().isPhoenixNotebooks ||
useNotebook.getState().isPhoenixFeatures;
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled); useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook.getState().setIsShellEnabled(useNotebook.getState().isPhoenix && isPublicInternetAccessAllowed()); useNotebook
.getState()
.setIsShellEnabled(useNotebook.getState().isPhoenixFeatures && isPublicInternetAccessAllowed());
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
isNotebookEnabled, isNotebookEnabled,
dataExplorerArea: Constants.Areas.Notebook, dataExplorerArea: Constants.Areas.Notebook,
}); });
if (useNotebook.getState().isPhoenix) { if (useNotebook.getState().isPhoenixNotebooks) {
await this.initNotebooks(userContext.databaseAccount); await this.initNotebooks(userContext.databaseAccount);
} }
} }

View File

@@ -53,7 +53,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor); const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (useNotebook.getState().isPhoenix) { if (useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) {
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus")); uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
} }

View File

@@ -31,28 +31,13 @@ describe("CommandBarComponentButtonFactory tests", () => {
}); });
}); });
it("Account is not serverless - button should be visible", () => { it("Button should be visible", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find( const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
); );
expect(enableAzureSynapseLinkBtn).toBeDefined(); expect(enableAzureSynapseLinkBtn).toBeDefined();
}); });
it("Account is serverless - button should be hidden", () => {
updateUserContext({
databaseAccount: {
properties: {
capabilities: [{ name: "EnableServerless" }],
},
} as DatabaseAccount,
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
expect(enableAzureSynapseLinkBtn).toBeUndefined();
});
}); });
describe("Enable notebook button", () => { describe("Enable notebook button", () => {

View File

@@ -10,7 +10,6 @@ import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg"; import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import GitHubIcon from "../../../../images/github.svg"; import GitHubIcon from "../../../../images/github.svg";
import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg"; import HostedTerminalIcon from "../../../../images/Hosted-Terminal.svg";
import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg";
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg"; import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg"; import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import OpenInTabIcon from "../../../../images/open-in-tab.svg"; import OpenInTabIcon from "../../../../images/open-in-tab.svg";
@@ -25,7 +24,6 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
import { JunoClient } from "../../../Juno/JunoClient"; import { JunoClient } from "../../../Juno/JunoClient";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils"; import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils"; import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
@@ -36,7 +34,6 @@ import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPa
import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel"; import { GitHubReposPanel } from "../../Panes/GitHubReposPanel/GitHubReposPanel";
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane"; import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane"; import { SettingsPane } from "../../Panes/SettingsPane/SettingsPane";
import { SetupNoteBooksPanel } from "../../Panes/SetupNotebooksPanel/SetupNotebooksPanel";
import { useDatabases } from "../../useDatabases"; import { useDatabases } from "../../useDatabases";
import { SelectedNodeState } from "../../useSelectedNode"; import { SelectedNodeState } from "../../useSelectedNode";
@@ -78,10 +75,10 @@ export function createStaticCommandBarButtons(
if (container.notebookManager?.gitHubOAuthService) { if (container.notebookManager?.gitHubOAuthService) {
notebookButtons.push(createManageGitHubAccountButton(container)); notebookButtons.push(createManageGitHubAccountButton(container));
} }
if (useNotebook.getState().isPhoenix && configContext.IS_MPAC) { if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
notebookButtons.push(createOpenTerminalButton(container)); notebookButtons.push(createOpenTerminalButton(container));
} }
if (selectedNodeState.isConnectedToContainer()) { if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
notebookButtons.push(createNotebookWorkspaceResetButton(container)); notebookButtons.push(createNotebookWorkspaceResetButton(container));
} }
if ( if (
@@ -99,22 +96,19 @@ export function createStaticCommandBarButtons(
} }
notebookButtons.forEach((btn) => { notebookButtons.forEach((btn) => {
if (!useNotebook.getState().isPhoenix) { if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) { if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg); applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.cassandraShellTemporarilyDownMsg);
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
} else {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
} }
} else if (btn.commandButtonLabel.indexOf("Mongo") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.mongoShellTemporarilyDownMsg);
}
} else if (!useNotebook.getState().isPhoenixNotebooks) {
applyNotebooksTemporarilyDownStyle(btn, Constants.Notebook.temporarilyDownMsg);
} }
buttons.push(btn); buttons.push(btn);
}); });
} else {
if (!isRunningOnNationalCloud() && useNotebook.getState().isPhoenix) {
buttons.push(createDivider());
buttons.push(createEnableNotebooksButton(container));
}
} }
if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) { if (!selectedNodeState.isDatabaseNodeOrNoneSelected()) {
@@ -169,9 +163,7 @@ export function createContextCommandBarButtons(
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) { if (useNotebook.getState().isShellEnabled) {
if (useNotebook.getState().isPhoenix) { container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
}
} else { } else {
selectedCollection && selectedCollection.onNewMongoShellClick(); selectedCollection && selectedCollection.onNewMongoShellClick();
} }
@@ -179,13 +171,6 @@ export function createContextCommandBarButtons(
commandButtonLabel: label, commandButtonLabel: label,
ariaLabel: label, ariaLabel: label,
hasPopup: true, hasPopup: true,
tooltipText:
useNotebook.getState().isShellEnabled && !useNotebook.getState().isPhoenix
? Constants.Notebook.mongoShellTemporarilyDownMsg
: undefined,
disabled:
(selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") ||
(useNotebook.getState().isShellEnabled && !useNotebook.getState().isPhoenix),
}; };
buttons.push(newMongoShellBtn); buttons.push(newMongoShellBtn);
} }
@@ -281,10 +266,6 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
return undefined; return undefined;
} }
if (isServerlessAccount()) {
return undefined;
}
if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) { if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) {
return undefined; return undefined;
} }
@@ -478,33 +459,6 @@ function createOpenQueryFromDiskButton(): CommandButtonComponentProps {
}; };
} }
function createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return undefined;
}
const label = "Enable Notebooks (Preview)";
const tooltip =
"Notebooks are not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const description =
"Looks like you have not yet created a notebooks workspace for this account. To proceed and start using notebooks, we'll need to create a default notebooks workspace in this account.";
return {
iconSrc: EnableNotebooksIcon,
iconAlt: label,
onCommandClick: () =>
useSidePanel
.getState()
.openSidePanel(
label,
<SetupNoteBooksPanel explorer={container} panelTitle={label} panelDescription={description} />
),
commandButtonLabel: label,
hasPopup: false,
disabled: !useNotebook.getState().isNotebooksEnabledForAccount,
ariaLabel: label,
tooltipText: useNotebook.getState().isNotebooksEnabledForAccount ? "" : tooltip,
};
}
function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps { function createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Terminal"; const label = "Open Terminal";
return { return {
@@ -522,9 +476,6 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
const label = "Open Mongo Shell"; const label = "Open Mongo Shell";
const tooltip = const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."; "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const title = "Set up workspace";
const description =
"Looks like you have not created a workspace for this account. To proceed and start using features including mongo shell and notebook, we will need to create a default workspace in this account.";
const disableButton = const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled; !useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return { return {
@@ -533,13 +484,6 @@ function createOpenMongoTerminalButton(container: Explorer): CommandButtonCompon
onCommandClick: () => { onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled) { if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
useSidePanel
.getState()
.openSidePanel(
title,
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
);
} }
}, },
commandButtonLabel: label, commandButtonLabel: label,
@@ -554,9 +498,6 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
const label = "Open Cassandra Shell"; const label = "Open Cassandra Shell";
const tooltip = const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks."; "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const title = "Set up workspace";
const description =
"Looks like you have not created a workspace for this account. To proceed and start using features including cassandra shell and notebook, we will need to create a default workspace in this account.";
const disableButton = const disableButton =
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled; !useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return { return {
@@ -565,13 +506,6 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
onCommandClick: () => { onCommandClick: () => {
if (useNotebook.getState().isNotebookEnabled) { if (useNotebook.getState().isNotebookEnabled) {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra); container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
} else {
useSidePanel
.getState()
.openSidePanel(
title,
<SetupNoteBooksPanel explorer={container} panelTitle={title} panelDescription={description} />
);
} }
}, },
commandButtonLabel: label, commandButtonLabel: label,

View File

@@ -0,0 +1,5 @@
.dataUploader {
width: 100%;
height: 100%;
overflow-y: auto;
}

View File

@@ -0,0 +1,209 @@
import { ImmutableOutput } from "@nteract/commutable";
import { actions, AppState, ContentRef, KernelRef, selectors } from "@nteract/core";
import Immutable from "immutable";
import React, { CSSProperties, useCallback, useMemo } from "react";
import { useDropzone } from "react-dropzone";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import "./DataUploader.less";
interface DataUploaderPureProps {
contentRef: ContentRef;
kernelRef: KernelRef;
databaseId: string;
collectionId: string;
}
const getColor = (props) => {
if (props.isDragAccept) {
return "#00e676";
}
if (props.isDragReject) {
return "#ff1744";
}
if (props.isFocused) {
return "#2196f3";
}
return "#eeeeee";
};
interface DataUploaderDispatchProps {
runCell: (contentRef: ContentRef, cellId: string) => void;
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
updateCell: (text: string, id: string, contentRef: ContentRef) => void;
}
type DataUploaderProps = DataUploaderPureProps & StateProps & DataUploaderDispatchProps;
const DataUploader: React.FC<DataUploaderProps> = (props) => {
// componentDidMount(): void {
// loadTransform(this.props);
// }
// private onAnalyzeButtonClick = (filter: string = DefaultFilter, sampleSize: string = this.state.sampleSize) => {
// const query = {
// command: "listSchema",
// database: this.props.databaseId,
// collection: this.props.collectionId,
// outputType: this.state.outputType,
// filter,
// sampleSize,
// };
// this.setState({
// isFiltering: true,
// });
// this.props.updateCell(JSON.stringify(query), this.props.firstCellId, this.props.contentRef);
// this.clickAnalyzeTelemetryStartKey = traceStart(Action.DataUploaderClickAnalyze, {
// database: this.props.databaseId,
// collection: this.props.collectionId,
// sampleSize,
// });
// this.props.runCell(this.props.contentRef, this.props.firstCellId);
// };
const { firstCellId: id, contentRef, kernelStatus } = props;
// eslint-disable-next-line no-console
console.log("firstCellId: id, contentRef, kernelStatus", id, contentRef, kernelStatus);
const isKernelBusy = kernelStatus === "busy";
const isKernelIdle = kernelStatus === "idle";
const baseStyle: CSSProperties = {
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: "20px",
borderWidth: 2,
borderRadius: 2,
borderColor: "#eeeeee",
borderStyle: "dashed",
backgroundColor: "#fafafa",
color: "#bdbdbd",
transition: "border .3s ease-in-out",
};
const activeStyle = {
borderColor: "#2196f3",
};
const acceptStyle = {
borderColor: "#00e676",
};
const rejectStyle = {
borderColor: "#ff1744",
};
const onDrop = useCallback((acceptedFiles) => {
//eslint-disable-next-line no-console
console.log("acceptedFiles", acceptedFiles);
}, []);
const { getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject } = useDropzone({
onDrop,
accept: ".json",
});
const style = useMemo(
() => ({
...baseStyle,
...(isDragActive ? activeStyle : {}),
...(isDragAccept ? acceptStyle : {}),
...(isDragReject ? rejectStyle : {}),
}),
[isDragActive, isDragReject, isDragAccept]
);
if (!id) {
return <></>;
}
return (
<div {...getRootProps({ style })}>
<input {...getInputProps()} />
<div>Drag and drop your json file here</div>
</div>
);
};
interface StateProps {
firstCellId: string;
kernelStatus: string;
outputs: Immutable.List<ImmutableOutput>;
}
interface InitialProps {
kernelRef: string;
contentRef: string;
}
// Redux
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
const { kernelRef, contentRef } = initialProps;
const mapStateToProps = (state: AppState) => {
let kernelStatus;
let firstCellId;
let outputs;
const kernel = selectors.kernel(state, { kernelRef });
if (kernel) {
kernelStatus = kernel.status;
}
const content = selectors.content(state, { contentRef });
if (content?.type === "notebook") {
const cellOrder = selectors.notebook.cellOrder(content.model);
if (cellOrder.size > 0) {
firstCellId = cellOrder.first() as string;
const model = selectors.model(state, { contentRef });
if (model && model.type === "notebook") {
const cell = selectors.notebook.cellById(model, { id: firstCellId });
if (cell) {
outputs = cell.get("outputs", Immutable.List());
}
}
}
}
return {
firstCellId,
kernelStatus,
outputs,
};
};
return mapStateToProps;
};
const makeMapDispatchToProps = () => {
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
return dispatch(
actions.addTransform({
mediaType: transform.MIMETYPE,
component: transform,
})
);
},
runCell: (contentRef: ContentRef, cellId: string) => {
return dispatch(
actions.executeCell({
contentRef,
id: cellId,
})
);
},
updateCell: (text: string, id: string, contentRef: ContentRef) => {
dispatch(actions.updateCellSource({ id, contentRef, value: text }));
},
};
};
return mapDispatchToProps;
};
export default connect(makeMapStateToProps, makeMapDispatchToProps)(DataUploader);

View File

@@ -0,0 +1,48 @@
import { actions, createContentRef, createKernelRef, KernelRef } from "@nteract/core";
import DataUploader from "Explorer/Notebook/DataUploader/DataUploader";
import * as React from "react";
import { Provider } from "react-redux";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import {
NotebookComponentBootstrapper,
NotebookComponentBootstrapperOptions,
} from "../NotebookComponent/NotebookComponentBootstrapper";
import { DataUploaderNotebook } from "./DataUploaderUtils";
export class DataUploaderAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
public parameters: unknown;
private kernelRef: KernelRef;
constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) {
super(options);
if (!this.contentRef) {
this.contentRef = createContentRef();
this.kernelRef = createKernelRef();
this.getStore().dispatch(
actions.fetchContent({
filepath: DataUploaderNotebook.path,
params: {},
kernelRef: this.kernelRef,
contentRef: this.contentRef,
})
);
}
}
public renderComponent(): JSX.Element {
const props = {
contentRef: this.contentRef,
kernelRef: this.kernelRef,
databaseId: this.databaseId,
collectionId: this.collectionId,
};
return (
<Provider store={this.getStore()}>
<DataUploader {...props} />;
</Provider>
);
}
}

View File

@@ -0,0 +1,44 @@
import { Notebook } from "@nteract/commutable";
import { IContent } from "@nteract/types";
import * as InMemoryContentProviderUtils from "../NotebookComponent/ContentProviders/InMemoryContentProviderUtils";
const notebookName = "data-uploader-component-notebook.ipynb";
const notebookPath = InMemoryContentProviderUtils.toContentUri(notebookName);
const notebook: Notebook = {
cells: [
{
cell_type: "code",
metadata: {},
execution_count: 0,
outputs: [],
source: "",
},
],
metadata: {
kernelspec: {
displayName: "Mongo",
language: "mongocli",
name: "mongo",
},
language_info: {
file_extension: "ipynb",
mimetype: "application/json",
name: "mongo",
version: "1.0",
},
},
nbformat: 4,
nbformat_minor: 4,
};
export const DataUploaderNotebook: IContent<"notebook"> = {
name: notebookName,
path: notebookPath,
type: "notebook",
writable: true,
created: "",
last_modified: "",
mimetype: "application/x-ipynb+json",
content: notebook,
format: "json",
};

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { Link } from "@fluentui/react";
import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable"; import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable";
// Vendor modules // Vendor modules
import { import {
@@ -14,13 +15,15 @@ import "@nteract/styles/editor-overrides.css";
import "@nteract/styles/global-variables.css"; import "@nteract/styles/global-variables.css";
import "codemirror/addon/hint/show-hint.css"; import "codemirror/addon/hint/show-hint.css";
import "codemirror/lib/codemirror.css"; import "codemirror/lib/codemirror.css";
import { Notebook } from "Common/Constants";
import { useDialog } from "Explorer/Controls/Dialog";
import * as Immutable from "immutable"; import * as Immutable from "immutable";
import * as React from "react"; import * as React from "react";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import "react-table/react-table.css"; import "react-table/react-table.css";
import { AnyAction, Store } from "redux"; import { AnyAction, Store } from "redux";
import { NotebookClientV2 } from "../NotebookClientV2"; import { NotebookClientV2 } from "../NotebookClientV2";
import { NotebookUtil } from "../NotebookUtil"; import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
import * as NteractUtil from "../NTeractUtil"; import * as NteractUtil from "../NTeractUtil";
import * as CdbActions from "./actions"; import * as CdbActions from "./actions";
import { NotebookComponent } from "./NotebookComponent"; import { NotebookComponent } from "./NotebookComponent";
@@ -99,6 +102,10 @@ export class NotebookComponentBootstrapper {
}; };
} }
public getNotebookPath(): string {
return this.getStore().getState().core.entities.contents.byRef.get(this.contentRef)?.filepath;
}
public setContent(name: string, content: unknown): void { public setContent(name: string, content: unknown): void {
this.getStore().dispatch( this.getStore().dispatch(
actions.fetchContentFulfilled({ actions.fetchContentFulfilled({
@@ -130,11 +137,32 @@ export class NotebookComponentBootstrapper {
/* Notebook operations. See nteract/packages/connected-components/src/notebook-menu/index.tsx */ /* Notebook operations. See nteract/packages/connected-components/src/notebook-menu/index.tsx */
public notebookSave(): void { public notebookSave(): void {
this.getStore().dispatch( if (
actions.save({ NotebookUtil.getContentProviderType(this.getNotebookPath()) ===
contentRef: this.contentRef, NotebookContentProviderType.JupyterContentProviderType
}) ) {
); useDialog.getState().showOkCancelModalDialog(
Notebook.saveNotebookModalTitle,
undefined,
"Save",
async () => {
this.getStore().dispatch(
actions.save({
contentRef: this.contentRef,
})
);
},
"Cancel",
undefined,
this.getSaveNotebookSubText()
);
} else {
this.getStore().dispatch(
actions.save({
contentRef: this.contentRef,
})
);
}
} }
public notebookChangeKernel(kernelSpecName: string): void { public notebookChangeKernel(kernelSpecName: string): void {
@@ -341,4 +369,19 @@ export class NotebookComponentBootstrapper {
protected getStore(): Store<AppState, AnyAction> { protected getStore(): Store<AppState, AnyAction> {
return this.notebookClient.getStore(); return this.notebookClient.getStore();
} }
private getSaveNotebookSubText(): JSX.Element {
return (
<>
<p>{Notebook.saveNotebookModalContent}</p>
<br />
<p>
{Notebook.newNotebookModalContent2}
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
} }

View File

@@ -12,11 +12,12 @@ import {
ServerConfig as JupyterServerConfig, ServerConfig as JupyterServerConfig,
} from "@nteract/core"; } from "@nteract/core";
import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging"; import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging";
import { defineConfigOption } from "@nteract/mythic-configuration";
import { RecordOf } from "immutable"; import { RecordOf } from "immutable";
import { AnyAction } from "redux"; import { Action, AnyAction } from "redux";
import { ofType, StateObservable } from "redux-observable"; import { ofType, StateObservable } from "redux-observable";
import { kernels, sessions } from "rx-jupyter"; import { kernels, sessions } from "rx-jupyter";
import { concat, EMPTY, from, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs"; import { concat, EMPTY, from, interval, merge, Observable, Observer, of, Subject, Subscriber, timer } from "rxjs";
import { import {
catchError, catchError,
concatMap, concatMap,
@@ -41,7 +42,7 @@ import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationCons
import { useDialog } from "../../Controls/Dialog"; import { useDialog } from "../../Controls/Dialog";
import * as FileSystemUtil from "../FileSystemUtil"; import * as FileSystemUtil from "../FileSystemUtil";
import * as cdbActions from "../NotebookComponent/actions"; import * as cdbActions from "../NotebookComponent/actions";
import { NotebookUtil } from "../NotebookUtil"; import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
import * as CdbActions from "./actions"; import * as CdbActions from "./actions";
import * as TextFile from "./contents/file/text-file"; import * as TextFile from "./contents/file/text-file";
import { CdbAppState } from "./types"; import { CdbAppState } from "./types";
@@ -948,6 +949,54 @@ const resetCellStatusOnExecuteCanceledEpic = (
); );
}; };
const { selector: autoSaveInterval } = defineConfigOption({
key: "autoSaveInterval",
label: "Auto-save interval",
defaultValue: 120_000,
});
/**
* Override autoSaveCurrentContentEpic to disable auto save for notebooks under temporary workspace.
* @param action$
*/
export function autoSaveCurrentContentEpic(
action$: Observable<Action>,
state$: StateObservable<AppState>
): Observable<actions.Save> {
return state$.pipe(
map((state) => autoSaveInterval(state)),
switchMap((time) => interval(time)),
mergeMap(() => {
const state = state$.value;
return from(
selectors
.contentByRef(state)
.filter(
/*
* Only save contents that are files or notebooks with
* a filepath already set.
*/
(content) => (content.type === "file" || content.type === "notebook") && content.filepath !== ""
)
.keys()
);
}),
filter((contentRef: ContentRef) => {
const model = selectors.model(state$.value, { contentRef });
const content = selectors.content(state$.value, { contentRef });
if (
model &&
model.type === "notebook" &&
NotebookUtil.getContentProviderType(content.filepath) !== NotebookContentProviderType.JupyterContentProviderType
) {
return selectors.notebook.isDirty(model);
}
return false;
}),
map((contentRef: ContentRef) => actions.save({ contentRef }))
);
}
export const allEpics = [ export const allEpics = [
addInitialCodeCellEpic, addInitialCodeCellEpic,
focusInitialCodeCellEpic, focusInitialCodeCellEpic,
@@ -965,4 +1014,5 @@ export const allEpics = [
traceNotebookInfoEpic, traceNotebookInfoEpic,
traceNotebookKernelEpic, traceNotebookKernelEpic,
resetCellStatusOnExecuteCanceledEpic, resetCellStatusOnExecuteCanceledEpic,
autoSaveCurrentContentEpic,
]; ];

View File

@@ -1,12 +1,12 @@
import { AppState, epics as coreEpics, reducers, IContentProvider } from "@nteract/core"; import { AppState, epics as coreEpics, IContentProvider, reducers } from "@nteract/core";
import { compose, Store, AnyAction, Middleware, Dispatch, MiddlewareAPI } from "redux";
import { Epic } from "redux-observable";
import { allEpics } from "./epics";
import { coreReducer, cdbReducer } from "./reducers";
import { catchError } from "rxjs/operators";
import { Observable } from "rxjs";
import { configuration } from "@nteract/mythic-configuration"; import { configuration } from "@nteract/mythic-configuration";
import { makeConfigureStore } from "@nteract/myths"; import { makeConfigureStore } from "@nteract/myths";
import { AnyAction, compose, Dispatch, Middleware, MiddlewareAPI, Store } from "redux";
import { Epic } from "redux-observable";
import { Observable } from "rxjs";
import { catchError } from "rxjs/operators";
import { allEpics } from "./epics";
import { cdbReducer, coreReducer } from "./reducers";
import { CdbAppState } from "./types"; import { CdbAppState } from "./types";
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
@@ -81,7 +81,6 @@ export const getCoreEpics = (autoStartKernelOnNotebookOpen: boolean): Epic[] =>
// This list needs to be consistent and in sync with core.allEpics until we figure // This list needs to be consistent and in sync with core.allEpics until we figure
// out how to safely filter out the ones we are overriding here. // out how to safely filter out the ones we are overriding here.
const filteredCoreEpics = [ const filteredCoreEpics = [
coreEpics.autoSaveCurrentContentEpic,
coreEpics.executeCellEpic, coreEpics.executeCellEpic,
coreEpics.executeFocusedCellEpic, coreEpics.executeFocusedCellEpic,
coreEpics.executeCellAfterKernelLaunchEpic, coreEpics.executeCellAfterKernelLaunchEpic,

View File

@@ -19,6 +19,7 @@ export class NotebookContainerClient {
private isResettingWorkspace: boolean; private isResettingWorkspace: boolean;
private phoenixClient: PhoenixClient; private phoenixClient: PhoenixClient;
private retryOptions: promiseRetry.Options; private retryOptions: promiseRetry.Options;
private scheduleTimerId: NodeJS.Timeout;
constructor(private onConnectionLost: () => void) { constructor(private onConnectionLost: () => void) {
this.phoenixClient = new PhoenixClient(); this.phoenixClient = new PhoenixClient();
@@ -27,34 +28,34 @@ export class NotebookContainerClient {
maxTimeout: Notebook.retryAttemptDelayMs, maxTimeout: Notebook.retryAttemptDelayMs,
minTimeout: Notebook.retryAttemptDelayMs, minTimeout: Notebook.retryAttemptDelayMs,
}; };
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) { this.initHeartbeat(Constants.Notebook.heartbeatDelayMs);
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
} else {
const unsub = useNotebook.subscribe(
(newServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => {
if (newServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
unsub();
},
(state) => state.notebookServerInfo
);
}
} }
/** private initHeartbeat(delayMs: number): void {
* Heartbeat: each ping schedules another ping this.scheduleHeartbeat(delayMs);
*/
private scheduleHeartbeat(delayMs: number): void { useNotebook.subscribe(
setTimeout(async () => { () => this.scheduleHeartbeat(delayMs),
const memoryUsageInfo = await this.getMemoryUsage(); (state) => state.notebookServerInfo
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo); );
const notebookServerInfo = useNotebook.getState().notebookServerInfo; }
if (notebookServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs); private scheduleHeartbeat(delayMs: number) {
} if (this.scheduleTimerId) {
}, delayMs); clearInterval(this.scheduleTimerId);
}
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
this.scheduleTimerId = setInterval(async () => {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
const memoryUsageInfo = await this.getMemoryUsage();
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
}
}, delayMs);
}
} }
public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> { public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
@@ -121,13 +122,10 @@ export class NotebookContainerClient {
} }
private shouldExecuteMemoryCall(): boolean { private shouldExecuteMemoryCall(): boolean {
if ( return (
useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Active && useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Active &&
useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected
) { );
return true;
}
return false;
} }
public async resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> { public async resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> {
@@ -152,7 +150,7 @@ export class NotebookContainerClient {
} }
try { try {
if (useNotebook.getState().isPhoenix) { if (useNotebook.getState().isPhoenixNotebooks) {
const provisionData: IProvisionData = { const provisionData: IProvisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint, cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
}; };

View File

@@ -303,8 +303,8 @@ export class NotebookContentClient {
private getServerConfig(): ServerConfig { private getServerConfig(): ServerConfig {
const notebookServerInfo = useNotebook.getState().notebookServerInfo; const notebookServerInfo = useNotebook.getState().notebookServerInfo;
return { return {
endpoint: notebookServerInfo.notebookServerEndpoint, endpoint: notebookServerInfo?.notebookServerEndpoint,
token: notebookServerInfo.authToken, token: notebookServerInfo?.authToken,
crossDomain: true, crossDomain: true,
}; };
} }

View File

@@ -4,6 +4,7 @@
import { ImmutableNotebook } from "@nteract/commutable"; import { ImmutableNotebook } from "@nteract/commutable";
import type { IContentProvider } from "@nteract/core"; import type { IContentProvider } from "@nteract/core";
import { DataUploaderNotebook } from "Explorer/Notebook/DataUploader/DataUploaderUtils";
import React from "react"; import React from "react";
import { contents } from "rx-jupyter"; import { contents } from "rx-jupyter";
import { Areas, HttpStatusCodes } from "../../Common/Constants"; import { Areas, HttpStatusCodes } from "../../Common/Constants";
@@ -68,6 +69,10 @@ export default class NotebookManager {
readonly: true, readonly: true,
content: SchemaAnalyzerNotebook, content: SchemaAnalyzerNotebook,
}, },
[DataUploaderNotebook.path]: {
readonly: true,
content: DataUploaderNotebook,
},
}); });
this.gitHubContentProvider = new GitHubContentProvider({ this.gitHubContentProvider = new GitHubContentProvider({

View File

@@ -5,11 +5,17 @@ import Html2Canvas from "html2canvas";
import path from "path"; import path from "path";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as StringUtils from "../../Utils/StringUtils"; import * as StringUtils from "../../Utils/StringUtils";
import * as InMemoryContentProviderUtils from "../Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils";
import { SnapshotFragment } from "./NotebookComponent/types"; import { SnapshotFragment } from "./NotebookComponent/types";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
// Must match rx-jupyter' FileType // Must match rx-jupyter' FileType
export type FileType = "directory" | "file" | "notebook"; export type FileType = "directory" | "file" | "notebook";
export enum NotebookContentProviderType {
GitHubContentProviderType,
InMemoryContentProviderType,
JupyterContentProviderType,
}
// Utilities for notebooks // Utilities for notebooks
export class NotebookUtil { export class NotebookUtil {
public static UntrustedNotebookRunHint = "Please trust notebook first before running any code cells"; public static UntrustedNotebookRunHint = "Please trust notebook first before running any code cells";
@@ -126,6 +132,18 @@ export class NotebookUtil {
return relativePath.split("/").pop(); return relativePath.split("/").pop();
} }
public static getContentProviderType(path: string): NotebookContentProviderType {
if (InMemoryContentProviderUtils.fromContentUri(path)) {
return NotebookContentProviderType.InMemoryContentProviderType;
}
if (GitHubUtils.fromContentUri(path)) {
return NotebookContentProviderType.GitHubContentProviderType;
}
return NotebookContentProviderType.JupyterContentProviderType;
}
public static replaceName(path: string, newName: string): string { public static replaceName(path: string, newName: string): string {
const contentInfo = GitHubUtils.fromContentUri(path); const contentInfo = GitHubUtils.fromContentUri(path);
if (contentInfo) { if (contentInfo) {

View File

@@ -1,3 +1,4 @@
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { PhoenixClient } from "Phoenix/PhoenixClient"; import { PhoenixClient } from "Phoenix/PhoenixClient";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
@@ -37,7 +38,8 @@ interface NotebookState {
isAllocating: boolean; isAllocating: boolean;
isRefreshed: boolean; isRefreshed: boolean;
containerStatus: ContainerInfo; containerStatus: ContainerInfo;
isPhoenix: boolean; isPhoenixNotebooks: boolean;
isPhoenixFeatures: boolean;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void; setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void; setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void; setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
@@ -60,7 +62,8 @@ interface NotebookState {
setIsRefreshed: (isAllocating: boolean) => void; setIsRefreshed: (isAllocating: boolean) => void;
setContainerStatus: (containerStatus: ContainerInfo) => void; setContainerStatus: (containerStatus: ContainerInfo) => void;
getPhoenixStatus: () => Promise<void>; getPhoenixStatus: () => Promise<void>;
setIsPhoenix: (isPhoenix: boolean) => void; setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => void;
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
} }
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
@@ -95,7 +98,8 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
durationLeftInMinutes: undefined, durationLeftInMinutes: undefined,
notebookServerInfo: undefined, notebookServerInfo: undefined,
}, },
isPhoenix: undefined, isPhoenixNotebooks: undefined,
isPhoenixFeatures: undefined,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
@@ -201,7 +205,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
}, },
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => { initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const notebookFolderName = get().isPhoenix === true ? "Temporary Notebooks" : "My Notebooks"; const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks";
set({ notebookFolderName }); set({ notebookFolderName });
const myNotebooksContentRoot = { const myNotebooksContentRoot = {
name: get().notebookFolderName, name: get().notebookFolderName,
@@ -298,11 +302,20 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }), setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }), setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
getPhoenixStatus: async () => { getPhoenixStatus: async () => {
if (get().isPhoenix === undefined) { if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) {
const phoenixClient = new PhoenixClient(); let isPhoenix = false;
const isPhoenix = userContext.features.phoenix === true && (await phoenixClient.IsDbAcountWhitelisted()); if (userContext.features.phoenixNotebooks || userContext.features.phoenixFeatures) {
set({ isPhoenix }); const phoenixClient = new PhoenixClient();
isPhoenix = isPublicInternetAccessAllowed() && (await phoenixClient.isDbAcountWhitelisted());
}
const isPhoenixNotebooks = userContext.features.phoenixNotebooks && isPhoenix;
const isPhoenixFeatures = userContext.features.phoenixFeatures && isPhoenix;
set({ isPhoenixNotebooks: isPhoenixNotebooks });
set({ isPhoenixFeatures: isPhoenixFeatures });
} }
}, },
setIsPhoenix: (isPhoenix: boolean) => set({ isPhoenix }), setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
})); }));

View File

@@ -26,6 +26,7 @@ describe("OpenActions", () => {
collection.onDocumentDBDocumentsClick = jest.fn(); collection.onDocumentDBDocumentsClick = jest.fn();
collection.onMongoDBDocumentsClick = jest.fn(); collection.onMongoDBDocumentsClick = jest.fn();
collection.onSchemaAnalyzerClick = jest.fn(); collection.onSchemaAnalyzerClick = jest.fn();
collection.onDataUploaderClick = jest.fn();
collection.onTableEntitiesClick = jest.fn(); collection.onTableEntitiesClick = jest.fn();
collection.onGraphDocumentsClick = jest.fn(); collection.onGraphDocumentsClick = jest.fn();
collection.onNewQueryClick = jest.fn(); collection.onNewQueryClick = jest.fn();

View File

@@ -86,6 +86,14 @@ function openCollectionTab(
break; break;
} }
if (
action.tabKind === ActionContracts.TabKind.DataUploader ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.DataUploader]
) {
collection.onDataUploaderClick();
break;
}
if ( if (
action.tabKind === ActionContracts.TabKind.TableEntities || action.tabKind === ActionContracts.TabKind.TableEntities ||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities] action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]

View File

@@ -25,6 +25,7 @@ export const OpenFullScreen: React.FunctionComponent = () => {
<TextField label="Read and Write" readOnly defaultValue={readWriteUrl} /> <TextField label="Read and Write" readOnly defaultValue={readWriteUrl} />
<Stack horizontal tokens={{ childrenGap: 10 }}> <Stack horizontal tokens={{ childrenGap: 10 }}>
<DefaultButton <DefaultButton
ariaLabel={isReadWriteUrlCopy ? "Copied url" : "Copy"}
onClick={() => { onClick={() => {
copyToClipboard(readWriteUrl); copyToClipboard(readWriteUrl);
setIsReadWriteUrlCopy(true); setIsReadWriteUrlCopy(true);
@@ -43,6 +44,7 @@ export const OpenFullScreen: React.FunctionComponent = () => {
<TextField label="Read Only" readOnly defaultValue={readUrl} /> <TextField label="Read Only" readOnly defaultValue={readUrl} />
<Stack horizontal tokens={{ childrenGap: 10 }}> <Stack horizontal tokens={{ childrenGap: 10 }}>
<DefaultButton <DefaultButton
ariaLabel={isReadUrlCopy ? "Copied url" : "Copy"}
onClick={() => { onClick={() => {
setIsReadUrlCopy(true); setIsReadUrlCopy(true);
copyToClipboard(readUrl); copyToClipboard(readUrl);

View File

@@ -249,6 +249,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated} showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={true} isDatabase={true}
isSharded={this.state.isSharded} isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)} setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)} setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
@@ -279,7 +280,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal> <Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
{`${getCollectionName()} ${userContext.apiType === "Mongo" ? "name" : "id"}`} {`${getCollectionName()} id`}
</Text> </Text>
<TooltipHost <TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
@@ -483,6 +484,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated} showFreeTierExceedThroughputTooltip={this.isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={false} isDatabase={false}
isSharded={this.state.isSharded} isSharded={this.state.isSharded}
isFreeTier={this.isFreeTierAccount()}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)} setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)} setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) => setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
@@ -667,7 +669,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{userContext.apiType === "SQL" && ( {userContext.apiType === "SQL" && (
<Checkbox <Checkbox
label="My partition key is larger than 100 bytes" label="My partition key is larger than 101 bytes"
checked={this.state.useHashV2} checked={this.state.useHashV2}
styles={{ styles={{
text: { fontSize: 12 }, text: { fontSize: 12 },
@@ -887,10 +889,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false; return false;
} }
if (isServerlessAccount()) {
return false;
}
switch (userContext.apiType) { switch (userContext.apiType) {
case "SQL": case "SQL":
case "Mongo": case "Mongo":

View File

@@ -146,9 +146,10 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
// TODO add feature flag that disables validation for customers with custom accounts // TODO add feature flag that disables validation for customers with custom accounts
if (isAutoscaleSelected) { if (isAutoscaleSelected) {
if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) { if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) {
setFormErrors( const minAutoPilotThroughput = userContext.features.freetierAutoscaleThroughput
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput` ? AutoPilotUtils.autoPilotThroughput1K
); : AutoPilotUtils.autoPilotThroughput4K;
setFormErrors(`Please enter a value greater than ${minAutoPilotThroughput} for autopilot throughput`);
return false; return false;
} }
} }
@@ -241,6 +242,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()} showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={true} isDatabase={true}
isSharded={databaseCreateNewShared} isSharded={databaseCreateNewShared}
isFreeTier={isFreeTierAccount}
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)} setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)} setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)} setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}

View File

@@ -262,6 +262,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
} }
isDatabase isDatabase
isSharded isSharded
isFreeTier={isFreeTierAccount}
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)} setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)} setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)} setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
@@ -334,7 +335,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<ThroughputInput <ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()} showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={false} isDatabase={false}
isSharded={false} isSharded
isFreeTier={isFreeTierAccount}
setThroughputValue={(throughput: number) => (tableThroughput = throughput)} setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)} setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)} setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}

View File

@@ -10,7 +10,6 @@ import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUti
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
import { useNotebook } from "../../Notebook/useNotebook"; import { useNotebook } from "../../Notebook/useNotebook";
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent"; import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
@@ -75,7 +74,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
selectedLocation.owner, selectedLocation.owner,
selectedLocation.repo selectedLocation.repo
)} - ${selectedLocation.branch}`; )} - ${selectedLocation.branch}`;
} else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenix) { } else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenixNotebooks) {
destination = useNotebook.getState().notebookFolderName; destination = useNotebook.getState().notebookFolderName;
} }
@@ -104,11 +103,14 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
switch (location.type) { switch (location.type) {
case "MyNotebooks": case "MyNotebooks":
parent = { parent = {
name: ResourceTreeAdapter.MyNotebooksTitle, name: useNotebook.getState().notebookFolderName,
path: useNotebook.getState().notebookBasePath, path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory, type: NotebookContentItemType.Directory,
}; };
isGithubTree = false; isGithubTree = false;
if (useNotebook.getState().isPhoenixNotebooks) {
await container.allocateContainer();
}
break; break;
case "GitHub": case "GitHub":

View File

@@ -38,7 +38,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
const onSubmit = async (): Promise<void> => { const onSubmit = async (): Promise<void> => {
const collection = useSelectedNode.getState().findSelectedCollection(); const collection = useSelectedNode.getState().findSelectedCollection();
if (!collection || inputCollectionName !== collection.id()) { if (!collection || inputCollectionName !== collection.id()) {
const errorMessage = "Input " + collectionName + " name does not match the selected " + collectionName; const errorMessage = "Input " + collectionName + " id does not match the selected " + collectionName;
setFormError(errorMessage); setFormError(errorMessage);
NotificationConsoleUtils.logConsoleError( NotificationConsoleUtils.logConsoleError(
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}` `Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`

View File

@@ -1,4 +1,4 @@
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton, Stack, Text } from "@fluentui/react"; import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react";
import * as Constants from "Common/Constants"; import * as Constants from "Common/Constants";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { configContext } from "ConfigContext"; import { configContext } from "ConfigContext";
@@ -115,9 +115,15 @@ export const SettingsPane: FunctionComponent = () => {
}; };
const choiceButtonStyles = { const choiceButtonStyles = {
root: {
clear: "both",
},
flexContainer: [ flexContainer: [
{ {
selectors: { selectors: {
".ms-ChoiceFieldGroup root-133": {
clear: "both",
},
".ms-ChoiceField-wrapper label": { ".ms-ChoiceField-wrapper label": {
fontSize: 12, fontSize: 12,
paddingTop: 0, paddingTop: 0,
@@ -135,22 +141,22 @@ export const SettingsPane: FunctionComponent = () => {
{shouldShowQueryPageOptions && ( {shouldShowQueryPageOptions && (
<div className="settingsSection"> <div className="settingsSection">
<div className="settingsSectionPart"> <div className="settingsSectionPart">
<Stack horizontal> <fieldset>
<Text id="pageOptions" className="settingsSectionLabel" variant="small"> <legend id="pageOptions" className="settingsSectionLabel legendLabel">
Page options Page Options
</Text> </legend>
<InfoTooltip> <InfoTooltip>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many
query results per page. query results per page.
</InfoTooltip> </InfoTooltip>
</Stack> <ChoiceGroup
<ChoiceGroup ariaLabelledBy="pageOptions"
ariaLabelledBy="pageOptions" selectedKey={pageOption}
selectedKey={pageOption} options={pageOptionList}
options={pageOptionList} styles={choiceButtonStyles}
styles={choiceButtonStyles} onChange={handleOnPageOptionChange}
onChange={handleOnPageOptionChange} />
/> </fieldset>
</div> </div>
<div className="tabs settingsSectionPart"> <div className="tabs settingsSectionPart">
{isCustomPageOptionSelected() && ( {isCustomPageOptionSelected() && (

View File

@@ -16,54 +16,57 @@ exports[`Settings Pane should render Default properly 1`] = `
<div <div
className="settingsSectionPart" className="settingsSectionPart"
> >
<Stack <fieldset>
horizontal={true} <legend
> className="settingsSectionLabel legendLabel"
<Text
className="settingsSectionLabel"
id="pageOptions" id="pageOptions"
variant="small"
> >
Page options Page Options
</Text> </legend>
<InfoTooltip> <InfoTooltip>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page. Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
</InfoTooltip> </InfoTooltip>
</Stack> <StyledChoiceGroup
<StyledChoiceGroup ariaLabelledBy="pageOptions"
ariaLabelledBy="pageOptions" onChange={[Function]}
onChange={[Function]} options={
options={ Array [
Array [
Object {
"key": "custom",
"text": "Custom",
},
Object {
"key": "unlimited",
"text": "Unlimited",
},
]
}
selectedKey="custom"
styles={
Object {
"flexContainer": Array [
Object { Object {
"selectors": Object { "key": "custom",
".ms-ChoiceField": Object { "text": "Custom",
"marginTop": 0, },
}, Object {
".ms-ChoiceField-wrapper label": Object { "key": "unlimited",
"fontSize": 12, "text": "Unlimited",
"paddingTop": 0, },
]
}
selectedKey="custom"
styles={
Object {
"flexContainer": Array [
Object {
"selectors": Object {
".ms-ChoiceField": Object {
"marginTop": 0,
},
".ms-ChoiceField-wrapper label": Object {
"fontSize": 12,
"paddingTop": 0,
},
".ms-ChoiceFieldGroup root-133": Object {
"clear": "both",
},
}, },
}, },
],
"root": Object {
"clear": "both",
}, },
], }
} }
} />
/> </fieldset>
</div> </div>
<div <div
className="tabs settingsSectionPart" className="tabs settingsSectionPart"

View File

@@ -1,50 +0,0 @@
import { PrimaryButton } from "@fluentui/react";
import { mount } from "enzyme";
import React from "react";
import Explorer from "../../Explorer";
import { SetupNoteBooksPanel } from "./SetupNotebooksPanel";
describe("Setup Notebooks Panel", () => {
it("should render Default properly", () => {
const fakeExplorer = {} as Explorer;
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined,
panelTitle: "",
panelDescription: "",
};
const wrapper = mount(<SetupNoteBooksPanel {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("should render button", () => {
const fakeExplorer = {} as Explorer;
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
openNotificationConsole: (): void => undefined,
panelTitle: "",
panelDescription: "",
};
const wrapper = mount(<SetupNoteBooksPanel {...props} />);
const button = wrapper.find("PrimaryButton").first();
expect(button).toBeDefined();
});
it("Button onClick should call onCompleteSetup", () => {
const onCompleteSetupClick = jest.fn();
const wrapper = mount(<PrimaryButton onClick={onCompleteSetupClick} />);
wrapper.find("button").simulate("click");
expect(onCompleteSetupClick).toHaveBeenCalled();
});
it("Button onKeyPress should call onCompleteSetupKeyPress", () => {
const onCompleteSetupKeyPress = jest.fn();
const wrapper = mount(<PrimaryButton onKeyPress={onCompleteSetupKeyPress} />);
wrapper.find("button").simulate("keypress");
expect(onCompleteSetupKeyPress).toHaveBeenCalled();
});
});

View File

@@ -1,121 +0,0 @@
import { PrimaryButton } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, KeyboardEvent, useState } from "react";
import { Areas, NormalizedEventKey } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import { createOrUpdate } from "../../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen";
interface SetupNoteBooksPanelProps {
explorer: Explorer;
panelTitle: string;
panelDescription: string;
}
export const SetupNoteBooksPanel: FunctionComponent<SetupNoteBooksPanelProps> = ({
explorer,
panelTitle,
panelDescription,
}: SetupNoteBooksPanelProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const description = panelDescription;
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [errorMessage, setErrorMessage] = useState<string>("");
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
const onCompleteSetupClick = async () => {
await setupNotebookWorkspace();
};
const onCompleteSetupKeyPress = async (event: KeyboardEvent<HTMLButtonElement>) => {
if (event.key === " " || event.key === NormalizedEventKey.Enter) {
await setupNotebookWorkspace();
event.stopPropagation();
return false;
}
return true;
};
const setupNotebookWorkspace = async (): Promise<void> => {
if (!explorer) {
return;
}
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNotebookWorkspace, {
dataExplorerArea: Areas.ContextualPane,
paneTitle: panelTitle,
});
const clear = NotificationConsoleUtils.logConsoleProgress("Creating a new default notebook workspace");
try {
setLoadingTrue();
await createOrUpdate(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
"default"
);
explorer.refreshExplorer();
closeSidePanel();
TelemetryProcessor.traceSuccess(
Action.CreateNotebookWorkspace,
{
dataExplorerArea: Areas.ContextualPane,
paneTitle: panelTitle,
},
startKey
);
NotificationConsoleUtils.logConsoleInfo("Successfully created a default notebook workspace for the account");
} catch (error) {
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure(
Action.CreateNotebookWorkspace,
{
dataExplorerArea: Areas.ContextualPane,
paneTitle: panelTitle,
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey
);
setErrorMessage(`Failed to setup a default notebook workspace: ${errorMessage}`);
setShowErrorDetails(true);
NotificationConsoleUtils.logConsoleError(`Failed to create a default notebook workspace: ${errorMessage}`);
} finally {
setLoadingFalse();
clear();
}
};
return (
<form className="panelFormWrapper">
{errorMessage && (
<PanelInfoErrorComponent message={errorMessage} messageType="error" showErrorDetails={showErrorDetails} />
)}
<div className="panelMainContent">
<div className="pkPadding">
<div>{description}</div>
<PrimaryButton
id="completeSetupBtn"
className="btncreatecoll1 btnSetupQueries"
text="Complete Setup"
onClick={onCompleteSetupClick}
onKeyPress={onCompleteSetupKeyPress}
aria-label="Complete setup"
/>
</div>
</div>
{isLoading && <PanelLoadingScreen />}
</form>
);
};

View File

@@ -24,6 +24,7 @@ const {
Ascii, Ascii,
Bigint, Bigint,
Blob, Blob,
Date: DateType,
Decimal, Decimal,
Float, Float,
Int, Int,
@@ -34,6 +35,60 @@ const {
Smallint, Smallint,
Tinyint, Tinyint,
Timestamp, Timestamp,
// List
List_Ascii,
List_Bigint,
List_Blob,
List_Boolean,
List_Date,
List_Decimal,
List_Double,
List_Float,
List_Int,
List_Text,
List_Timestamp,
List_Uuid,
List_Varchar,
List_Varint,
List_Inet,
List_Smallint,
List_Tinyint,
// Map
Map_Ascii,
Map_Bigint,
Map_Blob,
Map_Boolean,
Map_Date,
Map_Decimal,
Map_Double,
Map_Float,
Map_Int,
Map_Text,
Map_Timestamp,
Map_Uuid,
Map_Varchar,
Map_Varint,
Map_Inet,
Map_Smallint,
Map_Tinyint,
// Set
Set_Ascii,
Set_Bigint,
Set_Blob,
Set_Boolean,
Set_Date,
Set_Decimal,
Set_Double,
Set_Float,
Set_Int,
Set_Text,
Set_Timestamp,
Set_Uuid,
Set_Varchar,
Set_Varint,
Set_Inet,
Set_Smallint,
Set_Tinyint,
} = TableConstants.CassandraType; } = TableConstants.CassandraType;
export const cassandraOptions = [ export const cassandraOptions = [
{ key: Text, text: Text }, { key: Text, text: Text },
@@ -41,6 +96,7 @@ export const cassandraOptions = [
{ key: Bigint, text: Bigint }, { key: Bigint, text: Bigint },
{ key: Blob, text: Blob }, { key: Blob, text: Blob },
{ key: Boolean, text: Boolean }, { key: Boolean, text: Boolean },
{ key: DateType, text: DateType },
{ key: Decimal, text: Decimal }, { key: Decimal, text: Decimal },
{ key: Double, text: Double }, { key: Double, text: Double },
{ key: Float, text: Float }, { key: Float, text: Float },
@@ -52,6 +108,60 @@ export const cassandraOptions = [
{ key: Smallint, text: Smallint }, { key: Smallint, text: Smallint },
{ key: Tinyint, text: Tinyint }, { key: Tinyint, text: Tinyint },
{ key: Timestamp, text: Timestamp }, { key: Timestamp, text: Timestamp },
// List
{ key: List_Ascii, text: List_Ascii },
{ key: List_Bigint, text: List_Bigint },
{ key: List_Blob, text: List_Blob },
{ key: List_Boolean, text: List_Boolean },
{ key: List_Date, text: List_Date },
{ key: List_Decimal, text: List_Decimal },
{ key: List_Double, text: List_Double },
{ key: List_Float, text: List_Float },
{ key: List_Int, text: List_Int },
{ key: List_Text, text: List_Text },
{ key: List_Timestamp, text: List_Timestamp },
{ key: List_Uuid, text: List_Uuid },
{ key: List_Varchar, text: List_Varchar },
{ key: List_Varint, text: List_Varint },
{ key: List_Inet, text: List_Inet },
{ key: List_Smallint, text: List_Smallint },
{ key: List_Tinyint, text: List_Tinyint },
// Map
{ key: Map_Ascii, text: Map_Ascii },
{ key: Map_Bigint, text: Map_Bigint },
{ key: Map_Blob, text: Map_Blob },
{ key: Map_Boolean, text: Map_Boolean },
{ key: Map_Date, text: Map_Date },
{ key: Map_Decimal, text: Map_Decimal },
{ key: Map_Double, text: Map_Double },
{ key: Map_Float, text: Map_Float },
{ key: Map_Int, text: Map_Int },
{ key: Map_Text, text: Map_Text },
{ key: Map_Timestamp, text: Map_Timestamp },
{ key: Map_Uuid, text: Map_Uuid },
{ key: Map_Varchar, text: Map_Varchar },
{ key: Map_Varint, text: Map_Varint },
{ key: Map_Inet, text: Map_Inet },
{ key: Map_Smallint, text: Map_Smallint },
{ key: Map_Tinyint, text: Map_Tinyint },
// Set
{ key: Set_Ascii, text: Set_Ascii },
{ key: Set_Bigint, text: Set_Bigint },
{ key: Set_Blob, text: Set_Blob },
{ key: Set_Boolean, text: Set_Boolean },
{ key: Set_Date, text: Set_Date },
{ key: Set_Decimal, text: Set_Decimal },
{ key: Set_Double, text: Set_Double },
{ key: Set_Float, text: Set_Float },
{ key: Set_Int, text: Set_Int },
{ key: Set_Text, text: Set_Text },
{ key: Set_Timestamp, text: Set_Timestamp },
{ key: Set_Uuid, text: Set_Uuid },
{ key: Set_Varchar, text: Set_Varchar },
{ key: Set_Varint, text: Set_Varint },
{ key: Set_Inet, text: Set_Inet },
{ key: Set_Smallint, text: Set_Smallint },
{ key: Set_Tinyint, text: Set_Tinyint },
]; ];
export const imageProps: IImageProps = { export const imageProps: IImageProps = {

View File

@@ -221,7 +221,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
}); });
} }
if (useNotebook.getState().isNotebookEnabled && useNotebook.getState().isPhoenix) { if (useNotebook.getState().isPhoenixNotebooks) {
heroes.push({ heroes.push({
iconSrc: NewNotebookIcon, iconSrc: NewNotebookIcon,
title: "New Notebook", title: "New Notebook",

View File

@@ -14,6 +14,7 @@ export const CassandraType = {
Bigint: "Bigint", Bigint: "Bigint",
Blob: "Blob", Blob: "Blob",
Boolean: "Boolean", Boolean: "Boolean",
Date: "Date",
Decimal: "Decimal", Decimal: "Decimal",
Double: "Double", Double: "Double",
Float: "Float", Float: "Float",
@@ -26,6 +27,60 @@ export const CassandraType = {
Inet: "Inet", Inet: "Inet",
Smallint: "Smallint", Smallint: "Smallint",
Tinyint: "Tinyint", Tinyint: "Tinyint",
List_Ascii: "List<Ascii>",
List_Bigint: "List<Bigint>",
List_Blob: "List<Blob>",
List_Boolean: "List<Boolean>",
List_Date: "List<Date>",
List_Decimal: "List<Decimal>",
List_Double: "List<Double>",
List_Float: "List<Float>",
List_Int: "List<Int>",
List_Text: "List<Text>",
List_Timestamp: "List<Timestamp>",
List_Uuid: "List<Uuid>",
List_Varchar: "List<Varchar>",
List_Varint: "List<Varint>",
List_Inet: "List<Inet>",
List_Smallint: "List<Smallint>",
List_Tinyint: "List<Tinyint>",
Map_Ascii: "Map<Ascii>",
Map_Bigint: "Map<Bigint>",
Map_Blob: "Map<Blob>",
Map_Boolean: "Map<Boolean>",
Map_Date: "Map<Date>",
Map_Decimal: "Map<Decimal>",
Map_Double: "Map<Double>",
Map_Float: "Map<Float>",
Map_Int: "Map<Int>",
Map_Text: "Map<Text>",
Map_Timestamp: "Map<Timestamp>",
Map_Uuid: "Map<Uuid>",
Map_Varchar: "Map<Varchar>",
Map_Varint: "Map<Varint>",
Map_Inet: "Map<Inet>",
Map_Smallint: "Map<Smallint>",
Map_Tinyint: "Map<Tinyint>",
Set_Ascii: "Set<Ascii>",
Set_Bigint: "Set<Bigint>",
Set_Blob: "Set<Blob>",
Set_Boolean: "Set<Boolean>",
Set_Date: "Set<Date>",
Set_Decimal: "Set<Decimal>",
Set_Double: "Set<Double>",
Set_Float: "Set<Float>",
Set_Int: "Set<Int>",
Set_Text: "Set<Text>",
Set_Timestamp: "Set<Timestamp>",
Set_Uuid: "Set<Uuid>",
Set_Varchar: "Set<Varchar>",
Set_Varint: "Set<Varint>",
Set_Inet: "Set<Inet>",
Set_Smallint: "Set<Smallint>",
Set_Tinyint: "Set<Tinyint>",
}; };
export const ClauseRule = { export const ClauseRule = {

View File

@@ -536,7 +536,8 @@ export class CassandraAPIDataClient extends TableDataClient {
dataType === TableConstants.CassandraType.Inet || dataType === TableConstants.CassandraType.Inet ||
dataType === TableConstants.CassandraType.Ascii || dataType === TableConstants.CassandraType.Ascii ||
dataType === TableConstants.CassandraType.Varchar || dataType === TableConstants.CassandraType.Varchar ||
dataType === TableConstants.CassandraType.Timestamp dataType === TableConstants.CassandraType.Timestamp ||
dataType === TableConstants.CassandraType.Date
); );
} }

View File

@@ -0,0 +1,41 @@
import * as Constants from "../../Common/Constants";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
import { DataUploaderAdapter } from "../Notebook/DataUploader/DataUploaderAdapter";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
export default class DataUploaderTab extends NotebookTabBase {
public readonly html = '<div data-bind="react:DataUploaderAdapter" style="height: 100%"></div>';
private DataUploaderAdapter: DataUploaderAdapter;
constructor(options: NotebookTabBaseOptions) {
super(options);
this.DataUploaderAdapter = new DataUploaderAdapter(
{
contentRef: undefined,
notebookClient: NotebookTabBase.clientManager,
},
options.collection?.databaseId,
options.collection?.id()
);
}
public onActivate(): void {
traceSuccess(
Action.Tab,
{
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id,
dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Upload",
},
this.onLoadStartKey
);
super.onActivate();
}
protected buildCommandBarOptions(): void {
this.updateNavbarWithTabsButtons();
}
}

View File

@@ -215,13 +215,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
{ {
metric: "Request Charge", metric: "Request Charge",
value: this.state.requestChargeDisplayText, value: this.state.requestChargeDisplayText,
toolTip: "", toolTip: "Request Charge",
isQueryMetricsEnabled: true, isQueryMetricsEnabled: true,
}, },
{ {
metric: "Showing Results", metric: "Showing Results",
value: this.state.showingDocumentsDisplayText, value: this.state.showingDocumentsDisplayText,
toolTip: "", toolTip: "Showing Results",
isQueryMetricsEnabled: true, isQueryMetricsEnabled: true,
}, },
{ {
@@ -364,13 +364,11 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
} }
public onTabClick(): void { public onTabClick(): void {
setTimeout(() => { if (!this.isCloseClicked) {
if (!this.isCloseClicked) { useCommandBar.getState().setContextButtons(this.getTabsButtons());
useCommandBar.getState().setContextButtons(this.getTabsButtons()); } else {
} else { this.isCloseClicked = false;
this.isCloseClicked = false; }
}
}, 0);
} }
public onExecuteQueryClick = async (): Promise<void> => { public onExecuteQueryClick = async (): Promise<void> => {
@@ -875,9 +873,11 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
} }
render(): JSX.Element { componentDidMount(): void {
useCommandBar.getState().setContextButtons(this.getTabsButtons()); useCommandBar.getState().setContextButtons(this.getTabsButtons());
}
render(): JSX.Element {
return ( return (
<Fragment> <Fragment>
<div className="tab-pane" id={this.props.tabId} role="tabpanel"> <div className="tab-pane" id={this.props.tabId} role="tabpanel">

View File

@@ -25,7 +25,8 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
public parameters: ko.Computed<boolean>; public parameters: ko.Computed<boolean>;
constructor( constructor(
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo, private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
private getDatabaseAccount: () => DataModels.DatabaseAccount private getDatabaseAccount: () => DataModels.DatabaseAccount,
private getTabId: () => string
) {} ) {}
public renderComponent(): JSX.Element { public renderComponent(): JSX.Element {
@@ -33,6 +34,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
<NotebookTerminalComponent <NotebookTerminalComponent
notebookServerInfo={this.getNotebookServerInfo()} notebookServerInfo={this.getNotebookServerInfo()}
databaseAccount={this.getDatabaseAccount()} databaseAccount={this.getDatabaseAccount()}
tabId={this.getTabId()}
/> />
) : ( ) : (
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner> <Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
@@ -50,7 +52,8 @@ export default class TerminalTab extends TabsBase {
this.container = options.container; this.container = options.container;
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter( this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
() => this.getNotebookServerInfo(options), () => this.getNotebookServerInfo(options),
() => userContext?.databaseAccount () => userContext?.databaseAccount,
() => this.tabId
); );
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => { this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
if ( if (

View File

@@ -529,7 +529,7 @@ export default class Collection implements ViewModels.Collection {
}; };
public onSchemaAnalyzerClick = async () => { public onSchemaAnalyzerClick = async () => {
if (useNotebook.getState().isPhoenix) { if (useNotebook.getState().isPhoenixFeatures) {
await this.container.allocateContainer(); await this.container.allocateContainer();
} }
useSelectedNode.getState().setSelectedNode(this); useSelectedNode.getState().setSelectedNode(this);
@@ -574,6 +574,52 @@ export default class Collection implements ViewModels.Collection {
); );
}; };
public onDataUploaderClick = async () => {
if (useNotebook.getState().isPhoenixFeatures) {
await this.container.allocateContainer();
}
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
const DataUploaderTab = await (await import("../Tabs/DataUploaderTab")).default;
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Data uploader node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
for (const tab of useTabs.getState().openedTabs) {
if (
tab instanceof DataUploaderTab &&
tab.collection?.databaseId === this.databaseId &&
tab.collection?.id() === this.id()
) {
return useTabs.getState().activateTab(tab);
}
}
const startKey = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Upload",
});
this.documentIds([]);
useTabs.getState().activateNewTab(
new DataUploaderTab({
account: userContext.databaseAccount,
masterKey: userContext.masterKey || "",
container: this.container,
tabKind: ViewModels.CollectionTabKind.DataUploader,
title: "Upload",
tabPath: "",
collection: this,
node: this,
onLoadStartKey: startKey,
})
);
};
public onSettingsClick = async (): Promise<void> => { public onSettingsClick = async (): Promise<void> => {
useSelectedNode.getState().setSelectedNode(this); useSelectedNode.getState().setSelectedNode(this);
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit; const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;

View File

@@ -121,7 +121,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
children: [], children: [],
}; };
if (!useNotebook.getState().isPhoenix) { if (!useNotebook.getState().isPhoenixNotebooks) {
notebooksTree.children.push(buildNotebooksTemporarilyDownTree()); notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
} else { } else {
if (galleryContentRoot) { if (galleryContentRoot) {
@@ -130,7 +130,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
if ( if (
myNotebooksContentRoot && myNotebooksContentRoot &&
useNotebook.getState().isPhoenix && useNotebook.getState().isPhoenixNotebooks &&
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected
) { ) {
notebooksTree.children.push(buildMyNotebooksTree()); notebooksTree.children.push(buildMyNotebooksTree());
@@ -380,6 +380,11 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
}, },
]; ];
//disallow renaming of temporary notebook workspace
if (item?.path === useNotebook.getState().notebookBasePath) {
items = items.filter((item) => item.label !== "Rename");
}
// For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File"
if (GitHubUtils.fromContentUri(item.path)) { if (GitHubUtils.fromContentUri(item.path)) {
items = items.filter( items = items.filter(
@@ -511,7 +516,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
isNotebookEnabled && isNotebookEnabled &&
userContext.apiType === "Mongo" && userContext.apiType === "Mongo" &&
isPublicInternetAccessAllowed() && isPublicInternetAccessAllowed() &&
useNotebook.getState().isPhoenix useNotebook.getState().isPhoenixFeatures
) { ) {
children.push({ children.push({
label: "Schema (Preview)", label: "Schema (Preview)",
@@ -521,6 +526,15 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
.getState() .getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]), .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]),
}); });
children.push({
label: "Data Upload (Preview)",
onClick: collection.onDataUploaderClick.bind(collection),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.DataUploader]),
});
} }
if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) { if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) {

View File

@@ -283,6 +283,15 @@ export class ResourceTreeAdapter implements ReactAdapter {
.getState() .getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]), .isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.SchemaAnalyzer]),
}); });
children.push({
label: "Data Upload (Preview)",
onClick: collection.onDataUploaderClick.bind(collection),
isSelected: () =>
useSelectedNode
.getState()
.isDataNodeSelected(collection.databaseId, collection.id(), [ViewModels.CollectionTabKind.DataUploader]),
});
} }
if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) { if (userContext.apiType !== "Cassandra" || !isServerlessAccount()) {
@@ -808,6 +817,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
}, },
]; ];
//disallow renaming of temporary notebook workspace
if (item?.path === useNotebook.getState().notebookBasePath) {
items = items.filter((item) => item.label !== "Rename");
}
// For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File" // For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File"
if (GitHubUtils.fromContentUri(item.path)) { if (GitHubUtils.fromContentUri(item.path)) {
items = items.filter( items = items.filter(

View File

@@ -1,4 +1,5 @@
import ko from "knockout"; import ko from "knockout";
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
import { GetGithubClientId } from "Utils/GitHubUtils"; import { GetGithubClientId } from "Utils/GitHubUtils";
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants"; import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
@@ -484,7 +485,7 @@ export class JunoClient {
// public for tests // public for tests
public static getJunoEndpoint(): string { public static getJunoEndpoint(): string {
const junoEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT; const junoEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
if (configContext.allowedJunoOrigins.indexOf(new URL(junoEndpoint).origin) === -1) { if (!validateEndpoint(junoEndpoint, allowedJunoOrigins)) {
const error = `${junoEndpoint} not allowed as juno endpoint`; const error = `${junoEndpoint} not allowed as juno endpoint`;
console.error(error); console.error(error);
throw new Error(error); throw new Error(error);

View File

@@ -1,5 +1,14 @@
import promiseRetry, { AbortError } from "p-retry"; import promiseRetry, { AbortError } from "p-retry";
import { ConnectionStatusType, ContainerStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../Common/Constants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
import {
Areas,
ConnectionStatusType,
ContainerStatusType,
HttpHeaders,
HttpStatusCodes,
Notebook,
} from "../Common/Constants";
import { getErrorMessage } from "../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { configContext } from "../ConfigContext"; import { configContext } from "../ConfigContext";
@@ -9,9 +18,10 @@ import {
IContainerData, IContainerData,
IPhoenixConnectionInfoResult, IPhoenixConnectionInfoResult,
IProvisionData, IProvisionData,
IResponse IResponse,
} from "../Contracts/DataModels"; } from "../Contracts/DataModels";
import { useNotebook } from "../Explorer/Notebook/useNotebook"; import { useNotebook } from "../Explorer/Notebook/useNotebook";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
@@ -36,14 +46,11 @@ export class PhoenixClient {
operation: string operation: string
): Promise<IResponse<IPhoenixConnectionInfoResult>> { ): Promise<IResponse<IPhoenixConnectionInfoResult>> {
try { try {
const response = await fetch( const response = await fetch(`${this.getPhoenixControlPlanePathPrefix()}/containerconnections`, {
`${this.getPhoenixControlPlaneEndpoint()}/containerconnections`, method: operation === "allocate" ? "POST" : "PATCH",
{ headers: PhoenixClient.getHeaders(),
method: operation === "allocate" ? "POST" : "PATCH", body: JSON.stringify(provisionData),
headers: PhoenixClient.getHeaders(), });
body: JSON.stringify(provisionData),
}
);
let data: IPhoenixConnectionInfoResult; let data: IPhoenixConnectionInfoResult;
if (response.status === HttpStatusCodes.OK) { if (response.status === HttpStatusCodes.OK) {
@@ -76,7 +83,7 @@ export class PhoenixClient {
try { try {
const runContainerStatusAsync = async () => { const runContainerStatusAsync = async () => {
const response = await window.fetch( const response = await window.fetch(
`${this.getPhoenixControlPlaneEndpoint()}/${containerData.forwardingId}`, `${this.getPhoenixControlPlanePathPrefix()}/${containerData.forwardingId}`,
{ {
method: "GET", method: "GET",
headers: PhoenixClient.getHeaders(), headers: PhoenixClient.getHeaders(),
@@ -90,9 +97,15 @@ export class PhoenixClient {
status: ContainerStatusType.Active, status: ContainerStatusType.Active,
}; };
} else if (response.status === HttpStatusCodes.NotFound) { } else if (response.status === HttpStatusCodes.NotFound) {
const error = "Disconnected from compute workspace";
Logger.logError(error, "");
const connectionStatus: ContainerConnectionInfo = { const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Reconnect, status: ConnectionStatusType.Reconnect,
}; };
TelemetryProcessor.traceMark(Action.PhoenixHeartBeat, {
dataExplorerArea: Areas.Notebook,
message: getErrorMessage(error),
});
useNotebook.getState().resetContainerConnection(connectionStatus); useNotebook.getState().resetContainerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
throw new AbortError(response.statusText); throw new AbortError(response.statusText);
@@ -101,9 +114,12 @@ export class PhoenixClient {
}; };
return await promiseRetry(runContainerStatusAsync, this.retryOptions); return await promiseRetry(runContainerStatusAsync, this.retryOptions);
} catch (error) { } catch (error) {
Logger.logError(getErrorMessage(error), "PhoenixClient/getContainerStatus"); TelemetryProcessor.traceFailure(Action.PhoenixHeartBeat, {
dataExplorerArea: Areas.Notebook,
});
Logger.logError(getErrorMessage(error), "");
const connectionStatus: ContainerConnectionInfo = { const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Reconnect, status: ConnectionStatusType.Failed,
}; };
useNotebook.getState().resetContainerConnection(connectionStatus); useNotebook.getState().resetContainerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed); useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
@@ -123,9 +139,9 @@ export class PhoenixClient {
} }
} }
public async IsDbAcountWhitelisted() { public async isDbAcountWhitelisted(): Promise<boolean> {
try { try {
const response = await window.fetch(`${this.getPhoenixControlPlaneEndpoint()}`, { const response = await window.fetch(`${this.getPhoenixControlPlanePathPrefix()}`, {
method: "GET", method: "GET",
headers: PhoenixClient.getHeaders(), headers: PhoenixClient.getHeaders(),
}); });
@@ -139,7 +155,7 @@ export class PhoenixClient {
public static getPhoenixEndpoint(): string { public static getPhoenixEndpoint(): string {
const phoenixEndpoint = const phoenixEndpoint =
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT; userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) { if (!validateEndpoint(phoenixEndpoint, allowedJunoOrigins)) {
const error = `${phoenixEndpoint} not allowed as juno endpoint`; const error = `${phoenixEndpoint} not allowed as juno endpoint`;
console.error(error); console.error(error);
throw new Error(error); throw new Error(error);
@@ -148,8 +164,10 @@ export class PhoenixClient {
return phoenixEndpoint; return phoenixEndpoint;
} }
public getPhoenixControlPlaneEndpoint(): string { public getPhoenixControlPlanePathPrefix(): string {
return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer/cosmosaccounts${userContext.databaseAccount.id}`; return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer/cosmosaccounts${
userContext.databaseAccount.id
}`;
} }
private static getHeaders(): HeadersInit { private static getHeaders(): HeadersInit {

View File

@@ -1,4 +1,5 @@
export type Features = { export type Features = {
// set only via feature flags
readonly canExceedMaximumValue: boolean; readonly canExceedMaximumValue: boolean;
readonly cosmosdb: boolean; readonly cosmosdb: boolean;
readonly enableChangeFeedPolicy: boolean; readonly enableChangeFeedPolicy: boolean;
@@ -8,11 +9,6 @@ export type Features = {
readonly enableReactPane: boolean; readonly enableReactPane: boolean;
readonly enableRightPanelV2: boolean; readonly enableRightPanelV2: boolean;
readonly enableSchema: boolean; readonly enableSchema: boolean;
autoscaleDefault: boolean;
partitionKeyDefault: boolean;
partitionKeyDefault2: boolean;
phoenix: boolean;
notebooksDownBanner: boolean;
readonly enableSDKoperations: boolean; readonly enableSDKoperations: boolean;
readonly enableSpark: boolean; readonly enableSpark: boolean;
readonly enableTtl: boolean; readonly enableTtl: boolean;
@@ -22,7 +18,6 @@ export type Features = {
readonly hostedDataExplorer: boolean; readonly hostedDataExplorer: boolean;
readonly junoEndpoint?: string; readonly junoEndpoint?: string;
readonly phoenixEndpoint?: string; readonly phoenixEndpoint?: string;
readonly livyEndpoint?: string;
readonly notebookBasePath?: string; readonly notebookBasePath?: string;
readonly notebookServerToken?: string; readonly notebookServerToken?: string;
readonly notebookServerUrl?: string; readonly notebookServerUrl?: string;
@@ -34,11 +29,22 @@ export type Features = {
readonly mongoProxyEndpoint?: string; readonly mongoProxyEndpoint?: string;
readonly mongoProxyAPIs?: string; readonly mongoProxyAPIs?: string;
readonly enableThroughputCap: boolean; readonly enableThroughputCap: boolean;
// can be set via both flight and feature flag
autoscaleDefault: boolean;
partitionKeyDefault: boolean;
partitionKeyDefault2: boolean;
phoenixNotebooks?: boolean;
phoenixFeatures?: boolean;
notebooksDownBanner: boolean;
freetierAutoscaleThroughput: boolean;
}; };
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features { export function extractFeatures(given = new URLSearchParams(window.location.search)): Features {
const downcased = new URLSearchParams(); const downcased = new URLSearchParams();
const set = (value: string, key: string) => downcased.set(key.toLowerCase(), value); const set = (value: string, key: string) => {
downcased.set(key.toLowerCase(), value);
};
const get = (key: string, defaultValue?: string) => const get = (key: string, defaultValue?: string) =>
downcased.get("feature." + key) ?? downcased.get(key) ?? defaultValue; downcased.get("feature." + key) ?? downcased.get(key) ?? defaultValue;
@@ -71,7 +77,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
mongoProxyAPIs: get("mongoproxyapis"), mongoProxyAPIs: get("mongoproxyapis"),
junoEndpoint: get("junoendpoint"), junoEndpoint: get("junoendpoint"),
phoenixEndpoint: get("phoenixendpoint"), phoenixEndpoint: get("phoenixendpoint"),
livyEndpoint: get("livyendpoint"),
notebookBasePath: get("notebookbasepath"), notebookBasePath: get("notebookbasepath"),
notebookServerToken: get("notebookservertoken"), notebookServerToken: get("notebookservertoken"),
notebookServerUrl: get("notebookserverurl"), notebookServerUrl: get("notebookserverurl"),
@@ -83,9 +88,9 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
autoscaleDefault: "true" === get("autoscaledefault"), autoscaleDefault: "true" === get("autoscaledefault"),
partitionKeyDefault: "true" === get("partitionkeytest"), partitionKeyDefault: "true" === get("partitionkeytest"),
partitionKeyDefault2: "true" === get("pkpartitionkeytest"), partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
phoenix: "true" === get("phoenix"),
notebooksDownBanner: "true" === get("notebooksDownBanner"), notebooksDownBanner: "true" === get("notebooksDownBanner"),
enableThroughputCap: "true" === get("enablethroughputcap"), enableThroughputCap: "true" === get("enablethroughputcap"),
freetierAutoscaleThroughput: "true" === get("freetierautoscalethroughput"),
}; };
} }

View File

@@ -82,6 +82,7 @@ export enum Action {
NotebooksMoveCellUpFromMenu, NotebooksMoveCellUpFromMenu,
NotebooksMoveCellDownFromMenu, NotebooksMoveCellDownFromMenu,
PhoenixConnection, PhoenixConnection,
PhoenixHeartBeat,
PhoenixResetWorkspace, PhoenixResetWorkspace,
DeleteCellFromMenu, DeleteCellFromMenu,
OpenTerminal, OpenTerminal,

View File

@@ -2,15 +2,61 @@
* JupyterLab applications based on jupyterLab components * JupyterLab applications based on jupyterLab components
*/ */
import { ServerConnection, TerminalManager } from "@jupyterlab/services"; import { ServerConnection, TerminalManager } from "@jupyterlab/services";
import { IMessage } from "@jupyterlab/services/lib/terminal/terminal";
import { Terminal } from "@jupyterlab/terminal"; import { Terminal } from "@jupyterlab/terminal";
import { Panel, Widget } from "@phosphor/widgets"; import { Panel, Widget } from "@phosphor/widgets";
import { userContext } from "UserContext";
export class JupyterLabAppFactory { export class JupyterLabAppFactory {
public static async createTerminalApp(serverSettings: ServerConnection.ISettings) { private isShellStarted: boolean | undefined;
private checkShellStarted: ((content: string | undefined) => void) | undefined;
private onShellExited: () => void;
private isShellExited(content: string | undefined) {
return content?.includes("cosmosuser@");
}
private isMongoShellStarted(content: string | undefined) {
this.isShellStarted = content?.includes("MongoDB shell version");
}
private isCassandraShellStarted(content: string | undefined) {
this.isShellStarted = content?.includes("Connected to") && content?.includes("cqlsh");
}
constructor(closeTab: () => void) {
this.onShellExited = closeTab;
this.isShellStarted = false;
this.checkShellStarted = undefined;
switch (userContext.apiType) {
case "Mongo":
this.checkShellStarted = this.isMongoShellStarted;
break;
case "Cassandra":
this.checkShellStarted = this.isCassandraShellStarted;
break;
}
}
public async createTerminalApp(serverSettings: ServerConnection.ISettings) {
const manager = new TerminalManager({ const manager = new TerminalManager({
serverSettings: serverSettings, serverSettings: serverSettings,
}); });
const session = await manager.startNew(); const session = await manager.startNew();
session.messageReceived.connect(async (_, message: IMessage) => {
const content = message.content && message.content[0]?.toString();
if (this.checkShellStarted && message.type == "stdout") {
//Close the terminal tab once the shell closed messages are received
if (!this.isShellStarted) {
this.checkShellStarted(content);
} else if (this.isShellExited(content)) {
this.onShellExited();
}
}
}, this);
const term = new Terminal(session, { theme: "dark", shutdownOnClose: true }); const term = new Terminal(session, { theme: "dark", shutdownOnClose: true });
if (!term) { if (!term) {

View File

@@ -10,4 +10,5 @@ export interface TerminalProps {
authType: AuthType; authType: AuthType;
apiType: ApiType; apiType: ApiType;
subscriptionId: string; subscriptionId: string;
tabId: string;
} }

View File

@@ -1,5 +1,6 @@
import { ServerConnection } from "@jupyterlab/services"; import { ServerConnection } from "@jupyterlab/services";
import "@jupyterlab/terminal/style/index.css"; import "@jupyterlab/terminal/style/index.css";
import { MessageTypes } from "Contracts/ExplorerContracts";
import postRobot from "post-robot"; import postRobot from "post-robot";
import { HttpHeaders } from "../Common/Constants"; import { HttpHeaders } from "../Common/Constants";
import { Action } from "../Shared/Telemetry/TelemetryConstants"; import { Action } from "../Shared/Telemetry/TelemetryConstants";
@@ -54,13 +55,20 @@ const initTerminal = async (props: TerminalProps) => {
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data); const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
try { try {
await JupyterLabAppFactory.createTerminalApp(serverSettings); await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings);
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime); TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
} catch (error) { } catch (error) {
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime); TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
} }
}; };
const closeTab = (tabId: string): void => {
window.parent.postMessage(
{ type: MessageTypes.CloseTab, data: { tabId: tabId }, signature: "pcIframe" },
window.document.referrer
);
};
const main = async (): Promise<void> => { const main = async (): Promise<void> => {
postRobot.on( postRobot.on(
"props", "props",

View File

@@ -1,11 +1,16 @@
export const minAutoPilotThroughput = 4000; import { userContext } from "UserContext";
export const autoPilotThroughput1K = 1000;
export const autoPilotIncrementStep = 1000; export const autoPilotIncrementStep = 1000;
export const autoPilotThroughput4K = 4000;
export function isValidAutoPilotThroughput(maxThroughput: number): boolean { export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
if (!maxThroughput) { if (!maxThroughput) {
return false; return false;
} }
const minAutoPilotThroughput = userContext.features.freetierAutoscaleThroughput
? autoPilotThroughput4K
: autoPilotThroughput1K;
if (maxThroughput < minAutoPilotThroughput) { if (maxThroughput < minAutoPilotThroughput) {
return false; return false;
} }

View File

@@ -0,0 +1,87 @@
import { JunoEndpoints } from "Common/Constants";
import * as Logger from "../Common/Logger";
export function validateEndpoint(
endpointToValidate: string | undefined,
allowedEndpoints: ReadonlyArray<string>
): boolean {
try {
return validateEndpointInternal(
endpointToValidate,
allowedEndpoints.map((e) => e)
);
} catch (reason) {
Logger.logError(`${endpointToValidate} not allowed`, "validateEndpoint");
Logger.logError(`${JSON.stringify(reason)}`, "validateEndpoint");
return false;
}
}
function validateEndpointInternal(
endpointToValidate: string | undefined,
allowedEndpoints: ReadonlyArray<string>
): boolean {
if (endpointToValidate === undefined) {
return false;
}
const originToValidate: string = new URL(endpointToValidate).origin;
const allowedOrigins: string[] = allowedEndpoints.map((allowedEndpoint) => new URL(allowedEndpoint).origin) || [];
const valid = allowedOrigins.indexOf(originToValidate) >= 0;
if (!valid) {
throw new Error(
`${endpointToValidate} is not an allowed endpoint. Allowed endpoints are ${allowedEndpoints.toString()}`
);
}
return valid;
}
export const allowedArmEndpoints: ReadonlyArray<string> = [
"https://management.azure.com",
"https://management.usgovcloudapi.net",
"https://management.chinacloudapi.cn",
];
export const allowedAadEndpoints: ReadonlyArray<string> = ["https://login.microsoftonline.com/"];
export const allowedBackendEndpoints: ReadonlyArray<string> = [
"https://main.documentdb.ext.azure.com",
"https://main.documentdb.ext.azure.cn",
"https://main.documentdb.ext.azure.us",
"https://localhost:12901",
"https://localhost:1234",
];
export const allowedMongoProxyEndpoints: ReadonlyArray<string> = [
"https://main.documentdb.ext.azure.com",
"https://main.documentdb.ext.azure.cn",
"https://main.documentdb.ext.azure.us",
"https://localhost:12901",
];
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
export const allowedMongoBackendEndpoints: ReadonlyArray<string> = ["https://localhost:1234"];
export const allowedGraphEndpoints: ReadonlyArray<string> = ["https://graph.windows.net"];
export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspaceartifacts.projectarcadia.net"];
export const allowedHostedExplorerEndpoints: ReadonlyArray<string> = ["https://cosmos.azure.com/"];
export const allowedMsalRedirectEndpoints: ReadonlyArray<string> = [
"https://cosmos-explorer-preview.azurewebsites.net/",
];
export const allowedJunoOrigins: ReadonlyArray<string> = [
JunoEndpoints.Test,
JunoEndpoints.Test2,
JunoEndpoints.Test3,
JunoEndpoints.Prod,
JunoEndpoints.Stage,
"https://localhost",
];
export const allowedNotebookServerUrls: ReadonlyArray<string> = [];

View File

@@ -228,7 +228,7 @@ export function downloadItem(
undefined, undefined,
"Download", "Download",
async () => { async () => {
if (useNotebook.getState().isPhoenix) { if (useNotebook.getState().isPhoenixNotebooks) {
await container.allocateContainer(); await container.allocateContainer();
} }
const notebookServerInfo = useNotebook.getState().notebookServerInfo; const notebookServerInfo = useNotebook.getState().notebookServerInfo;

View File

@@ -4,7 +4,7 @@ export function isInvalidParentFrameOrigin(event: MessageEvent): boolean {
return !isValidOrigin(configContext.allowedParentFrameOrigins, event); return !isValidOrigin(configContext.allowedParentFrameOrigins, event);
} }
function isValidOrigin(allowedOrigins: string[], event: MessageEvent): boolean { function isValidOrigin(allowedOrigins: ReadonlyArray<string>, event: MessageEvent): boolean {
const eventOrigin = (event && event.origin) || ""; const eventOrigin = (event && event.origin) || "";
const windowOrigin = (window && window.origin) || ""; const windowOrigin = (window && window.origin) || "";
if (eventOrigin === windowOrigin) { if (eventOrigin === windowOrigin) {

View File

@@ -1,3 +1,4 @@
import { useTabs } from "hooks/useTabs";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { applyExplorerBindings } from "../applyExplorerBindings"; import { applyExplorerBindings } from "../applyExplorerBindings";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
@@ -69,16 +70,38 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
async function configureHosted(): Promise<Explorer> { async function configureHosted(): Promise<Explorer> {
const win = (window as unknown) as HostedExplorerChildFrame; const win = (window as unknown) as HostedExplorerChildFrame;
let explorer: Explorer;
if (win.hostedConfig.authType === AuthType.EncryptedToken) { if (win.hostedConfig.authType === AuthType.EncryptedToken) {
return configureHostedWithEncryptedToken(win.hostedConfig); explorer = configureHostedWithEncryptedToken(win.hostedConfig);
} else if (win.hostedConfig.authType === AuthType.ResourceToken) { } else if (win.hostedConfig.authType === AuthType.ResourceToken) {
return configureHostedWithResourceToken(win.hostedConfig); explorer = configureHostedWithResourceToken(win.hostedConfig);
} else if (win.hostedConfig.authType === AuthType.ConnectionString) { } else if (win.hostedConfig.authType === AuthType.ConnectionString) {
return configureHostedWithConnectionString(win.hostedConfig); explorer = configureHostedWithConnectionString(win.hostedConfig);
} else if (win.hostedConfig.authType === AuthType.AAD) { } else if (win.hostedConfig.authType === AuthType.AAD) {
return configureHostedWithAAD(win.hostedConfig); explorer = await configureHostedWithAAD(win.hostedConfig);
} else {
throw new Error(`Unknown hosted config: ${win.hostedConfig}`);
} }
throw new Error(`Unknown hosted config: ${win.hostedConfig}`);
window.addEventListener(
"message",
(event) => {
if (isInvalidParentFrameOrigin(event)) {
return;
}
if (!shouldProcessMessage(event)) {
return;
}
if (event.data?.type === MessageTypes.CloseTab) {
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
}
},
false
);
return explorer;
} }
async function configureHostedWithAAD(config: AAD): Promise<Explorer> { async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
@@ -261,6 +284,8 @@ async function configurePortal(): Promise<Explorer> {
} }
} else if (shouldForwardMessage(message, event.origin)) { } else if (shouldForwardMessage(message, event.origin)) {
sendMessage(message); sendMessage(message);
} else if (event.data?.type === MessageTypes.CloseTab) {
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
} }
}, },
false false
@@ -326,6 +351,11 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
if (inputs.features) { if (inputs.features) {
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features))); Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
} }
//Updating phoenix feature flags for MPAC based of config context
if (configContext.isPhoenixEnabled === true) {
userContext.features.phoenixNotebooks = true;
userContext.features.phoenixFeatures = true;
}
if (inputs.flights) { if (inputs.flights) {
if (inputs.flights.indexOf(Flights.AutoscaleTest) !== -1) { if (inputs.flights.indexOf(Flights.AutoscaleTest) !== -1) {
userContext.features.autoscaleDefault; userContext.features.autoscaleDefault;
@@ -339,12 +369,18 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) { if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) {
userContext.features.partitionKeyDefault2 = true; userContext.features.partitionKeyDefault2 = true;
} }
if (inputs.flights.indexOf(Flights.Phoenix) !== -1) { if (inputs.flights.indexOf(Flights.PhoenixNotebooks) !== -1) {
userContext.features.phoenix = true; userContext.features.phoenixNotebooks = true;
}
if (inputs.flights.indexOf(Flights.PhoenixFeatures) !== -1) {
userContext.features.phoenixFeatures = true;
} }
if (inputs.flights.indexOf(Flights.NotebooksDownBanner) !== -1) { if (inputs.flights.indexOf(Flights.NotebooksDownBanner) !== -1) {
userContext.features.notebooksDownBanner = true; userContext.features.notebooksDownBanner = true;
} }
if (inputs.flights.indexOf(Flights.FreeTierAutoscaleThroughput) !== -1) {
userContext.features.freetierAutoscaleThroughput = true;
}
} }
} }