Compare commits

...

57 Commits

Author SHA1 Message Date
Karthik Chakravarthy Cheekuri
272e88f351 Testing e2e coverage issue
wq
qa
2022-01-26 11:43:30 -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
victor-meng
ada95eae1f Fix execute sproc pane textfield focus issue (#1170)
* Fix execute sproc pane textfield focus issue

* Update snapshot
2021-12-08 15:41:27 -08:00
vaidankarswapnil
8a8c023d7b Fix Keyboard focus New Database button (#1167)
* Fix a11y new database button focus issue

* Update test snapshot and other issues

* fix issue for the menu button

* Issue fixed in Splash screen
2021-12-02 20:13:45 -08:00
Hardikkumar Nai
667b1e1486 1413651_Refresh_button_missing (#1169) 2021-12-02 20:12:57 -08:00
Sunil Kumar Yadav
203c2ac246 fixed horizontal scroll issue on zoom 400% (#1165)
Co-authored-by: sunilyadav <v-yadavsunil@microsoft.com>
2021-12-01 19:46:48 -08:00
victor-meng
5d235038ad Properly update table headers (#1166) 2021-11-30 15:36:35 -08:00
Srinath Narayanan
6b4d6f986e added github test env client id (#1168) 2021-12-01 03:38:38 +05:30
Karthik chakravarthy
e575b94ffa Add phoenix telemetry (#1164)
* Add phoenix telemetry

* Revert changes

* Update trace logs
2021-11-29 11:22:57 -05:00
vaidankarswapnil
42bdcaf8d1 Fix radio buttons present under 'Settings' blade like ‘Custom and Unlimited’ along with its label ‘Page options’ are not enclosed in fieldset/legend tag (#1100)
* Fix a11y setting pane radiobuttons issue

* Update test snapshot issue
2021-11-24 20:00:06 -08:00
victor-meng
94a03e5b03 Add Timestamp type to cassandra column types and wrap Timestamp value inside single quotes when creating queries (#1163) 2021-11-19 09:55:10 -08:00
victor-meng
1155557af1 Check for -1 throughput cap value (#1159) 2021-11-10 21:43:04 -08:00
tarazou9
27a49e9aa9 add juno test3 to allow list (#1158)
* add juno test3 to allow list

* remove extra line
2021-11-10 17:05:31 +05:30
Srinath Narayanan
fa8be2bc0f fixed quickstarts (#1157) 2021-11-10 17:05:17 +05:30
Karthik chakravarthy
3aa4bbe266 Users/kcheekuri/phoenix heart beat retry with delay (#1153)
* Health check retry addition

* format issue

* Address comments

* Test Check

* Added await

* code cleanup
2021-11-09 18:08:17 +05:30
siddjoshi-ms
2dfabf3c69 Sqlx currency code fix (#1149)
* using currency code from fetch prices api

* formatting & linting fixes

* Update SqlX.rp.ts
2021-11-09 00:04:22 +05:30
victor-meng
a3d88af175 Fix throughputcap check (#1156) 2021-11-05 10:23:21 -07:00
Srinath Narayanan
5597a1e8b6 Changes to reset container workflow (#1155)
* reset changes

* undid config context changes

* renamed method
2021-11-04 21:55:41 +05:30
victor-meng
e3d5ad2ce8 Fix ARM api version (#1154) 2021-11-02 12:23:48 -07:00
victor-meng
64f36e2d28 Add throughput cap error message (#1151) 2021-10-30 19:45:16 -07:00
Srinath Narayanan
4ce1252e58 master/main fix (#1150) 2021-10-28 17:08:34 +05:30
Karthik chakravarthy
7d9faec81e Phoenix runtime - Reset workspace (#1136)
* Phoenix runtime - Reset workspace

* Format and Lint issues

* Typo issue

* Reset warning text change and create new context on allcation of new container

* Closing only notebook related

* resolved comments from previous PR

* On Schema Analyser allocate call

Co-authored-by: Srinath Narayanan <srnara@microsoft.com>
2021-10-22 10:41:13 -04:00
Karthik chakravarthy
22da3b90ef Phoenix Reconnect Integration (#1123)
* Reconnect integration

* git connection issue

* format issue

* Typo issue

* added constants

* Removed math.round for remainingTime

* code refctor for container status check

* disconnect text change
2021-10-22 14:34:38 +05:30
Srinath Narayanan
361ac45e52 Added notebooksDownBanner flight (#1146)
* set isNotebookEnabled to true

* lint and format fixes

* modified shell enabled

* added notebooks down banner flight

* fixed typo
2021-10-22 13:27:52 +05:30
Srinath Narayanan
8aa764079a Setting isNotebooKEnabled to true by default (#1145)
* set isNotebookEnabled to true

* lint and format fixes

* modified shell enabled
2021-10-22 11:48:40 +05:30
victor-meng
55837db65b Revert "Fix keyboard focus does not retain on 'New Database' button a… (#1139)
* Revert "Fix keyboard focus does not retain on 'New Database' button after closing the 'New Database' blade via ESC key (#1109)"

This reverts commit f7e7240010.

* Revert "Fix ally database panel open issue (#1120)"

This reverts commit ed1ffb692f.
2021-10-15 17:36:48 -07:00
victor-meng
9f27cb95b9 Only use the SET keyword once in the update query (#1138) 2021-10-15 12:33:59 -07:00
Hardikkumar Nai
271256bffb resolve_eslint_NodePropertiesComponent (#921)
* resolve_eslint_NodePropertiesComponent

* address commit

* Open new screen: Screen reader does not pass the 'Copied' information after selecting 'Copy' button.

* resolve lint error
2021-10-12 08:43:35 -07:00
vaidankarswapnil
aff7133095 Fix eslint issues for TableCommands and other files (#1132) 2021-10-12 08:07:06 -07:00
Hardikkumar Nai
bfd4948fb9 absulte_path setting (#984)
* absulte_path setting

* resolve build time error
2021-10-12 07:38:34 -07:00
victor-meng
1c54459708 If unsharded is checked, set partition key to undefined (#1128) 2021-10-11 12:09:38 -07:00
Sunil Kumar Yadav
df3b18d585 fixed eslint of NotebookComponentBootstrapper and NotebookReadOnlyRenderer (#1122) 2021-10-11 08:29:21 -07:00
Sunil Kumar Yadav
882f0e1554 fixed GraphExplorer.tsx ellint issue (#1124) 2021-10-11 08:21:52 -07:00
vaidankarswapnil
b67b76cc87 Fix eslint issues for QueryBuilderViewModal and QueryClauseViewModel (#1125) 2021-10-11 08:20:38 -07:00
vaidankarswapnil
734ee1e436 Fix eslint issues for ClauseGroup and ClauseGroupViewModel files (#1127) 2021-10-11 07:56:12 -07:00
Sunil Kumar Yadav
ff498b51e2 fixed eslint of Trigger.ts GithubOAuthService.ts etc (#1126) 2021-10-11 07:55:21 -07:00
vaidankarswapnil
ed1ffb692f Fix ally database panel open issue (#1120) 2021-10-06 07:53:46 -07:00
Karthik chakravarthy
f7fa3f7c09 Fix Unit Test: Mock the class to its instance (#1117)
* mock to instance

* Update jest.config.js

Co-authored-by: Jordi Bunster <jbunster@microsoft.com>
2021-10-05 13:06:26 -07:00
Jordi Bunster
6ebf19c0c9 Close tab fixes (#1107)
* Close tab fixes

Ensure that when promoting a new tab to being the 'active' tab
(as a consequence of, say, closing the active tab) that the newly
promoted tab has a chance to install its buttons and what not.

* Set new active tab even if undefined
2021-10-05 09:25:35 -07:00
119 changed files with 7104 additions and 6024 deletions

View File

@@ -81,17 +81,9 @@ src/Explorer/Tables/DataTable/DataTableBindingManager.ts
src/Explorer/Tables/DataTable/DataTableBuilder.ts
src/Explorer/Tables/DataTable/DataTableContextMenu.ts
src/Explorer/Tables/DataTable/DataTableOperationManager.ts
src/Explorer/Tables/DataTable/DataTableOperations.ts
src/Explorer/Tables/DataTable/DataTableViewModel.ts
src/Explorer/Tables/DataTable/TableCommands.ts
src/Explorer/Tables/DataTable/TableEntityCache.ts
src/Explorer/Tables/DataTable/TableEntityListViewModel.ts
src/Explorer/Tables/Entities.ts
src/Explorer/Tables/QueryBuilder/ClauseGroup.ts
src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts
src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts
src/Explorer/Tables/TableDataClient.ts
src/Explorer/Tables/TableEntityProcessor.ts
src/Explorer/Tables/Utilities.ts
@@ -115,15 +107,10 @@ src/Explorer/Tree/ObjectId.ts
src/Explorer/Tree/ResourceTokenCollection.ts
src/Explorer/Tree/StoredProcedure.ts
src/Explorer/Tree/TreeComponents.ts
src/Explorer/Tree/Trigger.ts
src/Explorer/WaitsForTemplateViewModel.ts
src/GitHub/GitHubClient.test.ts
src/GitHub/GitHubClient.ts
src/GitHub/GitHubConnector.ts
src/GitHub/GitHubOAuthService.ts
src/Index.ts
src/Juno/JunoClient.test.ts
src/Juno/JunoClient.ts
src/Platform/Hosted/Authorization.ts
src/ReactDevTools.ts
src/Shared/Constants.ts
@@ -143,20 +130,13 @@ src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
src/Explorer/Controls/TreeComponent/TreeComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx
src/Explorer/Graph/GraphExplorerComponent/GraphVizComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/MiddlePaneComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.test.tsx
src/Explorer/Graph/GraphExplorerComponent/NodePropertiesComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx
src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx
src/Explorer/Notebook/NotebookComponent/VirtualCommandBarComponent.tsx
src/Explorer/Notebook/NotebookComponent/contents/index.tsx
src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx
src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx
src/Explorer/Notebook/NotebookRenderer/decorators/draggable/index.tsx
src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/index.tsx

View File

@@ -39,7 +39,6 @@ module.exports = {
"@typescript-eslint/switch-exhaustiveness-check": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-extraneous-class": "error",
"no-null/no-null": "error",
"@typescript-eslint/no-explicit-any": "error",
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
eqeqeq: "error",

6
.vscode/launch.json vendored
View File

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

View File

@@ -22,5 +22,6 @@
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
}
},
"typescript.preferences.importModuleSpecifier": "non-relative"
}

View File

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

View File

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

View File

@@ -37,8 +37,8 @@ module.exports = {
global: {
branches: 25,
functions: 25,
lines: 29.5,
statements: 29.5,
lines: 29,
statements: 29,
},
},
@@ -129,6 +129,8 @@ module.exports = {
// The test environment that will be used for testing
// testEnvironment: "jest-environment-jsdom",
modulePaths: ["node_modules", "<rootDir>/src"],
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},

View File

@@ -2077,7 +2077,7 @@ a:link {
.resourceTreeAndTabs {
display: flex;
flex: 1 1 auto;
overflow-x: auto;
overflow-x: clip;
overflow-y: auto;
height: 100%;
}
@@ -2245,7 +2245,7 @@ a:link {
}
.refreshColHeader {
padding: 3px 6px 6px 6px;
padding: 3px 6px 10px 0px !important;
}
.refreshColHeader:hover {
@@ -2869,31 +2869,39 @@ a:link {
}
}
settings-pane {
.settingsSection {
border-bottom: 1px solid @BaseMedium;
margin-right: 24px;
padding: @MediumSpace 0px;
.settingsSection {
border-bottom: 1px solid @BaseMedium;
margin-right: 24px;
padding: @MediumSpace 0px;
&:first-child {
padding-top: 0px;
}
&:first-child {
padding-top: 0px;
padding-bottom: 10px;
}
&:last-child {
border-bottom: none;
}
&:last-child {
border-bottom: none;
}
.settingsSectionPart {
padding-left: 8px;
}
.settingsSectionPart {
padding-left: 8px;
}
.settingsSectionLabel {
margin-bottom: @DefaultSpace;
}
.settingsSectionLabel {
margin-bottom: @DefaultSpace;
margin-right: 5px;
}
.pageOptionsPart {
padding-bottom: @MediumSpace;
}
.pageOptionsPart {
padding-bottom: @MediumSpace;
}
.legendLabel {
border-bottom: 0px;
width: auto;
font-size: @mediumFontSize;
display: inline !important;
float: left;
}
}

View File

@@ -96,7 +96,9 @@ export class Flights {
public static readonly AutoscaleTest = "autoscaletest";
public static readonly PartitionKeyTest = "partitionkeytest";
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";
}
export class AfecFeatures {
@@ -343,7 +345,12 @@ export enum ConnectionStatusType {
Connecting = "Connecting",
Connected = "Connected",
Failed = "Connection Failed",
ReConnect = "Reconnect",
Reconnect = "Reconnect",
}
export enum ContainerStatusType {
Active = "Active",
Disconnected = "Disconnected",
}
export const EmulatorMasterKey =
@@ -356,20 +363,25 @@ export const StyleConstants = require("less-vars-loader!../../less/Common/Consta
export class Notebook {
public static readonly defaultBasePath = "./notebooks";
public static readonly heartbeatDelayMs = 60000;
public static readonly containerStatusHeartbeatDelayMs = 30000;
public static readonly kernelRestartInitialDelayMs = 1000;
public static readonly kernelRestartMaxDelayMs = 20000;
public static readonly autoSaveIntervalMs = 120000;
public static readonly autoSaveIntervalMs = 300000;
public static readonly memoryGuageToGB = 1048576;
public static readonly lowMemoryThreshold = 0.8;
public static readonly remainingTimeForAlert = 10;
public static readonly retryAttempts = 3;
public static readonly retryAttemptDelayMs = 5000;
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
public static readonly mongoShellTemporarilyDownMsg =
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
public static readonly cassandraShellTemporarilyDownMsg =
"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 =
"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.";
public static newNotebookModalTitle = "Create Notebook in temporary workspace";
public static newNotebookUploadModalTitle = "Upload Notebook in temporary workspace";
"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 newNotebookUploadModalTitle = "Upload notebook to temporary workspace";
public static newNotebookModalContent1 =
"A temporary workspace will be created to enable you to work with notebooks. When the session expires, any notebooks in the workspace will be removed.";
public static newNotebookModalContent2 =
@@ -401,3 +413,11 @@ export class TerminalQueryParams {
public static readonly SubscriptionId = "subscriptionId";
public static readonly TerminalEndpoint = "terminalEndpoint";
}
export class JunoEndpoints {
public static readonly Test = "https://juno-test.documents-dev.windows-int.net";
public static readonly Test2 = "https://juno-test2.documents-dev.windows-int.net";
public static readonly Test3 = "https://juno-test3.documents-dev.windows-int.net";
public static readonly Prod = "https://tools.cosmos.azure.com";
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
}

View File

@@ -1,5 +1,6 @@
import * as Cosmos from "@azure/cosmos";
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
import { CosmosHeaders } from "@azure/cosmos/dist-esm";
import { configContext, Platform } from "../ConfigContext";
import { userContext } from "../UserContext";
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;
export function client(): Cosmos.CosmosClient {
if (_client) return _client;
let _defaultHeaders: CosmosHeaders = {};
_defaultHeaders["x-ms-cosmos-sdk-supported-capabilities"] =
SDKSupportedCapabilities.None | SDKSupportedCapabilities.PartitionMerge;
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
key: userContext.masterKey,
@@ -89,6 +101,7 @@ export function client(): Cosmos.CosmosClient {
enableEndpointDiscovery: false,
},
userAgentSuffix: "Azure Portal",
defaultHeaders: _defaultHeaders,
};
if (configContext.PROXY_PATH !== undefined) {

View File

@@ -236,13 +236,12 @@ describe("MongoProxyClient", () => {
});
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");
});
it("returns a development endpoint", () => {
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
const endpoint = getEndpoint();
const endpoint = getEndpoint("https://localhost:1234");
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
});
@@ -250,7 +249,7 @@ describe("MongoProxyClient", () => {
updateUserContext({
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");
});
});

View File

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

View File

@@ -1,3 +1,17 @@
import {
allowedAadEndpoints,
allowedArcadiaEndpoints,
allowedArmEndpoints,
allowedBackendEndpoints,
allowedEmulatorEndpoints,
allowedGraphEndpoints,
allowedHostedExplorerEndpoints,
allowedJunoOrigins,
allowedMongoBackendEndpoints,
allowedMsalRedirectEndpoints,
validateEndpoint,
} from "Utils/EndpointValidation";
export enum Platform {
Portal = "Portal",
Hosted = "Hosted",
@@ -6,7 +20,7 @@ export enum Platform {
export interface ConfigContext {
platform: Platform;
allowedParentFrameOrigins: string[];
allowedParentFrameOrigins: ReadonlyArray<string>;
gitSha?: string;
proxyPath?: string;
AAD_ENDPOINT: string;
@@ -23,10 +37,11 @@ export interface ConfigContext {
PROXY_PATH?: string;
JUNO_ENDPOINT: string;
GITHUB_CLIENT_ID: string;
GITHUB_TEST_ENV_CLIENT_ID: string;
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
isTerminalEnabled: boolean;
hostedExplorerURL: string;
armAPIVersion?: string;
allowedJunoOrigins: string[];
msalRedirectURI?: string;
}
@@ -40,8 +55,7 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.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,
hostedExplorerURL: "https://cosmos.azure.com/",
AAD_ENDPOINT: "https://login.microsoftonline.com/",
@@ -52,16 +66,11 @@ let configContext: Readonly<ConfigContext> = {
GRAPH_API_VERSION: "1.6",
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
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
JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
allowedJunoOrigins: [
"https://juno-test.documents-dev.windows-int.net",
"https://juno-test2.documents-dev.windows-int.net",
"https://tools.cosmos.azure.com",
"https://tools-staging.cosmos.azure.com",
"https://localhost",
],
isTerminalEnabled: false,
};
export function resetConfigContext(): void {
@@ -72,6 +81,50 @@ export function resetConfigContext(): 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);
}
@@ -95,18 +148,8 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
});
if (response.status === 200) {
try {
const { allowedParentFrameOrigins, allowedJunoOrigins, ...externalConfig } = await response.json();
Object.assign(configContext, externalConfig);
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
updateConfigContext({
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins],
});
}
if (allowedJunoOrigins && allowedJunoOrigins.length > 0) {
updateConfigContext({
allowedJunoOrigins: [...configContext.allowedJunoOrigins, ...allowedJunoOrigins],
});
}
const { ...externalConfig } = await response.json();
updateConfigContext(externalConfig);
} catch (error) {
console.error("Unable to parse json in config file");
console.error(error);

View File

@@ -1,4 +1,4 @@
import { ConnectionStatusType } from "../Common/Constants";
import { ConnectionStatusType, ContainerStatusType } from "../Common/Constants";
export interface DatabaseAccount {
id: string;
@@ -26,6 +26,8 @@ export interface DatabaseAccountExtendedProperties {
isVirtualNetworkFilterEnabled?: boolean;
ipRules?: IpRule[];
privateEndpointConnections?: unknown[];
capacity?: { totalThroughputLimit: number };
locations?: DatabaseAccountResponseLocation[];
}
export interface DatabaseAccountResponseLocation {
@@ -426,6 +428,32 @@ export interface OperationStatus {
export interface NotebookWorkspaceConnectionInfo {
authToken: string;
notebookServerEndpoint: string;
forwardingId: string;
}
export interface ContainerInfo {
durationLeftInMinutes: number;
notebookServerInfo: NotebookWorkspaceConnectionInfo;
status: ContainerStatusType;
}
export interface IProvisionData {
cosmosEndpoint: string;
}
export interface IContainerData {
forwardingId: string;
}
export interface IResponse<T> {
status: number;
data: T;
}
export interface IPhoenixConnectionInfoResult {
readonly notebookAuthToken?: string;
readonly notebookServerUrl?: string;
readonly forwardingId?: string;
}
export interface NotebookWorkspaceFeedResponse {

View File

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

View File

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

View File

@@ -13,7 +13,6 @@ import {
Link,
PrimaryButton,
ProgressIndicator,
Text,
TextField,
} from "@fluentui/react";
import React, { FC } from "react";
@@ -197,7 +196,7 @@ export const Dialog: FC = () => {
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
</Link>
)}
{contentHtml && <Text>{contentHtml}</Text>}
{contentHtml}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter>
<PrimaryButton {...primaryButtonProps} />

View File

@@ -1,14 +1,14 @@
import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "@fluentui/react";
import * as React from "react";
import * as Constants from "../../../Common/Constants";
import * as UrlUtility from "../../../Common/UrlUtility";
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import Explorer from "../../Explorer";
import { RepoListItem } from "./GitHubReposComponent";
import { ChildrenMargin } from "./GitHubStyleConstants";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as UrlUtility from "../../../Common/UrlUtility";
import Explorer from "../../Explorer";
export interface AddRepoComponentProps {
container: Explorer;
@@ -27,7 +27,6 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
private static readonly ButtonText = "Add";
private static readonly TextFieldPlaceholder = "https://github.com/owner/repo/tree/branch";
private static readonly TextFieldErrorMessage = "Invalid url";
private static readonly DefaultBranchName = "master";
constructor(props: AddRepoComponentProps) {
super(props);
@@ -78,7 +77,7 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
});
let enteredUrl = this.state.textFieldValue;
if (enteredUrl.indexOf("/tree/") === -1) {
enteredUrl = UrlUtility.createUri(enteredUrl, `tree/${AddRepoComponent.DefaultBranchName}`);
enteredUrl = UrlUtility.createUri(enteredUrl, `tree/`);
}
const repoInfo = GitHubUtils.fromRepoUri(enteredUrl);
@@ -93,11 +92,7 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
const item: RepoListItem = {
key: GitHubUtils.toRepoFullName(repo.owner, repo.name),
repo,
branches: [
{
name: repoInfo.branch,
},
],
branches: repoInfo.branch ? [{ name: repoInfo.branch }] : [],
};
TelemetryProcessor.traceSuccess(

View File

@@ -24,11 +24,11 @@ import { RepoListItem } from "./GitHubReposComponent";
import {
BranchesDropdownCheckboxStyles,
BranchesDropdownOptionContainerStyle,
BranchesDropdownStyles,
BranchesDropdownWidth,
ReposListBranchesColumnWidth,
ReposListCheckboxStyles,
ReposListRepoColumnMinWidth,
ReposListBranchesColumnWidth,
BranchesDropdownWidth,
BranchesDropdownStyles,
} from "./GitHubStyleConstants";
export interface ReposListComponentProps {
@@ -44,6 +44,7 @@ export interface BranchesProps {
lastPageInfo?: IGitHubPageInfo;
hasMore: boolean;
isLoading: boolean;
defaultBranchName: string;
loadMore: () => void;
}
@@ -64,7 +65,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
private static readonly BranchesColumnName = "Branches";
private static readonly LoadingText = "Loading...";
private static readonly LoadMoreText = "Load more";
private static readonly DefaultBranchName = "master";
private static readonly DefaultBranchNames = "master/main";
private static readonly FooterIndex = -1;
public render(): JSX.Element {
@@ -155,6 +156,10 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
}
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)];
if (item.branches.length === 0 && branchesProps.defaultBranchName) {
item.branches = [{ name: branchesProps.defaultBranchName }];
}
const options: IDropdownOption[] = branchesProps.branches.map((branch) => ({
key: branch.name,
text: branch.name,
@@ -198,7 +203,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
const dropdownProps: IDropdownProps = {
styles: BranchesDropdownStyles,
options: [],
placeholder: ReposListComponent.DefaultBranchName,
placeholder: ReposListComponent.DefaultBranchNames,
disabled: true,
};
@@ -272,7 +277,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
styles: ReposListCheckboxStyles,
onChange: () => {
const repoListItem = { ...item };
repoListItem.branches = [{ name: ReposListComponent.DefaultBranchName }];
repoListItem.branches = [];
this.props.pinRepo(repoListItem);
},
};

View File

@@ -35,16 +35,19 @@ const testCassandraAccount: DataModels.DatabaseAccount = {
const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
forwardingId: "Id",
};
const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
forwardingId: "Id",
};
const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
authToken: "authToken",
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
forwardingId: "Id",
};
describe("NotebookTerminalComponent", () => {
@@ -52,6 +55,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testAccount,
notebookServerInfo: testNotebookServerInfo,
tabId: undefined,
};
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
@@ -62,6 +66,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testMongo32Account,
notebookServerInfo: testMongoNotebookServerInfo,
tabId: undefined,
};
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
@@ -72,6 +77,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testMongo36Account,
notebookServerInfo: testMongoNotebookServerInfo,
tabId: undefined,
};
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
@@ -82,6 +88,7 @@ describe("NotebookTerminalComponent", () => {
const props: NotebookTerminalComponentProps = {
databaseAccount: testCassandraAccount,
notebookServerInfo: testCassandraNotebookServerInfo,
tabId: undefined,
};
const wrapper = shallow(<NotebookTerminalComponent {...props} />);

View File

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

View File

@@ -17,7 +17,6 @@ import Explorer from "../../Explorer";
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
import { NotebookUtil } from "../../Notebook/NotebookUtil";
import { useNotebook } from "../../Notebook/useNotebook";
import { Dialog, TextFieldProps, useDialog } from "../Dialog";
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
@@ -53,7 +52,7 @@ export class NotebookViewerComponent
super(props);
this.clientManager = new NotebookClientV2({
connectionInfo: { authToken: undefined, notebookServerEndpoint: undefined },
connectionInfo: { authToken: undefined, notebookServerEndpoint: undefined, forwardingId: undefined },
databaseAccountName: undefined,
defaultExperience: "NotebookViewer",
isReadOnly: true,
@@ -148,9 +147,7 @@ export class NotebookViewerComponent
<NotebookMetadataComponent
data={this.state.galleryItem}
isFavorite={this.state.isFavorite}
downloadButtonText={
this.props.container && NotebookUtil.getNotebookBtnTitle(useNotebook.getState().notebookFolderName)
}
downloadButtonText={this.props.container && `Download to ${useNotebook.getState().notebookFolderName}`}
onTagClick={this.props.onTagClick}
onFavoriteClick={this.favoriteItem}
onUnfavoriteClick={this.unfavoriteItem}

View File

@@ -1,4 +1,5 @@
import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react";
import { useDatabases } from "Explorer/useDatabases";
import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
@@ -71,6 +72,7 @@ export interface SettingsComponentState {
wasAutopilotOriginallySet: boolean;
isScaleSaveable: boolean;
isScaleDiscardable: boolean;
throughputError: string;
timeToLive: TtlType;
timeToLiveBaseline: TtlType;
@@ -124,6 +126,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private changeFeedPolicyVisible: boolean;
private isFixedContainer: boolean;
private shouldShowIndexingPolicyEditor: boolean;
private totalThroughputUsed: number;
public mongoDBCollectionResource: MongoDBCollectionResource;
constructor(props: SettingsComponentProps) {
@@ -155,6 +158,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
wasAutopilotOriginallySet: false,
isScaleSaveable: false,
isScaleDiscardable: false,
throughputError: undefined,
timeToLive: undefined,
timeToLiveBaseline: undefined,
@@ -208,6 +212,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
return true;
},
};
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
this.calculateTotalThroughputUsed();
}
}
componentDidMount(): void {
@@ -254,6 +263,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
return false;
}
if (this.state.throughputError) {
return false;
}
return (
this.state.isScaleSaveable ||
this.state.isSubSettingsSaveable ||
@@ -481,6 +494,26 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onMongoIndexingPolicyDiscardableChange = (isMongoIndexingPolicyDiscardable: boolean): void =>
this.setState({ isMongoIndexingPolicyDiscardable });
private calculateTotalThroughputUsed = (): void => {
this.totalThroughputUsed = 0;
(useDatabases.getState().databases || []).forEach(async (database) => {
if (database.offer()) {
const dbThroughput = database.offer().autoscaleMaxThroughput || database.offer().manualThroughput;
this.totalThroughputUsed += dbThroughput;
}
(database.collections() || []).forEach(async (collection) => {
if (collection.offer()) {
const colThroughput = collection.offer().autoscaleMaxThroughput || collection.offer().manualThroughput;
this.totalThroughputUsed += colThroughput;
}
});
});
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
this.totalThroughputUsed *= numberOfRegions;
};
public getAnalyticalStorageTtl = (): number => {
if (this.isAnalyticalStorageEnabled) {
if (this.state.analyticalStorageTtlSelection === TtlType.On) {
@@ -643,10 +676,31 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
return buttons;
};
private onMaxAutoPilotThroughputChange = (newThroughput: number): void =>
this.setState({ autoPilotThroughput: newThroughput });
private onMaxAutoPilotThroughputChange = (newThroughput: number): void => {
let throughputError = "";
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
const throughputDelta = (newThroughput - this.offer.autoscaleMaxThroughput) * numberOfRegions;
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
this.totalThroughputUsed + throughputDelta
} RU/s. Change total throughput limit in cost management.`;
}
this.setState({ autoPilotThroughput: newThroughput, throughputError });
};
private onThroughputChange = (newThroughput: number): void => this.setState({ throughput: newThroughput });
private onThroughputChange = (newThroughput: number): void => {
let throughputError = "";
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
const throughputDelta = (newThroughput - this.offer.manualThroughput) * numberOfRegions;
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
this.totalThroughputUsed + throughputDelta
} RU/s. Change total throughput limit in cost management.`;
}
this.setState({ throughput: newThroughput, throughputError });
};
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
@@ -893,6 +947,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
onScaleSaveableChange: this.onScaleSaveableChange,
onScaleDiscardableChange: this.onScaleDiscardableChange,
initialNotification: this.props.settingsTab.pendingNotification(),
throughputError: this.state.throughputError,
};
if (!this.isCollectionSettingsTab) {

View File

@@ -36,6 +36,7 @@ export interface ScaleComponentProps {
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
initialNotification: DataModels.Notification;
throughputError?: string;
}
export class ScaleComponent extends React.Component<ScaleComponentProps> {
@@ -189,6 +190,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
getThroughputWarningMessage={this.getThroughputWarningMessage}
usageSizeInKB={this.props.collection?.usageSizeInKB()}
throughputError={this.props.throughputError}
/>
);

View File

@@ -75,6 +75,7 @@ export interface ThroughputInputAutoPilotV3Props {
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
getThroughputWarningMessage: () => JSX.Element;
usageSizeInKB: number;
throughputError?: string;
}
interface ThroughputInputAutoPilotV3State {
@@ -540,6 +541,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
onChange={this.onAutoPilotThroughputChange}
min={minAutoPilotThroughput}
errorMessage={this.props.throughputError}
/>
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
{this.minRUperGBSurvey()}
@@ -579,6 +581,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
}
onChange={this.onThroughputChange}
min={this.props.minimum}
errorMessage={this.props.throughputError}
/>
{this.state.exceedFreeTierThroughput && (
<MessageBar

View File

@@ -34,7 +34,13 @@ exports[`SettingsComponent renders 1`] = `
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"phoenixClient": PhoenixClient {
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -102,7 +108,13 @@ exports[`SettingsComponent renders 1`] = `
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"phoenixClient": PhoenixClient {
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],

View File

@@ -7,6 +7,7 @@ const props = {
isSharded: true,
setThroughputValue: () => jest.fn(),
setIsAutoscale: () => jest.fn(),
setIsThroughputCapExceeded: () => jest.fn(),
onCostAcknowledgeChange: () => jest.fn(),
};
describe("ThroughputInput Pane", () => {

View File

@@ -1,5 +1,6 @@
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
import React, { FunctionComponent, useState } from "react";
import { useDatabases } from "Explorer/useDatabases";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import * as SharedConstants from "../../../Shared/Constants";
@@ -16,6 +17,7 @@ export interface ThroughputInputProps {
showFreeTierExceedThroughputTooltip: boolean;
setThroughputValue: (throughput: number) => void;
setIsAutoscale: (isAutoscale: boolean) => void;
setIsThroughputCapExceeded: (isThroughputCapExceeded: boolean) => void;
onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
}
@@ -24,6 +26,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
showFreeTierExceedThroughputTooltip,
setThroughputValue,
setIsAutoscale,
setIsThroughputCapExceeded,
isSharded,
onCostAcknowledgeChange,
}: ThroughputInputProps) => {
@@ -31,10 +34,60 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput);
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
const [throughputError, setThroughputError] = useState<string>("");
const [totalThroughputUsed, setTotalThroughputUsed] = useState<number>(0);
setIsAutoscale(isAutoscaleSelected);
setThroughputValue(throughput);
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
useEffect(() => {
// throughput cap check for the initial state
let totalThroughput = 0;
(useDatabases.getState().databases || []).forEach((database) => {
if (database.offer()) {
const dbThroughput = database.offer().autoscaleMaxThroughput || database.offer().manualThroughput;
totalThroughput += dbThroughput;
}
(database.collections() || []).forEach((collection) => {
if (collection.offer()) {
const colThroughput = collection.offer().autoscaleMaxThroughput || collection.offer().manualThroughput;
totalThroughput += colThroughput;
}
});
});
totalThroughput *= numberOfRegions;
setTotalThroughputUsed(totalThroughput);
if (throughputCap && throughputCap !== -1 && throughputCap - totalThroughput < throughput) {
setThroughputError(
`Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
totalThroughput + throughput * numberOfRegions
} RU/s. Change total throughput limit in cost management.`
);
setIsThroughputCapExceeded(true);
}
}, []);
const checkThroughputCap = (newThroughput: number): boolean => {
if (throughputCap && throughputCap !== -1 && throughputCap - totalThroughputUsed < newThroughput) {
setThroughputError(
`Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
totalThroughputUsed + newThroughput * numberOfRegions
} RU/s. Change total throughput limit in cost management.`
);
setIsThroughputCapExceeded(true);
return false;
}
setThroughputError("");
setIsThroughputCapExceeded(false);
return true;
};
const getThroughputLabelText = (): string => {
let throughputHeaderText: string;
if (isAutoscaleSelected) {
@@ -60,11 +113,17 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const newThroughput = parseInt(newInput);
setThroughput(newThroughput);
setThroughputValue(newThroughput);
if (!isSharded && newThroughput > 10000) {
setThroughputError("Unsharded collections support up to 10,000 RUs");
} else {
setThroughputError("");
return;
}
if (!checkThroughputCap(newThroughput)) {
return;
}
setThroughputError("");
};
const getAutoScaleTooltip = (): string => {
@@ -96,11 +155,13 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
setIsAutoScaleSelected(true);
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
setIsAutoscale(true);
checkThroughputCap(AutoPilotUtils.minAutoPilotThroughput);
} else {
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoScaleSelected(false);
setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoscale(false);
checkThroughputCap(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
}
};

View File

@@ -6,6 +6,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setIsThroughputCapExceeded={[Function]}
setThroughputValue={[Function]}
showFreeTierExceedThroughputTooltip={true}
>

View File

@@ -1,24 +1,31 @@
import { Link } from "@fluentui/react/lib/Link";
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { IGalleryItem } from "Juno/JunoClient";
import * as ko from "knockout";
import React from "react";
import _ from "underscore";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointValidation";
import shallow from "zustand/shallow";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants";
import { ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger";
import { QueriesClient } from "../Common/QueriesClient";
import * as DataModels from "../Contracts/DataModels";
import { ContainerConnectionInfo } from "../Contracts/DataModels";
import {
ContainerConnectionInfo,
IPhoenixConnectionInfoResult,
IProvisionData,
IResponse,
} from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
import { useSidePanel } from "../hooks/useSidePanel";
import { useTabs } from "../hooks/useTabs";
import { IGalleryItem } from "../Juno/JunoClient";
import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
@@ -29,7 +36,6 @@ import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import {
get as getWorkspace,
listByDatabaseAccount,
listConnectionInfo,
start,
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { stringToBlob } from "../Utils/BlobUtils";
@@ -45,7 +51,7 @@ import * as FileSystemUtil from "./Notebook/FileSystemUtil";
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import type NotebookManager from "./Notebook/NotebookManager";
import type { NotebookPaneContent } from "./Notebook/NotebookManager";
import { NotebookPaneContent } from "./Notebook/NotebookManager";
import { NotebookUtil } from "./Notebook/NotebookUtil";
import { useNotebook } from "./Notebook/useNotebook";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
@@ -165,20 +171,23 @@ export default class Explorer {
);
useNotebook.subscribe(
async () => {
this.initiateAndRefreshNotebookList();
useNotebook.getState().setIsRefreshed(false);
},
(state) => state.isNotebookEnabled || state.isRefreshed
async () => this.initiateAndRefreshNotebookList(),
(state) => [state.isNotebookEnabled, state.isRefreshed],
shallow
);
this.resourceTree = new ResourceTreeAdapter(this);
// 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({
notebookServerEndpoint: userContext.features.notebookServerUrl,
authToken: userContext.features.notebookServerToken,
forwardingId: undefined,
});
}
@@ -186,19 +195,6 @@ export default class Explorer {
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();
}
@@ -352,35 +348,19 @@ export default class Explorer {
return;
}
this._isInitializingNotebooks = true;
if (userContext.features.phoenix === false) {
await this.ensureNotebookWorkspaceRunning();
const connectionInfo = await listConnectionInfo(
userContext.subscriptionId,
userContext.resourceGroup,
databaseAccount.name,
"default"
);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
});
}
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
public async allocateContainer(): Promise<void> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
const isAllocating = useNotebook.getState().isAllocating;
if (isAllocating === false && notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined) {
const provisionData = {
aadToken: userContext.authorizationToken,
subscriptionId: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
dbAccountName: userContext.databaseAccount.name,
if (
isAllocating === false &&
(notebookServerInfo === undefined ||
(notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
};
const connectionStatus: ContainerConnectionInfo = {
@@ -388,38 +368,64 @@ export default class Explorer {
};
useNotebook.getState().setConnectionInfo(connectionStatus);
try {
TelemetryProcessor.traceStart(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
});
useNotebook.getState().setIsAllocating(true);
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
if (
connectionInfo.status === HttpStatusCodes.OK &&
connectionInfo.data &&
connectionInfo.data.notebookServerUrl
) {
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
});
this.notebookManager?.notebookClient
.getMemoryUsage()
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
useNotebook.getState().setIsAllocating(false);
} else {
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetConatinerConnection(connectionStatus);
const connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
if (connectionInfo.status !== HttpStatusCodes.OK) {
throw new Error(`Received status code: ${connectionInfo?.status}`);
}
if (!connectionInfo?.data?.notebookServerUrl) {
throw new Error(`NotebookServerUrl is invalid!`);
}
await this.setNotebookInfo(connectionInfo, connectionStatus);
TelemetryProcessor.traceSuccess(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
});
} catch (error) {
TelemetryProcessor.traceFailure(Action.PhoenixConnection, {
dataExplorerArea: Areas.Notebook,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
connectionStatus.status = ConnectionStatusType.Failed;
useNotebook.getState().resetConatinerConnection(connectionStatus);
useNotebook.getState().resetContainerConnection(connectionStatus);
throw error;
} finally {
useNotebook.getState().setIsAllocating(false);
this.refreshCommandBarButtons();
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
}
private async setNotebookInfo(
connectionInfo: IResponse<IPhoenixConnectionInfoResult>,
connectionStatus: DataModels.ContainerConnectionInfo
) {
const containerData = {
forwardingId: connectionInfo.data.forwardingId,
dbAccountName: userContext.databaseAccount.name,
};
await this.phoenixClient.initiateContainerHeartBeat(containerData);
connectionStatus.status = ConnectionStatusType.Connected;
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint:
(validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
userContext.features.notebookServerUrl) ||
connectionInfo.data.notebookServerUrl,
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
forwardingId: connectionInfo.data.forwardingId,
});
this.notebookManager?.notebookClient
.getMemoryUsage()
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
}
public resetNotebookWorkspace(): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
handleError(
@@ -428,11 +434,14 @@ export default class Explorer {
);
return;
}
const dialogContent = useNotebook.getState().isPhoenixNotebooks
? "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?";
const resetConfirmationDialogProps: DialogProps = {
isModal: true,
title: "Reset Workspace",
subText: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?",
subText: dialogContent,
primaryButtonText: "OK",
secondaryButtonText: "Cancel",
onPrimaryButtonClick: this._resetNotebookWorkspace,
@@ -490,16 +499,54 @@ export default class Explorer {
private _resetNotebookWorkspace = async () => {
useDialog.getState().closeDialog();
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
let connectionStatus: ContainerConnectionInfo;
try {
await this.notebookManager?.notebookClient.resetWorkspace();
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
logConsoleError(error);
return;
}
TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
});
if (useNotebook.getState().isPhoenixNotebooks) {
useTabs.getState().closeAllNotebookTabs(true);
connectionStatus = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
}
const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace();
if (connectionInfo?.status !== HttpStatusCodes.OK) {
throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`);
}
if (!connectionInfo?.data?.notebookServerUrl) {
throw new Error(`Reset Workspace: NotebookServerUrl is invalid!`);
}
if (useNotebook.getState().isPhoenixNotebooks) {
await this.setNotebookInfo(connectionInfo, connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
logConsoleInfo("Successfully reset notebook workspace");
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
});
} catch (error) {
logConsoleError(`Failed to reset notebook workspace: ${error}`);
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, {
TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
if (useNotebook.getState().isPhoenixNotebooks) {
connectionStatus = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().resetContainerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
throw error;
} finally {
clearInProgressMessage();
@@ -691,8 +738,8 @@ export default class Explorer {
if (!notebookContentItem || !notebookContentItem.path) {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
}
if (notebookContentItem.type === NotebookContentItemType.Notebook && NotebookUtil.isPhoenixEnabled()) {
this.allocateContainer();
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) {
await this.allocateContainer();
}
const notebookTabs = useTabs
@@ -909,20 +956,17 @@ export default class Explorer {
/**
* 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) {
const error = "Attempt to create new notebook, but notebook is not enabled";
handleError(error, "Explorer/onNewNotebookClicked");
throw new Error(error);
}
const isPhoenixEnabled = NotebookUtil.isPhoenixEnabled();
if (isPhoenixEnabled) {
if (useNotebook.getState().isPhoenixNotebooks) {
if (isGithubTree) {
async () => {
await this.allocateContainer();
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
};
await this.allocateContainer();
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
} else {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookModalTitle,
@@ -1007,7 +1051,7 @@ export default class Explorer {
}
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
if (NotebookUtil.isPhoenixEnabled()) {
if (useNotebook.getState().isPhoenixFeatures) {
await this.allocateContainer();
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
@@ -1016,8 +1060,8 @@ export default class Explorer {
useDialog
.getState()
.showOkModalDialog(
"Failed to Connect",
"Failed to connect temporary workspace, this could happen because of network issue please refresh and try again."
"Failed to connect",
"Failed to connect to temporary workspace. This could happen because of network issues. Please refresh the page and try again."
);
}
} else {
@@ -1047,7 +1091,7 @@ export default class Explorer {
const terminalTabs: TerminalTab[] = useTabs
.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;
if (terminalTabs.length > 0) {
@@ -1119,7 +1163,10 @@ export default class Explorer {
<CassandraAddCollectionPane explorer={this} cassandraApiClient={new CassandraAPIDataClient()} />
);
} else {
await useDatabases.getState().loadDatabaseOffers();
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
throughputCap && throughputCap !== -1
? await useDatabases.getState().loadAllOffers()
: await useDatabases.getState().loadDatabaseOffers();
useSidePanel
.getState()
.openSidePanel("New " + getCollectionName(), <AddCollectionPanel explorer={this} databaseId={databaseId} />);
@@ -1145,10 +1192,9 @@ export default class Explorer {
}
public async handleOpenFileAction(path: string): Promise<void> {
if (
userContext.features.phoenix === false &&
!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))
) {
if (useNotebook.getState().isPhoenixNotebooks) {
await this.allocateContainer();
} else if (!(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
this._openSetupNotebooksPaneForQuickstart();
}
@@ -1180,7 +1226,7 @@ export default class Explorer {
}
public openUploadFilePanel(parent?: NotebookContentItem): void {
if (NotebookUtil.isPhoenixEnabled()) {
if (useNotebook.getState().isPhoenixNotebooks) {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookUploadModalTitle,
undefined,
@@ -1210,7 +1256,7 @@ export default class Explorer {
}
public getDownloadModalConent(fileName: string): JSX.Element {
if (NotebookUtil.isPhoenixEnabled()) {
if (useNotebook.getState().isPhoenixNotebooks) {
return (
<>
<p>{Notebook.galleryNotebookDownloadContent1}</p>
@@ -1232,28 +1278,20 @@ export default class Explorer {
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
let isNotebookEnabled = true;
if (!userContext.features.phoenix) {
isNotebookEnabled =
userContext.authType !== AuthType.ResourceToken &&
((await this._containsDefaultNotebookWorkspace(userContext.databaseAccount)) ||
userContext.features.enableNotebooks);
}
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
const isNotebookEnabled = userContext.features.notebooksDownBanner || useNotebook.getState().isPhoenixNotebooks;
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook.getState().setIsShellEnabled(isNotebookEnabled && isPublicInternetAccessAllowed());
useNotebook
.getState()
.setIsShellEnabled(useNotebook.getState().isPhoenixFeatures && isPublicInternetAccessAllowed());
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
isNotebookEnabled,
dataExplorerArea: Constants.Areas.Notebook,
});
if (!userContext.features.notebooksTemporarilyDown) {
if (isNotebookEnabled) {
await this.initNotebooks(userContext.databaseAccount);
} else if (this.notebookToImport) {
// if notebooks is not enabled but the user is trying to do a quickstart setup with notebooks, open the SetupNotebooksPane
this._openSetupNotebooksPaneForQuickstart();
}
if (useNotebook.getState().isPhoenixNotebooks) {
await this.initNotebooks(userContext.databaseAccount);
}
}
}

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import * as Q from "q";
import * as React from "react";
@@ -294,8 +296,6 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.setGremlinParams();
}
const selectedNode = this.state.highlightedNode;
props.onGraphAccessorCreated({
applyFilter: this.submitQuery.bind(this),
addVertex: this.addVertex.bind(this),
@@ -303,7 +303,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
});
} // constructor
public shareIGraphConfig(igraphConfig: IGraphConfig) {
public shareIGraphConfig(igraphConfig: IGraphConfig): void {
this.setState({
igraphConfig: { ...igraphConfig },
});
@@ -330,10 +330,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
const partitionKeyProperty = this.props.collectionPartitionKeyProperty;
// aggregate all the properties, remove dropped ones
let finalProperties = editedProperties.existingProperties.concat(editedProperties.addedProperties);
const finalProperties = editedProperties.existingProperties.concat(editedProperties.addedProperties);
// Compose the query
let pkId = editedProperties.pkId;
const pkId = editedProperties.pkId;
let updateQueryFragment = "";
finalProperties.forEach((p) => {
@@ -422,7 +422,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* Called from ko binding
* @param id
*/
public selectNode(id: string) {
public selectNode(id: string): void {
if (!this.d3ForceGraph) {
console.warn("Attempting to select node, but d3ForceGraph not initialized, yet.");
return;
@@ -431,7 +431,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.d3ForceGraph.selectNode(id);
}
public deleteHighlightedNode() {
public deleteHighlightedNode(): void {
if (!this.state.highlightedNode) {
GraphExplorer.reportToConsole(ConsoleDataType.Error, "No highlighted node to remove.");
return;
@@ -467,23 +467,23 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* Is of type: {e: GremlinEdge, v: GremlinVertex}[]
* @param data
*/
public static isEdgeVertexPairArray(data: any) {
public static isEdgeVertexPairArray(data: any): boolean {
if (!(data instanceof Array)) {
GraphExplorer.reportToConsole(ConsoleDataType.Info, "Query result not an array", data);
return false;
}
let pairs: any[] = data;
const pairs: any[] = data;
for (let i = 0; i < pairs.length; i++) {
const item = pairs[i];
if (
!item.hasOwnProperty("e") ||
!item.hasOwnProperty("v") ||
!item["e"].hasOwnProperty("id") ||
!item["e"].hasOwnProperty("type") ||
!Object.prototype.hasOwnProperty.call(item, "e") ||
!Object.prototype.hasOwnProperty.call(item, "v") ||
!Object.prototype.hasOwnProperty.call(item["e"], "id") ||
!Object.prototype.hasOwnProperty.call(item["e"], "type") ||
item["e"].type !== "edge" ||
!item["v"].hasOwnProperty("id") ||
!item["v"].hasOwnProperty("type") ||
!Object.prototype.hasOwnProperty.call(item["v"], "id") ||
!Object.prototype.hasOwnProperty.call(item["e"], "type") ||
item["v"].type !== "vertex"
) {
return false;
@@ -514,7 +514,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// Try hitting cache first
const cache = outE ? this.outECache : this.inECache;
const pairs = cache.retrieve(vertex.id, startIndex, pageSize);
if (pairs != null && pairs.length === pageSize) {
if (pairs !== null && pairs.length === pageSize) {
const msg = `Retrieved ${pairs.length} ${outE ? "outE" : "inE"} edges from cache for vertex id: ${vertex.id}`;
GraphExplorer.reportToConsole(ConsoleDataType.Info, msg);
return Q.resolve(pairs);
@@ -588,7 +588,6 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
vertex._outEAllLoaded &&
vertex._inEAllLoaded
) {
console.info("No more edges to load for vertex " + vertex.id);
updateGraphData();
return Q.resolve(graphData);
}
@@ -668,7 +667,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}
);
return promise.then((nbPairsFetched: number) => {
return promise.then(() => {
if (offsetIndex >= GraphExplorer.LOAD_PAGE_SIZE || !vertex._outEAllLoaded || !vertex._inEAllLoaded) {
vertex._pagination = {
total:
@@ -754,7 +753,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* Create a new edge in docdb and update graph
* @param e
*/
public createNewEdge(e: GraphNewEdgeData): Q.Promise<any> {
public createNewEdge(e: GraphNewEdgeData): Q.Promise<unknown> {
const q = `g.V('${GraphUtil.escapeSingleQuotes(e.inputOutV)}').addE('${GraphUtil.escapeSingleQuotes(
e.label
)}').To(g.V('${GraphUtil.escapeSingleQuotes(e.inputInV)}'))`;
@@ -772,8 +771,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return;
}
let edge = edges[0];
let graphData = this.originalGraphData;
const edge = edges[0];
const graphData = this.originalGraphData;
graphData.addEdge(edge);
// Allow loadNeighbors to load list new edge
@@ -800,10 +799,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* Manually update in-memory graph.
* @param edgeId
*/
public removeEdge(edgeId: string): Q.Promise<any> {
public removeEdge(edgeId: string): Q.Promise<unknown> {
return this.submitToBackend(`g.E('${GraphUtil.escapeSingleQuotes(edgeId)}').drop()`).then(
() => {
let graphData = this.originalGraphData;
const graphData = this.originalGraphData;
graphData.removeEdge(edgeId, false);
this.updateGraphData(graphData, this.state.igraphConfig);
},
@@ -826,10 +825,14 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return false;
}
let vertices: any[] = data;
const vertices: any[] = data;
if (vertices.length > 0) {
let v0 = vertices[0];
if (!v0.hasOwnProperty("id") || !v0.hasOwnProperty("type") || v0.type !== "vertex") {
const v0 = vertices[0];
if (
!Object.prototype.hasOwnProperty.call(v0, "id") ||
!Object.prototype.hasOwnProperty.call(v0, "type") ||
v0.type !== "vertex"
) {
return false;
}
}
@@ -837,7 +840,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}
public processGremlinQueryResults(result: GremlinClient.GremlinRequestResult): void {
const data = result.data as any;
const data = result.data as GraphData.GremlinVertex[];
this.setFilterQueryStatus(FilterQueryStatus.GraphEmptyResult);
if (data === null) {
@@ -927,13 +930,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
throw { title: err };
}
if (vertices == null || vertices.length < 1) {
if (vertices === null || vertices.length < 1) {
const err = "Failed to create vertex (no vertex in response)";
GraphExplorer.reportToConsole(ConsoleDataType.Error, err, vertices);
throw { title: err };
}
let vertex = vertices[0];
const vertex = vertices[0];
const graphData = this.originalGraphData;
graphData.addVertex(vertex);
this.updateGraphData(graphData, this.state.igraphConfig);
@@ -1022,7 +1025,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.gremlinClient.destroy();
}
public componentDidMount(): void {
if (this.props.onLoadStartKey != null && this.props.onLoadStartKey != undefined) {
if (this.props.onLoadStartKey !== null && this.props.onLoadStartKey !== undefined) {
TelemetryProcessor.traceSuccess(
Action.Tab,
{
@@ -1082,7 +1085,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void;
public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) {
let errorDataStr: string = "";
let errorDataStr = "";
if (errorData && errorData.length > 0) {
console.error(msg, errorData);
errorDataStr = ": " + JSON.stringify(errorData);
@@ -1161,12 +1164,15 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
)}"`
).then(
(documents: DataModels.DocumentId[]) => {
$.each(documents, (index: number, doc: any) => {
newIconsMap[doc["_graph_icon_property_value"]] = {
data: doc["icon"],
format: doc["format"],
};
});
$.each(
documents,
(index: number, doc: { _graph_icon_property_value: string; icon: string; format: string }) => {
newIconsMap[doc["_graph_icon_property_value"]] = {
data: doc["icon"],
format: doc["format"],
};
}
);
// Update graph configuration
this.setState({
@@ -1223,8 +1229,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
const key = this.state.igraphConfig.nodeCaption;
return $.map(
this.state.rootMap,
(value: any, index: number): LeftPane.CaptionId => {
let result = GraphData.GraphData.getNodePropValue(value, key);
(value: any): LeftPane.CaptionId => {
const result = GraphData.GraphData.getNodePropValue(value, key);
return {
caption: result !== undefined ? result : value.id,
id: value.id,
@@ -1237,7 +1243,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* Selecting a root node means
* @param node
*/
private selectRootNode(id: string): Q.Promise<any> {
private selectRootNode(id: string): Q.Promise<unknown> {
if (!this.d3ForceGraph) {
console.warn("Attempting to reset zoom, but d3ForceGraph not initialized, yet.");
} else {
@@ -1282,7 +1288,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.collectNodeProperties(this.originalGraphData.vertices);
this.updatePropertiesPane(id);
},
(reason: any) => {
(reason: string) => {
GraphExplorer.reportToConsole(ConsoleDataType.Error, `Failed to select root node. Reason:${reason}`);
}
);
@@ -1349,10 +1355,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
private getPkIdFromVertex(v: GraphData.GremlinVertex): string {
if (
this.props.collectionPartitionKeyProperty &&
v.hasOwnProperty("properties") &&
v.properties.hasOwnProperty(this.props.collectionPartitionKeyProperty) &&
Object.prototype.hasOwnProperty.call(v, "properties") &&
Object.prototype.hasOwnProperty.call(v.properties, this.props.collectionPartitionKeyProperty) &&
v.properties[this.props.collectionPartitionKeyProperty].length > 0 &&
v.properties[this.props.collectionPartitionKeyProperty][0].hasOwnProperty("value")
Object.prototype.hasOwnProperty.call(v.properties[this.props.collectionPartitionKeyProperty][0], "value")
) {
const pk = v.properties[this.props.collectionPartitionKeyProperty][0].value;
return GraphExplorer.generatePkIdPair(pk, v.id);
@@ -1370,8 +1376,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
private getPkIdFromNodeData(v: GraphHighlightedNodeData): string {
if (
this.props.collectionPartitionKeyProperty &&
v.hasOwnProperty("properties") &&
v.properties.hasOwnProperty(this.props.collectionPartitionKeyProperty)
Object.prototype.hasOwnProperty.call(v, "properties") &&
Object.prototype.hasOwnProperty.call(v.properties, this.props.collectionPartitionKeyProperty)
) {
const pk = v.properties[this.props.collectionPartitionKeyProperty];
return GraphExplorer.generatePkIdPair(pk[0] as PartitionKeyValueType, v.id);
@@ -1388,14 +1394,14 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @return id
*/
public static getPkIdFromDocumentId(d: DataModels.DocumentId, collectionPartitionKeyProperty: string): string {
let { id } = d;
const { id } = d;
if (typeof id !== "string") {
const error = `Vertex id is not a string: ${JSON.stringify(id)}.`;
logConsoleError(error);
throw new Error(error);
}
if (collectionPartitionKeyProperty && d.hasOwnProperty(collectionPartitionKeyProperty)) {
if (collectionPartitionKeyProperty && Object.prototype.hasOwnProperty.call(d, collectionPartitionKeyProperty)) {
let pk = (d as any)[collectionPartitionKeyProperty];
if (typeof pk !== "string" && typeof pk !== "number" && typeof pk !== "boolean") {
if (Array.isArray(pk) && pk.length > 0) {
@@ -1425,7 +1431,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`;
return this.executeNonPagedDocDbQuery(q).then(
(documents: DataModels.DocumentId[]) => {
let possibleVertices = [] as PossibleVertex[];
const possibleVertices = [] as PossibleVertex[];
$.each(documents, (index: number, item: any) => {
if (highlightedNodeId && item.id === highlightedNodeId) {
// Exclude highlighed node in the list
@@ -1439,7 +1445,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
caption: item.p,
});
} else {
if (item.hasOwnProperty("p")) {
if (Object.prototype.hasOwnProperty.call(item, "p")) {
possibleVertices.push({
value: item.id,
caption: item.p[0]["_value"],
@@ -1462,17 +1468,17 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @param addedEdges
* @return promise when done
*/
private editGraphEdges(editedEdges: EditedEdges): Q.Promise<any> {
let promises = [];
private editGraphEdges(editedEdges: EditedEdges): Q.Promise<unknown> {
const promises = [];
// Drop edges
for (let i = 0; i < editedEdges.droppedIds.length; i++) {
let id = editedEdges.droppedIds[i];
const id = editedEdges.droppedIds[i];
promises.push(this.removeEdge(id));
}
// Add edges
for (let i = 0; i < editedEdges.addedEdges.length; i++) {
let e = editedEdges.addedEdges[i];
const e = editedEdges.addedEdges[i];
promises.push(
this.createNewEdge(e).then(() => {
// Reload neighbors in case we linked to a vertex that isn't loaded in the graph
@@ -1525,7 +1531,9 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
/**
* For unit testing purposes
*/
public onGraphUpdated(timestamp: number): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public onGraphUpdated(_timestamp: number): void {}
/**
* Get node properties for styling purposes. Result is the union of all properties of all nodes.
@@ -1533,17 +1541,17 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
private collectNodeProperties(vertices: GraphData.GremlinVertex[]) {
const props = {} as any; // Hashset
$.each(vertices, (index: number, item: GraphData.GremlinVertex) => {
for (var p in item) {
for (const p in item) {
// DocDB: Exclude type because it's always 'vertex'
if (p !== "type" && typeof (item as any)[p] === "string") {
props[p] = true;
}
}
// Inspect properties
if (item.hasOwnProperty("properties")) {
if (Object.prototype.hasOwnProperty.call(item, "properties")) {
// TODO This is DocDB-graph specific
// Assume each property value is [{value:... }]
for (var f in item.properties) {
for (const f in item.properties) {
props[f] = true;
}
}
@@ -1570,21 +1578,21 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return;
}
let data = this.originalGraphData.getVertexById(id);
const data = this.originalGraphData.getVertexById(id);
// A bit of translation to make it easier to display
let props: { [id: string]: ViewModels.GremlinPropertyValueType[] } = {};
for (let p in data.properties) {
const props: { [id: string]: ViewModels.GremlinPropertyValueType[] } = {};
for (const p in data.properties) {
props[p] = data.properties[p].map((gremlinProperty) => gremlinProperty.value);
}
// update neighbors
let sources: NeighborVertexBasicInfo[] = [];
let targets: NeighborVertexBasicInfo[] = [];
const sources: NeighborVertexBasicInfo[] = [];
const targets: NeighborVertexBasicInfo[] = [];
this.props.onResetDefaultGraphConfigValues();
let nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
const nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets);
let sData: GraphHighlightedNodeData = {
const sData: GraphHighlightedNodeData = {
id: data.id,
label: data.label,
properties: props,
@@ -1611,16 +1619,16 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
targets: NeighborVertexBasicInfo[]
): void {
// update neighbors
let gd = this.originalGraphData;
let v = gd.getVertexById(id);
const gd = this.originalGraphData;
const v = gd.getVertexById(id);
// Clear the array while keeping the references
sources.length = 0;
targets.length = 0;
let possibleEdgeLabels = {} as any; // Collect all edge labels in a hashset
const possibleEdgeLabels = {} as any; // Collect all edge labels in a hashset
for (let p in v.inE) {
for (const p in v.inE) {
possibleEdgeLabels[p] = true;
const edges = v.inE[p];
$.each(edges, (index: number, edge: GraphData.GremlinShortInEdge) => {
@@ -1629,7 +1637,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet
return;
}
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
const caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
sources.push({
name: caption,
id: neighborId,
@@ -1639,7 +1647,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
});
}
for (let p in v.outE) {
for (const p in v.outE) {
possibleEdgeLabels[p] = true;
const edges = v.outE[p];
$.each(edges, (index: number, edge: GraphData.GremlinShortOutEdge) => {
@@ -1648,7 +1656,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet
return;
}
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
const caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
targets.push({
name: caption,
id: neighborId,
@@ -1660,7 +1668,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.setState({
possibleEdgeLabels: Object.keys(possibleEdgeLabels).map(
(value: string, index: number, array: string[]): InputTypeaheadComponent.Item => {
(value: string): InputTypeaheadComponent.Item => {
return { caption: value, value: value };
}
),
@@ -1681,20 +1689,20 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return;
}
let updatedVertex = vertices[0];
const updatedVertex = vertices[0];
if (this.originalGraphData.hasVertexId(updatedVertex.id)) {
let currentVertex = this.originalGraphData.getVertexById(updatedVertex.id);
const currentVertex = this.originalGraphData.getVertexById(updatedVertex.id);
// Copy updated properties
if (currentVertex.hasOwnProperty("properties")) {
if (Object.prototype.hasOwnProperty.call(currentVertex, "properties")) {
delete currentVertex["properties"];
}
for (var p in updatedVertex) {
for (const p in updatedVertex) {
(currentVertex as any)[p] = updatedVertex[p];
}
}
// TODO This kind of assumes saveVertexProperty is done from property panes.
let hn = this.state.highlightedNode;
const hn = this.state.highlightedNode;
if (hn && hn.id === updatedVertex.id) {
this.updatePropertiesPane(hn.id);
}
@@ -1708,7 +1716,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
igraphConfig?: IGraphConfig
) {
this.originalGraphData = graphData;
let gd = JSON.parse(JSON.stringify(this.originalGraphData));
const gd = JSON.parse(JSON.stringify(this.originalGraphData));
if (!this.d3ForceGraph) {
console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet.");
return;
@@ -1873,7 +1881,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
promise
.then((result: GremlinClient.GremlinRequestResult) => this.processGremlinQueryResults(result))
.catch((error: any) => {
.catch((error: Error) => {
const errorMsg = `Failed to process query result: ${getErrorMessage(error)}`;
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
this.setState({

View File

@@ -58,7 +58,7 @@ export class LeftPaneComponent extends React.Component<LeftPaneComponentProps> {
className={className}
as="tr"
aria-label={node.caption}
onActivated={(e) => this.props.onRootNodeSelected(node.id)}
onActivated={() => this.props.onRootNodeSelected(node.id)}
key={node.id}
>
<td className="resultItem">

View File

@@ -1,8 +1,8 @@
import React from "react";
import { mount, ReactWrapper } from "enzyme";
import * as Q from "q";
import { NodePropertiesComponent, NodePropertiesComponentProps, Mode } from "./NodePropertiesComponent";
import { GraphHighlightedNodeData, EditedProperties, EditedEdges, PossibleVertex } from "./GraphExplorer";
import React from "react";
import { GraphHighlightedNodeData, PossibleVertex } from "./GraphExplorer";
import { Mode, NodePropertiesComponent, NodePropertiesComponentProps } from "./NodePropertiesComponent";
describe("Property pane", () => {
const title = "My Title";
@@ -37,17 +37,18 @@ describe("Property pane", () => {
return {
expandedTitle: title,
isCollapsed: false,
onCollapsedChanged: (newValue: boolean): void => {},
onCollapsedChanged: jest.fn(),
node: highlightedNode,
getPkIdFromNodeData: (v: GraphHighlightedNodeData): string => null,
collectionPartitionKeyProperty: null,
updateVertexProperties: (editedProperties: EditedProperties): Q.Promise<void> => Q.resolve(),
selectNode: (id: string): void => {},
updatePossibleVertices: (): Q.Promise<PossibleVertex[]> => Q.resolve(null),
possibleEdgeLabels: null,
editGraphEdges: (editedEdges: EditedEdges): Q.Promise<any> => Q.resolve(),
deleteHighlightedNode: (): void => {},
onModeChanged: (newMode: Mode): void => {},
getPkIdFromNodeData: (): string => undefined,
collectionPartitionKeyProperty: undefined,
updateVertexProperties: (): Q.Promise<void> => Q.resolve(),
selectNode: jest.fn(),
updatePossibleVertices: (): Q.Promise<PossibleVertex[]> => Q.resolve(undefined),
possibleEdgeLabels: undefined,
//eslint-disable-next-line
editGraphEdges: (): Q.Promise<any> => Q.resolve(),
deleteHighlightedNode: jest.fn(),
onModeChanged: jest.fn(),
viewMode: Mode.READONLY_PROP,
};
};

View File

@@ -72,7 +72,7 @@ export class NodePropertiesComponent extends React.Component<
super(props);
this.state = {
editedProperties: {
pkId: null,
pkId: undefined,
readOnlyProperties: [],
existingProperties: [],
addedProperties: [],
@@ -98,15 +98,12 @@ export class NodePropertiesComponent extends React.Component<
};
}
public static getDerivedStateFromProps(
props: NodePropertiesComponentProps,
state: NodePropertiesComponentState
): Partial<NodePropertiesComponentState> {
public static getDerivedStateFromProps(props: NodePropertiesComponentProps): Partial<NodePropertiesComponentState> {
if (props.viewMode !== Mode.READONLY_PROP) {
return { isDeleteConfirm: false };
}
return null;
return undefined;
}
public render(): JSX.Element {
@@ -138,10 +135,10 @@ export class NodePropertiesComponent extends React.Component<
* @param value
*/
private static getTypeOption(value: any): ViewModels.InputPropertyValueTypeString {
if (value == null) {
if (value === undefined) {
return "null";
}
let type = typeof value;
const type = typeof value;
switch (type) {
case "number":
case "boolean":
@@ -172,10 +169,9 @@ export class NodePropertiesComponent extends React.Component<
];
const existingProps: ViewModels.InputProperty[] = [];
if (this.props.node.hasOwnProperty("properties")) {
const hProps = this.props.node["properties"];
for (let p in hProps) {
for (const p in hProps) {
const propValues = hProps[p];
(p === partitionKeyProperty ? readOnlyProps : existingProps).push({
key: p,
@@ -437,7 +433,7 @@ export class NodePropertiesComponent extends React.Component<
</div>
);
} else {
return null;
return undefined;
}
}

View File

@@ -4,15 +4,12 @@
* and update any knockout observables passed from the parent.
*/
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import * as React from "react";
import create, { UseStore } from "zustand";
import { StyleConstants } from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import { userContext } from "../../../UserContext";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { NotebookUtil } from "../../Notebook/NotebookUtil";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
import * as CommandBarUtil from "./CommandBarUtil";
@@ -56,18 +53,10 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (NotebookUtil.isPhoenixEnabled()) {
if (useNotebook.getState().isPhoenixNotebooks || useNotebook.getState().isPhoenixFeatures) {
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
}
if (
userContext.features.phoenix === false &&
userContext.features.notebooksTemporarilyDown === false &&
useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2
) {
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
}
return (
<div className="commandBarContainer">
<FluentCommandBar

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 enableAzureSynapseLinkBtn = buttons.find(
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
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", () => {

View File

@@ -25,7 +25,6 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
import { JunoClient } from "../../../Juno/JunoClient";
import { userContext } from "../../../UserContext";
import { getCollectionName, getDatabaseName } from "../../../Utils/APITypeUtils";
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
@@ -78,9 +77,10 @@ export function createStaticCommandBarButtons(
if (container.notebookManager?.gitHubOAuthService) {
notebookButtons.push(createManageGitHubAccountButton(container));
}
notebookButtons.push(createOpenTerminalButton(container));
if (userContext.features.phoenix === false) {
if (useNotebook.getState().isPhoenixFeatures && configContext.isTerminalEnabled) {
notebookButtons.push(createOpenTerminalButton(container));
}
if (useNotebook.getState().isPhoenixNotebooks && selectedNodeState.isConnectedToContainer()) {
notebookButtons.push(createNotebookWorkspaceResetButton(container));
}
if (
@@ -98,19 +98,21 @@ export function createStaticCommandBarButtons(
}
notebookButtons.forEach((btn) => {
if (userContext.features.notebooksTemporarilyDown) {
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
if (btn.commandButtonLabel.indexOf("Cassandra") !== -1) {
if (!useNotebook.getState().isPhoenixFeatures) {
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);
});
} else {
if (!isRunningOnNationalCloud() && !userContext.features.notebooksTemporarilyDown) {
if (!isRunningOnNationalCloud() && useNotebook.getState().isPhoenixNotebooks) {
buttons.push(createDivider());
buttons.push(createEnableNotebooksButton(container));
}
@@ -168,9 +170,7 @@ export function createContextCommandBarButtons(
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled) {
if (!userContext.features.notebooksTemporarilyDown) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
}
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
@@ -178,13 +178,6 @@ export function createContextCommandBarButtons(
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
tooltipText:
useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown
? Constants.Notebook.mongoShellTemporarilyDownMsg
: undefined,
disabled:
(selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") ||
(useNotebook.getState().isShellEnabled && userContext.features.notebooksTemporarilyDown),
};
buttons.push(newMongoShellBtn);
}
@@ -280,10 +273,6 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
return undefined;
}
if (isServerlessAccount()) {
return undefined;
}
if (userContext?.databaseAccount?.properties?.enableAnalyticalStorage) {
return undefined;
}
@@ -310,8 +299,13 @@ function createNewDatabase(container: Explorer): CommandButtonComponentProps {
return {
iconSrc: AddDatabaseIcon,
iconAlt: label,
onCommandClick: () =>
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />),
onCommandClick: async () => {
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
await useDatabases.getState().loadAllOffers();
}
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />);
},
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,

View File

@@ -1,8 +1,20 @@
import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react";
import { ActionButton } from "@fluentui/react/lib/Button";
import {
FocusTrapCallout,
FocusZone,
FocusZoneTabbableElements,
FontWeights,
Icon,
mergeStyleSets,
ProgressIndicator,
Stack,
Text,
TooltipHost,
} from "@fluentui/react";
import { useId } from "@fluentui/react-hooks";
import { ActionButton, DefaultButton } from "@fluentui/react/lib/Button";
import * as React from "react";
import "../../../../less/hostedexplorer.less";
import { ConnectionStatusType, Notebook } from "../../../Common/Constants";
import { ConnectionStatusType, ContainerStatusType, Notebook } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import "../CommandBar/ConnectionStatusComponent.less";
@@ -10,12 +22,33 @@ interface Props {
container: Explorer;
}
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
const connectionInfo = useNotebook((state) => state.connectionInfo);
const [second, setSecond] = React.useState("00");
const [minute, setMinute] = React.useState("00");
const [isActive, setIsActive] = React.useState(false);
const [counter, setCounter] = React.useState(0);
const [statusColor, setStatusColor] = React.useState("");
const [toolTipContent, setToolTipContent] = React.useState("Connect to temporary workspace.");
const [isBarDismissed, setIsBarDismissed] = React.useState<boolean>(false);
const buttonId = useId("callout-button");
const containerInfo = useNotebook((state) => state.containerStatus);
const styles = mergeStyleSets({
callout: {
width: 320,
padding: "20px 24px",
},
title: {
marginBottom: 12,
fontWeight: FontWeights.semilight,
},
buttons: {
display: "flex",
justifyContent: "flex-end",
marginTop: 20,
},
});
React.useEffect(() => {
let intervalId: NodeJS.Timeout;
@@ -35,6 +68,15 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
return () => clearInterval(intervalId);
}, [isActive, counter]);
React.useEffect(() => {
if (connectionInfo?.status === ConnectionStatusType.Reconnect) {
setToolTipContent("Click here to Reconnect to temporary workspace.");
} else if (connectionInfo?.status === ConnectionStatusType.Failed) {
setStatusColor("status failed is-animating");
setToolTipContent("Click here to Reconnect to temporary workspace.");
}
}, [connectionInfo.status]);
const stopTimer = () => {
setIsActive(false);
setCounter(0);
@@ -42,15 +84,13 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
setMinute("00");
};
const connectionInfo = useNotebook((state) => state.connectionInfo);
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0;
const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0;
if (
connectionInfo &&
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.ReConnect)
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect)
) {
return (
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
@@ -65,6 +105,7 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
}
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) {
stopTimer();
setIsActive(true);
setStatusColor("status connecting is-animating");
setToolTipContent("Connecting to temporary workspace.");
@@ -78,30 +119,68 @@ export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Ele
setToolTipContent("Click here to Reconnect to temporary workspace.");
}
return (
<ActionButton
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
}
>
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<i className={statusColor}></i>
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
{connectionInfo.status}
</span>
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
<ProgressIndicator description={minute + ":" + second} />
)}
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
<ProgressIndicator
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={usedGB / totalGB}
/>
)}
</Stack>
<>
<TooltipHost
content={
containerInfo?.status === ContainerStatusType.Active
? `Connected to temporary workspace. This temporary workspace will get disconnected in ${Math.round(
containerInfo.durationLeftInMinutes
)} minutes.`
: toolTipContent
}
>
<ActionButton
id={buttonId}
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
}
>
<Stack className="connectionStatusContainer" horizontal>
<i className={statusColor}></i>
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
{connectionInfo.status}
</span>
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
<ProgressIndicator description={minute + ":" + second} />
)}
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
<ProgressIndicator
className={totalGB !== 0 && usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={totalGB !== 0 ? usedGB / totalGB : 0}
/>
)}
</Stack>
{!isBarDismissed &&
containerInfo.status &&
containerInfo.status === ContainerStatusType.Active &&
Math.round(containerInfo.durationLeftInMinutes) <= Notebook.remainingTimeForAlert ? (
<FocusTrapCallout
role="alertdialog"
className={styles.callout}
gapSpace={0}
target={`#${buttonId}`}
onDismiss={() => setIsBarDismissed(true)}
setInitialFocus
>
<Text block variant="xLarge" className={styles.title}>
Remaining Time
</Text>
<Text block variant="small">
This temporary workspace will get disconnected in {Math.round(containerInfo.durationLeftInMinutes)}{" "}
minutes. To save your work permanently, save your notebooks to a GitHub repository or download the
notebooks to your local machine before the session ends.
</Text>
<FocusZone handleTabKey={FocusZoneTabbableElements.all} isCircularNavigation>
<Stack className={styles.buttons} gap={8} horizontal>
<DefaultButton onClick={() => setIsBarDismissed(true)}>Dimiss</DefaultButton>
</Stack>
</FocusZone>
</FocusTrapCallout>
) : undefined}
</ActionButton>
</TooltipHost>
</ActionButton>
</>
);
};

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Link } from "@fluentui/react";
import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable";
// Vendor modules
import {
@@ -13,13 +15,15 @@ import "@nteract/styles/editor-overrides.css";
import "@nteract/styles/global-variables.css";
import "codemirror/addon/hint/show-hint.css";
import "codemirror/lib/codemirror.css";
import { Notebook } from "Common/Constants";
import { useDialog } from "Explorer/Controls/Dialog";
import * as Immutable from "immutable";
import * as React from "react";
import { Provider } from "react-redux";
import "react-table/react-table.css";
import { AnyAction, Store } from "redux";
import { NotebookClientV2 } from "../NotebookClientV2";
import { NotebookUtil } from "../NotebookUtil";
import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
import * as NteractUtil from "../NTeractUtil";
import * as CdbActions from "./actions";
import { NotebookComponent } from "./NotebookComponent";
@@ -30,6 +34,19 @@ export interface NotebookComponentBootstrapperOptions {
contentRef: ContentRef;
}
interface IWrapModel {
name: string;
path: string;
last_modified: Date;
created: string;
content: unknown;
format: string;
mimetype: unknown;
size: number;
writeable: boolean;
type: string;
}
export class NotebookComponentBootstrapper {
public contentRef: ContentRef;
protected renderExtraComponent: () => JSX.Element;
@@ -41,7 +58,7 @@ export class NotebookComponentBootstrapper {
this.contentRef = options.contentRef;
}
protected static wrapModelIntoContent(name: string, path: string, content: any) {
protected static wrapModelIntoContent(name: string, path: string, content: unknown): IWrapModel {
return {
name,
path,
@@ -49,7 +66,7 @@ export class NotebookComponentBootstrapper {
created: "",
content,
format: "json",
mimetype: null as any,
mimetype: undefined,
size: 0,
writeable: false,
type: "notebook",
@@ -85,7 +102,11 @@ export class NotebookComponentBootstrapper {
};
}
public setContent(name: string, content: any): void {
public getNotebookPath(): string {
return this.getStore().getState().core.entities.contents.byRef.get(this.contentRef)?.filepath;
}
public setContent(name: string, content: unknown): void {
this.getStore().dispatch(
actions.fetchContentFulfilled({
filepath: undefined,
@@ -116,11 +137,32 @@ export class NotebookComponentBootstrapper {
/* Notebook operations. See nteract/packages/connected-components/src/notebook-menu/index.tsx */
public notebookSave(): void {
this.getStore().dispatch(
actions.save({
contentRef: this.contentRef,
})
);
if (
NotebookUtil.getContentProviderType(this.getNotebookPath()) ===
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 {
@@ -270,7 +312,6 @@ export class NotebookComponentBootstrapper {
public isContentDirty(): boolean {
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
if (!content) {
console.log("No error");
return false;
}
@@ -328,4 +369,19 @@ export class NotebookComponentBootstrapper {
protected getStore(): Store<AppState, AnyAction> {
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,
} from "@nteract/core";
import { Channels, childOf, createMessage, JupyterMessage, message, ofMessageType } from "@nteract/messaging";
import { defineConfigOption } from "@nteract/mythic-configuration";
import { RecordOf } from "immutable";
import { AnyAction } from "redux";
import { Action, AnyAction } from "redux";
import { ofType, StateObservable } from "redux-observable";
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 {
catchError,
concatMap,
@@ -41,7 +42,7 @@ import { logConsoleError, logConsoleInfo } from "../../../Utils/NotificationCons
import { useDialog } from "../../Controls/Dialog";
import * as FileSystemUtil from "../FileSystemUtil";
import * as cdbActions from "../NotebookComponent/actions";
import { NotebookUtil } from "../NotebookUtil";
import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
import * as CdbActions from "./actions";
import * as TextFile from "./contents/file/text-file";
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 = [
addInitialCodeCellEpic,
focusInitialCodeCellEpic,
@@ -965,4 +1014,5 @@ export const allEpics = [
traceNotebookInfoEpic,
traceNotebookKernelEpic,
resetCellStatusOnExecuteCanceledEpic,
autoSaveCurrentContentEpic,
];

View File

@@ -1,12 +1,12 @@
import { AppState, epics as coreEpics, reducers, IContentProvider } 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 { AppState, epics as coreEpics, IContentProvider, reducers } from "@nteract/core";
import { configuration } from "@nteract/mythic-configuration";
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";
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
// out how to safely filter out the ones we are overriding here.
const filteredCoreEpics = [
coreEpics.autoSaveCurrentContentEpic,
coreEpics.executeCellEpic,
coreEpics.executeFocusedCellEpic,
coreEpics.executeCellAfterKernelLaunchEpic,

View File

@@ -1,23 +1,32 @@
/**
* Notebook container related stuff
*/
import promiseRetry, { AbortError } from "p-retry";
import { PhoenixClient } from "Phoenix/PhoenixClient";
import * as Constants from "../../Common/Constants";
import { ConnectionStatusType } from "../../Common/Constants";
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook } from "../../Common/Constants";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels";
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
import { IPhoenixConnectionInfoResult, IProvisionData, IResponse } from "../../Contracts/DataModels";
import { userContext } from "../../UserContext";
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { NotebookUtil } from "./NotebookUtil";
import { useNotebook } from "./useNotebook";
export class NotebookContainerClient {
private clearReconnectionAttemptMessage? = () => {};
private isResettingWorkspace: boolean;
private phoenixClient: PhoenixClient;
private retryOptions: promiseRetry.Options;
constructor(private onConnectionLost: () => void) {
this.phoenixClient = new PhoenixClient();
this.retryOptions = {
retries: Notebook.retryAttempts,
maxTimeout: Notebook.retryAttemptDelayMs,
minTimeout: Notebook.retryAttemptDelayMs,
};
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
@@ -38,10 +47,13 @@ export class NotebookContainerClient {
* Heartbeat: each ping schedules another ping
*/
private scheduleHeartbeat(delayMs: number): void {
setTimeout(() => {
this.getMemoryUsage()
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo))
.finally(() => this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs));
setTimeout(async () => {
const memoryUsageInfo = await this.getMemoryUsage();
useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo?.notebookServerEndpoint) {
this.scheduleHeartbeat(Constants.Notebook.heartbeatDelayMs);
}
}, delayMs);
}
@@ -59,6 +71,27 @@ export class NotebookContainerClient {
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
try {
const runMemoryAsync = async () => {
return await this._getMemoryAsync(notebookServerEndpoint, authToken);
};
return await promiseRetry(runMemoryAsync, this.retryOptions);
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage");
if (!this.clearReconnectionAttemptMessage) {
this.clearReconnectionAttemptMessage = logConsoleProgress(
"Connection lost with Notebook server. Attempting to reconnect..."
);
}
this.onConnectionLost();
return undefined;
}
}
private async _getMemoryAsync(
notebookServerEndpoint: string,
authToken: string
): Promise<DataModels.MemoryUsageInfo> {
if (this.shouldExecuteMemoryCall()) {
const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, {
method: "GET",
headers: {
@@ -78,44 +111,36 @@ export class NotebookContainerClient {
freeKB: memoryUsageInfo.free,
};
}
} else if (NotebookUtil.isPhoenixEnabled()) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.ReConnect,
};
useNotebook.getState().resetConatinerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(true);
} else if (response.status === HttpStatusCodes.NotFound) {
throw new AbortError(response.statusText);
}
return undefined;
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage");
if (!this.clearReconnectionAttemptMessage) {
this.clearReconnectionAttemptMessage = logConsoleProgress(
"Connection lost with Notebook server. Attempting to reconnect..."
);
}
if (NotebookUtil.isPhoenixEnabled()) {
const connectionStatus: ContainerConnectionInfo = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().resetConatinerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(true);
}
this.onConnectionLost();
throw new Error(response.statusText);
} else {
return undefined;
}
}
public async resetWorkspace(): Promise<void> {
private shouldExecuteMemoryCall(): boolean {
return (
useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Active &&
useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected
);
}
public async resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> {
this.isResettingWorkspace = true;
let response: IResponse<IPhoenixConnectionInfoResult>;
try {
await this._resetWorkspace();
response = await this._resetWorkspace();
} catch (error) {
Promise.reject(error);
return response;
}
this.isResettingWorkspace = false;
return response;
}
private async _resetWorkspace(): Promise<void> {
private async _resetWorkspace(): Promise<IResponse<IPhoenixConnectionInfoResult>> {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
@@ -123,15 +148,17 @@ export class NotebookContainerClient {
return Promise.reject(error);
}
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
try {
await fetch(`${notebookServerEndpoint}/api/shutdown`, {
method: "POST",
headers: { Authorization: authToken },
});
if (useNotebook.getState().isPhoenixNotebooks) {
const provisionData: IProvisionData = {
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
};
return await this.phoenixClient.resetContainer(provisionData);
}
return null;
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
await this.recreateNotebookWorkspaceAsync();
throw error;
}
}
@@ -145,22 +172,11 @@ export class NotebookContainerClient {
};
}
private async recreateNotebookWorkspaceAsync(): Promise<void> {
const { databaseAccount } = userContext;
if (!databaseAccount?.id) {
throw new Error("DataExplorer not initialized");
}
try {
await destroy(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
await createOrUpdate(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
"default"
);
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookContainerClient/recreateNotebookWorkspaceAsync");
return Promise.reject(error);
}
private getHeaders(): HeadersInit {
const authorizationHeader = getAuthorizationHeader();
return {
[authorizationHeader.header]: authorizationHeader.token,
[HttpHeaders.contentType]: "application/json",
};
}
}

View File

@@ -16,9 +16,10 @@ import "./NotebookReadOnlyRenderer.less";
import SandboxOutputs from "./outputs/SandboxOutputs";
export interface NotebookRendererProps {
contentRef: any;
contentRef: ContentRef;
hideInputs?: boolean;
hidePrompts?: boolean;
addTransform: (component: React.ComponentType & { MIMETYPE: string }) => void;
}
/**
@@ -27,7 +28,7 @@ export interface NotebookRendererProps {
class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
componentDidMount() {
if (!userContext.features.sandboxNotebookOutputs) {
loadTransform(this.props as any);
loadTransform(this.props as NotebookRendererProps);
}
}
@@ -59,7 +60,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
<div className="NotebookReadOnlyRender">
<Cells contentRef={this.props.contentRef}>
{{
code: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
code: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
<CodeCell id={id} contentRef={contentRef}>
{{
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
@@ -73,14 +74,14 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
}}
</CodeCell>
),
markdown: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
markdown: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
<MarkdownCell id={id} contentRef={contentRef} cell_type="markdown">
{{
editor: {},
}}
</MarkdownCell>
),
raw: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
raw: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
<RawCell id={id} contentRef={contentRef} cell_type="raw">
{{
editor: {
@@ -98,6 +99,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererProps) => {
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
@@ -114,4 +116,4 @@ const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: Noteboo
return mapDispatchToProps;
};
export default connect(null, makeMapDispatchToProps)(NotebookReadOnlyRenderer);
export default connect(undefined, makeMapDispatchToProps)(NotebookReadOnlyRenderer);

View File

@@ -3,14 +3,19 @@ import { AppState, selectors } from "@nteract/core";
import domtoimage from "dom-to-image";
import Html2Canvas from "html2canvas";
import path from "path";
import { userContext } from "../../UserContext";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import * as StringUtils from "../../Utils/StringUtils";
import * as InMemoryContentProviderUtils from "../Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils";
import { SnapshotFragment } from "./NotebookComponent/types";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
// Must match rx-jupyter' FileType
export type FileType = "directory" | "file" | "notebook";
export enum NotebookContentProviderType {
GitHubContentProviderType,
InMemoryContentProviderType,
JupyterContentProviderType,
}
// Utilities for notebooks
export class NotebookUtil {
public static UntrustedNotebookRunHint = "Please trust notebook first before running any code cells";
@@ -127,6 +132,18 @@ export class NotebookUtil {
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 {
const contentInfo = GitHubUtils.fromContentUri(path);
if (contentInfo) {
@@ -329,16 +346,4 @@ export class NotebookUtil {
link.click();
document.body.removeChild(link);
}
public static getNotebookBtnTitle(fileName: string): string {
if (this.isPhoenixEnabled()) {
return `Download to ${fileName}`;
} else {
return `Download to my notebooks`;
}
}
public static isPhoenixEnabled(): boolean {
return userContext.features.notebooksTemporarilyDown === false && userContext.features.phoenix === true;
}
}

View File

@@ -35,6 +35,7 @@ describe("auto start kernel", () => {
connectionInfo: {
authToken: "autToken",
notebookServerEndpoint: "notebookServerEndpoint",
forwardingId: "Id",
},
databaseAccountName: undefined,
defaultExperience: undefined,

View File

@@ -1,4 +1,6 @@
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { cloneDeep } from "lodash";
import { PhoenixClient } from "Phoenix/PhoenixClient";
import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
@@ -7,7 +9,8 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger";
import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
import { useTabs } from "../../hooks/useTabs";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@@ -16,7 +19,6 @@ import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import NotebookManager from "./NotebookManager";
import { NotebookUtil } from "./NotebookUtil";
interface NotebookState {
isNotebookEnabled: boolean;
@@ -35,6 +37,9 @@ interface NotebookState {
notebookFolderName: string;
isAllocating: boolean;
isRefreshed: boolean;
containerStatus: ContainerInfo;
isPhoenixNotebooks: boolean;
isPhoenixFeatures: boolean;
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
@@ -53,8 +58,12 @@ interface NotebookState {
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void;
setIsAllocating: (isAllocating: boolean) => void;
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo) => void;
resetContainerConnection: (connectionStatus: ContainerConnectionInfo) => void;
setIsRefreshed: (isAllocating: boolean) => void;
setContainerStatus: (containerStatus: ContainerInfo) => void;
getPhoenixStatus: () => Promise<void>;
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => void;
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
}
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
@@ -63,6 +72,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
forwardingId: undefined,
},
sparkClusterConnectionInfo: {
userName: undefined,
@@ -83,6 +93,13 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
notebookFolderName: undefined,
isAllocating: false,
isRefreshed: false,
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
notebookServerInfo: undefined,
},
isPhoenixNotebooks: undefined,
isPhoenixFeatures: undefined,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
@@ -95,6 +112,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
await get().getPhoenixStatus();
const { databaseAccount, authType } = userContext;
if (
authType === AuthType.EncryptedToken ||
@@ -187,7 +205,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const notebookFolderName = NotebookUtil.isPhoenixEnabled() === true ? "Temporary Notebooks" : "My Notebooks";
const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks";
set({ notebookFolderName });
const myNotebooksContentRoot = {
name: get().notebookFolderName,
@@ -270,13 +288,34 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
},
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo): void => {
resetContainerConnection: (connectionStatus: ContainerConnectionInfo): void => {
useTabs.getState().closeAllNotebookTabs(true);
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo({
notebookServerEndpoint: undefined,
authToken: undefined,
});
useNotebook.getState().setNotebookServerInfo(undefined);
useNotebook.getState().setIsAllocating(false);
useNotebook.getState().setContainerStatus({
status: undefined,
durationLeftInMinutes: undefined,
notebookServerInfo: undefined,
});
},
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
getPhoenixStatus: async () => {
if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) {
let isPhoenix = false;
if (userContext.features.phoenixNotebooks || userContext.features.phoenixFeatures) {
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 });
}
},
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
}));

View File

@@ -4,6 +4,8 @@ import * as React from "react";
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
export const OpenFullScreen: React.FunctionComponent = () => {
const [isReadUrlCopy, setIsReadUrlCopy] = React.useState<boolean>(false);
const [isReadWriteUrlCopy, setIsReadWriteUrlCopy] = React.useState<boolean>(false);
const result = useFullScreenURLs();
if (!result) {
return <Spinner label="Generating URLs..." ariaLive="assertive" labelPosition="right" />;
@@ -23,10 +25,12 @@ export const OpenFullScreen: React.FunctionComponent = () => {
<TextField label="Read and Write" readOnly defaultValue={readWriteUrl} />
<Stack horizontal tokens={{ childrenGap: 10 }}>
<DefaultButton
ariaLabel={isReadWriteUrlCopy ? "Copied url" : "Copy"}
onClick={() => {
copyToClipboard(readWriteUrl);
setIsReadWriteUrlCopy(true);
}}
text="Copy"
text={isReadWriteUrlCopy ? "Copied" : "Copy"}
iconProps={{ iconName: "Copy" }}
/>
<PrimaryButton
@@ -40,10 +44,12 @@ export const OpenFullScreen: React.FunctionComponent = () => {
<TextField label="Read Only" readOnly defaultValue={readUrl} />
<Stack horizontal tokens={{ childrenGap: 10 }}>
<DefaultButton
ariaLabel={isReadUrlCopy ? "Copied url" : "Copy"}
onClick={() => {
setIsReadUrlCopy(true);
copyToClipboard(readUrl);
}}
text="Copy"
text={isReadUrlCopy ? "Copied" : "Copy"}
iconProps={{ iconName: "Copy" }}
/>
<PrimaryButton

View File

@@ -13,21 +13,21 @@ import {
Text,
TooltipHost,
} from "@fluentui/react";
import * as Constants from "Common/Constants";
import { createCollection } from "Common/dataAccess/createCollection";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { SubscriptionType } from "Contracts/SubscriptionType";
import { useSidePanel } from "hooks/useSidePanel";
import React from "react";
import * as Constants from "../../Common/Constants";
import { createCollection } from "../../Common/dataAccess/createCollection";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import { useSidePanel } from "../../hooks/useSidePanel";
import { CollectionCreation } from "../../Shared/Constants";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { getCollectionName } from "../../Utils/APITypeUtils";
import { isCapabilityEnabled, isServerlessAccount } from "../../Utils/CapabilityUtils";
import { getUpsellMessage } from "../../Utils/PricingUtils";
import { CollectionCreation } from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils";
import { isCapabilityEnabled, isServerlessAccount } from "Utils/CapabilityUtils";
import { getUpsellMessage } from "Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../Explorer";
@@ -92,6 +92,7 @@ export interface AddCollectionPanelState {
errorMessage: string;
showErrorDetails: boolean;
isExecuting: boolean;
isThroughputCapExceeded: boolean;
}
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
@@ -122,6 +123,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
errorMessage: "",
showErrorDetails: false,
isExecuting: false,
isThroughputCapExceeded: false,
};
}
@@ -249,6 +251,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
this.setState({ isThroughputCapExceeded })
}
onCostAcknowledgeChange={(isAcknowledge: boolean) => (this.isCostAcknowledged = isAcknowledge)}
/>
)}
@@ -274,7 +279,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{`${getCollectionName()} ${userContext.apiType === "Mongo" ? "name" : "id"}`}
{`${getCollectionName()} id`}
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
@@ -480,6 +485,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isThroughputCapExceeded: boolean) =>
this.setState({ isThroughputCapExceeded })
}
onCostAcknowledgeChange={(isAcknowledged: boolean) => {
this.isCostAcknowledged = isAcknowledged;
}}
@@ -659,7 +667,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{userContext.apiType === "SQL" && (
<Checkbox
label="My partition key is larger than 100 bytes"
label="My partition key is larger than 101 bytes"
checked={this.state.useHashV2}
styles={{
text: { fontSize: 12 },
@@ -676,7 +684,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
</div>
<PanelFooterComponent buttonLabel="OK" />
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={this.state.isThroughputCapExceeded} />
{this.state.isExecuting && <PanelLoadingScreen />}
</form>
@@ -879,10 +887,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false;
}
if (isServerlessAccount()) {
return false;
}
switch (userContext.apiType) {
case "SQL":
case "Mongo":
@@ -999,7 +1003,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
const collectionId: string = this.state.collectionId.trim();
let databaseId = this.state.createNewDatabase ? this.state.newDatabaseId.trim() : this.state.selectedDatabaseId;
let partitionKeyString = this.state.partitionKey.trim();
let partitionKeyString = this.state.isSharded ? this.state.partitionKey.trim() : undefined;
if (userContext.apiType === "Tables") {
// Table require fixed Database: TablesDB, and fixed Partition Key: /'$pk'

View File

@@ -52,6 +52,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
);
const [formErrors, setFormErrors] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>(false);
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
@@ -79,7 +80,9 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
dataExplorerArea: Constants.Areas.ContextualPane,
};
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
buttonElement.focus();
if (buttonElement) {
buttonElement.focus();
}
}, []);
const onSubmit = () => {
@@ -169,6 +172,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
formError: formErrors,
isExecuting,
submitButtonText: "OK",
isSubmitButtonDisabled: isThroughputCapExceeded,
onSubmit,
};
@@ -239,6 +243,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
isSharded={databaseCreateNewShared}
setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
/>
)}

View File

@@ -4,6 +4,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
<RightPaneForm
formError=""
isExecuting={false}
isSubmitButtonDisabled={false}
onSubmit={[Function]}
submitButtonText="OK"
>
@@ -92,6 +93,7 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setIsThroughputCapExceeded={[Function]}
setThroughputValue={[Function]}
/>
</div>

View File

@@ -1,14 +1,14 @@
import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } from "@fluentui/react";
import * as Constants from "Common/Constants";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import * as Constants from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import { useSidePanel } from "../../../hooks/useSidePanel";
import * as SharedConstants from "../../../Shared/Constants";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import * as SharedConstants from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { isServerlessAccount } from "Utils/CapabilityUtils";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
@@ -43,6 +43,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
const [dedicateTableThroughput, setDedicateTableThroughput] = useState<boolean>(false);
const [isExecuting, setIsExecuting] = useState<boolean>();
const [formError, setFormError] = useState<string>("");
const [isThroughputCapExceeded, setIsThroughputCapExceeded] = useState<boolean>(false);
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
const addCollectionPaneOpenMessage = {
@@ -149,6 +150,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
formError,
isExecuting,
submitButtonText: "OK",
isSubmitButtonDisabled: isThroughputCapExceeded,
onSubmit,
};
@@ -262,6 +264,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
isSharded
setThroughputValue={(throughput: number) => (newKeySpaceThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (isNewKeySpaceAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
/>
)}
@@ -331,9 +334,10 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !useDatabases.getState().isFirstResourceCreated()}
isDatabase={false}
isSharded={false}
isSharded
setThroughputValue={(throughput: number) => (tableThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (isTableAutoscale = isAutoscale)}
setIsThroughputCapExceeded={(isCapExceeded: boolean) => setIsThroughputCapExceeded(isCapExceeded)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
/>
)}

View File

@@ -5,7 +5,6 @@ import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
import { userContext } from "../../../UserContext";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
@@ -76,7 +75,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
selectedLocation.owner,
selectedLocation.repo
)} - ${selectedLocation.branch}`;
} else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) {
} else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenixNotebooks) {
destination = useNotebook.getState().notebookFolderName;
}

View File

@@ -1,18 +1,18 @@
import { Text, TextField } from "@fluentui/react";
import { Areas } from "Common/Constants";
import { deleteCollection } from "Common/dataAccess/deleteCollection";
import DeleteFeedback from "Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { Collection } from "Contracts/ViewModels";
import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs";
import React, { FunctionComponent, useState } from "react";
import { Areas } from "../../../Common/Constants";
import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
import DeleteFeedback from "../../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { Collection } from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { useTabs } from "../../../hooks/useTabs";
import { DefaultExperienceUtility } from "../../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
import { getCollectionName } from "../../../Utils/APITypeUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils";
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
@@ -38,7 +38,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
const onSubmit = async (): Promise<void> => {
const collection = useSelectedNode.getState().findSelectedCollection();
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);
NotificationConsoleUtils.logConsoleError(
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`

View File

@@ -369,18 +369,21 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
</div>
<PanelFooterComponent
buttonLabel="OK"
isButtonDisabled={false}
>
<div
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
text="OK"
type="submit"
>
<PrimaryButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
text="OK"
theme={
@@ -660,6 +663,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<CustomizedDefaultButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -941,6 +945,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
>
<DefaultButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1223,6 +1228,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
<BaseButton
ariaLabel="OK"
baseClassName="ms-Button"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}

View File

@@ -1,18 +1,18 @@
import { Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import { Areas } from "Common/Constants";
import { deleteDatabase } from "Common/dataAccess/deleteDatabase";
import DeleteFeedback from "Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { Collection, Database } from "Contracts/ViewModels";
import { useSidePanel } from "hooks/useSidePanel";
import { useTabs } from "hooks/useTabs";
import React, { FunctionComponent, useState } from "react";
import { Areas } from "../../Common/Constants";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
import DeleteFeedback from "../../Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { Collection, Database } from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";

View File

@@ -1,6 +1,6 @@
import { IDropdownOption, IImageProps, Image, Stack, Text } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useState } from "react";
import React, { FunctionComponent, useRef, useState } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
@@ -25,19 +25,16 @@ interface UnwrappedExecuteSprocParam {
export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPaneProps> = ({
storedProcedure,
}: ExecuteSprocParamsPaneProps): JSX.Element => {
const paramKeyValuesRef = useRef<UnwrappedExecuteSprocParam[]>([{ key: "string", text: "" }]);
const partitionValueRef = useRef<string>();
const partitionKeyRef = useRef<string>("string");
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [numberOfParams, setNumberOfParams] = useState<number>(1);
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
const [paramKeyValues, setParamKeyValues] = useState<UnwrappedExecuteSprocParam[]>([{ key: "string", text: "" }]);
const [partitionValue, setPartitionValue] = useState<string>(); // Defaulting to undefined here is important. It is not the same partition key as ""
const [selectedKey, setSelectedKey] = React.useState<IDropdownOption>({ key: "string", text: "" });
const [formError, setFormError] = useState<string>("");
const onPartitionKeyChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
setSelectedKey(item);
};
const validateUnwrappedParams = (): boolean => {
const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValues;
const unwrappedParams: UnwrappedExecuteSprocParam[] = paramKeyValuesRef.current;
for (let i = 0; i < unwrappedParams.length; i++) {
const { key: paramType, text: paramValue } = unwrappedParams[i];
if (paramType === "custom" && (paramValue === "" || paramValue === undefined)) {
@@ -53,8 +50,9 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
};
const submit = (): void => {
const wrappedSprocParams: UnwrappedExecuteSprocParam[] = paramKeyValues;
const { key: partitionKey } = selectedKey;
const wrappedSprocParams: UnwrappedExecuteSprocParam[] = paramKeyValuesRef.current;
const partitionValue: string = partitionValueRef.current;
const partitionKey: string = partitionKeyRef.current;
if (partitionKey === "custom" && (partitionValue === "" || partitionValue === undefined)) {
setInvalidParamError(partitionValue);
return;
@@ -78,37 +76,21 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
};
const deleteParamAtIndex = (indexToRemove: number): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue.splice(indexToRemove, 1);
setParamKeyValues(cloneParamKeyValue);
paramKeyValuesRef.current.splice(indexToRemove, 1);
setNumberOfParams(numberOfParams - 1);
};
const addNewParamAtIndex = (indexToAdd: number): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue.splice(indexToAdd, 0, { key: "string", text: "" });
setParamKeyValues(cloneParamKeyValue);
};
const paramValueChange = (value: string, indexOfInput: number): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue[indexOfInput].text = value;
setParamKeyValues(cloneParamKeyValue);
};
const paramKeyChange = (
_event: React.FormEvent<HTMLDivElement>,
selectedParam: IDropdownOption,
indexOfParam: number
): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue[indexOfParam].key = selectedParam.key.toString();
setParamKeyValues(cloneParamKeyValue);
paramKeyValuesRef.current.splice(indexToAdd, 0, { key: "string", text: "" });
setNumberOfParams(numberOfParams + 1);
};
const addNewParamAtLastIndex = (): void => {
const cloneParamKeyValue = [...paramKeyValues];
cloneParamKeyValue.splice(cloneParamKeyValue.length, 0, { key: "string", text: "" });
setParamKeyValues(cloneParamKeyValue);
paramKeyValuesRef.current.push({
key: "string",
text: "",
});
setNumberOfParams(numberOfParams + 1);
};
const props: RightPaneFormProps = {
@@ -118,46 +100,52 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
onSubmit: () => submit(),
};
const getInputParameterComponent = (): JSX.Element[] => {
const inputParameters: JSX.Element[] = [];
for (let i = 0; i < numberOfParams; i++) {
const paramKeyValue = paramKeyValuesRef.current[i];
inputParameters.push(
<InputParameter
key={paramKeyValue.text + i}
dropdownLabel={i === 0 ? "Key" : ""}
inputParameterTitle={i === 0 ? "Enter input parameters (if any)" : ""}
inputLabel={i === 0 ? "Param" : ""}
isAddRemoveVisible={true}
onDeleteParamKeyPress={() => deleteParamAtIndex(i)}
onAddNewParamKeyPress={() => addNewParamAtIndex(i + 1)}
onParamValueChange={(_event, newInput?: string) => (paramKeyValuesRef.current[i].text = newInput)}
onParamKeyChange={(_event, selectedParam: IDropdownOption) =>
(paramKeyValuesRef.current[i].key = selectedParam.key.toString())
}
paramValue={paramKeyValue.text}
selectedKey={paramKeyValue.key}
/>
);
}
return inputParameters;
};
return (
<RightPaneForm {...props}>
<div className="panelFormWrapper">
<div className="panelMainContent">
<InputParameter
dropdownLabel="Key"
inputParameterTitle="Partition key value"
inputLabel="Value"
isAddRemoveVisible={false}
onParamValueChange={(_event, newInput?: string) => {
setPartitionValue(newInput);
}}
onParamKeyChange={onPartitionKeyChange}
paramValue={partitionValue}
selectedKey={selectedKey.key}
/>
{paramKeyValues.map((paramKeyValue, index) => (
<InputParameter
key={paramKeyValue && paramKeyValue.text + index}
dropdownLabel={!index && "Key"}
inputParameterTitle={!index && "Enter input parameters (if any)"}
inputLabel={!index && "Param"}
isAddRemoveVisible={true}
onDeleteParamKeyPress={() => deleteParamAtIndex(index)}
onAddNewParamKeyPress={() => addNewParamAtIndex(index + 1)}
onParamValueChange={(event, newInput?: string) => {
paramValueChange(newInput, index);
}}
onParamKeyChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
paramKeyChange(event, selectedParam, index);
}}
paramValue={paramKeyValue && paramKeyValue.text}
selectedKey={paramKeyValue && paramKeyValue.key}
/>
))}
<Stack horizontal onClick={addNewParamAtLastIndex} tabIndex={0}>
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
<Text className="addNewParamStyle">Add New Param</Text>
</Stack>
</div>
<div className="panelMainContent">
<InputParameter
dropdownLabel="Key"
inputParameterTitle="Partition key value"
inputLabel="Value"
isAddRemoveVisible={false}
onParamValueChange={(_event, newInput?: string) => (partitionValueRef.current = newInput)}
onParamKeyChange={(_event: React.FormEvent<HTMLDivElement>, item: IDropdownOption) =>
(partitionKeyRef.current = item.key.toString())
}
paramValue={partitionValueRef.current}
selectedKey={partitionKeyRef.current}
/>
{getInputParameterComponent()}
<Stack horizontal onClick={() => addNewParamAtLastIndex()} tabIndex={0}>
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
<Text className="addNewParamStyle">Add New Param</Text>
</Stack>
</div>
</RightPaneForm>
);

View File

@@ -55,7 +55,7 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
<Stack horizontal>
<Dropdown
label={dropdownLabel && dropdownLabel}
selectedKey={selectedKey}
defaultSelectedKey={selectedKey}
onChange={onParamKeyChange}
options={options}
styles={dropdownStyles}
@@ -64,8 +64,9 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
<TextField
label={inputLabel && inputLabel}
id="confirmCollectionId"
value={paramValue}
defaultValue={paramValue}
onChange={onParamValueChange}
tabIndex={0}
/>
{isAddRemoveVisible && (
<>

View File

@@ -35,6 +35,9 @@ interface IGitHubReposPanelState {
}
export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IGitHubReposPanelState> {
private static readonly PageSize = 30;
private static readonly MasterBranchName = "master";
private static readonly MainBranchName = "main";
private isAddedRepo = false;
private gitHubClient: GitHubClient;
private junoClient: JunoClient;
@@ -116,6 +119,8 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
if (response.status !== HttpStatusCodes.OK) {
throw new Error(`Received HTTP ${response.status} when saving pinned repos`);
}
this.props.explorer.notebookManager?.refreshPinnedRepos();
} catch (error) {
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
}
@@ -207,6 +212,14 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
if (response.data) {
branchesProps.branches = branchesProps.branches.concat(response.data);
branchesProps.lastPageInfo = response.pageInfo;
branchesProps.defaultBranchName = branchesProps.branches[0].name;
const defaultbranchName = branchesProps.branches.find(
(branch) =>
branch.name === GitHubReposPanel.MasterBranchName || branch.name === GitHubReposPanel.MainBranchName
)?.name;
if (defaultbranchName) {
branchesProps.defaultBranchName = defaultbranchName;
}
}
} catch (error) {
handleError(error, "GitHubReposPane/loadMoreBranches", "Failed to fetch branches");
@@ -298,6 +311,17 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
const existingRepo = this.pinnedReposProps.repos.find((repo) => repo.key === item.key);
if (existingRepo) {
existingRepo.branches = item.branches;
this.setState({
gitHubReposState: {
...this.state.gitHubReposState,
reposListProps: {
...this.state.gitHubReposState.reposListProps,
pinnedReposProps: {
repos: this.pinnedReposProps.repos,
},
},
},
});
} else {
this.pinnedReposProps.repos = [...this.pinnedReposProps.repos, item];
}
@@ -374,6 +398,7 @@ export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IG
lastPageInfo: undefined,
hasMore: true,
isLoading: true,
defaultBranchName: undefined,
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo),
};
this.loadMoreBranches(item.repo);

View File

@@ -23,7 +23,13 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"phoenixClient": PhoenixClient {
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],

View File

@@ -3,12 +3,20 @@ import React from "react";
export interface PanelFooterProps {
buttonLabel: string;
isButtonDisabled?: boolean;
}
export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = ({
buttonLabel,
isButtonDisabled,
}: PanelFooterProps): JSX.Element => (
<div className="panelFooter">
<PrimaryButton type="submit" id="sidePanelOkButton" text={buttonLabel} ariaLabel={buttonLabel} />
<PrimaryButton
type="submit"
id="sidePanelOkButton"
text={buttonLabel}
ariaLabel={buttonLabel}
disabled={!!isButtonDisabled}
/>
</div>
);

View File

@@ -9,6 +9,7 @@ export interface RightPaneFormProps {
onSubmit: () => void;
submitButtonText: string;
isSubmitButtonHidden?: boolean;
isSubmitButtonDisabled?: boolean;
children?: ReactNode;
}
@@ -18,6 +19,7 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
onSubmit,
submitButtonText,
isSubmitButtonHidden = false,
isSubmitButtonDisabled = false,
children,
}: RightPaneFormProps) => {
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>) => {
@@ -30,7 +32,9 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
<form className="panelFormWrapper" onSubmit={handleOnSubmit}>
{formError && <PanelInfoErrorComponent messageType="error" message={formError} showErrorDetails={true} />}
{children}
{!isSubmitButtonHidden && <PanelFooterComponent buttonLabel={submitButtonText} />}
{!isSubmitButtonHidden && (
<PanelFooterComponent buttonLabel={submitButtonText} isButtonDisabled={isSubmitButtonDisabled} />
)}
</form>
{isExecuting && <PanelLoadingScreen />}
</>

View File

@@ -14,18 +14,21 @@ exports[`Right Pane Form should render Default properly 1`] = `
>
<PanelFooterComponent
buttonLabel="Load"
isButtonDisabled={false}
>
<div
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="Load"
disabled={false}
id="sidePanelOkButton"
text="Load"
type="submit"
>
<PrimaryButton
ariaLabel="Load"
disabled={false}
id="sidePanelOkButton"
text="Load"
theme={
@@ -305,6 +308,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="Load"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -586,6 +590,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
>
<DefaultButton
ariaLabel="Load"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -868,6 +873,7 @@ exports[`Right Pane Form should render Default properly 1`] = `
<BaseButton
ariaLabel="Load"
baseClassName="ms-Button"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}

View File

@@ -1,13 +1,13 @@
import { Checkbox, ChoiceGroup, IChoiceGroupOption, SpinButton } from "@fluentui/react";
import * as Constants from "Common/Constants";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { configContext } from "ConfigContext";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, MouseEvent, useState } from "react";
import * as Constants from "../../../Common/Constants";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import { configContext } from "../../../ConfigContext";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
import * as StringUtility from "../../../Shared/StringUtility";
import { userContext } from "../../../UserContext";
import { logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import * as StringUtility from "Shared/StringUtility";
import { userContext } from "UserContext";
import { logConsoleInfo } from "Utils/NotificationConsoleUtils";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
export const SettingsPane: FunctionComponent = () => {
@@ -113,20 +113,50 @@ export const SettingsPane: FunctionComponent = () => {
const handleOnPageOptionChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
setPageOption(option.key);
};
const choiceButtonStyles = {
root: {
clear: "both",
},
flexContainer: [
{
selectors: {
".ms-ChoiceFieldGroup root-133": {
clear: "both",
},
".ms-ChoiceField-wrapper label": {
fontSize: 12,
paddingTop: 0,
},
".ms-ChoiceField": {
marginTop: 0,
},
},
},
],
};
return (
<RightPaneForm {...genericPaneProps}>
<div className="paneMainContent">
{shouldShowQueryPageOptions && (
<div className="settingsSection">
<div className="settingsSectionPart pageOptionsPart">
<div className="settingsSectionLabel">
Page options
<div className="settingsSectionPart">
<fieldset>
<legend id="pageOptions" className="settingsSectionLabel legendLabel">
Page Options
</legend>
<InfoTooltip>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many
query results per page.
</InfoTooltip>
</div>
<ChoiceGroup selectedKey={pageOption} options={pageOptionList} onChange={handleOnPageOptionChange} />
<ChoiceGroup
ariaLabelledBy="pageOptions"
selectedKey={pageOption}
options={pageOptionList}
styles={choiceButtonStyles}
onChange={handleOnPageOptionChange}
/>
</fieldset>
</div>
<div className="tabs settingsSectionPart">
{isCustomPageOptionSelected() && (

View File

@@ -14,32 +14,59 @@ exports[`Settings Pane should render Default properly 1`] = `
className="settingsSection"
>
<div
className="settingsSectionPart pageOptionsPart"
className="settingsSectionPart"
>
<div
className="settingsSectionLabel"
>
Page options
<fieldset>
<legend
className="settingsSectionLabel legendLabel"
id="pageOptions"
>
Page Options
</legend>
<InfoTooltip>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.
</InfoTooltip>
</div>
<StyledChoiceGroup
onChange={[Function]}
options={
Array [
<StyledChoiceGroup
ariaLabelledBy="pageOptions"
onChange={[Function]}
options={
Array [
Object {
"key": "custom",
"text": "Custom",
},
Object {
"key": "unlimited",
"text": "Unlimited",
},
]
}
selectedKey="custom"
styles={
Object {
"key": "custom",
"text": "Custom",
},
Object {
"key": "unlimited",
"text": "Unlimited",
},
]
}
selectedKey="custom"
/>
"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
className="tabs settingsSectionPart"

View File

@@ -1,8 +1,8 @@
import { TextField } from "@fluentui/react";
import * as ViewModels from "Contracts/ViewModels";
import { useTabs } from "hooks/useTabs";
import React, { FormEvent, FunctionComponent, useState } from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { useTabs } from "../../../hooks/useTabs";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import NotebookV2Tab from "../../Tabs/NotebookV2Tab";

View File

@@ -13,7 +13,13 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {},
"phoenixClient": PhoenixClient {
"retryOptions": Object {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -675,18 +681,21 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
</div>
<PanelFooterComponent
buttonLabel="Create"
isButtonDisabled={false}
>
<div
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="Create"
disabled={false}
id="sidePanelOkButton"
text="Create"
type="submit"
>
<PrimaryButton
ariaLabel="Create"
disabled={false}
id="sidePanelOkButton"
text="Create"
theme={
@@ -966,6 +975,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="Create"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1247,6 +1257,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
>
<DefaultButton
ariaLabel="Create"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1529,6 +1540,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
<BaseButton
ariaLabel="Create"
baseClassName="ms-Button"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}

View File

@@ -1262,18 +1262,21 @@ exports[`Table query select Panel should render Default properly 1`] = `
</div>
<PanelFooterComponent
buttonLabel="OK"
isButtonDisabled={false}
>
<div
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
text="OK"
type="submit"
>
<PrimaryButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
text="OK"
theme={
@@ -1553,6 +1556,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1834,6 +1838,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
>
<DefaultButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -2116,6 +2121,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
<BaseButton
ariaLabel="OK"
baseClassName="ms-Button"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}

View File

@@ -24,6 +24,7 @@ const {
Ascii,
Bigint,
Blob,
Date: DateType,
Decimal,
Float,
Int,
@@ -33,6 +34,7 @@ const {
Inet,
Smallint,
Tinyint,
Timestamp,
} = TableConstants.CassandraType;
export const cassandraOptions = [
{ key: Text, text: Text },
@@ -40,6 +42,7 @@ export const cassandraOptions = [
{ key: Bigint, text: Bigint },
{ key: Blob, text: Blob },
{ key: Boolean, text: Boolean },
{ key: DateType, text: DateType },
{ key: Decimal, text: Decimal },
{ key: Double, text: Double },
{ key: Float, text: Float },
@@ -50,6 +53,7 @@ export const cassandraOptions = [
{ key: Inet, text: Inet },
{ key: Smallint, text: Smallint },
{ key: Tinyint, text: Tinyint },
{ key: Timestamp, text: Timestamp },
];
export const imageProps: IImageProps = {

View File

@@ -356,18 +356,21 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
</div>
<PanelFooterComponent
buttonLabel="Add Entity"
isButtonDisabled={false}
>
<div
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="Add Entity"
disabled={false}
id="sidePanelOkButton"
text="Add Entity"
type="submit"
>
<PrimaryButton
ariaLabel="Add Entity"
disabled={false}
id="sidePanelOkButton"
text="Add Entity"
theme={
@@ -647,6 +650,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="Add Entity"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -928,6 +932,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
>
<DefaultButton
ariaLabel="Add Entity"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1210,6 +1215,7 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
<BaseButton
ariaLabel="Add Entity"
baseClassName="ms-Button"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}

View File

@@ -357,18 +357,21 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
</div>
<PanelFooterComponent
buttonLabel="Update"
isButtonDisabled={false}
>
<div
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="Update"
disabled={false}
id="sidePanelOkButton"
text="Update"
type="submit"
>
<PrimaryButton
ariaLabel="Update"
disabled={false}
id="sidePanelOkButton"
text="Update"
theme={
@@ -648,6 +651,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
>
<CustomizedDefaultButton
ariaLabel="Update"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -929,6 +933,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
>
<DefaultButton
ariaLabel="Update"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1211,6 +1216,7 @@ exports[`Excute Edit Table Entity Pane should render Default properly 1`] = `
<BaseButton
ariaLabel="Update"
baseClassName="ms-Button"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}

View File

@@ -1,7 +1,7 @@
import { Upload } from "Common/Upload/Upload";
import { useSidePanel } from "hooks/useSidePanel";
import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { Upload } from "../../../Common/Upload/Upload";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";

View File

@@ -1,8 +1,8 @@
import { DetailsList, DetailsListLayoutMode, IColumn, SelectionMode } from "@fluentui/react";
import { Upload } from "Common/Upload/Upload";
import { UploadDetailsRecord } from "Contracts/ViewModels";
import React, { ChangeEvent, FunctionComponent, useState } from "react";
import { Upload } from "../../../Common/Upload/Upload";
import { UploadDetailsRecord } from "../../../Contracts/ViewModels";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import { getErrorMessage } from "../../Tables/Utilities";
import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";

View File

@@ -1041,18 +1041,21 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
</div>
<PanelFooterComponent
buttonLabel="OK"
isButtonDisabled={false}
>
<div
className="panelFooter"
>
<CustomizedPrimaryButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
text="OK"
type="submit"
>
<PrimaryButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
text="OK"
theme={
@@ -1332,6 +1335,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
>
<CustomizedDefaultButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1613,6 +1617,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
>
<DefaultButton
ariaLabel="OK"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}
@@ -1895,6 +1900,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<BaseButton
ariaLabel="OK"
baseClassName="ms-Button"
disabled={false}
id="sidePanelOkButton"
onRenderDescription={[Function]}
primary={true}

View File

@@ -84,9 +84,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
const mainItems = this.createMainItems();
const commonTaskItems = this.createCommonTaskItems();
let recentItems = this.createRecentItems();
if (userContext.features.notebooksTemporarilyDown) {
recentItems = recentItems.filter((item) => item.description !== "Notebook");
}
recentItems = recentItems.filter((item) => item.description !== "Notebook");
const tipsItems = this.createTipsItems();
const onClearRecent = this.clearMostRecent;
@@ -223,7 +221,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
});
}
if (useNotebook.getState().isNotebookEnabled && !userContext.features.notebooksTemporarilyDown) {
if (useNotebook.getState().isPhoenixNotebooks) {
heroes.push({
iconSrc: NewNotebookIcon,
title: "New Notebook",
@@ -307,23 +305,24 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
iconSrc: AddDatabaseIcon,
title: "New " + getDatabaseName(),
description: undefined,
onClick: () => this.openAddDatabasePanel(),
onClick: async () => {
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
await useDatabases.getState().loadAllOffers();
}
useSidePanel
.getState()
.openSidePanel(
"New " + getDatabaseName(),
<AddDatabasePanel explorer={this.container} buttonElement={document.activeElement as HTMLElement} />
);
},
});
}
return items;
}
private openAddDatabasePanel() {
const newDatabaseButton = document.activeElement as HTMLElement;
useSidePanel
.getState()
.openSidePanel(
"New " + getDatabaseName(),
<AddDatabasePanel explorer={this.container} buttonElement={newDatabaseButton} />
);
}
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
return {
iconSrc: NotebookIcon,

View File

@@ -14,11 +14,13 @@ export const CassandraType = {
Bigint: "Bigint",
Blob: "Blob",
Boolean: "Boolean",
Date: "Date",
Decimal: "Decimal",
Double: "Double",
Float: "Float",
Int: "Int",
Text: "Text",
Timestamp: "Timestamp",
Uuid: "Uuid",
Varchar: "Varchar",
Varint: "Varint",

View File

@@ -1,12 +1,11 @@
import _ from "underscore";
import Q from "q";
import * as Entities from "../Entities";
import _ from "underscore";
import * as QueryBuilderConstants from "../Constants";
import * as Entities from "../Entities";
import * as Utilities from "../Utilities";
export function getRowSelector(selectorSchema: Entities.IProperty[]): string {
var selector: string = "";
let selector = "";
selectorSchema &&
selectorSchema.forEach((p: Entities.IProperty) => {
selector += "[" + p.key + '="' + Utilities.jQuerySelectorEscape(p.value) + '"]';
@@ -15,10 +14,10 @@ export function getRowSelector(selectorSchema: Entities.IProperty[]): string {
}
export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElement): boolean {
var isVisible = false;
let isVisible = false;
if (dataTableScrollBodyQuery.length && element) {
var elementRect: ClientRect = element.getBoundingClientRect(),
const elementRect: ClientRect = element.getBoundingClientRect(),
dataTableScrollBodyRect: ClientRect = dataTableScrollBodyQuery.get(0).getBoundingClientRect();
isVisible = elementRect.bottom <= dataTableScrollBodyRect.bottom && dataTableScrollBodyRect.top <= elementRect.top;
@@ -29,17 +28,17 @@ export function isRowVisible(dataTableScrollBodyQuery: JQuery, element: HTMLElem
export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number, isScrollUp: boolean): void {
if (dataTableRows.length) {
var dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector),
const dataTableScrollBodyQuery: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector),
selectedRowElement: HTMLElement = dataTableRows.get(currentIndex);
if (dataTableScrollBodyQuery.length && selectedRowElement) {
var isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement);
const isVisible: boolean = isRowVisible(dataTableScrollBodyQuery, selectedRowElement);
if (!isVisible) {
var selectedRowQuery: JQuery = $(selectedRowElement),
const selectedRowQuery: JQuery = $(selectedRowElement),
scrollPosition: number = dataTableScrollBodyQuery.scrollTop(),
selectedElementPosition: number = selectedRowQuery.position().top,
newScrollPosition: number = 0;
selectedElementPosition: number = selectedRowQuery.position().top;
let newScrollPosition = 0;
if (isScrollUp) {
newScrollPosition = scrollPosition + selectedElementPosition;
@@ -55,7 +54,7 @@ export function scrollToRowIfNeeded(dataTableRows: JQuery, currentIndex: number,
}
export function scrollToTopIfNeeded(): void {
var $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector),
const $dataTableRows: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableAllRowsSelector),
$dataTableScrollBody: JQuery = $(QueryBuilderConstants.htmlSelectors.dataTableScrollBodySelector);
if ($dataTableRows.length && $dataTableScrollBody.length) {
@@ -88,13 +87,14 @@ export function reorderColumns(
table: DataTables.DataTable,
targetOrder: number[],
currentOrder?: number[]
//eslint-disable-next-line
): Q.Promise<any> {
var columnsCount: number = targetOrder.length;
var isCurrentOrderPassedIn: boolean = !!currentOrder;
const columnsCount: number = targetOrder.length;
const isCurrentOrderPassedIn = !!currentOrder;
if (!isCurrentOrderPassedIn) {
currentOrder = getInitialOrder(columnsCount);
}
var isSameOrder: boolean = Utilities.isEqual(currentOrder, targetOrder);
const isSameOrder: boolean = Utilities.isEqual(currentOrder, targetOrder);
// if the targetOrder is the same as current order, do nothing.
if (!isSameOrder) {
@@ -104,7 +104,7 @@ export function reorderColumns(
// Then the transformation order will be the same as target order.
// If current order is specified, then a transformation order is calculated.
// Refer to calculateTransformationOrder for details about transformation order.
var transformationOrder: number[] = isCurrentOrderPassedIn
const transformationOrder: number[] = isCurrentOrderPassedIn
? calculateTransformationOrder(currentOrder, targetOrder)
: targetOrder;
try {
@@ -143,7 +143,7 @@ export function getCurrentOrder(table: DataTables.DataTable): number[] {
* Result: [0, 1, 2, 5, 6, 7, 3, 4, 8]
*/
export function invertIndexValues(inputArray: number[]): number[] {
var invertedArray: number[] = [];
const invertedArray: number[] = [];
if (inputArray) {
inputArray.forEach((value: number, index: number) => {
invertedArray[inputArray[index]] = index;
@@ -170,20 +170,21 @@ export function invertIndexValues(inputArray: number[]): number[] {
* transformation order: Trans = [0, 1, 2, 7, 3, 4, 8, 5, 6]
*/
export function calculateTransformationOrder(currentOrder: number[], targetOrder: number[]): number[] {
var transformationOrder: number[] = [];
let transformationOrder: number[] = [];
if (currentOrder && targetOrder && currentOrder.length === targetOrder.length) {
var invertedCurrentOrder: number[] = invertIndexValues(currentOrder);
const invertedCurrentOrder: number[] = invertIndexValues(currentOrder);
transformationOrder = targetOrder.map((value: number) => invertedCurrentOrder[value]);
}
return transformationOrder;
}
export function getDataTableHeaders(table: DataTables.DataTable): string[] {
var columns: DataTables.ColumnsMethods = table.columns();
var headers: string[] = [];
const columns: DataTables.ColumnsMethods = table.columns();
let headers: string[] = [];
if (columns) {
// table.columns() return ColumnsMethods which is an array of arrays
var columnIndexes: number[] = (<any>columns)[0];
//eslint-disable-next-line
const columnIndexes: number[] = (<any>columns)[0];
if (columnIndexes) {
headers = columnIndexes.map((value: number) => $(table.columns(value).header()).html());
}

View File

@@ -8,11 +8,11 @@ import TableEntityListViewModel from "./TableEntityListViewModel";
export default class TableCommands {
// Command Ids
public static editEntityCommand: string = "edit";
public static deleteEntitiesCommand: string = "delete";
public static reorderColumnsCommand: string = "reorder";
public static resetColumnsCommand: string = "reset";
public static customizeColumnsCommand: string = "customizeColumns";
public static editEntityCommand = "edit";
public static deleteEntitiesCommand = "delete";
public static reorderColumnsCommand = "reorder";
public static resetColumnsCommand = "reset";
public static customizeColumnsCommand = "customizeColumns";
private _container: Explorer;
@@ -21,8 +21,8 @@ export default class TableCommands {
}
public isEnabled(commandName: string, selectedEntites: Entities.ITableEntity[]): boolean {
var singleItemSelected: boolean = DataTableUtilities.containSingleItem(selectedEntites);
var atLeastOneItemSelected: boolean = DataTableUtilities.containItems(selectedEntites);
const singleItemSelected = DataTableUtilities.containSingleItem(selectedEntites);
const atLeastOneItemSelected = DataTableUtilities.containItems(selectedEntites);
switch (commandName) {
case TableCommands.editEntityCommand:
return singleItemSelected;
@@ -47,6 +47,7 @@ export default class TableCommands {
/**
* Edit entity
*/
//eslint-disable-next-line
public editEntityCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
if (!viewModel) {
return null; // Error
@@ -56,12 +57,9 @@ export default class TableCommands {
return null; // Erorr
}
var entityToUpdate: Entities.ITableEntity = viewModel.selected()[0];
var originalNumberOfProperties = entityToUpdate ? 0 : Object.keys(entityToUpdate).length - 1; // .metadata is always a property for etag
return null;
}
//eslint-disable-next-line
public deleteEntitiesCommand(viewModel: TableEntityListViewModel): Q.Promise<any> {
if (!viewModel) {
return null; // Error
@@ -69,7 +67,7 @@ export default class TableCommands {
if (!DataTableUtilities.containItems(viewModel.selected())) {
return null; // Error
}
var entitiesToDelete: Entities.ITableEntity[] = viewModel.selected();
const entitiesToDelete: Entities.ITableEntity[] = viewModel.selected();
const deleteMessage: string =
userContext.apiType === "Cassandra"
? "Are you sure you want to delete the selected rows?"
@@ -82,7 +80,7 @@ export default class TableCommands {
() => {
viewModel.queryTablesTab.container.tableDataClient
.deleteDocuments(viewModel.queryTablesTab.collection, entitiesToDelete)
.then((results: any) => {
.then(() => {
return viewModel.removeEntitiesFromCache(entitiesToDelete).then(() => {
viewModel.redrawTableThrottled();
});

View File

@@ -1,5 +1,5 @@
import * as Utilities from "../Utilities";
import * as Entities from "../Entities";
import * as Utilities from "../Utilities";
import CacheBase from "./CacheBase";
export default class TableEntityCache extends CacheBase<Entities.ITableEntity> {
@@ -21,7 +21,7 @@ export default class TableEntityCache extends CacheBase<Entities.ITableEntity> {
this._tableQuery = Utilities.copyTableQuery(tableQuery);
}
public preClear() {
public preClear(): void {
this.tableQuery = null;
}
}

View File

@@ -431,7 +431,7 @@ export default class TableEntityListViewModel extends DataTableViewModel {
if (newHeaders.length > 0) {
// Any new columns found will be added into headers array, which will trigger a re-render of the DataTable.
// So there is no need to call it here.
this.updateHeaders(newHeaders, /* notifyColumnChanges */ true);
this.updateHeaders(selectedHeadersUnion, /* notifyColumnChanges */ true);
} else {
if (columnSortOrder) {
this.sortColumns(columnSortOrder, oSettings);

View File

@@ -1,4 +1,4 @@
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
export interface ITableEntity {
[property: string]: ITableEntityAttribute;
@@ -17,6 +17,7 @@ export interface ITableEntityAttribute {
export interface IListTableEntitiesResult {
Results: ITableEntity[];
//eslint-disable-next-line
ContinuationToken: any;
iterator?: QueryIterator<ItemDefinition & Resource>;
}

View File

@@ -1,8 +1,9 @@
import QueryClauseViewModel from "./QueryClauseViewModel";
import * as Utilities from "../Utilities";
import QueryClauseViewModel from "./QueryClauseViewModel";
export default class ClauseGroup {
public isRootGroup: boolean;
//eslint-disable-next-line
public children = new Array();
public parentGroup: ClauseGroup;
private _id: string;
@@ -17,7 +18,7 @@ export default class ClauseGroup {
* Flattens the clause tree into an array, depth-first, left to right.
*/
public flattenClauses(targetArray: ko.ObservableArray<QueryClauseViewModel>): void {
var tempArray = new Array<QueryClauseViewModel>();
const tempArray = new Array<QueryClauseViewModel>();
this.flattenClausesImpl(this, tempArray);
targetArray.removeAll();
@@ -31,10 +32,10 @@ export default class ClauseGroup {
newClause.clauseGroup = this;
this.children.push(newClause);
} else {
var targetGroup = insertBefore.clauseGroup;
const targetGroup = insertBefore.clauseGroup;
if (targetGroup) {
var insertBeforeIndex = targetGroup.children.indexOf(insertBefore);
const insertBeforeIndex = targetGroup.children.indexOf(insertBefore);
newClause.clauseGroup = targetGroup;
targetGroup.children.splice(insertBeforeIndex, 0, newClause);
}
@@ -42,19 +43,19 @@ export default class ClauseGroup {
}
public deleteClause(clause: QueryClauseViewModel): void {
var targetGroup = clause.clauseGroup;
const targetGroup = clause.clauseGroup;
if (targetGroup) {
var index = targetGroup.children.indexOf(clause);
const index = targetGroup.children.indexOf(clause);
targetGroup.children.splice(index, 1);
clause.dispose();
if (targetGroup.children.length <= 1 && !targetGroup.isRootGroup) {
var parent = targetGroup.parentGroup;
var targetGroupIndex = parent.children.indexOf(targetGroup);
const parent = targetGroup.parentGroup;
const targetGroupIndex = parent.children.indexOf(targetGroup);
if (targetGroup.children.length === 1) {
var orphan = targetGroup.children.shift();
const orphan = targetGroup.children.shift();
if (orphan instanceof QueryClauseViewModel) {
(<QueryClauseViewModel>orphan).clauseGroup = parent;
@@ -71,14 +72,14 @@ export default class ClauseGroup {
}
public removeAll(): void {
var allClauses: QueryClauseViewModel[] = new Array<QueryClauseViewModel>();
const allClauses: QueryClauseViewModel[] = new Array<QueryClauseViewModel>();
this.flattenClausesImpl(this, allClauses);
while (allClauses.length > 0) {
allClauses.shift().dispose();
}
//eslint-disable-next-line
this.children = new Array<any>();
}
@@ -87,12 +88,12 @@ export default class ClauseGroup {
*/
public groupSelectedItems(): boolean {
// Find the selection start & end, also check for gaps between selected items (if found, cannot proceed).
var selection = this.getCheckedItemsInfo();
const selection = this.getCheckedItemsInfo();
if (selection.canGroup) {
var newGroup = new ClauseGroup(false, this);
const newGroup = new ClauseGroup(false, this);
// Replace the selected items with the new group, and then move the selected items into the new group.
var groupedItems = this.children.splice(selection.begin, selection.end - selection.begin + 1, newGroup);
const groupedItems = this.children.splice(selection.begin, selection.end - selection.begin + 1, newGroup);
groupedItems &&
groupedItems.forEach((element) => {
@@ -118,13 +119,13 @@ export default class ClauseGroup {
return;
}
var parentGroup = this.parentGroup;
var index = parentGroup.children.indexOf(this);
const parentGroup = this.parentGroup;
let index = parentGroup.children.indexOf(this);
if (index >= 0) {
parentGroup.children.splice(index, 1);
var toPromote = this.children.splice(0, this.children.length);
const toPromote = this.children.splice(0, this.children.length);
// Move all children one level up.
toPromote &&
@@ -146,16 +147,16 @@ export default class ClauseGroup {
}
public findDeepestGroupInChildren(skipIndex?: number): ClauseGroup {
var deepest: ClauseGroup = this;
var level: number = 0;
var func = (currentGroup: ClauseGroup): void => {
let deepest = <ClauseGroup>this;
let level = 0;
const func = (currentGroup: ClauseGroup): void => {
level++;
if (currentGroup.getCurrentGroupDepth() > deepest.getCurrentGroupDepth()) {
deepest = currentGroup;
}
for (var i = 0; i < currentGroup.children.length; i++) {
var currentItem = currentGroup.children[i];
for (let i = 0; i < currentGroup.children.length; i++) {
const currentItem = currentGroup.children[i];
if ((i !== skipIndex || level > 1) && currentItem instanceof ClauseGroup) {
func(currentItem);
@@ -170,16 +171,16 @@ export default class ClauseGroup {
}
private getCheckedItemsInfo(): { canGroup: boolean; begin: number; end: number } {
var beginIndex = -1;
var endIndex = -1;
let beginIndex = -1;
let endIndex = -1;
// In order to perform group, all selected items must be next to each other.
// If one or more items are not selected between the first and the last selected item, the gapFlag will be set to True, meaning cannot perform group.
var gapFlag = false;
var count = 0;
let gapFlag = false;
let count = 0;
for (var i = 0; i < this.children.length; i++) {
var currentItem = this.children[i];
var subGroupSelectionState: { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean };
for (let i = 0; i < this.children.length; i++) {
const currentItem = this.children[i];
let subGroupSelectionState: { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean };
if (currentItem instanceof ClauseGroup) {
subGroupSelectionState = (<ClauseGroup>currentItem).getSelectionState();
@@ -235,10 +236,10 @@ export default class ClauseGroup {
}
private getSelectionState(): { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean } {
var selectedCount = 0;
let selectedCount = 0;
for (var i = 0; i < this.children.length; i++) {
var currentItem = this.children[i];
for (let i = 0; i < this.children.length; i++) {
const currentItem = this.children[i];
if (currentItem instanceof ClauseGroup && (<ClauseGroup>currentItem).getSelectionState().allSelected) {
selectedCount++;
@@ -260,8 +261,8 @@ export default class ClauseGroup {
}
private unselectAll(): void {
for (var i = 0; i < this.children.length; i++) {
var currentItem = this.children[i];
for (let i = 0; i < this.children.length; i++) {
const currentItem = this.children[i];
if (currentItem instanceof ClauseGroup) {
(<ClauseGroup>currentItem).unselectAll();
@@ -278,8 +279,8 @@ export default class ClauseGroup {
targetArray.splice(0, targetArray.length);
}
for (var i = 0; i < queryGroup.children.length; i++) {
var currentItem = queryGroup.children[i];
for (let i = 0; i < queryGroup.children.length; i++) {
const currentItem = queryGroup.children[i];
if (currentItem instanceof ClauseGroup) {
this.flattenClausesImpl(currentItem, targetArray);
@@ -292,13 +293,13 @@ export default class ClauseGroup {
}
public getTreeDepth(): number {
var currentDepth = this.getCurrentGroupDepth();
let currentDepth = this.getCurrentGroupDepth();
for (var i = 0; i < this.children.length; i++) {
var currentItem = this.children[i];
for (let i = 0; i < this.children.length; i++) {
const currentItem = this.children[i];
if (currentItem instanceof ClauseGroup) {
var newDepth = (<ClauseGroup>currentItem).getTreeDepth();
const newDepth = (<ClauseGroup>currentItem).getTreeDepth();
if (newDepth > currentDepth) {
currentDepth = newDepth;
@@ -310,8 +311,8 @@ export default class ClauseGroup {
}
public getCurrentGroupDepth(): number {
var group = <ClauseGroup>this;
var depth = 0;
let group = <ClauseGroup>this;
let depth = 0;
while (!group.isRootGroup) {
depth++;

View File

@@ -1,7 +1,7 @@
import * as ko from "knockout";
import * as Constants from "../Constants";
import ClauseGroup from "./ClauseGroup";
import QueryBuilderViewModel from "./QueryBuilderViewModel";
import * as Constants from "../Constants";
/**
* View model for showing group indicators on UI, contains information such as group color and border styles.
@@ -38,7 +38,7 @@ export default class ClauseGroupViewModel {
};
private getGroupBackgroundColor(group: ClauseGroup): string {
var colorCount = Constants.clauseGroupColors.length;
const colorCount = Constants.clauseGroupColors.length;
if (group.isRootGroup) {
return Constants.transparentColor;

View File

@@ -29,7 +29,7 @@ export default class QueryBuilderViewModel {
public removeThisFilterLine = "Remove this filter line"; // localize
public groupSelectedClauses = "Group selected clauses"; // localize
public clauseArray = ko.observableArray<QueryClauseViewModel>(); // This is for storing the clauses in flattened form queryClauses for easier UI data binding.
public queryClauses = new ClauseGroup(true, null); // The actual data structure containing the clause information.
public queryClauses = new ClauseGroup(true, undefined); // The actual data structure containing the clause information.
public columnOptions: ko.ObservableArray<string>;
public canGroupClauses = ko.observable<boolean>(false);
@@ -107,7 +107,7 @@ export default class QueryBuilderViewModel {
}
public setExample() {
var example1 = new QueryClauseViewModel(
const example1 = new QueryClauseViewModel(
this,
"",
"PartitionKey",
@@ -121,7 +121,7 @@ export default class QueryBuilderViewModel {
//null,
true
);
var example2 = new QueryClauseViewModel(
const example2 = new QueryClauseViewModel(
this,
"And",
"RowKey",
@@ -140,13 +140,13 @@ export default class QueryBuilderViewModel {
}
public getODataFilterFromClauses = (): string => {
var filterString: string = "";
var treeTraversal = (group: ClauseGroup): void => {
for (var i = 0; i < group.children.length; i++) {
var currentItem = group.children[i];
let filterString = "";
const treeTraversal = (group: ClauseGroup): void => {
for (let i = 0; i < group.children.length; i++) {
const currentItem = group.children[i];
if (currentItem instanceof QueryClauseViewModel) {
var clause = <QueryClauseViewModel>currentItem;
const clause = <QueryClauseViewModel>currentItem;
this.timestampToValue(clause);
filterString = filterString.concat(
this.constructODataClause(
@@ -173,7 +173,7 @@ export default class QueryBuilderViewModel {
};
public getSqlFilterFromClauses = (): string => {
var filterString: string = "SELECT * FROM c";
let filterString = "SELECT * FROM c";
if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) {
filterString = "SELECT";
const selectText = this._queryViewModel && this._queryViewModel.selectText && this._queryViewModel.selectText();
@@ -199,15 +199,15 @@ export default class QueryBuilderViewModel {
return filterString;
}
filterString = filterString.concat(" WHERE");
var first = true;
var treeTraversal = (group: ClauseGroup): void => {
for (var i = 0; i < group.children.length; i++) {
var currentItem = group.children[i];
let first = true;
const treeTraversal = (group: ClauseGroup): void => {
for (let i = 0; i < group.children.length; i++) {
const currentItem = group.children[i];
if (currentItem instanceof QueryClauseViewModel) {
var clause = <QueryClauseViewModel>currentItem;
let timeStampValue: string = this.timestampToSqlValue(clause);
var value = clause.value();
const clause = <QueryClauseViewModel>currentItem;
const timeStampValue: string = this.timestampToSqlValue(clause);
let value = clause.value();
if (!clause.isValue()) {
value = timeStampValue;
}
@@ -240,7 +240,7 @@ export default class QueryBuilderViewModel {
const databaseId = this._queryViewModel.queryTablesTab.collection.databaseId;
const collectionId = this._queryViewModel.queryTablesTab.collection.id();
const tableToQuery = `${getQuotedCqlIdentifier(databaseId)}.${getQuotedCqlIdentifier(collectionId)}`;
var filterString: string = `SELECT * FROM ${tableToQuery}`;
let filterString = `SELECT * FROM ${tableToQuery}`;
if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) {
filterString = "SELECT";
const selectText = this._queryViewModel && this._queryViewModel.selectText && this._queryViewModel.selectText();
@@ -255,15 +255,15 @@ export default class QueryBuilderViewModel {
return filterString;
}
filterString = filterString.concat(" WHERE");
var first = true;
var treeTraversal = (group: ClauseGroup): void => {
for (var i = 0; i < group.children.length; i++) {
var currentItem = group.children[i];
let first = true;
const treeTraversal = (group: ClauseGroup): void => {
for (let i = 0; i < group.children.length; i++) {
const currentItem = group.children[i];
if (currentItem instanceof QueryClauseViewModel) {
var clause = <QueryClauseViewModel>currentItem;
let timeStampValue: string = this.timestampToSqlValue(clause);
var value = clause.value();
const clause = <QueryClauseViewModel>currentItem;
const timeStampValue = this.timestampToSqlValue(clause);
let value = clause.value();
if (!clause.isValue()) {
value = timeStampValue;
}
@@ -293,13 +293,13 @@ export default class QueryBuilderViewModel {
};
public updateColumnOptions = (): void => {
let originalHeaders = this.columnOptions();
let newHeaders = this.tableEntityListViewModel.headers;
// let originalHeaders = this.columnOptions();
const newHeaders = this.tableEntityListViewModel.headers;
this.columnOptions(newHeaders.sort(DataTableUtilities.compareTableColumns));
};
private generateLeftParentheses(clause: QueryClauseViewModel): string {
var result = "";
let result = "";
if (clause.clauseGroup.isRootGroup || clause.clauseGroup.children.indexOf(clause) !== 0) {
return result;
@@ -307,7 +307,7 @@ export default class QueryBuilderViewModel {
result = result.concat("(");
}
var currentGroup: ClauseGroup = clause.clauseGroup;
let currentGroup: ClauseGroup = clause.clauseGroup;
while (
!currentGroup.isRootGroup &&
@@ -322,7 +322,7 @@ export default class QueryBuilderViewModel {
}
private generateRightParentheses(clause: QueryClauseViewModel): string {
var result = "";
let result = "";
if (
clause.clauseGroup.isRootGroup ||
@@ -333,7 +333,7 @@ export default class QueryBuilderViewModel {
result = result.concat(")");
}
var currentGroup: ClauseGroup = clause.clauseGroup;
let currentGroup: ClauseGroup = clause.clauseGroup;
while (
!currentGroup.isRootGroup &&
@@ -364,14 +364,17 @@ export default class QueryBuilderViewModel {
case Constants.TableType.String:
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
operator
// eslint-disable-next-line no-useless-escape
)} \'${value}\'${rightParentheses}`;
case Constants.TableType.Guid:
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
operator
// eslint-disable-next-line no-useless-escape
)} guid\'${value}\'${rightParentheses}`;
case Constants.TableType.Binary:
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
operator
// eslint-disable-next-line no-useless-escape
)} binary\'${value}\'${rightParentheses}`;
default:
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
@@ -391,9 +394,11 @@ export default class QueryBuilderViewModel {
): string => {
if (propertyName === Constants.EntityKeyNames.PartitionKey) {
propertyName = TableEntityProcessor.keyProperties.PartitionKey;
// eslint-disable-next-line no-useless-escape
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c["${propertyName}"] ${operator} \'${value}\'${rightParentheses}`;
} else if (propertyName === Constants.EntityKeyNames.RowKey) {
propertyName = TableEntityProcessor.keyProperties.Id;
// eslint-disable-next-line no-useless-escape
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName} ${operator} \'${value}\'${rightParentheses}`;
} else if (propertyName === Constants.EntityKeyNames.Timestamp) {
propertyName = TableEntityProcessor.keyProperties.Timestamp;
@@ -403,16 +408,21 @@ export default class QueryBuilderViewModel {
}
switch (type) {
case Constants.TableType.DateTime:
// eslint-disable-next-line no-useless-escape
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${DateTimeUtilities.convertJSDateToTicksWithPadding(
value
// eslint-disable-next-line no-useless-escape
)}\'${rightParentheses}`;
case Constants.TableType.Int64:
// eslint-disable-next-line no-useless-escape
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${Utilities.padLongWithZeros(
value
// eslint-disable-next-line no-useless-escape
)}\'${rightParentheses}`;
case Constants.TableType.String:
case Constants.TableType.Guid:
case Constants.TableType.Binary:
// eslint-disable-next-line no-useless-escape
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${value}\'${rightParentheses}`;
default:
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} ${value}${rightParentheses}`;
@@ -434,6 +444,7 @@ export default class QueryBuilderViewModel {
type === Constants.CassandraType.Ascii ||
type === Constants.CassandraType.Varchar
) {
// eslint-disable-next-line no-useless-escape
return ` ${clauseRule.toLowerCase()} ${leftParentheses} ${propertyName} ${operator} \'${value}\'${rightParentheses}`;
}
return ` ${clauseRule.toLowerCase()} ${leftParentheses} ${propertyName} ${operator} ${value}${rightParentheses}`;
@@ -454,7 +465,7 @@ export default class QueryBuilderViewModel {
case Constants.Operator.NotEqualTo:
return Constants.ODataOperator.NotEqualTo;
}
return null;
return undefined;
};
public groupClauses = (): void => {
@@ -463,11 +474,11 @@ export default class QueryBuilderViewModel {
this.updateCanGroupClauses();
};
public addClauseIndex = (index: number, data: any): void => {
public addClauseIndex = (index: number): void => {
if (index < 0) {
index = 0;
}
var newClause = new QueryClauseViewModel(
const newClause = new QueryClauseViewModel(
this,
"And",
"",
@@ -492,28 +503,28 @@ export default class QueryBuilderViewModel {
// adds a new clause to the end of the array
public addNewClause = (): void => {
this.addClauseIndex(this.clauseArray().length, null);
this.addClauseIndex(this.clauseArray().length);
};
public onAddClauseKeyDown = (index: number, data: any, event: KeyboardEvent, source: any): boolean => {
public onAddClauseKeyDown = (index: number, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.addClauseIndex(index, data);
this.addClauseIndex(index);
event.stopPropagation();
return false;
}
return true;
};
public onAddNewClauseKeyDown = (source: any, event: KeyboardEvent): boolean => {
public onAddNewClauseKeyDown = (event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.addClauseIndex(this.clauseArray().length - 1, null);
this.addClauseIndex(this.clauseArray().length - 1);
event.stopPropagation();
return false;
}
return true;
};
public deleteClause = (index: number, data: any): void => {
public deleteClause = (index: number): void => {
this.deleteClauseImpl(index);
if (this.clauseArray().length !== 0) {
this.clauseArray()[0].and_or("");
@@ -523,9 +534,9 @@ export default class QueryBuilderViewModel {
$(window).resize();
};
public onDeleteClauseKeyDown = (index: number, data: any, event: KeyboardEvent, source: any): boolean => {
public onDeleteClauseKeyDown = (index: number, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.deleteClause(index, data);
this.deleteClause(index);
event.stopPropagation();
return false;
}
@@ -539,25 +550,26 @@ export default class QueryBuilderViewModel {
* (transparent) or its parent group view models.
*/
public getClauseGroupViewModels = (clause: QueryClauseViewModel): ClauseGroupViewModel[] => {
var placeHolderGroupViewModel = new ClauseGroupViewModel(this.queryClauses, false, this);
var treeDepth = this.queryClauses.getTreeDepth();
var groupViewModels = new Array<ClauseGroupViewModel>(treeDepth);
const placeHolderGroupViewModel = new ClauseGroupViewModel(this.queryClauses, false, this);
const treeDepth = this.queryClauses.getTreeDepth();
const groupViewModels = new Array<ClauseGroupViewModel>(treeDepth);
// Prefill the arry with placeholders.
for (var i = 0; i < groupViewModels.length; i++) {
for (let i = 0; i < groupViewModels.length; i++) {
groupViewModels[i] = placeHolderGroupViewModel;
}
var currentGroup = clause.clauseGroup;
let currentGroup = clause.clauseGroup;
// This function determines whether the path from clause to the current group is on the left most.
var isLeftMostPath = (): boolean => {
var group = clause.clauseGroup;
const isLeftMostPath = (): boolean => {
let group = clause.clauseGroup;
if (group.children.indexOf(clause) !== 0) {
return false;
}
// eslint-disable-next-line no-constant-condition
while (true) {
if (group.getId() === currentGroup.getId()) {
break;
@@ -573,13 +585,14 @@ export default class QueryBuilderViewModel {
};
// This function determines whether the path from clause to the current group is on the right most.
var isRightMostPath = (): boolean => {
var group = clause.clauseGroup;
const isRightMostPath = (): boolean => {
let group = clause.clauseGroup;
if (group.children.indexOf(clause) !== group.children.length - 1) {
return false;
}
// eslint-disable-next-line no-constant-condition
while (true) {
if (group.getId() === currentGroup.getId()) {
break;
@@ -594,26 +607,26 @@ export default class QueryBuilderViewModel {
return true;
};
var vmIndex = groupViewModels.length - 1;
var skipIndex = -1;
var lastDepth = clause.groupDepth;
let vmIndex = groupViewModels.length - 1;
let skipIndex = -1;
let lastDepth = clause.groupDepth;
while (!currentGroup.isRootGroup) {
// The current group will be rendered at least once, and if there are any sibling groups deeper
// than the current group, we will repeat rendering the current group to fill up the gap between
// current & deepest sibling.
var deepestInSiblings = currentGroup.findDeepestGroupInChildren(skipIndex).getCurrentGroupDepth();
const deepestInSiblings = currentGroup.findDeepestGroupInChildren(skipIndex).getCurrentGroupDepth();
// Find out the depth difference between the deepest group under the siblings of currentGroup and
// the deepest group under currentGroup. If the result n is a positive number, it means there are
// deeper groups in siblings and we need to draw n + 1 group blocks on UI to fill up the depth
// differences. If the result n is a negative number, it means current group contains the deepest
// sub-group, we only need to draw the group block once.
var repeatCount = Math.max(deepestInSiblings - lastDepth, 0);
const repeatCount = Math.max(deepestInSiblings - lastDepth, 0);
for (var i = 0; i <= repeatCount; i++) {
var isLeftMost = isLeftMostPath();
var isRightMost = isRightMostPath();
var groupViewModel = new ClauseGroupViewModel(currentGroup, i === 0 && isLeftMost, this);
for (let i = 0; i <= repeatCount; i++) {
const isLeftMost = isLeftMostPath();
const isRightMost = isRightMostPath();
const groupViewModel = new ClauseGroupViewModel(currentGroup, i === 0 && isLeftMost, this);
groupViewModel.showTopBorder(isLeftMost);
groupViewModel.showBottomBorder(isRightMost);
@@ -635,9 +648,9 @@ export default class QueryBuilderViewModel {
};
public addCustomRange(timestamp: CustomTimestampHelper.ITimestampQuery, clauseToAdd: QueryClauseViewModel): void {
var index = this.clauseArray.peek().indexOf(clauseToAdd);
const index = this.clauseArray.peek().indexOf(clauseToAdd);
var newClause = new QueryClauseViewModel(
const newClause = new QueryClauseViewModel(
this,
//this._tableEntityListViewModel.tableExplorerContext.hostProxy,
"And",
@@ -662,10 +675,10 @@ export default class QueryBuilderViewModel {
}
private scrollToBottom(): void {
var scrollBox = document.getElementById("scroll");
const scrollBox = document.getElementById("scroll");
if (!this.scrollEventListener) {
scrollBox.addEventListener("scroll", function () {
var translate = "translate(0," + this.scrollTop + "px)";
const translate = "translate(0," + this.scrollTop + "px)";
const allTh = <NodeListOf<HTMLElement>>this.querySelectorAll("thead td");
for (let i = 0; i < allTh.length; i++) {
allTh[i].style.transform = translate;
@@ -673,7 +686,7 @@ export default class QueryBuilderViewModel {
});
this.scrollEventListener = true;
}
var isScrolledToBottom = scrollBox.scrollHeight - scrollBox.clientHeight <= scrollBox.scrollHeight + 1;
const isScrolledToBottom = scrollBox.scrollHeight - scrollBox.clientHeight <= scrollBox.scrollHeight + 1;
if (isScrolledToBottom) {
scrollBox.scrollTop = scrollBox.scrollHeight - scrollBox.clientHeight;
}
@@ -685,8 +698,8 @@ export default class QueryBuilderViewModel {
}
private deleteClauseImpl(index: number): void {
var clause = this.clauseArray()[index];
var previousClause = index === 0 ? 0 : index - 1;
const clause = this.clauseArray()[index];
const previousClause = index === 0 ? 0 : index - 1;
this.queryClauses.deleteClause(clause);
this.updateClauseArray();
if (this.clauseArray()[previousClause]) {
@@ -731,7 +744,7 @@ export default class QueryBuilderViewModel {
private timestampToSqlValue(clause: QueryClauseViewModel): string {
if (clause.isValue()) {
return null;
return undefined;
} else if (clause.isTimestamp()) {
return this.getTimeStampToSqlQuery(clause);
// } else if (clause.isCustomLastTimestamp()) {
@@ -743,7 +756,7 @@ export default class QueryBuilderViewModel {
return clause.customTimeValue();
}
}
return null;
return undefined;
}
private getTimeStampToQuery(clause: QueryClauseViewModel): void {
@@ -789,7 +802,7 @@ export default class QueryBuilderViewModel {
case Constants.timeOptions.currentYear:
return CustomTimestampHelper._queryCurrentYearLocal();
}
return null;
return undefined;
}
public checkIfClauseChanged(): void {

View File

@@ -14,7 +14,7 @@ export default class QueryClauseViewModel {
public field: ko.Observable<string>;
public type: ko.Observable<string>;
public operator: ko.Observable<string>;
public value: ko.Observable<any>;
public value: ko.Observable<string>;
public timeValue: ko.Observable<string>;
public customTimeValue: ko.Observable<string>;
public canAnd: ko.Observable<boolean>;
@@ -39,7 +39,7 @@ export default class QueryClauseViewModel {
field: string,
type: string,
operator: string,
value: any,
value: string,
canAnd: boolean,
timeValue: string,
customTimeValue: string,
@@ -88,30 +88,30 @@ export default class QueryClauseViewModel {
userContext.apiType !== "Cassandra"
);
this.and_or.subscribe((value) => {
this.and_or.subscribe(() => {
this._queryBuilderViewModel.checkIfClauseChanged();
});
this.field.subscribe((value) => {
this.field.subscribe(() => {
this.changeField();
});
this.type.subscribe((value) => {
this.type.subscribe(() => {
this.changeType();
});
this.timeValue.subscribe((value) => {
this.timeValue.subscribe(() => {
// if (this.timeValue() === QueryBuilderConstants.timeOptions.custom) {
// this.customTimestampDialog();
// }
});
this.customTimeValue.subscribe((value) => {
this.customTimeValue.subscribe(() => {
this._queryBuilderViewModel.checkIfClauseChanged();
});
this.value.subscribe((value) => {
this.value.subscribe(() => {
this._queryBuilderViewModel.checkIfClauseChanged();
});
this.operator.subscribe((value) => {
this.operator.subscribe(() => {
this._queryBuilderViewModel.checkIfClauseChanged();
});
this._groupCheckSubscription = this.checkedForGrouping.subscribe((value) => {
this._groupCheckSubscription = this.checkedForGrouping.subscribe(() => {
this._queryBuilderViewModel.updateCanGroupClauses();
});
this.isAndOrFocused = ko.observable<boolean>(false);
@@ -280,7 +280,7 @@ export default class QueryClauseViewModel {
this._groupCheckSubscription.dispose();
}
this.clauseGroup = null;
this._queryBuilderViewModel = null;
this.clauseGroup = undefined;
this._queryBuilderViewModel = undefined;
}
}

View File

@@ -202,14 +202,21 @@ export class CassandraAPIDataClient extends TableDataClient {
let updateQuery = `UPDATE ${collection.databaseId}.${collection.id()}`;
let isPropertyUpdated = false;
let isFirstPropertyToUpdate = true;
for (let property in newEntity) {
if (
!originalDocument[property] ||
newEntity[property]._.toString() !== originalDocument[property]._.toString()
) {
updateQuery += this.isStringType(newEntity[property].$)
? ` SET ${property} = '${newEntity[property]._}',`
: ` SET ${property} = ${newEntity[property]._},`;
let propertyQuerySegment = this.isStringType(newEntity[property].$)
? `${property} = '${newEntity[property]._}',`
: `${property} = ${newEntity[property]._},`;
// Only add the "SET" keyword once
if (isFirstPropertyToUpdate) {
propertyQuerySegment = " SET " + propertyQuerySegment;
isFirstPropertyToUpdate = false;
}
updateQuery += propertyQuerySegment;
isPropertyUpdated = true;
}
}
@@ -528,7 +535,9 @@ export class CassandraAPIDataClient extends TableDataClient {
dataType === TableConstants.CassandraType.Text ||
dataType === TableConstants.CassandraType.Inet ||
dataType === TableConstants.CassandraType.Ascii ||
dataType === TableConstants.CassandraType.Varchar
dataType === TableConstants.CassandraType.Varchar ||
dataType === TableConstants.CassandraType.Timestamp ||
dataType === TableConstants.CassandraType.Date
);
}

View File

@@ -17,6 +17,7 @@ export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
/**
* Every notebook-based tab inherits from this class. It holds the static reference to a notebook client (singleton)
* Re-initiating the constructor when ever a new container got allocated.
*/
export default class NotebookTabBase extends TabsBase {
protected static clientManager: NotebookClientV2;
@@ -27,6 +28,15 @@ export default class NotebookTabBase extends TabsBase {
this.container = options.container;
useNotebook.subscribe(
() => {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint) {
NotebookTabBase.clientManager = undefined;
}
},
(state) => state.notebookServerInfo
);
if (!NotebookTabBase.clientManager) {
NotebookTabBase.clientManager = new NotebookClientV2({
connectionInfo: useNotebook.getState().notebookServerInfo,

View File

@@ -53,14 +53,16 @@ export default class NotebookTabV2 extends NotebookTabBase {
onUpdateKernelInfo: this.onKernelUpdate,
});
}
public onCloseTabButtonClick(): Q.Promise<any> {
/*
* Hard cleaning the workspace(Closing tabs connected with old container connection) when new container got allocated.
*/
public onCloseTabButtonClick(hardClose = false): Q.Promise<any> {
const cleanup = () => {
this.notebookComponentAdapter.notebookShutdown();
super.onCloseTabButtonClick();
};
if (this.notebookComponentAdapter.isContentDirty()) {
if (this.notebookComponentAdapter.isContentDirty() && hardClose === false) {
useDialog
.getState()
.showOkCancelModalDialog(
@@ -118,7 +120,7 @@ export default class NotebookTabV2 extends NotebookTabBase {
const saveButtonChildren = [];
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
saveButtonChildren.push({
iconName: "Copy",
iconName: copyToLabel,
onCommandClick: () => this.copyNotebook(),
commandButtonLabel: copyToLabel,
hasPopup: false,

View File

@@ -364,13 +364,11 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}
public onTabClick(): void {
setTimeout(() => {
if (!this.isCloseClicked) {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
} else {
this.isCloseClicked = false;
}
}, 0);
if (!this.isCloseClicked) {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
} else {
this.isCloseClicked = false;
}
}
public onExecuteQueryClick = async (): Promise<void> => {
@@ -875,9 +873,11 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}
render(): JSX.Element {
componentDidMount(): void {
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}
render(): JSX.Element {
return (
<Fragment>
<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>;
constructor(
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
private getDatabaseAccount: () => DataModels.DatabaseAccount
private getDatabaseAccount: () => DataModels.DatabaseAccount,
private getTabId: () => string
) {}
public renderComponent(): JSX.Element {
@@ -33,6 +34,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
<NotebookTerminalComponent
notebookServerInfo={this.getNotebookServerInfo()}
databaseAccount={this.getDatabaseAccount()}
tabId={this.getTabId()}
/>
) : (
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
@@ -50,7 +52,8 @@ export default class TerminalTab extends TabsBase {
this.container = options.container;
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
() => this.getNotebookServerInfo(options),
() => userContext?.databaseAccount
() => userContext?.databaseAccount,
() => this.tabId
);
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
if (
@@ -100,6 +103,7 @@ export default class TerminalTab extends TabsBase {
return {
authToken: info.authToken,
notebookServerEndpoint: `${info.notebookServerEndpoint.replace(/\/+$/, "")}/${endpointSuffix}`,
forwardingId: info.forwardingId,
};
}
}

View File

@@ -1,4 +1,5 @@
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import * as ko from "knockout";
import * as _ from "underscore";
import * as Constants from "../../Common/Constants";
@@ -528,6 +529,9 @@ export default class Collection implements ViewModels.Collection {
};
public onSchemaAnalyzerClick = async () => {
if (useNotebook.getState().isPhoenixFeatures) {
await this.container.allocateContainer();
}
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default;
@@ -572,7 +576,8 @@ export default class Collection implements ViewModels.Collection {
public onSettingsClick = async (): Promise<void> => {
useSelectedNode.getState().setSelectedNode(this);
await this.loadOffer();
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
throughputCap && throughputCap !== -1 ? await useDatabases.getState().loadAllOffers() : await this.loadOffer();
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Settings node",

View File

@@ -57,7 +57,7 @@ export default class Database implements ViewModels.Database {
this.isOfferRead = false;
}
public onSettingsClick = (): void => {
public onSettingsClick = async (): Promise<void> => {
useSelectedNode.getState().setSelectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
@@ -66,6 +66,11 @@ export default class Database implements ViewModels.Database {
dataExplorerArea: Constants.Areas.ResourceTree,
});
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
if (throughputCap && throughputCap !== -1) {
await useDatabases.getState().loadAllOffers();
}
const pendingNotificationsPromise: Promise<DataModels.Notification> = this.getPendingThroughputSplitNotification();
const tabKind = ViewModels.CollectionTabKind.DatabaseSettingsV2;
const matchingTabs = useTabs.getState().getTabs(tabKind, (tab) => tab.node?.id() === this.id());

View File

@@ -121,7 +121,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
children: [],
};
if (userContext.features.notebooksTemporarilyDown) {
if (!useNotebook.getState().isPhoenixNotebooks) {
notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
} else {
if (galleryContentRoot) {
@@ -130,9 +130,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
if (
myNotebooksContentRoot &&
((NotebookUtil.isPhoenixEnabled() &&
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected) ||
userContext.features.phoenix === false)
useNotebook.getState().isPhoenixNotebooks &&
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected
) {
notebooksTree.children.push(buildMyNotebooksTree());
}
@@ -166,15 +165,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const myNotebooksTree: TreeNode = buildNotebookDirectoryNode(
myNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => {
if (
hasOpened &&
userContext.features.notebooksTemporarilyDown === false &&
userContext.features.phoenix === false
) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
container.openNotebook(item);
}
);
@@ -189,15 +180,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
gitHubNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item).then((hasOpened) => {
if (
hasOpened &&
userContext.features.notebooksTemporarilyDown === false &&
userContext.features.phoenix === false
) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
container.openNotebook(item);
},
true
);
@@ -397,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"
if (GitHubUtils.fromContentUri(item.path)) {
items = items.filter(
@@ -528,7 +516,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
isNotebookEnabled &&
userContext.apiType === "Mongo" &&
isPublicInternetAccessAllowed() &&
!userContext.features.notebooksTemporarilyDown
useNotebook.getState().isPhoenixFeatures
) {
children.push({
label: "Schema (Preview)",

View File

@@ -808,6 +808,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"
if (GitHubUtils.fromContentUri(item.path)) {
items = items.filter(

View File

@@ -22,6 +22,7 @@ export default class Trigger {
public triggerType: ko.Observable<string>;
public triggerOperation: ko.Observable<string>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(container: Explorer, collection: ViewModels.Collection, data: any) {
this.nodeKind = "Trigger";
this.container = container;
@@ -34,7 +35,7 @@ export default class Trigger {
this.triggerType = ko.observable(data.triggerType);
}
public select() {
public select(): void {
useSelectedNode.getState().setSelectedNode(this);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Trigger node",
@@ -43,7 +44,8 @@ export default class Trigger {
});
}
public static create(source: ViewModels.Collection, event: MouseEvent) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public static create(source: ViewModels.Collection, _event: MouseEvent): void {
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Triggers).length + 1;
const trigger = <StoredProcedureDefinition>{
id: "",
@@ -99,7 +101,7 @@ export default class Trigger {
}
};
public delete() {
public delete(): void {
useDialog.getState().showOkCancelModalDialog(
"Confirm delete",
"Are you sure you want to delete the trigger?",
@@ -110,7 +112,8 @@ export default class Trigger {
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
this.collection.children.remove(this);
},
(reason) => {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
() => {}
);
},
"Cancel",

View File

@@ -18,6 +18,7 @@ interface DatabasesState {
findCollection: (databaseId: string, collectionId: string) => ViewModels.Collection;
isLastCollection: () => boolean;
loadDatabaseOffers: () => Promise<void>;
loadAllOffers: () => Promise<void>;
isFirstResourceCreated: () => boolean;
findSelectedDatabase: () => ViewModels.Database;
validateDatabaseId: (id: string) => boolean;
@@ -97,6 +98,19 @@ export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
})
);
},
loadAllOffers: async () => {
await Promise.all(
get().databases?.map(async (database: ViewModels.Database) => {
await database.loadOffer();
await database.loadCollections();
await Promise.all(
(database.collections() || []).map(async (collection: ViewModels.Collection) => {
await collection.loadOffer();
})
);
})
);
},
isFirstResourceCreated: () => {
const databases = get().databases;

View File

@@ -1,3 +1,5 @@
import { ConnectionStatusType } from "Common/Constants";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels";
import { useTabs } from "../hooks/useTabs";
@@ -12,6 +14,7 @@ export interface SelectedNodeState {
collectionId?: string,
subnodeKinds?: ViewModels.CollectionTabKind[]
) => boolean;
isConnectedToContainer: () => boolean;
}
export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) => ({
@@ -59,4 +62,7 @@ export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) =>
subnodeKinds.includes(selectedSubnodeKind)
);
},
isConnectedToContainer: (): boolean => {
return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected;
},
}));

View File

@@ -1,8 +1,8 @@
import ko from "knockout";
import postRobot from "post-robot";
import { GetGithubClientId } from "Utils/GitHubUtils";
import { HttpStatusCodes } from "../Common/Constants";
import { handleError } from "../Common/ErrorHandlingUtils";
import { configContext } from "../ConfigContext";
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
import { JunoClient } from "../Juno/JunoClient";
import { logConsoleInfo } from "../Utils/NotificationConsoleUtils";
@@ -55,7 +55,7 @@ export class GitHubOAuthService {
const params = {
scope,
client_id: configContext.GITHUB_CLIENT_ID,
client_id: GetGithubClientId(),
redirect_uri: new URL("./connectToGitHub.html", window.location.href).href,
state: this.resetState(),
};
@@ -64,7 +64,7 @@ export class GitHubOAuthService {
return params.state;
}
public async finishOAuth(params: IGitHubConnectorParams) {
public async finishOAuth(params: IGitHubConnectorParams): Promise<void> {
try {
this.validateState(params.state);
const response = await this.junoClient.getGitHubToken(params.code);
@@ -113,7 +113,7 @@ export class GitHubOAuthService {
return this.state;
}
public resetToken() {
public resetToken(): void {
this.token(undefined);
}

View File

@@ -50,7 +50,8 @@ describe("Pinned repos", () => {
});
it("updatePinnedRepos invokes pinned repos subscribers", async () => {
const callback = jest.fn().mockImplementation((pinnedRepos: IPinnedRepo[]) => {});
// eslint-disable-next-line @typescript-eslint/no-empty-function
const callback = jest.fn().mockImplementation(() => {});
junoClient.subscribeToPinnedRepos(callback);
const response = await junoClient.updatePinnedRepos(samplePinnedRepos);
@@ -60,7 +61,8 @@ describe("Pinned repos", () => {
});
it("getPinnedRepos invokes pinned repos subscribers", async () => {
const callback = jest.fn().mockImplementation((pinnedRepos: IPinnedRepo[]) => {});
// eslint-disable-next-line @typescript-eslint/no-empty-function
const callback = jest.fn().mockImplementation(() => {});
junoClient.subscribeToPinnedRepos(callback);
const response = await junoClient.getPinnedRepos("scope");
@@ -153,7 +155,7 @@ describe("Gallery", () => {
it("getSampleNotebooks", async () => {
window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK,
json: () => undefined as any,
json: () => undefined as undefined,
});
const response = await junoClient.getSampleNotebooks();
@@ -165,7 +167,7 @@ describe("Gallery", () => {
it("getPublicNotebooks", async () => {
window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK,
json: () => undefined as any,
json: () => undefined as undefined,
});
const response = await junoClient.getPublicNotebooks();
@@ -178,7 +180,7 @@ describe("Gallery", () => {
const id = "id";
window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK,
json: () => undefined as any,
json: () => undefined as undefined,
});
const response = await junoClient.getNotebookInfo(id);
@@ -191,7 +193,7 @@ describe("Gallery", () => {
const id = "id";
window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK,
text: () => undefined as any,
text: () => undefined as undefined,
});
const response = await junoClient.getNotebookContent(id);
@@ -204,7 +206,7 @@ describe("Gallery", () => {
const id = "id";
window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK,
json: () => undefined as any,
json: () => undefined as undefined,
});
const response = await junoClient.increaseNotebookViews(id);
@@ -218,7 +220,7 @@ describe("Gallery", () => {
const id = "id";
window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK,
json: () => undefined as any,
json: () => undefined as undefined,
});
const response = await junoClient.increaseNotebookDownloadCount(id);
@@ -243,7 +245,7 @@ describe("Gallery", () => {
const id = "id";
window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK,
json: () => undefined as any,
json: () => undefined as undefined,
});
const response = await junoClient.favoriteNotebook(id);
@@ -268,7 +270,7 @@ describe("Gallery", () => {
const id = "id";
window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK,
json: () => undefined as any,
json: () => undefined as undefined,
});
const response = await junoClient.unfavoriteNotebook(id);
@@ -292,7 +294,7 @@ describe("Gallery", () => {
it("getFavoriteNotebooks", async () => {
window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK,
json: () => undefined as any,
json: () => undefined as undefined,
});
const response = await junoClient.getFavoriteNotebooks();
@@ -315,7 +317,7 @@ describe("Gallery", () => {
it("getPublishedNotebooks", async () => {
window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK,
json: () => undefined as any,
json: () => undefined as undefined,
});
const response = await junoClient.getPublishedNotebooks();
@@ -339,7 +341,7 @@ describe("Gallery", () => {
const id = "id";
window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK,
json: () => undefined as any,
json: () => undefined as undefined,
});
const response = await junoClient.deleteNotebook(id);
@@ -369,7 +371,7 @@ describe("Gallery", () => {
const addLinkToNotebookViewer = true;
window.fetch = jest.fn().mockReturnValue({
status: HttpStatusCodes.OK,
json: () => undefined as any,
json: () => undefined as undefined,
});
const response = await junoClient.publishNotebook(name, description, tags, thumbnailUrl, content);

View File

@@ -1,4 +1,6 @@
import ko from "knockout";
import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointValidation";
import { GetGithubClientId } from "Utils/GitHubUtils";
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
@@ -62,7 +64,7 @@ export interface IPublishNotebookRequest {
description: string;
tags: string[];
thumbnailUrl: string;
content: any;
content: unknown;
addLinkToNotebookViewer: boolean;
}
@@ -483,7 +485,7 @@ export class JunoClient {
// public for tests
public static getJunoEndpoint(): string {
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`;
console.error(error);
throw new Error(error);
@@ -522,7 +524,7 @@ export class JunoClient {
private static getGitHubClientParams(): URLSearchParams {
const githubParams = new URLSearchParams({
client_id: configContext.GITHUB_CLIENT_ID,
client_id: GetGithubClientId(),
});
if (configContext.GITHUB_CLIENT_SECRET) {

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